diff --git a/.changeset/cuddly-adults-beg.md b/.changeset/cuddly-adults-beg.md new file mode 100644 index 00000000000..b47c8101273 --- /dev/null +++ b/.changeset/cuddly-adults-beg.md @@ -0,0 +1,5 @@ +--- +'@atproto/syntax': minor +--- + +allow colon character in record-key syntax diff --git a/.changeset/dull-hotels-beam.md b/.changeset/dull-hotels-beam.md new file mode 100644 index 00000000000..40a41470ec4 --- /dev/null +++ b/.changeset/dull-hotels-beam.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Prevent hashtag emoji from being parsed as a tag diff --git a/.changeset/lovely-dogs-run.md b/.changeset/lovely-dogs-run.md new file mode 100644 index 00000000000..b2ac95215b6 --- /dev/null +++ b/.changeset/lovely-dogs-run.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Fix mute word upsert logic by ensuring we're comparing sanitized word values diff --git a/.changeset/short-suits-destroy.md b/.changeset/short-suits-destroy.md new file mode 100644 index 00000000000..210d9f00468 --- /dev/null +++ b/.changeset/short-suits-destroy.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Properly calculate length of tag diff --git a/.github/workflows/build-and-push-bsky-aws.yaml b/.github/workflows/build-and-push-bsky-aws.yaml index f534e015ea5..36b1aa23cb3 100644 --- a/.github/workflows/build-and-push-bsky-aws.yaml +++ b/.github/workflows/build-and-push-bsky-aws.yaml @@ -3,7 +3,6 @@ on: push: branches: - main - - appview-v1-courier env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/.github/workflows/build-and-push-bsky-ghcr.yaml b/.github/workflows/build-and-push-bsky-ghcr.yaml index 5d22cd9a389..f1bf0bd10f5 100644 --- a/.github/workflows/build-and-push-bsky-ghcr.yaml +++ b/.github/workflows/build-and-push-bsky-ghcr.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - appview-v2 env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} diff --git a/.prettierignore b/.prettierignore index 6340cc359c2..7bb137f13ea 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,4 @@ pnpm-lock.yaml .pnpm* .changeset *.d.ts +packages/bsky/src/data-plane/gen \ No newline at end of file diff --git a/README.md b/README.md index a54fa91fe5f..96b7ee350f2 100644 --- a/README.md +++ b/README.md @@ -61,12 +61,12 @@ make help ## About AT Protocol -The Authenticated Transfer Protocol ("ATP" or "atproto") is a decentralized social media protocol, developed by [Bluesky PBC](https://blueskyweb.xyz). Learn more at: +The Authenticated Transfer Protocol ("ATP" or "atproto") is a decentralized social media protocol, developed by [Bluesky PBC](https://bsky.social). Learn more at: - [Overview and Guides](https://atproto.com/guides/overview) 👈 Best starting point - [Github Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions - [Protocol Specifications](https://atproto.com/specs/atp) -- [Blogpost on self-authenticating data structures](https://blueskyweb.xyz/blog/3-6-2022-a-self-authenticating-social-protocol) +- [Blogpost on self-authenticating data structures](https://bsky.social/about/blog/3-6-2022-a-self-authenticating-social-protocol) The Bluesky Social application encompasses a set of schemas and APIs built in the overall AT Protocol framework. The namespace for these "Lexicons" is `app.bsky.*`. diff --git a/interop-test-files/syntax/aturi_syntax_valid.txt b/interop-test-files/syntax/aturi_syntax_valid.txt index 2552a964ce0..fd4523d0de9 100644 --- a/interop-test-files/syntax/aturi_syntax_valid.txt +++ b/interop-test-files/syntax/aturi_syntax_valid.txt @@ -24,3 +24,11 @@ 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 + +at://did:abc:123/io.nsid.someFunc/self. +at://did:abc:123/io.nsid.someFunc/lang: +at://did:abc:123/io.nsid.someFunc/: +at://did:abc:123/io.nsid.someFunc/- +at://did:abc:123/io.nsid.someFunc/_ +at://did:abc:123/io.nsid.someFunc/~ +at://did:abc:123/io.nsid.someFunc/... diff --git a/interop-test-files/syntax/recordkey_syntax_invalid.txt b/interop-test-files/syntax/recordkey_syntax_invalid.txt index 1da3d1e7dbc..52106d873a0 100644 --- a/interop-test-files/syntax/recordkey_syntax_invalid.txt +++ b/interop-test-files/syntax/recordkey_syntax_invalid.txt @@ -1,5 +1,4 @@ # specs -literal:self alpha/beta . .. @@ -10,5 +9,7 @@ any+space number[3] number(3) "quote" -pre:fix dHJ1ZQ== + +# too long: 'o'.repeat(513) +ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo diff --git a/interop-test-files/syntax/recordkey_syntax_valid.txt b/interop-test-files/syntax/recordkey_syntax_valid.txt index 8d77d04d2b7..92e8b7e31c9 100644 --- a/interop-test-files/syntax/recordkey_syntax_valid.txt +++ b/interop-test-files/syntax/recordkey_syntax_valid.txt @@ -3,6 +3,19 @@ self example.com ~1.2-3_ dHJ1ZQ +_ +literal:self +pre:fix + +# more corner-cases +: +- +_ +~ +... +self. +lang: +:lang # very long: 'o'.repeat(512) oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index 9f8e2ea97c8..4764c7c14ae 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -1,7 +1,6 @@ { "lexicon": 1, "id": "app.bsky.actor.defs", - "description": "A reference to an actor in the network.", "defs": { "profileViewBasic": { "type": "object", @@ -78,6 +77,7 @@ }, "viewerState": { "type": "object", + "description": "Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.", "properties": { "muted": { "type": "boolean" }, "mutedByList": { @@ -105,7 +105,9 @@ "#personalDetailsPref", "#feedViewPref", "#threadViewPref", - "#interestsPref" + "#interestsPref", + "#mutedWordsPref", + "#hiddenPostsPref" ] } }, @@ -144,6 +146,9 @@ "type": "string", "format": "at-uri" } + }, + "timelineIndex": { + "type": "integer" } } }, @@ -212,6 +217,58 @@ "description": "A list of tags which describe the account owner's interests gathered during onboarding." } } + }, + "mutedWordTarget": { + "type": "string", + "knownValues": ["content", "tag"], + "maxLength": 640, + "maxGraphemes": 64 + }, + "mutedWord": { + "type": "object", + "description": "A word that the account owner has muted.", + "required": ["value", "targets"], + "properties": { + "value": { + "type": "string", + "description": "The muted word itself.", + "maxLength": 10000, + "maxGraphemes": 1000 + }, + "targets": { + "type": "array", + "description": "The intended targets of the muted word.", + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#mutedWordTarget" + } + } + } + }, + "mutedWordsPref": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#mutedWord" + }, + "description": "A list of words the account owner has muted." + } + } + }, + "hiddenPostsPref": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { "type": "string", "format": "at-uri" }, + "description": "A list of URIs of posts the account owner has hidden." + } + } } } } diff --git a/lexicons/app/bsky/actor/getPreferences.json b/lexicons/app/bsky/actor/getPreferences.json index cbd6b60bd6a..e6356a86f47 100644 --- a/lexicons/app/bsky/actor/getPreferences.json +++ b/lexicons/app/bsky/actor/getPreferences.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get private preferences attached to the account.", + "description": "Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth.", "parameters": { "type": "params", "properties": {} diff --git a/lexicons/app/bsky/actor/getProfile.json b/lexicons/app/bsky/actor/getProfile.json index 1bb2ad2fea1..15b0fcc2ec0 100644 --- a/lexicons/app/bsky/actor/getProfile.json +++ b/lexicons/app/bsky/actor/getProfile.json @@ -4,12 +4,16 @@ "defs": { "main": { "type": "query", - "description": "Get detailed profile view of an actor.", + "description": "Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.", "parameters": { "type": "params", "required": ["actor"], "properties": { - "actor": { "type": "string", "format": "at-identifier" } + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Handle or DID of account to fetch profile of." + } } }, "output": { diff --git a/lexicons/app/bsky/actor/getSuggestions.json b/lexicons/app/bsky/actor/getSuggestions.json index 74465dfdf2e..2004ae6f23e 100644 --- a/lexicons/app/bsky/actor/getSuggestions.json +++ b/lexicons/app/bsky/actor/getSuggestions.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a list of suggested actors, used for discovery.", + "description": "Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding.", "parameters": { "type": "params", "properties": { diff --git a/lexicons/app/bsky/actor/profile.json b/lexicons/app/bsky/actor/profile.json index e1b7c6a2b96..feb083d500a 100644 --- a/lexicons/app/bsky/actor/profile.json +++ b/lexicons/app/bsky/actor/profile.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "record", - "description": "A declaration of a profile.", + "description": "A declaration of a Bluesky account profile.", "key": "literal:self", "record": { "type": "object", @@ -16,21 +16,25 @@ }, "description": { "type": "string", + "description": "Free-form profile description text.", "maxGraphemes": 256, "maxLength": 2560 }, "avatar": { "type": "blob", + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'", "accept": ["image/png", "image/jpeg"], "maxSize": 1000000 }, "banner": { "type": "blob", + "description": "Larger horizontal image to display behind profile view.", "accept": ["image/png", "image/jpeg"], "maxSize": 1000000 }, "labels": { "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account.", "refs": ["com.atproto.label.defs#selfLabels"] } } diff --git a/lexicons/app/bsky/actor/searchActors.json b/lexicons/app/bsky/actor/searchActors.json index 48fbacf4fcc..15ccb082238 100644 --- a/lexicons/app/bsky/actor/searchActors.json +++ b/lexicons/app/bsky/actor/searchActors.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Find actors (profiles) matching search criteria.", + "description": "Find actors (profiles) matching search criteria. Does not require auth.", "parameters": { "type": "params", "properties": { diff --git a/lexicons/app/bsky/actor/searchActorsTypeahead.json b/lexicons/app/bsky/actor/searchActorsTypeahead.json index 495b7081c38..4e3cb1b4e88 100644 --- a/lexicons/app/bsky/actor/searchActorsTypeahead.json +++ b/lexicons/app/bsky/actor/searchActorsTypeahead.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Find actor suggestions for a prefix search term.", + "description": "Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth.", "parameters": { "type": "params", "properties": { diff --git a/lexicons/app/bsky/embed/external.json b/lexicons/app/bsky/embed/external.json index 8946382835f..b9c8c1596d5 100644 --- a/lexicons/app/bsky/embed/external.json +++ b/lexicons/app/bsky/embed/external.json @@ -1,10 +1,10 @@ { "lexicon": 1, "id": "app.bsky.embed.external", - "description": "A representation of some externally linked content, embedded in another form of content.", "defs": { "main": { "type": "object", + "description": "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).", "required": ["external"], "properties": { "external": { diff --git a/lexicons/app/bsky/embed/images.json b/lexicons/app/bsky/embed/images.json index 5baa7ab3f74..307607bb7c2 100644 --- a/lexicons/app/bsky/embed/images.json +++ b/lexicons/app/bsky/embed/images.json @@ -1,7 +1,7 @@ { "lexicon": 1, "id": "app.bsky.embed.images", - "description": "A set of images embedded in some other form of content.", + "description": "A set of images embedded in a Bluesky record (eg, a post).", "defs": { "main": { "type": "object", @@ -23,7 +23,10 @@ "accept": ["image/*"], "maxSize": 1000000 }, - "alt": { "type": "string" }, + "alt": { + "type": "string", + "description": "Alt text description of the image, for accessibility." + }, "aspectRatio": { "type": "ref", "ref": "#aspectRatio" } } }, @@ -51,9 +54,18 @@ "type": "object", "required": ["thumb", "fullsize", "alt"], "properties": { - "thumb": { "type": "string" }, - "fullsize": { "type": "string" }, - "alt": { "type": "string" }, + "thumb": { + "type": "string", + "description": "Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View." + }, + "fullsize": { + "type": "string", + "description": "Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View." + }, + "alt": { + "type": "string", + "description": "Alt text description of the image, for accessibility." + }, "aspectRatio": { "type": "ref", "ref": "#aspectRatio" } } } diff --git a/lexicons/app/bsky/embed/record.json b/lexicons/app/bsky/embed/record.json index 4b3d4f814a5..fff9730237d 100644 --- a/lexicons/app/bsky/embed/record.json +++ b/lexicons/app/bsky/embed/record.json @@ -1,7 +1,7 @@ { "lexicon": 1, "id": "app.bsky.embed.record", - "description": "A representation of a record embedded in another form of content.", + "description": "A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.", "defs": { "main": { "type": "object", @@ -36,7 +36,10 @@ "type": "ref", "ref": "app.bsky.actor.defs#profileViewBasic" }, - "value": { "type": "unknown" }, + "value": { + "type": "unknown", + "description": "The record data itself." + }, "labels": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } diff --git a/lexicons/app/bsky/embed/recordWithMedia.json b/lexicons/app/bsky/embed/recordWithMedia.json index 9bc5fe09048..46145464fe2 100644 --- a/lexicons/app/bsky/embed/recordWithMedia.json +++ b/lexicons/app/bsky/embed/recordWithMedia.json @@ -1,7 +1,7 @@ { "lexicon": 1, "id": "app.bsky.embed.recordWithMedia", - "description": "A representation of a record embedded in another form of content, alongside other compatible embeds.", + "description": "A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.", "defs": { "main": { "type": "object", diff --git a/lexicons/app/bsky/feed/defs.json b/lexicons/app/bsky/feed/defs.json index 15a7cb7a719..7f121e88403 100644 --- a/lexicons/app/bsky/feed/defs.json +++ b/lexicons/app/bsky/feed/defs.json @@ -36,6 +36,7 @@ }, "viewerState": { "type": "object", + "description": "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.", "properties": { "repost": { "type": "string", "format": "at-uri" }, "like": { "type": "string", "format": "at-uri" }, diff --git a/lexicons/app/bsky/feed/describeFeedGenerator.json b/lexicons/app/bsky/feed/describeFeedGenerator.json index f95027183a1..0c7b8c8638e 100644 --- a/lexicons/app/bsky/feed/describeFeedGenerator.json +++ b/lexicons/app/bsky/feed/describeFeedGenerator.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get information about a feed generator, including policies and offered feed URIs.", + "description": "Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View).", "output": { "encoding": "application/json", "schema": { diff --git a/lexicons/app/bsky/feed/generator.json b/lexicons/app/bsky/feed/generator.json index 8c00884ad28..d0e361b72cb 100644 --- a/lexicons/app/bsky/feed/generator.json +++ b/lexicons/app/bsky/feed/generator.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "record", - "description": "A declaration of the existence of a feed generator.", + "description": "Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository.", "key": "any", "record": { "type": "object", @@ -32,6 +32,7 @@ }, "labels": { "type": "union", + "description": "Self-label values", "refs": ["com.atproto.label.defs#selfLabels"] }, "createdAt": { "type": "string", "format": "datetime" } diff --git a/lexicons/app/bsky/feed/getActorFeeds.json b/lexicons/app/bsky/feed/getActorFeeds.json index a0620477bc3..9a7dae0ad5d 100644 --- a/lexicons/app/bsky/feed/getActorFeeds.json +++ b/lexicons/app/bsky/feed/getActorFeeds.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a list of feeds created by the actor.", + "description": "Get a list of feeds (feed generator records) created by the actor (in the actor's repo).", "parameters": { "type": "params", "required": ["actor"], diff --git a/lexicons/app/bsky/feed/getActorLikes.json b/lexicons/app/bsky/feed/getActorLikes.json index b3baa58a457..22f8ed984ac 100644 --- a/lexicons/app/bsky/feed/getActorLikes.json +++ b/lexicons/app/bsky/feed/getActorLikes.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a list of posts liked by an actor.", + "description": "Get a list of posts liked by an actor. Does not require auth.", "parameters": { "type": "params", "required": ["actor"], diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json index 1939fa9a49d..90e4d1a7708 100644 --- a/lexicons/app/bsky/feed/getAuthorFeed.json +++ b/lexicons/app/bsky/feed/getAuthorFeed.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a view of an actor's feed.", + "description": "Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.", "parameters": { "type": "params", "required": ["actor"], @@ -19,6 +19,7 @@ "cursor": { "type": "string" }, "filter": { "type": "string", + "description": "Combinations of post/repost types to include in response.", "knownValues": [ "posts_with_replies", "posts_no_replies", diff --git a/lexicons/app/bsky/feed/getFeed.json b/lexicons/app/bsky/feed/getFeed.json index 84407bde155..ada3098b56f 100644 --- a/lexicons/app/bsky/feed/getFeed.json +++ b/lexicons/app/bsky/feed/getFeed.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a hydrated feed from an actor's selected feed generator.", + "description": "Get a hydrated feed from an actor's selected feed generator. Implemented by App View.", "parameters": { "type": "params", "required": ["feed"], diff --git a/lexicons/app/bsky/feed/getFeedGenerator.json b/lexicons/app/bsky/feed/getFeedGenerator.json index 8b3d4d0551a..190eca85b26 100644 --- a/lexicons/app/bsky/feed/getFeedGenerator.json +++ b/lexicons/app/bsky/feed/getFeedGenerator.json @@ -4,12 +4,16 @@ "defs": { "main": { "type": "query", - "description": "Get information about a feed generator.", + "description": "Get information about a feed generator. Implemented by AppView.", "parameters": { "type": "params", "required": ["feed"], "properties": { - "feed": { "type": "string", "format": "at-uri" } + "feed": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the feed generator record." + } } }, "output": { @@ -22,8 +26,14 @@ "type": "ref", "ref": "app.bsky.feed.defs#generatorView" }, - "isOnline": { "type": "boolean" }, - "isValid": { "type": "boolean" } + "isOnline": { + "type": "boolean", + "description": "Indicates whether the feed generator service has been online recently, or else seems to be inactive." + }, + "isValid": { + "type": "boolean", + "description": "Indicates whether the feed generator service is compatible with the record declaration." + } } } } diff --git a/lexicons/app/bsky/feed/getFeedSkeleton.json b/lexicons/app/bsky/feed/getFeedSkeleton.json index 03f3ba04c0f..2bcaa13d4f0 100644 --- a/lexicons/app/bsky/feed/getFeedSkeleton.json +++ b/lexicons/app/bsky/feed/getFeedSkeleton.json @@ -4,12 +4,16 @@ "defs": { "main": { "type": "query", - "description": "Get a skeleton of a feed provided by a feed generator.", + "description": "Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service.", "parameters": { "type": "params", "required": ["feed"], "properties": { - "feed": { "type": "string", "format": "at-uri" }, + "feed": { + "type": "string", + "format": "at-uri", + "description": "Reference to feed generator record describing the specific feed being requested." + }, "limit": { "type": "integer", "minimum": 1, diff --git a/lexicons/app/bsky/feed/getLikes.json b/lexicons/app/bsky/feed/getLikes.json index ffcbc01ac53..d2c5b1a77df 100644 --- a/lexicons/app/bsky/feed/getLikes.json +++ b/lexicons/app/bsky/feed/getLikes.json @@ -4,13 +4,21 @@ "defs": { "main": { "type": "query", - "description": "Get the list of likes.", + "description": "Get like records which reference a subject (by AT-URI and CID).", "parameters": { "type": "params", "required": ["uri"], "properties": { - "uri": { "type": "string", "format": "at-uri" }, - "cid": { "type": "string", "format": "cid" }, + "uri": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the subject (eg, a post record)." + }, + "cid": { + "type": "string", + "format": "cid", + "description": "CID of the subject record (aka, specific version of record), to filter likes." + }, "limit": { "type": "integer", "minimum": 1, diff --git a/lexicons/app/bsky/feed/getListFeed.json b/lexicons/app/bsky/feed/getListFeed.json index 4c5358fcfd7..9dd9fdc70f3 100644 --- a/lexicons/app/bsky/feed/getListFeed.json +++ b/lexicons/app/bsky/feed/getListFeed.json @@ -4,12 +4,16 @@ "defs": { "main": { "type": "query", - "description": "Get a view of a recent posts from actors in a list.", + "description": "Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth.", "parameters": { "type": "params", "required": ["list"], "properties": { - "list": { "type": "string", "format": "at-uri" }, + "list": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to the list record." + }, "limit": { "type": "integer", "minimum": 1, diff --git a/lexicons/app/bsky/feed/getPostThread.json b/lexicons/app/bsky/feed/getPostThread.json index b983617041f..89e99d9c6d7 100644 --- a/lexicons/app/bsky/feed/getPostThread.json +++ b/lexicons/app/bsky/feed/getPostThread.json @@ -4,20 +4,26 @@ "defs": { "main": { "type": "query", - "description": "Get posts in a thread.", + "description": "Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests.", "parameters": { "type": "params", "required": ["uri"], "properties": { - "uri": { "type": "string", "format": "at-uri" }, + "uri": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to post record." + }, "depth": { "type": "integer", + "description": "How many levels of reply depth should be included in response.", "default": 6, "minimum": 0, "maximum": 1000 }, "parentHeight": { "type": "integer", + "description": "How many levels of parent (and grandparent, etc) post to include.", "default": 80, "minimum": 0, "maximum": 1000 diff --git a/lexicons/app/bsky/feed/getPosts.json b/lexicons/app/bsky/feed/getPosts.json index c985a5cf033..e555ee16326 100644 --- a/lexicons/app/bsky/feed/getPosts.json +++ b/lexicons/app/bsky/feed/getPosts.json @@ -4,13 +4,14 @@ "defs": { "main": { "type": "query", - "description": "Get a view of an actor's feed.", + "description": "Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'.", "parameters": { "type": "params", "required": ["uris"], "properties": { "uris": { "type": "array", + "description": "List of post AT-URIs to return hydrated views for.", "items": { "type": "string", "format": "at-uri" }, "maxLength": 25 } diff --git a/lexicons/app/bsky/feed/getRepostedBy.json b/lexicons/app/bsky/feed/getRepostedBy.json index 99abc6d5cde..db39534658b 100644 --- a/lexicons/app/bsky/feed/getRepostedBy.json +++ b/lexicons/app/bsky/feed/getRepostedBy.json @@ -4,13 +4,21 @@ "defs": { "main": { "type": "query", - "description": "Get a list of reposts.", + "description": "Get a list of reposts for a given post.", "parameters": { "type": "params", "required": ["uri"], "properties": { - "uri": { "type": "string", "format": "at-uri" }, - "cid": { "type": "string", "format": "cid" }, + "uri": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) of post record" + }, + "cid": { + "type": "string", + "format": "cid", + "description": "If supplied, filters to reposts of specific version (by CID) of the post record." + }, "limit": { "type": "integer", "minimum": 1, diff --git a/lexicons/app/bsky/feed/getSuggestedFeeds.json b/lexicons/app/bsky/feed/getSuggestedFeeds.json index de7c4fef753..e643d3391e5 100644 --- a/lexicons/app/bsky/feed/getSuggestedFeeds.json +++ b/lexicons/app/bsky/feed/getSuggestedFeeds.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a list of suggested feeds for the viewer.", + "description": "Get a list of suggested feeds (feed generators) for the requesting account.", "parameters": { "type": "params", "properties": { diff --git a/lexicons/app/bsky/feed/getTimeline.json b/lexicons/app/bsky/feed/getTimeline.json index c3b116381c6..816380fe680 100644 --- a/lexicons/app/bsky/feed/getTimeline.json +++ b/lexicons/app/bsky/feed/getTimeline.json @@ -4,11 +4,14 @@ "defs": { "main": { "type": "query", - "description": "Get a view of the actor's home timeline.", + "description": "Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed.", "parameters": { "type": "params", "properties": { - "algorithm": { "type": "string" }, + "algorithm": { + "type": "string", + "description": "Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism." + }, "limit": { "type": "integer", "minimum": 1, diff --git a/lexicons/app/bsky/feed/like.json b/lexicons/app/bsky/feed/like.json index d82f93bbb1b..c0de3b71e92 100644 --- a/lexicons/app/bsky/feed/like.json +++ b/lexicons/app/bsky/feed/like.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "record", - "description": "A declaration of a like.", + "description": "Record declaring a 'like' of a piece of subject content.", "key": "tid", "record": { "type": "object", diff --git a/lexicons/app/bsky/feed/post.json b/lexicons/app/bsky/feed/post.json index d5f92969253..b9b236b4f81 100644 --- a/lexicons/app/bsky/feed/post.json +++ b/lexicons/app/bsky/feed/post.json @@ -4,20 +4,26 @@ "defs": { "main": { "type": "record", - "description": "A declaration of a post.", + "description": "Record containing a Bluesky post.", "key": "tid", "record": { "type": "object", "required": ["text", "createdAt"], "properties": { - "text": { "type": "string", "maxLength": 3000, "maxGraphemes": 300 }, + "text": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300, + "description": "The primary post content. May be an empty string, if there are embeds." + }, "entities": { "type": "array", - "description": "Deprecated: replaced by app.bsky.richtext.facet.", + "description": "DEPRECATED: replaced by app.bsky.richtext.facet.", "items": { "type": "ref", "ref": "#entity" } }, "facets": { "type": "array", + "description": "Annotations of text (mentions, URLs, hashtags, etc)", "items": { "type": "ref", "ref": "app.bsky.richtext.facet" } }, "reply": { "type": "ref", "ref": "#replyRef" }, @@ -32,20 +38,26 @@ }, "langs": { "type": "array", + "description": "Indicates human language of post primary text content.", "maxLength": 3, "items": { "type": "string", "format": "language" } }, "labels": { "type": "union", + "description": "Self-label values for this post. Effectively content warnings.", "refs": ["com.atproto.label.defs#selfLabels"] }, "tags": { "type": "array", + "description": "Additional hashtags, in addition to any included in post text and facets.", "maxLength": 8, - "items": { "type": "string", "maxLength": 640, "maxGraphemes": 64 }, - "description": "Additional non-inline tags describing this post." + "items": { "type": "string", "maxLength": 640, "maxGraphemes": 64 } }, - "createdAt": { "type": "string", "format": "datetime" } + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this post was originally created." + } } } }, diff --git a/lexicons/app/bsky/feed/repost.json b/lexicons/app/bsky/feed/repost.json index 4dbef10b319..028fd627152 100644 --- a/lexicons/app/bsky/feed/repost.json +++ b/lexicons/app/bsky/feed/repost.json @@ -3,7 +3,7 @@ "id": "app.bsky.feed.repost", "defs": { "main": { - "description": "A declaration of a repost.", + "description": "Record representing a 'repost' of an existing Bluesky post.", "type": "record", "key": "tid", "record": { diff --git a/lexicons/app/bsky/feed/searchPosts.json b/lexicons/app/bsky/feed/searchPosts.json index a3e0bc47f03..c89655dd9db 100644 --- a/lexicons/app/bsky/feed/searchPosts.json +++ b/lexicons/app/bsky/feed/searchPosts.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Find posts matching search criteria.", + "description": "Find posts matching search criteria, returning views of those posts.", "parameters": { "type": "params", "required": ["q"], diff --git a/lexicons/app/bsky/feed/threadgate.json b/lexicons/app/bsky/feed/threadgate.json index 7969b6360a6..ff258da4d30 100644 --- a/lexicons/app/bsky/feed/threadgate.json +++ b/lexicons/app/bsky/feed/threadgate.json @@ -5,12 +5,16 @@ "main": { "type": "record", "key": "tid", - "description": "Defines interaction gating rules for a thread. The rkey of the threadgate record should match the rkey of the thread's root post.", + "description": "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository..", "record": { "type": "object", "required": ["post", "createdAt"], "properties": { - "post": { "type": "string", "format": "at-uri" }, + "post": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to the post record." + }, "allow": { "type": "array", "maxLength": 5, diff --git a/lexicons/app/bsky/graph/block.json b/lexicons/app/bsky/graph/block.json index 6231eb04e10..b64b11a956d 100644 --- a/lexicons/app/bsky/graph/block.json +++ b/lexicons/app/bsky/graph/block.json @@ -4,13 +4,17 @@ "defs": { "main": { "type": "record", - "description": "A declaration of a block.", + "description": "Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details.", "key": "tid", "record": { "type": "object", "required": ["subject", "createdAt"], "properties": { - "subject": { "type": "string", "format": "did" }, + "subject": { + "type": "string", + "format": "did", + "description": "DID of the account to be blocked." + }, "createdAt": { "type": "string", "format": "datetime" } } } diff --git a/lexicons/app/bsky/graph/follow.json b/lexicons/app/bsky/graph/follow.json index df4f4319d92..dd6347ac76d 100644 --- a/lexicons/app/bsky/graph/follow.json +++ b/lexicons/app/bsky/graph/follow.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "record", - "description": "A declaration of a social follow.", + "description": "Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.", "key": "tid", "record": { "type": "object", diff --git a/lexicons/app/bsky/graph/getBlocks.json b/lexicons/app/bsky/graph/getBlocks.json index bbfe956fbe0..79a28f66a52 100644 --- a/lexicons/app/bsky/graph/getBlocks.json +++ b/lexicons/app/bsky/graph/getBlocks.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a list of who the actor is blocking.", + "description": "Enumerates which accounts the requesting account is currently blocking. Requires auth.", "parameters": { "type": "params", "properties": { diff --git a/lexicons/app/bsky/graph/getFollowers.json b/lexicons/app/bsky/graph/getFollowers.json index 378c7a7a339..a6c4facd653 100644 --- a/lexicons/app/bsky/graph/getFollowers.json +++ b/lexicons/app/bsky/graph/getFollowers.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a list of an actor's followers.", + "description": "Enumerates accounts which follow a specified account (actor).", "parameters": { "type": "params", "required": ["actor"], diff --git a/lexicons/app/bsky/graph/getFollows.json b/lexicons/app/bsky/graph/getFollows.json index b90f7613889..81f1b6abe49 100644 --- a/lexicons/app/bsky/graph/getFollows.json +++ b/lexicons/app/bsky/graph/getFollows.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a list of who the actor follows.", + "description": "Enumerates accounts which a specified account (actor) follows.", "parameters": { "type": "params", "required": ["actor"], diff --git a/lexicons/app/bsky/graph/getList.json b/lexicons/app/bsky/graph/getList.json index fd24668e5bd..cb95a003a56 100644 --- a/lexicons/app/bsky/graph/getList.json +++ b/lexicons/app/bsky/graph/getList.json @@ -4,12 +4,16 @@ "defs": { "main": { "type": "query", - "description": "Get a list of actors.", + "description": "Gets a 'view' (with additional context) of a specified list.", "parameters": { "type": "params", "required": ["list"], "properties": { - "list": { "type": "string", "format": "at-uri" }, + "list": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) of the list record to hydrate." + }, "limit": { "type": "integer", "minimum": 1, diff --git a/lexicons/app/bsky/graph/getListBlocks.json b/lexicons/app/bsky/graph/getListBlocks.json index 9f9f59821f2..1bc976617ef 100644 --- a/lexicons/app/bsky/graph/getListBlocks.json +++ b/lexicons/app/bsky/graph/getListBlocks.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get lists that the actor is blocking.", + "description": "Get mod lists that the requesting account (actor) is blocking. Requires auth.", "parameters": { "type": "params", "properties": { diff --git a/lexicons/app/bsky/graph/getListMutes.json b/lexicons/app/bsky/graph/getListMutes.json index 8d42ac40f9c..a56a8257643 100644 --- a/lexicons/app/bsky/graph/getListMutes.json +++ b/lexicons/app/bsky/graph/getListMutes.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get lists that the actor is muting.", + "description": "Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth.", "parameters": { "type": "params", "properties": { diff --git a/lexicons/app/bsky/graph/getLists.json b/lexicons/app/bsky/graph/getLists.json index 602dd15307d..127b13e5558 100644 --- a/lexicons/app/bsky/graph/getLists.json +++ b/lexicons/app/bsky/graph/getLists.json @@ -4,12 +4,16 @@ "defs": { "main": { "type": "query", - "description": "Get a list of lists that belong to an actor.", + "description": "Enumerates the lists created by a specified account (actor).", "parameters": { "type": "params", "required": ["actor"], "properties": { - "actor": { "type": "string", "format": "at-identifier" }, + "actor": { + "type": "string", + "format": "at-identifier", + "description": "The account (actor) to enumerate lists from." + }, "limit": { "type": "integer", "minimum": 1, diff --git a/lexicons/app/bsky/graph/getMutes.json b/lexicons/app/bsky/graph/getMutes.json index 8ceae00f607..22eaf0d384c 100644 --- a/lexicons/app/bsky/graph/getMutes.json +++ b/lexicons/app/bsky/graph/getMutes.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a list of who the actor mutes.", + "description": "Enumerates accounts that the requesting account (actor) currently has muted. Requires auth.", "parameters": { "type": "params", "properties": { diff --git a/lexicons/app/bsky/graph/getRelationships.json b/lexicons/app/bsky/graph/getRelationships.json index ccd495dea7d..03490a25ec2 100644 --- a/lexicons/app/bsky/graph/getRelationships.json +++ b/lexicons/app/bsky/graph/getRelationships.json @@ -4,14 +4,19 @@ "defs": { "main": { "type": "query", - "description": "Enumerates public relationships between one account, and a list of other accounts", + "description": "Enumerates public relationships between one account, and a list of other accounts. Does not require auth.", "parameters": { "type": "params", "required": ["actor"], "properties": { - "actor": { "type": "string", "format": "at-identifier" }, + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Primary account requesting relationships for." + }, "others": { "type": "array", + "description": "List of 'other' accounts to be related back to the primary.", "maxLength": 30, "items": { "type": "string", diff --git a/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json b/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json index 32873a537c9..5b0cfdebb70 100644 --- a/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json +++ b/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get suggested follows related to a given actor.", + "description": "Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account.", "parameters": { "type": "params", "required": ["actor"], diff --git a/lexicons/app/bsky/graph/list.json b/lexicons/app/bsky/graph/list.json index ccc845a6926..131114126d3 100644 --- a/lexicons/app/bsky/graph/list.json +++ b/lexicons/app/bsky/graph/list.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "record", - "description": "A declaration of a list of actors.", + "description": "Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists.", "key": "tid", "record": { "type": "object", @@ -12,9 +12,15 @@ "properties": { "purpose": { "type": "ref", + "description": "Defines the purpose of the list (aka, moderation-oriented or curration-oriented)", "ref": "app.bsky.graph.defs#listPurpose" }, - "name": { "type": "string", "maxLength": 64, "minLength": 1 }, + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "description": "Display name for list; can not be empty." + }, "description": { "type": "string", "maxGraphemes": 300, diff --git a/lexicons/app/bsky/graph/listblock.json b/lexicons/app/bsky/graph/listblock.json index b3a839c5316..df2e17f3c30 100644 --- a/lexicons/app/bsky/graph/listblock.json +++ b/lexicons/app/bsky/graph/listblock.json @@ -4,13 +4,17 @@ "defs": { "main": { "type": "record", - "description": "A block of an entire list of actors.", + "description": "Record representing a block relationship against an entire an entire list of accounts (actors).", "key": "tid", "record": { "type": "object", "required": ["subject", "createdAt"], "properties": { - "subject": { "type": "string", "format": "at-uri" }, + "subject": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to the mod list record." + }, "createdAt": { "type": "string", "format": "datetime" } } } diff --git a/lexicons/app/bsky/graph/listitem.json b/lexicons/app/bsky/graph/listitem.json index 2eafb1340be..adbd96e77da 100644 --- a/lexicons/app/bsky/graph/listitem.json +++ b/lexicons/app/bsky/graph/listitem.json @@ -4,14 +4,22 @@ "defs": { "main": { "type": "record", - "description": "An item under a declared list of actors.", + "description": "Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records.", "key": "tid", "record": { "type": "object", "required": ["subject", "list", "createdAt"], "properties": { - "subject": { "type": "string", "format": "did" }, - "list": { "type": "string", "format": "at-uri" }, + "subject": { + "type": "string", + "format": "did", + "description": "The account which is included on the list." + }, + "list": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to the list record (app.bsky.graph.list)." + }, "createdAt": { "type": "string", "format": "datetime" } } } diff --git a/lexicons/app/bsky/graph/muteActor.json b/lexicons/app/bsky/graph/muteActor.json index f1c3dd18f64..c2bf09a3b37 100644 --- a/lexicons/app/bsky/graph/muteActor.json +++ b/lexicons/app/bsky/graph/muteActor.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Mute an actor by DID or handle.", + "description": "Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth.", "input": { "encoding": "application/json", "schema": { diff --git a/lexicons/app/bsky/graph/muteActorList.json b/lexicons/app/bsky/graph/muteActorList.json index c75cc783c14..ad05e6349d7 100644 --- a/lexicons/app/bsky/graph/muteActorList.json +++ b/lexicons/app/bsky/graph/muteActorList.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Mute a list of actors.", + "description": "Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth.", "input": { "encoding": "application/json", "schema": { diff --git a/lexicons/app/bsky/graph/unmuteActor.json b/lexicons/app/bsky/graph/unmuteActor.json index 114af204890..bcea72db59d 100644 --- a/lexicons/app/bsky/graph/unmuteActor.json +++ b/lexicons/app/bsky/graph/unmuteActor.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Unmute an actor by DID or handle.", + "description": "Unmutes the specified account. Requires auth.", "input": { "encoding": "application/json", "schema": { diff --git a/lexicons/app/bsky/graph/unmuteActorList.json b/lexicons/app/bsky/graph/unmuteActorList.json index d9644cddc8e..a597838e112 100644 --- a/lexicons/app/bsky/graph/unmuteActorList.json +++ b/lexicons/app/bsky/graph/unmuteActorList.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Unmute a list of actors.", + "description": "Unmutes the specified list of accounts. Requires auth.", "input": { "encoding": "application/json", "schema": { diff --git a/lexicons/app/bsky/notification/getUnreadCount.json b/lexicons/app/bsky/notification/getUnreadCount.json index ab716b2a436..5eebbbf4eb5 100644 --- a/lexicons/app/bsky/notification/getUnreadCount.json +++ b/lexicons/app/bsky/notification/getUnreadCount.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get the count of unread notifications.", + "description": "Count the number of unread notifications for the requesting account. Requires auth.", "parameters": { "type": "params", "properties": { diff --git a/lexicons/app/bsky/notification/listNotifications.json b/lexicons/app/bsky/notification/listNotifications.json index ea74c5fba53..6c5095e1eb2 100644 --- a/lexicons/app/bsky/notification/listNotifications.json +++ b/lexicons/app/bsky/notification/listNotifications.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a list of notifications.", + "description": "Enumerate notifications for the requesting account. Requires auth.", "parameters": { "type": "params", "properties": { diff --git a/lexicons/app/bsky/notification/registerPush.json b/lexicons/app/bsky/notification/registerPush.json index 80819ece46f..c4e50d1108c 100644 --- a/lexicons/app/bsky/notification/registerPush.json +++ b/lexicons/app/bsky/notification/registerPush.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Register for push notifications with a service.", + "description": "Register to receive push notifications, via a specified service, for the requesting account. Requires auth.", "input": { "encoding": "application/json", "schema": { diff --git a/lexicons/app/bsky/notification/updateSeen.json b/lexicons/app/bsky/notification/updateSeen.json index 33626343e51..84bb0e7d52f 100644 --- a/lexicons/app/bsky/notification/updateSeen.json +++ b/lexicons/app/bsky/notification/updateSeen.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Notify server that the user has seen notifications.", + "description": "Notify server that the requesting account has seen notifications. Requires auth.", "input": { "encoding": "application/json", "schema": { diff --git a/lexicons/app/bsky/richtext/facet.json b/lexicons/app/bsky/richtext/facet.json index ea8f2cba288..388a3a5e0ef 100644 --- a/lexicons/app/bsky/richtext/facet.json +++ b/lexicons/app/bsky/richtext/facet.json @@ -4,6 +4,7 @@ "defs": { "main": { "type": "object", + "description": "Annotation of a sub-string within rich text.", "required": ["index", "features"], "properties": { "index": { "type": "ref", "ref": "#byteSlice" }, @@ -15,7 +16,7 @@ }, "mention": { "type": "object", - "description": "A facet feature for actor mentions.", + "description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.", "required": ["did"], "properties": { "did": { "type": "string", "format": "did" } @@ -23,7 +24,7 @@ }, "link": { "type": "object", - "description": "A facet feature for links.", + "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.", "required": ["uri"], "properties": { "uri": { "type": "string", "format": "uri" } @@ -31,7 +32,7 @@ }, "tag": { "type": "object", - "description": "A hashtag.", + "description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').", "required": ["tag"], "properties": { "tag": { "type": "string", "maxLength": 640, "maxGraphemes": 64 } @@ -39,7 +40,7 @@ }, "byteSlice": { "type": "object", - "description": "A text segment. Start is inclusive, end is exclusive. Indices are for utf8-encoded strings.", + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.", "required": ["byteStart", "byteEnd"], "properties": { "byteStart": { "type": "integer", "minimum": 0 }, diff --git a/lexicons/com/atproto/admin/defs.json b/lexicons/com/atproto/admin/defs.json index 5a65ae31562..e1315eb7473 100644 --- a/lexicons/com/atproto/admin/defs.json +++ b/lexicons/com/atproto/admin/defs.json @@ -33,7 +33,8 @@ "#modEventAcknowledge", "#modEventEscalate", "#modEventMute", - "#modEventEmail" + "#modEventEmail", + "#modEventResolveAppeal" ] }, "subject": { @@ -70,6 +71,7 @@ "#modEventAcknowledge", "#modEventEscalate", "#modEventMute", + "#modEventEmail", "#modEventResolveAppeal" ] }, @@ -183,6 +185,10 @@ "suspendUntil": { "type": "string", "format": "datetime" + }, + "tags": { + "type": "array", + "items": { "type": "string" } } } }, @@ -585,6 +591,27 @@ } } }, + "modEventTag": { + "type": "object", + "description": "Add/Remove a tag on a subject", + "required": ["add", "remove"], + "properties": { + "add": { + "type": "array", + "items": { "type": "string" }, + "description": "Tags to be added to the subject. If already exists, won't be duplicated." + }, + "remove": { + "type": "array", + "items": { "type": "string" }, + "description": "Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated." + }, + "comment": { + "type": "string", + "description": "Additional comment about added/removed tags." + } + } + }, "communicationTemplateView": { "type": "object", "required": [ diff --git a/lexicons/com/atproto/admin/emitModerationEvent.json b/lexicons/com/atproto/admin/emitModerationEvent.json index f32ad18461c..44ef72aad5b 100644 --- a/lexicons/com/atproto/admin/emitModerationEvent.json +++ b/lexicons/com/atproto/admin/emitModerationEvent.json @@ -23,7 +23,8 @@ "com.atproto.admin.defs#modEventMute", "com.atproto.admin.defs#modEventReverseTakedown", "com.atproto.admin.defs#modEventUnmute", - "com.atproto.admin.defs#modEventEmail" + "com.atproto.admin.defs#modEventEmail", + "com.atproto.admin.defs#modEventTag" ] }, "subject": { diff --git a/lexicons/com/atproto/admin/queryModerationEvents.json b/lexicons/com/atproto/admin/queryModerationEvents.json index 70af1bf8ae5..239c17bd115 100644 --- a/lexicons/com/atproto/admin/queryModerationEvents.json +++ b/lexicons/com/atproto/admin/queryModerationEvents.json @@ -23,6 +23,16 @@ "enum": ["asc", "desc"], "description": "Sort direction for the events. Defaults to descending order of created at timestamp." }, + "createdAfter": { + "type": "string", + "format": "datetime", + "description": "Retrieve events created after a given timestamp" + }, + "createdBefore": { + "type": "string", + "format": "datetime", + "description": "Retrieve events created before a given timestamp" + }, "subject": { "type": "string", "format": "uri" }, "includeAllUserRecords": { "type": "boolean", @@ -35,6 +45,40 @@ "maximum": 100, "default": 50 }, + "hasComment": { + "type": "boolean", + "description": "If true, only events with comments are returned" + }, + "comment": { + "type": "string", + "description": "If specified, only events with comments containing the keyword are returned" + }, + "addedLabels": { + "type": "array", + "items": { "type": "string" }, + "description": "If specified, only events where all of these labels were added are returned" + }, + "removedLabels": { + "type": "array", + "items": { "type": "string" }, + "description": "If specified, only events where all of these labels were removed are returned" + }, + "addedTags": { + "type": "array", + "items": { "type": "string" }, + "description": "If specified, only events where all of these tags were added are returned" + }, + "removedTags": { + "type": "array", + "items": { "type": "string" }, + "description": "If specified, only events where all of these tags were removed are returned" + }, + "reportTypes": { + "type": "array", + "items": { + "type": "string" + } + }, "cursor": { "type": "string" } } }, diff --git a/lexicons/com/atproto/admin/queryModerationStatuses.json b/lexicons/com/atproto/admin/queryModerationStatuses.json index e3e2a859bd2..5ac915ceef1 100644 --- a/lexicons/com/atproto/admin/queryModerationStatuses.json +++ b/lexicons/com/atproto/admin/queryModerationStatuses.json @@ -74,6 +74,14 @@ "maximum": 100, "default": 50 }, + "tags": { + "type": "array", + "items": { "type": "string" } + }, + "excludeTags": { + "type": "array", + "items": { "type": "string" } + }, "cursor": { "type": "string" } } }, diff --git a/lexicons/com/atproto/admin/updateAccountPassword.json b/lexicons/com/atproto/admin/updateAccountPassword.json new file mode 100644 index 00000000000..76c69fec0b6 --- /dev/null +++ b/lexicons/com/atproto/admin/updateAccountPassword.json @@ -0,0 +1,21 @@ +{ + "lexicon": 1, + "id": "com.atproto.admin.updateAccountPassword", + "defs": { + "main": { + "type": "procedure", + "description": "Update the password for a user account as an administrator.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["did", "password"], + "properties": { + "did": { "type": "string", "format": "did" }, + "password": { "type": "string" } + } + } + } + } + } +} diff --git a/lexicons/com/atproto/identity/getRecommendedDidCredentials.json b/lexicons/com/atproto/identity/getRecommendedDidCredentials.json new file mode 100644 index 00000000000..3506dbec351 --- /dev/null +++ b/lexicons/com/atproto/identity/getRecommendedDidCredentials.json @@ -0,0 +1,29 @@ +{ + "lexicon": 1, + "id": "com.atproto.identity.getRecommendedDidCredentials", + "defs": { + "main": { + "type": "query", + "description": "Describe the credentials that should be included in the DID doc of an account that is migrating to this service.", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "rotationKeys": { + "description": "Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.", + "type": "array", + "items": { "type": "string" } + }, + "alsoKnownAs": { + "type": "array", + "items": { "type": "string" } + }, + "verificationMethods": { "type": "unknown" }, + "services": { "type": "unknown" } + } + } + } + } + } +} diff --git a/lexicons/com/atproto/identity/requestPlcOperationSignature.json b/lexicons/com/atproto/identity/requestPlcOperationSignature.json new file mode 100644 index 00000000000..4aa8b18fee9 --- /dev/null +++ b/lexicons/com/atproto/identity/requestPlcOperationSignature.json @@ -0,0 +1,10 @@ +{ + "lexicon": 1, + "id": "com.atproto.identity.requestPlcOperationSignature", + "defs": { + "main": { + "type": "procedure", + "description": "Request an email with a code to in order to request a signed PLC operation. Requires Auth." + } + } +} diff --git a/lexicons/com/atproto/identity/resolveHandle.json b/lexicons/com/atproto/identity/resolveHandle.json index ae5aab8f8fc..95885088a7b 100644 --- a/lexicons/com/atproto/identity/resolveHandle.json +++ b/lexicons/com/atproto/identity/resolveHandle.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Provides the DID of a repo.", + "description": "Resolves a handle (domain name) to a DID.", "parameters": { "type": "params", "required": ["handle"], diff --git a/lexicons/com/atproto/identity/signPlcOperation.json b/lexicons/com/atproto/identity/signPlcOperation.json new file mode 100644 index 00000000000..05a952cab6c --- /dev/null +++ b/lexicons/com/atproto/identity/signPlcOperation.json @@ -0,0 +1,45 @@ +{ + "lexicon": 1, + "id": "com.atproto.identity.signPlcOperation", + "defs": { + "main": { + "type": "procedure", + "description": "Signs a PLC operation to update some value(s) in the requesting DID's document.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "token": { + "description": "A token received through com.atproto.identity.requestPlcOperationSignature", + "type": "string" + }, + "rotationKeys": { + "type": "array", + "items": { "type": "string" } + }, + "alsoKnownAs": { + "type": "array", + "items": { "type": "string" } + }, + "verificationMethods": { "type": "unknown" }, + "services": { "type": "unknown" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["operation"], + "properties": { + "operation": { + "type": "unknown", + "description": "A signed DID PLC operation." + } + } + } + } + } + } +} diff --git a/lexicons/com/atproto/identity/submitPlcOperation.json b/lexicons/com/atproto/identity/submitPlcOperation.json new file mode 100644 index 00000000000..280f5003b0d --- /dev/null +++ b/lexicons/com/atproto/identity/submitPlcOperation.json @@ -0,0 +1,20 @@ +{ + "lexicon": 1, + "id": "com.atproto.identity.submitPlcOperation", + "defs": { + "main": { + "type": "procedure", + "description": "Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["operation"], + "properties": { + "operation": { "type": "unknown" } + } + } + } + } + } +} diff --git a/lexicons/com/atproto/identity/updateHandle.json b/lexicons/com/atproto/identity/updateHandle.json index 5fe392bb838..9bb5c347d38 100644 --- a/lexicons/com/atproto/identity/updateHandle.json +++ b/lexicons/com/atproto/identity/updateHandle.json @@ -4,14 +4,18 @@ "defs": { "main": { "type": "procedure", - "description": "Updates the handle of the account.", + "description": "Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.", "input": { "encoding": "application/json", "schema": { "type": "object", "required": ["handle"], "properties": { - "handle": { "type": "string", "format": "handle" } + "handle": { + "type": "string", + "format": "handle", + "description": "The new handle." + } } } } diff --git a/lexicons/com/atproto/label/queryLabels.json b/lexicons/com/atproto/label/queryLabels.json index 7b6fbe23d54..6c81cb0bba6 100644 --- a/lexicons/com/atproto/label/queryLabels.json +++ b/lexicons/com/atproto/label/queryLabels.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Find labels relevant to the provided URI patterns.", + "description": "Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.", "parameters": { "type": "params", "required": ["uriPatterns"], diff --git a/lexicons/com/atproto/label/subscribeLabels.json b/lexicons/com/atproto/label/subscribeLabels.json index 9813ffc192e..5fb1a852d3d 100644 --- a/lexicons/com/atproto/label/subscribeLabels.json +++ b/lexicons/com/atproto/label/subscribeLabels.json @@ -4,13 +4,13 @@ "defs": { "main": { "type": "subscription", - "description": "Subscribe to label updates.", + "description": "Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.", "parameters": { "type": "params", "properties": { "cursor": { "type": "integer", - "description": "The last known event to backfill from." + "description": "The last known event seq number to backfill from." } } }, diff --git a/lexicons/com/atproto/moderation/createReport.json b/lexicons/com/atproto/moderation/createReport.json index 161d622fcf2..f41d28d0b15 100644 --- a/lexicons/com/atproto/moderation/createReport.json +++ b/lexicons/com/atproto/moderation/createReport.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Report a repo or a record.", + "description": "Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.", "input": { "encoding": "application/json", "schema": { @@ -13,9 +13,13 @@ "properties": { "reasonType": { "type": "ref", + "description": "Indicates the broad category of violation the report is for.", "ref": "com.atproto.moderation.defs#reasonType" }, - "reason": { "type": "string" }, + "reason": { + "type": "string", + "description": "Additional context about the content and violation." + }, "subject": { "type": "union", "refs": [ diff --git a/lexicons/com/atproto/repo/applyWrites.json b/lexicons/com/atproto/repo/applyWrites.json index 050b6efbfab..427fc84c4a5 100644 --- a/lexicons/com/atproto/repo/applyWrites.json +++ b/lexicons/com/atproto/repo/applyWrites.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Apply a batch transaction of creates, updates, and deletes.", + "description": "Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.", "input": { "encoding": "application/json", "schema": { @@ -14,12 +14,12 @@ "repo": { "type": "string", "format": "at-identifier", - "description": "The handle or DID of the repo." + "description": "The handle or DID of the repo (aka, current account)." }, "validate": { "type": "boolean", "default": true, - "description": "Flag for validating the records." + "description": "Can be set to 'false' to skip Lexicon schema validation of record data, for all operations." }, "writes": { "type": "array", @@ -31,16 +31,22 @@ }, "swapCommit": { "type": "string", + "description": "If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.", "format": "cid" } } } }, - "errors": [{ "name": "InvalidSwap" }] + "errors": [ + { + "name": "InvalidSwap", + "description": "Indicates that the 'swapCommit' parameter did not match current commit." + } + ] }, "create": { "type": "object", - "description": "Create a new record.", + "description": "Operation which creates a new record.", "required": ["collection", "value"], "properties": { "collection": { "type": "string", "format": "nsid" }, @@ -50,7 +56,7 @@ }, "update": { "type": "object", - "description": "Update an existing record.", + "description": "Operation which updates an existing record.", "required": ["collection", "rkey", "value"], "properties": { "collection": { "type": "string", "format": "nsid" }, @@ -60,7 +66,7 @@ }, "delete": { "type": "object", - "description": "Delete an existing record.", + "description": "Operation which deletes an existing record.", "required": ["collection", "rkey"], "properties": { "collection": { "type": "string", "format": "nsid" }, diff --git a/lexicons/com/atproto/repo/createRecord.json b/lexicons/com/atproto/repo/createRecord.json index baef20c88f0..185f5250850 100644 --- a/lexicons/com/atproto/repo/createRecord.json +++ b/lexicons/com/atproto/repo/createRecord.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Create a new record.", + "description": "Create a single new repository record. Requires auth, implemented by PDS.", "input": { "encoding": "application/json", "schema": { @@ -14,7 +14,7 @@ "repo": { "type": "string", "format": "at-identifier", - "description": "The handle or DID of the repo." + "description": "The handle or DID of the repo (aka, current account)." }, "collection": { "type": "string", @@ -23,17 +23,17 @@ }, "rkey": { "type": "string", - "description": "The key of the record.", + "description": "The Record Key.", "maxLength": 15 }, "validate": { "type": "boolean", "default": true, - "description": "Flag for validating the record." + "description": "Can be set to 'false' to skip Lexicon schema validation of record data." }, "record": { "type": "unknown", - "description": "The record to create." + "description": "The record itself. Must contain a $type field." }, "swapCommit": { "type": "string", @@ -54,7 +54,12 @@ } } }, - "errors": [{ "name": "InvalidSwap" }] + "errors": [ + { + "name": "InvalidSwap", + "description": "Indicates that 'swapCommit' didn't match current repo commit." + } + ] } } } diff --git a/lexicons/com/atproto/repo/deleteRecord.json b/lexicons/com/atproto/repo/deleteRecord.json index d8d7955b6a9..65b9f8f9536 100644 --- a/lexicons/com/atproto/repo/deleteRecord.json +++ b/lexicons/com/atproto/repo/deleteRecord.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Delete a record, or ensure it doesn't exist.", + "description": "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.", "input": { "encoding": "application/json", "schema": { @@ -14,7 +14,7 @@ "repo": { "type": "string", "format": "at-identifier", - "description": "The handle or DID of the repo." + "description": "The handle or DID of the repo (aka, current account)." }, "collection": { "type": "string", @@ -23,7 +23,7 @@ }, "rkey": { "type": "string", - "description": "The key of the record." + "description": "The Record Key." }, "swapRecord": { "type": "string", diff --git a/lexicons/com/atproto/repo/describeRepo.json b/lexicons/com/atproto/repo/describeRepo.json index b7f283bff70..b1ce2b6cf9e 100644 --- a/lexicons/com/atproto/repo/describeRepo.json +++ b/lexicons/com/atproto/repo/describeRepo.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get information about the repo, including the list of collections.", + "description": "Get information about an account and repository, including the list of collections. Does not require auth.", "parameters": { "type": "params", "required": ["repo"], @@ -30,12 +30,19 @@ "properties": { "handle": { "type": "string", "format": "handle" }, "did": { "type": "string", "format": "did" }, - "didDoc": { "type": "unknown" }, + "didDoc": { + "type": "unknown", + "description": "The complete DID document for this account." + }, "collections": { "type": "array", + "description": "List of all the collections (NSIDs) for which this repo contains at least one record.", "items": { "type": "string", "format": "nsid" } }, - "handleIsCorrect": { "type": "boolean" } + "handleIsCorrect": { + "type": "boolean", + "description": "Indicates if handle is currently valid (resolves bi-directionally)" + } } } } diff --git a/lexicons/com/atproto/repo/getRecord.json b/lexicons/com/atproto/repo/getRecord.json index ec4d17e4260..5d8bb173470 100644 --- a/lexicons/com/atproto/repo/getRecord.json +++ b/lexicons/com/atproto/repo/getRecord.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a record.", + "description": "Get a single record from a repository. Does not require auth.", "parameters": { "type": "params", "required": ["repo", "collection", "rkey"], @@ -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 Record Key." }, "cid": { "type": "string", "format": "cid", diff --git a/lexicons/com/atproto/repo/importRepo.json b/lexicons/com/atproto/repo/importRepo.json new file mode 100644 index 00000000000..fc850b1a2b6 --- /dev/null +++ b/lexicons/com/atproto/repo/importRepo.json @@ -0,0 +1,13 @@ +{ + "lexicon": 1, + "id": "com.atproto.repo.importRepo", + "defs": { + "main": { + "type": "procedure", + "description": "Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.", + "input": { + "encoding": "application/vnd.ipld.car" + } + } + } +} diff --git a/lexicons/com/atproto/repo/listMissingBlobs.json b/lexicons/com/atproto/repo/listMissingBlobs.json new file mode 100644 index 00000000000..c39913d566c --- /dev/null +++ b/lexicons/com/atproto/repo/listMissingBlobs.json @@ -0,0 +1,44 @@ +{ + "lexicon": 1, + "id": "com.atproto.repo.listMissingBlobs", + "defs": { + "main": { + "type": "query", + "description": "Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 500 + }, + "cursor": { "type": "string" } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["blobs"], + "properties": { + "cursor": { "type": "string" }, + "blobs": { + "type": "array", + "items": { "type": "ref", "ref": "#recordBlob" } + } + } + } + } + }, + "recordBlob": { + "type": "object", + "required": ["cid", "recordUri"], + "properties": { + "cid": { "type": "string", "format": "cid" }, + "recordUri": { "type": "string", "format": "at-uri" } + } + } + } +} diff --git a/lexicons/com/atproto/repo/listRecords.json b/lexicons/com/atproto/repo/listRecords.json index ac04e3e8782..bc91c952bb1 100644 --- a/lexicons/com/atproto/repo/listRecords.json +++ b/lexicons/com/atproto/repo/listRecords.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "List a range of records in a collection.", + "description": "List a range of records in a repository, matching a specific collection. Does not require auth.", "parameters": { "type": "params", "required": ["repo", "collection"], diff --git a/lexicons/com/atproto/repo/putRecord.json b/lexicons/com/atproto/repo/putRecord.json index ae39bd95ead..51f11c0f13f 100644 --- a/lexicons/com/atproto/repo/putRecord.json +++ b/lexicons/com/atproto/repo/putRecord.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Write a record, creating or updating it as needed.", + "description": "Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.", "input": { "encoding": "application/json", "schema": { @@ -15,7 +15,7 @@ "repo": { "type": "string", "format": "at-identifier", - "description": "The handle or DID of the repo." + "description": "The handle or DID of the repo (aka, current account)." }, "collection": { "type": "string", @@ -24,13 +24,13 @@ }, "rkey": { "type": "string", - "description": "The key of the record.", + "description": "The Record Key.", "maxLength": 15 }, "validate": { "type": "boolean", "default": true, - "description": "Flag for validating the record." + "description": "Can be set to 'false' to skip Lexicon schema validation of record data." }, "record": { "type": "unknown", @@ -39,7 +39,7 @@ "swapRecord": { "type": "string", "format": "cid", - "description": "Compare and swap with the previous record by CID." + "description": "Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation" }, "swapCommit": { "type": "string", diff --git a/lexicons/com/atproto/repo/uploadBlob.json b/lexicons/com/atproto/repo/uploadBlob.json index 63d1671bd3e..547a995a051 100644 --- a/lexicons/com/atproto/repo/uploadBlob.json +++ b/lexicons/com/atproto/repo/uploadBlob.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Upload a new blob to be added to repo in a later request.", + "description": "Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.", "input": { "encoding": "*/*" }, diff --git a/lexicons/com/atproto/server/activateAccount.json b/lexicons/com/atproto/server/activateAccount.json new file mode 100644 index 00000000000..e06935fcd07 --- /dev/null +++ b/lexicons/com/atproto/server/activateAccount.json @@ -0,0 +1,10 @@ +{ + "lexicon": 1, + "id": "com.atproto.server.activateAccount", + "defs": { + "main": { + "type": "procedure", + "description": "Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup." + } + } +} diff --git a/lexicons/com/atproto/server/checkAccountStatus.json b/lexicons/com/atproto/server/checkAccountStatus.json new file mode 100644 index 00000000000..d34596e60e1 --- /dev/null +++ b/lexicons/com/atproto/server/checkAccountStatus.json @@ -0,0 +1,38 @@ +{ + "lexicon": 1, + "id": "com.atproto.server.checkAccountStatus", + "defs": { + "main": { + "type": "query", + "description": "Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself.", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "activated", + "validDid", + "repoCommit", + "repoRev", + "repoBlocks", + "indexedRecords", + "privateStateValues", + "expectedBlobs", + "importedBlobs" + ], + "properties": { + "activated": { "type": "boolean" }, + "validDid": { "type": "boolean" }, + "repoCommit": { "type": "string", "format": "cid" }, + "repoRev": { "type": "string" }, + "repoBlocks": { "type": "integer" }, + "indexedRecords": { "type": "integer" }, + "privateStateValues": { "type": "integer" }, + "expectedBlobs": { "type": "integer" }, + "importedBlobs": { "type": "integer" } + } + } + } + } + } +} diff --git a/lexicons/com/atproto/server/createAccount.json b/lexicons/com/atproto/server/createAccount.json index d1456e095ae..b32bbe1569d 100644 --- a/lexicons/com/atproto/server/createAccount.json +++ b/lexicons/com/atproto/server/createAccount.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Create an account.", + "description": "Create an account. Implemented by PDS.", "input": { "encoding": "application/json", "schema": { @@ -12,14 +12,31 @@ "required": ["handle"], "properties": { "email": { "type": "string" }, - "handle": { "type": "string", "format": "handle" }, - "did": { "type": "string", "format": "did" }, + "handle": { + "type": "string", + "format": "handle", + "description": "Requested handle for the account." + }, + "did": { + "type": "string", + "format": "did", + "description": "Pre-existing atproto DID, being imported to a new account." + }, "inviteCode": { "type": "string" }, "verificationCode": { "type": "string" }, "verificationPhone": { "type": "string" }, - "password": { "type": "string" }, - "recoveryKey": { "type": "string" }, - "plcOp": { "type": "unknown" } + "password": { + "type": "string", + "description": "Initial account password. May need to meet instance-specific password strength requirements." + }, + "recoveryKey": { + "type": "string", + "description": "DID PLC rotation key (aka, recovery key) to be included in PLC creation operation." + }, + "plcOp": { + "type": "unknown", + "description": "A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented." + } } } }, @@ -27,13 +44,21 @@ "encoding": "application/json", "schema": { "type": "object", + "description": "Account login session returned on successful account creation.", "required": ["accessJwt", "refreshJwt", "handle", "did"], "properties": { "accessJwt": { "type": "string" }, "refreshJwt": { "type": "string" }, "handle": { "type": "string", "format": "handle" }, - "did": { "type": "string", "format": "did" }, - "didDoc": { "type": "unknown" } + "did": { + "type": "string", + "format": "did", + "description": "The DID of the new account." + }, + "didDoc": { + "type": "unknown", + "description": "Complete DID document." + } } } }, diff --git a/lexicons/com/atproto/server/createAppPassword.json b/lexicons/com/atproto/server/createAppPassword.json index f12e8e2557e..0a60e4e30b0 100644 --- a/lexicons/com/atproto/server/createAppPassword.json +++ b/lexicons/com/atproto/server/createAppPassword.json @@ -11,7 +11,10 @@ "type": "object", "required": ["name"], "properties": { - "name": { "type": "string" } + "name": { + "type": "string", + "description": "A short name for the App Password, to help distinguish them." + } } } }, diff --git a/lexicons/com/atproto/server/deactivateAccount.json b/lexicons/com/atproto/server/deactivateAccount.json new file mode 100644 index 00000000000..698fccf202e --- /dev/null +++ b/lexicons/com/atproto/server/deactivateAccount.json @@ -0,0 +1,23 @@ +{ + "lexicon": 1, + "id": "com.atproto.server.deactivateAccount", + "defs": { + "main": { + "type": "procedure", + "description": "Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "deleteAfter": { + "type": "string", + "format": "datetime", + "description": "A recommendation to server as to how long they should hold onto the deactivated account before deleting." + } + } + } + } + } + } +} diff --git a/lexicons/com/atproto/server/deleteAccount.json b/lexicons/com/atproto/server/deleteAccount.json index 3747189dca3..cf4babfe7da 100644 --- a/lexicons/com/atproto/server/deleteAccount.json +++ b/lexicons/com/atproto/server/deleteAccount.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Delete an actor's account with a token and password.", + "description": "Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.", "input": { "encoding": "application/json", "schema": { diff --git a/lexicons/com/atproto/server/deleteSession.json b/lexicons/com/atproto/server/deleteSession.json index e05d019024a..807a89dc9bd 100644 --- a/lexicons/com/atproto/server/deleteSession.json +++ b/lexicons/com/atproto/server/deleteSession.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Delete the current session." + "description": "Delete the current session. Requires auth." } } } diff --git a/lexicons/com/atproto/server/describeServer.json b/lexicons/com/atproto/server/describeServer.json index 3c60a58ecaf..d7bd4ab76f2 100644 --- a/lexicons/com/atproto/server/describeServer.json +++ b/lexicons/com/atproto/server/describeServer.json @@ -4,20 +4,35 @@ "defs": { "main": { "type": "query", - "description": "Get a document describing the service's accounts configuration.", + "description": "Describes the server's account creation requirements and capabilities. Implemented by PDS.", "output": { "encoding": "application/json", "schema": { "type": "object", - "required": ["availableUserDomains"], + "required": ["did", "availableUserDomains"], "properties": { - "inviteCodeRequired": { "type": "boolean" }, - "phoneVerificationRequired": { "type": "boolean" }, + "inviteCodeRequired": { + "type": "boolean", + "description": "If true, an invite code must be supplied to create an account on this instance." + }, + "phoneVerificationRequired": { + "type": "boolean", + "description": "If true, a phone verification token must be supplied to create an account on this instance." + }, "availableUserDomains": { "type": "array", + "description": "List of domain suffixes that can be used in account handles.", "items": { "type": "string" } }, - "links": { "type": "ref", "ref": "#links" } + "links": { + "type": "ref", + "description": "URLs of service policy documents.", + "ref": "#links" + }, + "did": { + "type": "string", + "format": "did" + } } } } diff --git a/lexicons/com/atproto/server/getAccountInviteCodes.json b/lexicons/com/atproto/server/getAccountInviteCodes.json index ac23b11f23f..72f0822703d 100644 --- a/lexicons/com/atproto/server/getAccountInviteCodes.json +++ b/lexicons/com/atproto/server/getAccountInviteCodes.json @@ -4,12 +4,16 @@ "defs": { "main": { "type": "query", - "description": "Get all invite codes for a given account.", + "description": "Get all invite codes for the current account. Requires auth.", "parameters": { "type": "params", "properties": { "includeUsed": { "type": "boolean", "default": true }, - "createAvailable": { "type": "boolean", "default": true } + "createAvailable": { + "type": "boolean", + "default": true, + "description": "Controls whether any new 'earned' but not 'created' invites should be created." + } } }, "output": { diff --git a/lexicons/com/atproto/server/getServiceAuth.json b/lexicons/com/atproto/server/getServiceAuth.json new file mode 100644 index 00000000000..95984c186ad --- /dev/null +++ b/lexicons/com/atproto/server/getServiceAuth.json @@ -0,0 +1,33 @@ +{ + "lexicon": 1, + "id": "com.atproto.server.getServiceAuth", + "defs": { + "main": { + "type": "query", + "description": "Get a signed token on behalf of the requesting DID for the requested service.", + "parameters": { + "type": "params", + "required": ["aud"], + "properties": { + "aud": { + "type": "string", + "format": "did", + "description": "The DID of the service that the token will be used to authenticate with" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["token"], + "properties": { + "token": { + "type": "string" + } + } + } + } + } + } +} diff --git a/lexicons/com/atproto/server/getSession.json b/lexicons/com/atproto/server/getSession.json index 5f7700882da..6b5f280e746 100644 --- a/lexicons/com/atproto/server/getSession.json +++ b/lexicons/com/atproto/server/getSession.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get information about the current session.", + "description": "Get information about the current auth session. Requires auth.", "output": { "encoding": "application/json", "schema": { diff --git a/lexicons/com/atproto/server/refreshSession.json b/lexicons/com/atproto/server/refreshSession.json index 3f4d7fdf272..0b067f86b7f 100644 --- a/lexicons/com/atproto/server/refreshSession.json +++ b/lexicons/com/atproto/server/refreshSession.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Refresh an authentication session.", + "description": "Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').", "output": { "encoding": "application/json", "schema": { diff --git a/lexicons/com/atproto/server/reserveSigningKey.json b/lexicons/com/atproto/server/reserveSigningKey.json index 3a67ad0a3c8..a33e1ede68e 100644 --- a/lexicons/com/atproto/server/reserveSigningKey.json +++ b/lexicons/com/atproto/server/reserveSigningKey.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Reserve a repo signing key for account creation.", + "description": "Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented.", "input": { "encoding": "application/json", "schema": { @@ -12,7 +12,8 @@ "properties": { "did": { "type": "string", - "description": "The did to reserve a new did:key for" + "format": "did", + "description": "The DID to reserve a key for." } } } @@ -25,7 +26,7 @@ "properties": { "signingKey": { "type": "string", - "description": "Public signing key in the form of a did:key." + "description": "The public key for the reserved signing key, in did:key serialization." } } } diff --git a/lexicons/com/atproto/sync/getBlob.json b/lexicons/com/atproto/sync/getBlob.json index 23e18a4f3b5..57ece7a9d8a 100644 --- a/lexicons/com/atproto/sync/getBlob.json +++ b/lexicons/com/atproto/sync/getBlob.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a blob associated with a given repo.", + "description": "Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.", "parameters": { "type": "params", "required": ["did", "cid"], @@ -12,7 +12,7 @@ "did": { "type": "string", "format": "did", - "description": "The DID of the repo." + "description": "The DID of the account." }, "cid": { "type": "string", diff --git a/lexicons/com/atproto/sync/getBlocks.json b/lexicons/com/atproto/sync/getBlocks.json index cf776a0c88f..29dd4971904 100644 --- a/lexicons/com/atproto/sync/getBlocks.json +++ b/lexicons/com/atproto/sync/getBlocks.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get blocks from a given repo.", + "description": "Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.", "parameters": { "type": "params", "required": ["did", "cids"], diff --git a/lexicons/com/atproto/sync/getLatestCommit.json b/lexicons/com/atproto/sync/getLatestCommit.json index d8754f09062..ac7faf57570 100644 --- a/lexicons/com/atproto/sync/getLatestCommit.json +++ b/lexicons/com/atproto/sync/getLatestCommit.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get the current commit CID & revision of the repo.", + "description": "Get the current commit CID & revision of the specified repo. Does not require auth.", "parameters": { "type": "params", "required": ["did"], diff --git a/lexicons/com/atproto/sync/getRecord.json b/lexicons/com/atproto/sync/getRecord.json index cbd0ad3a5ac..718245a5195 100644 --- a/lexicons/com/atproto/sync/getRecord.json +++ b/lexicons/com/atproto/sync/getRecord.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get blocks needed for existence or non-existence of record.", + "description": "Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.", "parameters": { "type": "params", "required": ["did", "collection", "rkey"], @@ -15,7 +15,7 @@ "description": "The DID of the repo." }, "collection": { "type": "string", "format": "nsid" }, - "rkey": { "type": "string" }, + "rkey": { "type": "string", "description": "Record Key" }, "commit": { "type": "string", "format": "cid", diff --git a/lexicons/com/atproto/sync/getRepo.json b/lexicons/com/atproto/sync/getRepo.json index fb68ab670ee..7fa710abfb5 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 DID's repo, optionally catching up from a specific revision.", + "description": "Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.", "parameters": { "type": "params", "required": ["did"], @@ -16,7 +16,7 @@ }, "since": { "type": "string", - "description": "The revision of the repo to catch up from." + "description": "The revision ('rev') of the repo to create a diff from." } } }, diff --git a/lexicons/com/atproto/sync/listBlobs.json b/lexicons/com/atproto/sync/listBlobs.json index 46815eeb49a..b4c954d999a 100644 --- a/lexicons/com/atproto/sync/listBlobs.json +++ b/lexicons/com/atproto/sync/listBlobs.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "List blob CIDs since some revision.", + "description": "List blob CIDso for an account, since some repo revision. Does not require auth; implemented by PDS.", "parameters": { "type": "params", "required": ["did"], diff --git a/lexicons/com/atproto/sync/listRepos.json b/lexicons/com/atproto/sync/listRepos.json index 440e8693d5e..07ae35e2c5e 100644 --- a/lexicons/com/atproto/sync/listRepos.json +++ b/lexicons/com/atproto/sync/listRepos.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "List DIDs and root CIDs of hosted repos.", + "description": "Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.", "parameters": { "type": "params", "properties": { @@ -37,7 +37,11 @@ "required": ["did", "head", "rev"], "properties": { "did": { "type": "string", "format": "did" }, - "head": { "type": "string", "format": "cid" }, + "head": { + "type": "string", + "format": "cid", + "description": "Current repo commit CID" + }, "rev": { "type": "string" } } } diff --git a/lexicons/com/atproto/sync/notifyOfUpdate.json b/lexicons/com/atproto/sync/notifyOfUpdate.json index 48cb4b24678..034a9655a08 100644 --- a/lexicons/com/atproto/sync/notifyOfUpdate.json +++ b/lexicons/com/atproto/sync/notifyOfUpdate.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Notify a crawling service of a recent update; often when a long break between updates causes the connection with the crawling service to break.", + "description": "Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay.", "input": { "encoding": "application/json", "schema": { @@ -13,7 +13,7 @@ "properties": { "hostname": { "type": "string", - "description": "Hostname of the service that is notifying of update." + "description": "Hostname of the current service (usually a PDS) that is notifying of update." } } } diff --git a/lexicons/com/atproto/sync/requestCrawl.json b/lexicons/com/atproto/sync/requestCrawl.json index a3520a33180..8e075a376fc 100644 --- a/lexicons/com/atproto/sync/requestCrawl.json +++ b/lexicons/com/atproto/sync/requestCrawl.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "procedure", - "description": "Request a service to persistently crawl hosted repos.", + "description": "Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.", "input": { "encoding": "application/json", "schema": { @@ -13,7 +13,7 @@ "properties": { "hostname": { "type": "string", - "description": "Hostname of the service that is requesting to be crawled." + "description": "Hostname of the current service (eg, PDS) that is requesting to be crawled." } } } diff --git a/lexicons/com/atproto/sync/subscribeRepos.json b/lexicons/com/atproto/sync/subscribeRepos.json index 9a5c0f6153c..31d68b91c07 100644 --- a/lexicons/com/atproto/sync/subscribeRepos.json +++ b/lexicons/com/atproto/sync/subscribeRepos.json @@ -4,26 +4,40 @@ "defs": { "main": { "type": "subscription", - "description": "Subscribe to repo updates.", + "description": "Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.", "parameters": { "type": "params", "properties": { "cursor": { "type": "integer", - "description": "The last known event to backfill from." + "description": "The last known event seq number to backfill from." } } }, "message": { "schema": { "type": "union", - "refs": ["#commit", "#handle", "#migrate", "#tombstone", "#info"] + "refs": [ + "#commit", + "#identity", + "#handle", + "#migrate", + "#tombstone", + "#info" + ] } }, - "errors": [{ "name": "FutureCursor" }, { "name": "ConsumerTooSlow" }] + "errors": [ + { "name": "FutureCursor" }, + { + "name": "ConsumerTooSlow", + "description": "If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection." + } + ] }, "commit": { "type": "object", + "description": "Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.", "required": [ "seq", "rebase", @@ -39,39 +53,77 @@ ], "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", + "description": "The stream sequence number of this message." + }, + "rebase": { "type": "boolean", "description": "DEPRECATED -- unused" }, + "tooBig": { + "type": "boolean", + "description": "Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data." + }, + "repo": { + "type": "string", + "format": "did", + "description": "The repo this event comes from." + }, + "commit": { + "type": "cid-link", + "description": "Repo commit object CID." + }, + "prev": { + "type": "cid-link", + "description": "DEPRECATED -- unused. WARNING -- nullable and optional; stick with optional to ensure golang interoperability." + }, "rev": { "type": "string", - "description": "The rev of the emitted commit." + "description": "The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event." }, "since": { "type": "string", - "description": "The rev of the last emitted commit from this repo." + "description": "The rev of the last emitted commit from this repo (if any)." }, "blocks": { "type": "bytes", - "description": "CAR file containing relevant blocks.", + "description": "CAR file containing relevant blocks, as a diff since the previous repo state.", "maxLength": 1000000 }, "ops": { "type": "array", - "items": { "type": "ref", "ref": "#repoOp" }, + "items": { + "type": "ref", + "ref": "#repoOp", + "description": "List of repo mutation operations in this commit (eg, records created, updated, or deleted)." + }, "maxLength": 200 }, "blobs": { "type": "array", - "items": { "type": "cid-link" } + "items": { + "type": "cid-link", + "description": "List of new blobs (by CID) referenced by records in this commit." + } }, + "time": { + "type": "string", + "format": "datetime", + "description": "Timestamp of when this message was originally broadcast." + } + } + }, + "identity": { + "type": "object", + "description": "Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.", + "required": ["seq", "did", "time"], + "properties": { + "seq": { "type": "integer" }, + "did": { "type": "string", "format": "did" }, "time": { "type": "string", "format": "datetime" } } }, "handle": { "type": "object", + "description": "Represents an update of the account's handle, or transition to/from invalid state. NOTE: Will be deprecated in favor of #identity.", "required": ["seq", "did", "handle", "time"], "properties": { "seq": { "type": "integer" }, @@ -82,6 +134,7 @@ }, "migrate": { "type": "object", + "description": "Represents an account moving from one PDS instance to another. NOTE: not implemented; account migration uses #identity instead", "required": ["seq", "did", "migrateTo", "time"], "nullable": ["migrateTo"], "properties": { @@ -93,6 +146,7 @@ }, "tombstone": { "type": "object", + "description": "Indicates that an account has been deleted. NOTE: may be deprecated in favor of #identity or a future #account event", "required": ["seq", "did", "time"], "properties": { "seq": { "type": "integer" }, @@ -115,7 +169,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.", + "description": "A repo operation, ie a mutation of a single record.", "required": ["action", "path", "cid"], "nullable": ["cid"], "properties": { @@ -124,7 +178,10 @@ "knownValues": ["create", "update", "delete"] }, "path": { "type": "string" }, - "cid": { "type": "cid-link" } + "cid": { + "type": "cid-link", + "description": "For creates and updates, the new record CID. For deletions, null." + } } } } diff --git a/lexicons/com/atproto/temp/fetchLabels.json b/lexicons/com/atproto/temp/fetchLabels.json index 14e392fd5e7..57c3f732cdb 100644 --- a/lexicons/com/atproto/temp/fetchLabels.json +++ b/lexicons/com/atproto/temp/fetchLabels.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Fetch all labels from a labeler created after a certain date.", + "description": "DEPRECATED: use queryLabels or subscribeLabels instead -- Fetch all labels from a labeler created after a certain date.", "parameters": { "type": "params", "properties": { diff --git a/lexicons/com/atproto/temp/importRepo.json b/lexicons/com/atproto/temp/importRepo.json deleted file mode 100644 index f06daa09d73..00000000000 --- a/lexicons/com/atproto/temp/importRepo.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "lexicon": 1, - "id": "com.atproto.temp.importRepo", - "defs": { - "main": { - "type": "procedure", - "description": "Gets the did's repo, optionally catching up from a specific revision.", - "parameters": { - "type": "params", - "required": ["did"], - "properties": { - "did": { - "type": "string", - "format": "did", - "description": "The DID of the repo." - } - } - }, - "input": { - "encoding": "application/vnd.ipld.car" - }, - "output": { - "encoding": "text/plain" - } - } - } -} diff --git a/lexicons/com/atproto/temp/pushBlob.json b/lexicons/com/atproto/temp/pushBlob.json deleted file mode 100644 index 9babc8f8e43..00000000000 --- a/lexicons/com/atproto/temp/pushBlob.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "lexicon": 1, - "id": "com.atproto.temp.pushBlob", - "defs": { - "main": { - "type": "procedure", - "description": "Gets the did's repo, optionally catching up from a specific revision.", - "parameters": { - "type": "params", - "required": ["did"], - "properties": { - "did": { - "type": "string", - "format": "did", - "description": "The DID of the repo." - } - } - }, - "input": { - "encoding": "*/*" - } - } - } -} diff --git a/lexicons/com/atproto/temp/transferAccount.json b/lexicons/com/atproto/temp/transferAccount.json deleted file mode 100644 index 3cb2035ac0e..00000000000 --- a/lexicons/com/atproto/temp/transferAccount.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "lexicon": 1, - "id": "com.atproto.temp.transferAccount", - "defs": { - "main": { - "type": "procedure", - "description": "Transfer an account.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["handle", "did", "plcOp"], - "properties": { - "handle": { "type": "string", "format": "handle" }, - "did": { "type": "string", "format": "did" }, - "plcOp": { "type": "unknown" } - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["accessJwt", "refreshJwt", "handle", "did"], - "properties": { - "accessJwt": { "type": "string" }, - "refreshJwt": { "type": "string" }, - "handle": { "type": "string", "format": "handle" }, - "did": { "type": "string", "format": "did" } - } - } - }, - "errors": [ - { "name": "InvalidHandle" }, - { "name": "InvalidPassword" }, - { "name": "InvalidInviteCode" }, - { "name": "HandleNotAvailable" }, - { "name": "UnsupportedDomain" }, - { "name": "UnresolvableDid" }, - { "name": "IncompatibleDidDoc" } - ] - } - } -} diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 997f86931fa..58732dbd074 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,39 @@ # @atproto/api +## 0.10.1 + +### Patch Changes + +- [#2215](https://github.com/bluesky-social/atproto/pull/2215) [`514aab92d`](https://github.com/bluesky-social/atproto/commit/514aab92d26acd43859285f46318e386846522b1) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add missing `getPreferences` union return types + +## 0.10.0 + +### Minor Changes + +- [#2170](https://github.com/bluesky-social/atproto/pull/2170) [`4c511b3d9`](https://github.com/bluesky-social/atproto/commit/4c511b3d9de41ffeae3fc11db941e7df04f4468a) Thanks [@dholms](https://github.com/dholms)! - Add lexicons and methods for account migration + +### Patch Changes + +- [#2195](https://github.com/bluesky-social/atproto/pull/2195) [`b60719480`](https://github.com/bluesky-social/atproto/commit/b60719480f5f00bffd074a40e8ddc03aa93d137d) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add muted words/tags and hidden posts prefs and methods" + +## 0.9.8 + +### Patch Changes + +- [#2192](https://github.com/bluesky-social/atproto/pull/2192) [`f79cc6339`](https://github.com/bluesky-social/atproto/commit/f79cc63390ae9dbd47a4ff5d694eec25b78b788e) Thanks [@foysalit](https://github.com/foysalit)! - Tag event on moderation subjects and allow filtering events and subjects by tags + +## 0.9.7 + +### Patch Changes + +- [#2188](https://github.com/bluesky-social/atproto/pull/2188) [`8c94979f7`](https://github.com/bluesky-social/atproto/commit/8c94979f73fc5057449e24e66ef2e09b0e17e55b) Thanks [@dholms](https://github.com/dholms)! - Added timelineIndex to savedFeedsPref + +## 0.9.6 + +### Patch Changes + +- [#2124](https://github.com/bluesky-social/atproto/pull/2124) [`e4ec7af03`](https://github.com/bluesky-social/atproto/commit/e4ec7af03608949fc3b00a845f547a77599b5ad0) Thanks [@foysalit](https://github.com/foysalit)! - Allow filtering for comment, label, report type and date range on queryModerationEvents endpoint. + ## 0.9.5 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index e00c9438d2f..ab76c7b4249 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.9.5", + "version": "0.10.1", "license": "MIT", "description": "Client library for atproto and Bluesky", "keywords": [ diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 606e06dcda8..305348bea0b 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -327,6 +327,8 @@ export class BskyAgent extends AtpAgent { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], } const res = await this.app.bsky.actor.getPreferences({}) for (const pref of res.data.preferences) { @@ -380,6 +382,20 @@ export class BskyAgent extends AtpAgent { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { $type, ...v } = pref prefs.interests = { ...prefs.interests, ...v } + } else if ( + AppBskyActorDefs.isMutedWordsPref(pref) && + AppBskyActorDefs.validateMutedWordsPref(pref).success + ) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { $type, ...v } = pref + prefs.mutedWords = v.items + } else if ( + AppBskyActorDefs.isHiddenPostsPref(pref) && + AppBskyActorDefs.validateHiddenPostsPref(pref).success + ) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { $type, ...v } = pref + prefs.hiddenPosts = v.items } } return prefs @@ -548,6 +564,26 @@ export class BskyAgent extends AtpAgent { .concat([{ ...pref, $type: 'app.bsky.actor.defs#interestsPref' }]) }) } + + async upsertMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) { + await updateMutedWords(this, mutedWords, 'upsert') + } + + async updateMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { + await updateMutedWords(this, [mutedWord], 'update') + } + + async removeMutedWord(mutedWord: AppBskyActorDefs.MutedWord) { + await updateMutedWords(this, [mutedWord], 'remove') + } + + async hidePost(postUri: string) { + await updateHiddenPost(this, postUri, 'hide') + } + + async unhidePost(postUri: string) { + await updateHiddenPost(this, postUri, 'unhide') + } } /** @@ -609,3 +645,103 @@ async function updateFeedPreferences( }) return res } + +/** + * A helper specifically for updating muted words preferences + */ +async function updateMutedWords( + agent: BskyAgent, + mutedWords: AppBskyActorDefs.MutedWord[], + action: 'upsert' | 'update' | 'remove', +) { + const sanitizeMutedWord = (word: AppBskyActorDefs.MutedWord) => ({ + value: word.value.replace(/^#/, ''), + targets: word.targets, + }) + + await updatePreferences(agent, (prefs: AppBskyActorDefs.Preferences) => { + let mutedWordsPref = prefs.findLast( + (pref) => + AppBskyActorDefs.isMutedWordsPref(pref) && + AppBskyActorDefs.validateMutedWordsPref(pref).success, + ) + + if (mutedWordsPref && AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)) { + if (action === 'upsert' || action === 'update') { + for (const word of mutedWords) { + let foundMatch = false + + for (const existingItem of mutedWordsPref.items) { + if (existingItem.value === sanitizeMutedWord(word).value) { + existingItem.targets = + action === 'upsert' + ? Array.from( + new Set([...existingItem.targets, ...word.targets]), + ) + : word.targets + foundMatch = true + break + } + } + + if (action === 'upsert' && !foundMatch) { + mutedWordsPref.items.push(sanitizeMutedWord(word)) + } + } + } else if (action === 'remove') { + for (const word of mutedWords) { + for (let i = 0; i < mutedWordsPref.items.length; i++) { + const existing = mutedWordsPref.items[i] + if (existing.value === sanitizeMutedWord(word).value) { + mutedWordsPref.items.splice(i, 1) + break + } + } + } + } + } else { + // if the pref doesn't exist, create it + if (action === 'upsert') { + mutedWordsPref = { + items: mutedWords.map(sanitizeMutedWord), + } + } + } + + return prefs + .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p)) + .concat([ + { ...mutedWordsPref, $type: 'app.bsky.actor.defs#mutedWordsPref' }, + ]) + }) +} + +async function updateHiddenPost( + agent: BskyAgent, + postUri: string, + action: 'hide' | 'unhide', +) { + await updatePreferences(agent, (prefs: AppBskyActorDefs.Preferences) => { + let pref = prefs.findLast( + (pref) => + AppBskyActorDefs.isHiddenPostsPref(pref) && + AppBskyActorDefs.validateHiddenPostsPref(pref).success, + ) + if (pref && AppBskyActorDefs.isHiddenPostsPref(pref)) { + pref.items = + action === 'hide' + ? Array.from(new Set([...pref.items, postUri])) + : pref.items.filter((uri) => uri !== postUri) + } else { + if (action === 'hide') { + pref = { + $type: 'app.bsky.actor.defs#hiddenPostsPref', + items: [postUri], + } + } + } + return prefs + .filter((p) => !AppBskyActorDefs.isInterestsPref(p)) + .concat([{ ...pref, $type: 'app.bsky.actor.defs#hiddenPostsPref' }]) + }) +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 35c784cbea3..846c379b7a8 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -29,9 +29,14 @@ import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRep import * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' +import * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword' import * as ComAtprotoAdminUpdateCommunicationTemplate from './types/com/atproto/admin/updateCommunicationTemplate' import * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus' +import * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials' +import * as ComAtprotoIdentityRequestPlcOperationSignature from './types/com/atproto/identity/requestPlcOperationSignature' import * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle' +import * as ComAtprotoIdentitySignPlcOperation from './types/com/atproto/identity/signPlcOperation' +import * as ComAtprotoIdentitySubmitPlcOperation from './types/com/atproto/identity/submitPlcOperation' import * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle' import * as ComAtprotoLabelDefs from './types/com/atproto/label/defs' import * as ComAtprotoLabelQueryLabels from './types/com/atproto/label/queryLabels' @@ -43,21 +48,27 @@ import * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createReco import * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord' import * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRepo' import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord' +import * as ComAtprotoRepoImportRepo from './types/com/atproto/repo/importRepo' +import * as ComAtprotoRepoListMissingBlobs from './types/com/atproto/repo/listMissingBlobs' import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords' import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' +import * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' +import * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword' import * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode' import * as ComAtprotoServerCreateInviteCodes from './types/com/atproto/server/createInviteCodes' import * as ComAtprotoServerCreateSession from './types/com/atproto/server/createSession' +import * as ComAtprotoServerDeactivateAccount from './types/com/atproto/server/deactivateAccount' import * as ComAtprotoServerDefs from './types/com/atproto/server/defs' import * as ComAtprotoServerDeleteAccount from './types/com/atproto/server/deleteAccount' import * as ComAtprotoServerDeleteSession from './types/com/atproto/server/deleteSession' import * as ComAtprotoServerDescribeServer from './types/com/atproto/server/describeServer' import * as ComAtprotoServerGetAccountInviteCodes from './types/com/atproto/server/getAccountInviteCodes' +import * as ComAtprotoServerGetServiceAuth from './types/com/atproto/server/getServiceAuth' import * as ComAtprotoServerGetSession from './types/com/atproto/server/getSession' import * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords' import * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession' @@ -83,10 +94,7 @@ import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCra import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' import * as ComAtprotoTempCheckSignupQueue from './types/com/atproto/temp/checkSignupQueue' import * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels' -import * as ComAtprotoTempImportRepo from './types/com/atproto/temp/importRepo' -import * as ComAtprotoTempPushBlob from './types/com/atproto/temp/pushBlob' import * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification' -import * as ComAtprotoTempTransferAccount from './types/com/atproto/temp/transferAccount' 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' @@ -175,9 +183,14 @@ export * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRep export * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' export * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' export * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' +export * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword' export * as ComAtprotoAdminUpdateCommunicationTemplate from './types/com/atproto/admin/updateCommunicationTemplate' export * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus' +export * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials' +export * as ComAtprotoIdentityRequestPlcOperationSignature from './types/com/atproto/identity/requestPlcOperationSignature' export * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle' +export * as ComAtprotoIdentitySignPlcOperation from './types/com/atproto/identity/signPlcOperation' +export * as ComAtprotoIdentitySubmitPlcOperation from './types/com/atproto/identity/submitPlcOperation' export * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle' export * as ComAtprotoLabelDefs from './types/com/atproto/label/defs' export * as ComAtprotoLabelQueryLabels from './types/com/atproto/label/queryLabels' @@ -189,21 +202,27 @@ export * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createReco export * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord' export * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRepo' export * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord' +export * as ComAtprotoRepoImportRepo from './types/com/atproto/repo/importRepo' +export * as ComAtprotoRepoListMissingBlobs from './types/com/atproto/repo/listMissingBlobs' export * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords' export * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' export * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' export * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' +export * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' +export * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' export * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' export * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' export * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword' export * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode' export * as ComAtprotoServerCreateInviteCodes from './types/com/atproto/server/createInviteCodes' export * as ComAtprotoServerCreateSession from './types/com/atproto/server/createSession' +export * as ComAtprotoServerDeactivateAccount from './types/com/atproto/server/deactivateAccount' export * as ComAtprotoServerDefs from './types/com/atproto/server/defs' export * as ComAtprotoServerDeleteAccount from './types/com/atproto/server/deleteAccount' export * as ComAtprotoServerDeleteSession from './types/com/atproto/server/deleteSession' export * as ComAtprotoServerDescribeServer from './types/com/atproto/server/describeServer' export * as ComAtprotoServerGetAccountInviteCodes from './types/com/atproto/server/getAccountInviteCodes' +export * as ComAtprotoServerGetServiceAuth from './types/com/atproto/server/getServiceAuth' export * as ComAtprotoServerGetSession from './types/com/atproto/server/getSession' export * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords' export * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession' @@ -229,10 +248,7 @@ export * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCra export * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' export * as ComAtprotoTempCheckSignupQueue from './types/com/atproto/temp/checkSignupQueue' export * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels' -export * as ComAtprotoTempImportRepo from './types/com/atproto/temp/importRepo' -export * as ComAtprotoTempPushBlob from './types/com/atproto/temp/pushBlob' export * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification' -export * as ComAtprotoTempTransferAccount from './types/com/atproto/temp/transferAccount' 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' @@ -640,6 +656,17 @@ export class ComAtprotoAdminNS { }) } + updateAccountPassword( + data?: ComAtprotoAdminUpdateAccountPassword.InputSchema, + opts?: ComAtprotoAdminUpdateAccountPassword.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.admin.updateAccountPassword', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoAdminUpdateAccountPassword.toKnownErr(e) + }) + } + updateCommunicationTemplate( data?: ComAtprotoAdminUpdateCommunicationTemplate.InputSchema, opts?: ComAtprotoAdminUpdateCommunicationTemplate.CallOptions, @@ -675,6 +702,38 @@ export class ComAtprotoIdentityNS { this._service = service } + getRecommendedDidCredentials( + params?: ComAtprotoIdentityGetRecommendedDidCredentials.QueryParams, + opts?: ComAtprotoIdentityGetRecommendedDidCredentials.CallOptions, + ): Promise { + return this._service.xrpc + .call( + 'com.atproto.identity.getRecommendedDidCredentials', + params, + undefined, + opts, + ) + .catch((e) => { + throw ComAtprotoIdentityGetRecommendedDidCredentials.toKnownErr(e) + }) + } + + requestPlcOperationSignature( + data?: ComAtprotoIdentityRequestPlcOperationSignature.InputSchema, + opts?: ComAtprotoIdentityRequestPlcOperationSignature.CallOptions, + ): Promise { + return this._service.xrpc + .call( + 'com.atproto.identity.requestPlcOperationSignature', + opts?.qp, + data, + opts, + ) + .catch((e) => { + throw ComAtprotoIdentityRequestPlcOperationSignature.toKnownErr(e) + }) + } + resolveHandle( params?: ComAtprotoIdentityResolveHandle.QueryParams, opts?: ComAtprotoIdentityResolveHandle.CallOptions, @@ -686,6 +745,28 @@ export class ComAtprotoIdentityNS { }) } + signPlcOperation( + data?: ComAtprotoIdentitySignPlcOperation.InputSchema, + opts?: ComAtprotoIdentitySignPlcOperation.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.identity.signPlcOperation', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoIdentitySignPlcOperation.toKnownErr(e) + }) + } + + submitPlcOperation( + data?: ComAtprotoIdentitySubmitPlcOperation.InputSchema, + opts?: ComAtprotoIdentitySubmitPlcOperation.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.identity.submitPlcOperation', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoIdentitySubmitPlcOperation.toKnownErr(e) + }) + } + updateHandle( data?: ComAtprotoIdentityUpdateHandle.InputSchema, opts?: ComAtprotoIdentityUpdateHandle.CallOptions, @@ -798,6 +879,28 @@ export class ComAtprotoRepoNS { }) } + importRepo( + data?: ComAtprotoRepoImportRepo.InputSchema, + opts?: ComAtprotoRepoImportRepo.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.repo.importRepo', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoRepoImportRepo.toKnownErr(e) + }) + } + + listMissingBlobs( + params?: ComAtprotoRepoListMissingBlobs.QueryParams, + opts?: ComAtprotoRepoListMissingBlobs.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.repo.listMissingBlobs', params, undefined, opts) + .catch((e) => { + throw ComAtprotoRepoListMissingBlobs.toKnownErr(e) + }) + } + listRecords( params?: ComAtprotoRepoListRecords.QueryParams, opts?: ComAtprotoRepoListRecords.CallOptions, @@ -839,6 +942,28 @@ export class ComAtprotoServerNS { this._service = service } + activateAccount( + data?: ComAtprotoServerActivateAccount.InputSchema, + opts?: ComAtprotoServerActivateAccount.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.server.activateAccount', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoServerActivateAccount.toKnownErr(e) + }) + } + + checkAccountStatus( + params?: ComAtprotoServerCheckAccountStatus.QueryParams, + opts?: ComAtprotoServerCheckAccountStatus.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.server.checkAccountStatus', params, undefined, opts) + .catch((e) => { + throw ComAtprotoServerCheckAccountStatus.toKnownErr(e) + }) + } + confirmEmail( data?: ComAtprotoServerConfirmEmail.InputSchema, opts?: ComAtprotoServerConfirmEmail.CallOptions, @@ -905,6 +1030,17 @@ export class ComAtprotoServerNS { }) } + deactivateAccount( + data?: ComAtprotoServerDeactivateAccount.InputSchema, + opts?: ComAtprotoServerDeactivateAccount.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.server.deactivateAccount', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoServerDeactivateAccount.toKnownErr(e) + }) + } + deleteAccount( data?: ComAtprotoServerDeleteAccount.InputSchema, opts?: ComAtprotoServerDeleteAccount.CallOptions, @@ -949,6 +1085,17 @@ export class ComAtprotoServerNS { }) } + getServiceAuth( + params?: ComAtprotoServerGetServiceAuth.QueryParams, + opts?: ComAtprotoServerGetServiceAuth.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.server.getServiceAuth', params, undefined, opts) + .catch((e) => { + throw ComAtprotoServerGetServiceAuth.toKnownErr(e) + }) + } + getSession( params?: ComAtprotoServerGetSession.QueryParams, opts?: ComAtprotoServerGetSession.CallOptions, @@ -1229,28 +1376,6 @@ export class ComAtprotoTempNS { }) } - importRepo( - data?: ComAtprotoTempImportRepo.InputSchema, - opts?: ComAtprotoTempImportRepo.CallOptions, - ): Promise { - return this._service.xrpc - .call('com.atproto.temp.importRepo', opts?.qp, data, opts) - .catch((e) => { - throw ComAtprotoTempImportRepo.toKnownErr(e) - }) - } - - pushBlob( - data?: ComAtprotoTempPushBlob.InputSchema, - opts?: ComAtprotoTempPushBlob.CallOptions, - ): Promise { - return this._service.xrpc - .call('com.atproto.temp.pushBlob', opts?.qp, data, opts) - .catch((e) => { - throw ComAtprotoTempPushBlob.toKnownErr(e) - }) - } - requestPhoneVerification( data?: ComAtprotoTempRequestPhoneVerification.InputSchema, opts?: ComAtprotoTempRequestPhoneVerification.CallOptions, @@ -1261,17 +1386,6 @@ export class ComAtprotoTempNS { throw ComAtprotoTempRequestPhoneVerification.toKnownErr(e) }) } - - transferAccount( - data?: ComAtprotoTempTransferAccount.InputSchema, - opts?: ComAtprotoTempTransferAccount.CallOptions, - ): Promise { - return this._service.xrpc - .call('com.atproto.temp.transferAccount', opts?.qp, data, opts) - .catch((e) => { - throw ComAtprotoTempTransferAccount.toKnownErr(e) - }) - } } export class AppNS { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 08a70f8ca1d..14e4c1cb81e 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -91,6 +91,7 @@ export const schemaDict = { 'lex:com.atproto.admin.defs#modEventEscalate', 'lex:com.atproto.admin.defs#modEventMute', 'lex:com.atproto.admin.defs#modEventEmail', + 'lex:com.atproto.admin.defs#modEventResolveAppeal', ], }, subject: { @@ -147,6 +148,7 @@ export const schemaDict = { 'lex:com.atproto.admin.defs#modEventAcknowledge', 'lex:com.atproto.admin.defs#modEventEscalate', 'lex:com.atproto.admin.defs#modEventMute', + 'lex:com.atproto.admin.defs#modEventEmail', 'lex:com.atproto.admin.defs#modEventResolveAppeal', ], }, @@ -301,6 +303,12 @@ export const schemaDict = { type: 'string', format: 'datetime', }, + tags: { + type: 'array', + items: { + type: 'string', + }, + }, }, }, reportViewDetail: { @@ -895,6 +903,33 @@ export const schemaDict = { }, }, }, + modEventTag: { + type: 'object', + description: 'Add/Remove a tag on a subject', + required: ['add', 'remove'], + properties: { + add: { + type: 'array', + items: { + type: 'string', + }, + description: + "Tags to be added to the subject. If already exists, won't be duplicated.", + }, + remove: { + type: 'array', + items: { + type: 'string', + }, + description: + "Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated.", + }, + comment: { + type: 'string', + description: 'Additional comment about added/removed tags.', + }, + }, + }, communicationTemplateView: { type: 'object', required: [ @@ -1073,6 +1108,7 @@ export const schemaDict = { 'lex:com.atproto.admin.defs#modEventReverseTakedown', 'lex:com.atproto.admin.defs#modEventUnmute', 'lex:com.atproto.admin.defs#modEventEmail', + 'lex:com.atproto.admin.defs#modEventTag', ], }, subject: { @@ -1450,6 +1486,16 @@ export const schemaDict = { description: 'Sort direction for the events. Defaults to descending order of created at timestamp.', }, + createdAfter: { + type: 'string', + format: 'datetime', + description: 'Retrieve events created after a given timestamp', + }, + createdBefore: { + type: 'string', + format: 'datetime', + description: 'Retrieve events created before a given timestamp', + }, subject: { type: 'string', format: 'uri', @@ -1466,6 +1512,53 @@ export const schemaDict = { maximum: 100, default: 50, }, + hasComment: { + type: 'boolean', + description: 'If true, only events with comments are returned', + }, + comment: { + type: 'string', + description: + 'If specified, only events with comments containing the keyword are returned', + }, + addedLabels: { + type: 'array', + items: { + type: 'string', + }, + description: + 'If specified, only events where all of these labels were added are returned', + }, + removedLabels: { + type: 'array', + items: { + type: 'string', + }, + description: + 'If specified, only events where all of these labels were removed are returned', + }, + addedTags: { + type: 'array', + items: { + type: 'string', + }, + description: + 'If specified, only events where all of these tags were added are returned', + }, + removedTags: { + type: 'array', + items: { + type: 'string', + }, + description: + 'If specified, only events where all of these tags were removed are returned', + }, + reportTypes: { + type: 'array', + items: { + type: 'string', + }, + }, cursor: { type: 'string', }, @@ -1577,6 +1670,18 @@ export const schemaDict = { maximum: 100, default: 50, }, + tags: { + type: 'array', + items: { + type: 'string', + }, + }, + excludeTags: { + type: 'array', + items: { + type: 'string', + }, + }, cursor: { type: 'string', }, @@ -1758,6 +1863,33 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminUpdateAccountPassword: { + lexicon: 1, + id: 'com.atproto.admin.updateAccountPassword', + defs: { + main: { + type: 'procedure', + description: + 'Update the password for a user account as an administrator.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'password'], + properties: { + did: { + type: 'string', + format: 'did', + }, + password: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminUpdateCommunicationTemplate: { lexicon: 1, id: 'com.atproto.admin.updateCommunicationTemplate', @@ -1863,13 +1995,63 @@ export const schemaDict = { }, }, }, + ComAtprotoIdentityGetRecommendedDidCredentials: { + lexicon: 1, + id: 'com.atproto.identity.getRecommendedDidCredentials', + defs: { + main: { + type: 'query', + description: + 'Describe the credentials that should be included in the DID doc of an account that is migrating to this service.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + rotationKeys: { + description: + 'Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.', + type: 'array', + items: { + type: 'string', + }, + }, + alsoKnownAs: { + type: 'array', + items: { + type: 'string', + }, + }, + verificationMethods: { + type: 'unknown', + }, + services: { + type: 'unknown', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoIdentityRequestPlcOperationSignature: { + lexicon: 1, + id: 'com.atproto.identity.requestPlcOperationSignature', + defs: { + main: { + type: 'procedure', + description: + 'Request an email with a code to in order to request a signed PLC operation. Requires Auth.', + }, + }, + }, ComAtprotoIdentityResolveHandle: { lexicon: 1, id: 'com.atproto.identity.resolveHandle', defs: { main: { type: 'query', - description: 'Provides the DID of a repo.', + description: 'Resolves a handle (domain name) to a DID.', parameters: { type: 'params', required: ['handle'], @@ -1897,13 +2079,92 @@ export const schemaDict = { }, }, }, + ComAtprotoIdentitySignPlcOperation: { + lexicon: 1, + id: 'com.atproto.identity.signPlcOperation', + defs: { + main: { + type: 'procedure', + description: + "Signs a PLC operation to update some value(s) in the requesting DID's document.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + token: { + description: + 'A token received through com.atproto.identity.requestPlcOperationSignature', + type: 'string', + }, + rotationKeys: { + type: 'array', + items: { + type: 'string', + }, + }, + alsoKnownAs: { + type: 'array', + items: { + type: 'string', + }, + }, + verificationMethods: { + type: 'unknown', + }, + services: { + type: 'unknown', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['operation'], + properties: { + operation: { + type: 'unknown', + description: 'A signed DID PLC operation.', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoIdentitySubmitPlcOperation: { + lexicon: 1, + id: 'com.atproto.identity.submitPlcOperation', + defs: { + main: { + type: 'procedure', + description: + "Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['operation'], + properties: { + operation: { + type: 'unknown', + }, + }, + }, + }, + }, + }, + }, ComAtprotoIdentityUpdateHandle: { lexicon: 1, id: 'com.atproto.identity.updateHandle', defs: { main: { type: 'procedure', - description: 'Updates the handle of the account.', + description: + "Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.", input: { encoding: 'application/json', schema: { @@ -1913,6 +2174,7 @@ export const schemaDict = { handle: { type: 'string', format: 'handle', + description: 'The new handle.', }, }, }, @@ -2003,7 +2265,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Find labels relevant to the provided URI patterns.', + description: + 'Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.', parameters: { type: 'params', required: ['uriPatterns'], @@ -2064,13 +2327,14 @@ export const schemaDict = { defs: { main: { type: 'subscription', - description: 'Subscribe to label updates.', + description: + 'Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.', parameters: { type: 'params', properties: { cursor: { type: 'integer', - description: 'The last known event to backfill from.', + description: 'The last known event seq number to backfill from.', }, }, }, @@ -2126,7 +2390,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Report a repo or a record.', + description: + 'Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.', input: { encoding: 'application/json', schema: { @@ -2135,10 +2400,14 @@ export const schemaDict = { properties: { reasonType: { type: 'ref', + description: + 'Indicates the broad category of violation the report is for.', ref: 'lex:com.atproto.moderation.defs#reasonType', }, reason: { type: 'string', + description: + 'Additional context about the content and violation.', }, subject: { type: 'union', @@ -2249,7 +2518,7 @@ export const schemaDict = { main: { type: 'procedure', description: - 'Apply a batch transaction of creates, updates, and deletes.', + 'Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.', input: { encoding: 'application/json', schema: { @@ -2259,12 +2528,14 @@ export const schemaDict = { repo: { type: 'string', format: 'at-identifier', - description: 'The handle or DID of the repo.', + description: + 'The handle or DID of the repo (aka, current account).', }, validate: { type: 'boolean', default: true, - description: 'Flag for validating the records.', + description: + "Can be set to 'false' to skip Lexicon schema validation of record data, for all operations.", }, writes: { type: 'array', @@ -2280,6 +2551,8 @@ export const schemaDict = { }, swapCommit: { type: 'string', + description: + 'If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.', format: 'cid', }, }, @@ -2288,12 +2561,14 @@ export const schemaDict = { errors: [ { name: 'InvalidSwap', + description: + "Indicates that the 'swapCommit' parameter did not match current commit.", }, ], }, create: { type: 'object', - description: 'Create a new record.', + description: 'Operation which creates a new record.', required: ['collection', 'value'], properties: { collection: { @@ -2311,7 +2586,7 @@ export const schemaDict = { }, update: { type: 'object', - description: 'Update an existing record.', + description: 'Operation which updates an existing record.', required: ['collection', 'rkey', 'value'], properties: { collection: { @@ -2328,7 +2603,7 @@ export const schemaDict = { }, delete: { type: 'object', - description: 'Delete an existing record.', + description: 'Operation which deletes an existing record.', required: ['collection', 'rkey'], properties: { collection: { @@ -2348,7 +2623,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Create a new record.', + description: + 'Create a single new repository record. Requires auth, implemented by PDS.', input: { encoding: 'application/json', schema: { @@ -2358,7 +2634,8 @@ export const schemaDict = { repo: { type: 'string', format: 'at-identifier', - description: 'The handle or DID of the repo.', + description: + 'The handle or DID of the repo (aka, current account).', }, collection: { type: 'string', @@ -2367,17 +2644,18 @@ export const schemaDict = { }, rkey: { type: 'string', - description: 'The key of the record.', + description: 'The Record Key.', maxLength: 15, }, validate: { type: 'boolean', default: true, - description: 'Flag for validating the record.', + description: + "Can be set to 'false' to skip Lexicon schema validation of record data.", }, record: { type: 'unknown', - description: 'The record to create.', + description: 'The record itself. Must contain a $type field.', }, swapCommit: { type: 'string', @@ -2408,6 +2686,8 @@ export const schemaDict = { errors: [ { name: 'InvalidSwap', + description: + "Indicates that 'swapCommit' didn't match current repo commit.", }, ], }, @@ -2419,7 +2699,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: "Delete a record, or ensure it doesn't exist.", + description: + "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.", input: { encoding: 'application/json', schema: { @@ -2429,7 +2710,8 @@ export const schemaDict = { repo: { type: 'string', format: 'at-identifier', - description: 'The handle or DID of the repo.', + description: + 'The handle or DID of the repo (aka, current account).', }, collection: { type: 'string', @@ -2438,7 +2720,7 @@ export const schemaDict = { }, rkey: { type: 'string', - description: 'The key of the record.', + description: 'The Record Key.', }, swapRecord: { type: 'string', @@ -2470,7 +2752,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Get information about the repo, including the list of collections.', + 'Get information about an account and repository, including the list of collections. Does not require auth.', parameters: { type: 'params', required: ['repo'], @@ -2504,9 +2786,12 @@ export const schemaDict = { }, didDoc: { type: 'unknown', + description: 'The complete DID document for this account.', }, collections: { type: 'array', + description: + 'List of all the collections (NSIDs) for which this repo contains at least one record.', items: { type: 'string', format: 'nsid', @@ -2514,6 +2799,8 @@ export const schemaDict = { }, handleIsCorrect: { type: 'boolean', + description: + 'Indicates if handle is currently valid (resolves bi-directionally)', }, }, }, @@ -2527,7 +2814,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a record.', + description: + 'Get a single record from a repository. Does not require auth.', parameters: { type: 'params', required: ['repo', 'collection', 'rkey'], @@ -2544,7 +2832,7 @@ export const schemaDict = { }, rkey: { type: 'string', - description: 'The key of the record.', + description: 'The Record Key.', }, cid: { type: 'string', @@ -2577,13 +2865,86 @@ export const schemaDict = { }, }, }, + ComAtprotoRepoImportRepo: { + lexicon: 1, + id: 'com.atproto.repo.importRepo', + defs: { + main: { + type: 'procedure', + description: + 'Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.', + input: { + encoding: 'application/vnd.ipld.car', + }, + }, + }, + }, + ComAtprotoRepoListMissingBlobs: { + lexicon: 1, + id: 'com.atproto.repo.listMissingBlobs', + defs: { + main: { + type: 'query', + description: + 'Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 1000, + default: 500, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['blobs'], + properties: { + cursor: { + type: 'string', + }, + blobs: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.repo.listMissingBlobs#recordBlob', + }, + }, + }, + }, + }, + }, + recordBlob: { + type: 'object', + required: ['cid', 'recordUri'], + properties: { + cid: { + type: 'string', + format: 'cid', + }, + recordUri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + }, ComAtprotoRepoListRecords: { lexicon: 1, id: 'com.atproto.repo.listRecords', defs: { main: { type: 'query', - description: 'List a range of records in a collection.', + description: + 'List a range of records in a repository, matching a specific collection. Does not require auth.', parameters: { type: 'params', required: ['repo', 'collection'], @@ -2669,7 +3030,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Write a record, creating or updating it as needed.', + description: + 'Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.', input: { encoding: 'application/json', schema: { @@ -2680,7 +3042,8 @@ export const schemaDict = { repo: { type: 'string', format: 'at-identifier', - description: 'The handle or DID of the repo.', + description: + 'The handle or DID of the repo (aka, current account).', }, collection: { type: 'string', @@ -2689,13 +3052,14 @@ export const schemaDict = { }, rkey: { type: 'string', - description: 'The key of the record.', + description: 'The Record Key.', maxLength: 15, }, validate: { type: 'boolean', default: true, - description: 'Flag for validating the record.', + description: + "Can be set to 'false' to skip Lexicon schema validation of record data.", }, record: { type: 'unknown', @@ -2705,7 +3069,7 @@ export const schemaDict = { type: 'string', format: 'cid', description: - 'Compare and swap with the previous record by CID.', + 'Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation', }, swapCommit: { type: 'string', @@ -2769,7 +3133,7 @@ export const schemaDict = { main: { type: 'procedure', description: - 'Upload a new blob to be added to repo in a later request.', + 'Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.', input: { encoding: '*/*', }, @@ -2788,6 +3152,75 @@ export const schemaDict = { }, }, }, + ComAtprotoServerActivateAccount: { + lexicon: 1, + id: 'com.atproto.server.activateAccount', + defs: { + main: { + type: 'procedure', + description: + "Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup.", + }, + }, + }, + ComAtprotoServerCheckAccountStatus: { + lexicon: 1, + id: 'com.atproto.server.checkAccountStatus', + defs: { + main: { + type: 'query', + description: + 'Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: [ + 'activated', + 'validDid', + 'repoCommit', + 'repoRev', + 'repoBlocks', + 'indexedRecords', + 'privateStateValues', + 'expectedBlobs', + 'importedBlobs', + ], + properties: { + activated: { + type: 'boolean', + }, + validDid: { + type: 'boolean', + }, + repoCommit: { + type: 'string', + format: 'cid', + }, + repoRev: { + type: 'string', + }, + repoBlocks: { + type: 'integer', + }, + indexedRecords: { + type: 'integer', + }, + privateStateValues: { + type: 'integer', + }, + expectedBlobs: { + type: 'integer', + }, + importedBlobs: { + type: 'integer', + }, + }, + }, + }, + }, + }, + }, ComAtprotoServerConfirmEmail: { lexicon: 1, id: 'com.atproto.server.confirmEmail', @@ -2834,7 +3267,7 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Create an account.', + description: 'Create an account. Implemented by PDS.', input: { encoding: 'application/json', schema: { @@ -2847,10 +3280,13 @@ export const schemaDict = { handle: { type: 'string', format: 'handle', + description: 'Requested handle for the account.', }, did: { type: 'string', format: 'did', + description: + 'Pre-existing atproto DID, being imported to a new account.', }, inviteCode: { type: 'string', @@ -2863,12 +3299,18 @@ export const schemaDict = { }, password: { type: 'string', + description: + 'Initial account password. May need to meet instance-specific password strength requirements.', }, recoveryKey: { type: 'string', + description: + 'DID PLC rotation key (aka, recovery key) to be included in PLC creation operation.', }, plcOp: { type: 'unknown', + description: + 'A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented.', }, }, }, @@ -2877,6 +3319,8 @@ export const schemaDict = { encoding: 'application/json', schema: { type: 'object', + description: + 'Account login session returned on successful account creation.', required: ['accessJwt', 'refreshJwt', 'handle', 'did'], properties: { accessJwt: { @@ -2892,9 +3336,11 @@ export const schemaDict = { did: { type: 'string', format: 'did', + description: 'The DID of the new account.', }, didDoc: { type: 'unknown', + description: 'Complete DID document.', }, }, }, @@ -2940,6 +3386,8 @@ export const schemaDict = { properties: { name: { type: 'string', + description: + 'A short name for the App Password, to help distinguish them.', }, }, }, @@ -3141,6 +3589,31 @@ export const schemaDict = { }, }, }, + ComAtprotoServerDeactivateAccount: { + lexicon: 1, + id: 'com.atproto.server.deactivateAccount', + defs: { + main: { + type: 'procedure', + description: + 'Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + deleteAfter: { + type: 'string', + format: 'datetime', + description: + 'A recommendation to server as to how long they should hold onto the deactivated account before deleting.', + }, + }, + }, + }, + }, + }, + }, ComAtprotoServerDefs: { lexicon: 1, id: 'com.atproto.server.defs', @@ -3207,7 +3680,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: "Delete an actor's account with a token and password.", + description: + "Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.", input: { encoding: 'application/json', schema: { @@ -3244,7 +3718,7 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Delete the current session.', + description: 'Delete the current session. Requires auth.', }, }, }, @@ -3255,29 +3729,40 @@ export const schemaDict = { main: { type: 'query', description: - "Get a document describing the service's accounts configuration.", + "Describes the server's account creation requirements and capabilities. Implemented by PDS.", output: { encoding: 'application/json', schema: { type: 'object', - required: ['availableUserDomains'], + required: ['did', 'availableUserDomains'], properties: { inviteCodeRequired: { type: 'boolean', + description: + 'If true, an invite code must be supplied to create an account on this instance.', }, phoneVerificationRequired: { type: 'boolean', + description: + 'If true, a phone verification token must be supplied to create an account on this instance.', }, availableUserDomains: { type: 'array', + description: + 'List of domain suffixes that can be used in account handles.', items: { type: 'string', }, }, links: { type: 'ref', + description: 'URLs of service policy documents.', ref: 'lex:com.atproto.server.describeServer#links', }, + did: { + type: 'string', + format: 'did', + }, }, }, }, @@ -3301,7 +3786,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get all invite codes for a given account.', + description: + 'Get all invite codes for the current account. Requires auth.', parameters: { type: 'params', properties: { @@ -3312,6 +3798,8 @@ export const schemaDict = { createAvailable: { type: 'boolean', default: true, + description: + "Controls whether any new 'earned' but not 'created' invites should be created.", }, }, }, @@ -3339,13 +3827,49 @@ export const schemaDict = { }, }, }, + ComAtprotoServerGetServiceAuth: { + lexicon: 1, + id: 'com.atproto.server.getServiceAuth', + defs: { + main: { + type: 'query', + description: + 'Get a signed token on behalf of the requesting DID for the requested service.', + parameters: { + type: 'params', + required: ['aud'], + properties: { + aud: { + type: 'string', + format: 'did', + description: + 'The DID of the service that the token will be used to authenticate with', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['token'], + properties: { + token: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoServerGetSession: { lexicon: 1, id: 'com.atproto.server.getSession', defs: { main: { type: 'query', - description: 'Get information about the current session.', + description: + 'Get information about the current auth session. Requires auth.', output: { encoding: 'application/json', schema: { @@ -3425,7 +3949,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Refresh an authentication session.', + description: + "Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').", output: { encoding: 'application/json', schema: { @@ -3531,7 +4056,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Reserve a repo signing key for account creation.', + description: + 'Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented.', input: { encoding: 'application/json', schema: { @@ -3539,7 +4065,8 @@ export const schemaDict = { properties: { did: { type: 'string', - description: 'The did to reserve a new did:key for', + format: 'did', + description: 'The DID to reserve a key for.', }, }, }, @@ -3552,7 +4079,8 @@ export const schemaDict = { properties: { signingKey: { type: 'string', - description: 'Public signing key in the form of a did:key.', + description: + 'The public key for the reserved signing key, in did:key serialization.', }, }, }, @@ -3659,7 +4187,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a blob associated with a given repo.', + description: + 'Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.', parameters: { type: 'params', required: ['did', 'cid'], @@ -3667,7 +4196,7 @@ export const schemaDict = { did: { type: 'string', format: 'did', - description: 'The DID of the repo.', + description: 'The DID of the account.', }, cid: { type: 'string', @@ -3688,7 +4217,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get blocks from a given repo.', + description: + 'Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.', parameters: { type: 'params', required: ['did', 'cids'], @@ -3783,7 +4313,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get the current commit CID & revision of the repo.', + description: + 'Get the current commit CID & revision of the specified repo. Does not require auth.', parameters: { type: 'params', required: ['did'], @@ -3826,7 +4357,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Get blocks needed for existence or non-existence of record.', + 'Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.', parameters: { type: 'params', required: ['did', 'collection', 'rkey'], @@ -3842,6 +4373,7 @@ export const schemaDict = { }, rkey: { type: 'string', + description: 'Record Key', }, commit: { type: 'string', @@ -3863,7 +4395,7 @@ export const schemaDict = { main: { type: 'query', description: - "Gets the DID's repo, optionally catching up from a specific revision.", + "Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.", parameters: { type: 'params', required: ['did'], @@ -3875,7 +4407,8 @@ export const schemaDict = { }, since: { type: 'string', - description: 'The revision of the repo to catch up from.', + description: + "The revision ('rev') of the repo to create a diff from.", }, }, }, @@ -3891,7 +4424,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'List blob CIDs since some revision.', + description: + 'List blob CIDso for an account, since some repo revision. Does not require auth; implemented by PDS.', parameters: { type: 'params', required: ['did'], @@ -3944,7 +4478,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'List DIDs and root CIDs of hosted repos.', + description: + 'Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.', parameters: { type: 'params', properties: { @@ -3990,6 +4525,7 @@ export const schemaDict = { head: { type: 'string', format: 'cid', + description: 'Current repo commit CID', }, rev: { type: 'string', @@ -4005,7 +4541,7 @@ export const schemaDict = { main: { type: 'procedure', description: - 'Notify a crawling service of a recent update; often when a long break between updates causes the connection with the crawling service to break.', + 'Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay.', input: { encoding: 'application/json', schema: { @@ -4015,7 +4551,7 @@ export const schemaDict = { hostname: { type: 'string', description: - 'Hostname of the service that is notifying of update.', + 'Hostname of the current service (usually a PDS) that is notifying of update.', }, }, }, @@ -4029,7 +4565,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Request a service to persistently crawl hosted repos.', + description: + 'Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.', input: { encoding: 'application/json', schema: { @@ -4039,7 +4576,7 @@ export const schemaDict = { hostname: { type: 'string', description: - 'Hostname of the service that is requesting to be crawled.', + 'Hostname of the current service (eg, PDS) that is requesting to be crawled.', }, }, }, @@ -4053,13 +4590,14 @@ export const schemaDict = { defs: { main: { type: 'subscription', - description: 'Subscribe to repo updates.', + description: + 'Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.', parameters: { type: 'params', properties: { cursor: { type: 'integer', - description: 'The last known event to backfill from.', + description: 'The last known event seq number to backfill from.', }, }, }, @@ -4068,6 +4606,7 @@ export const schemaDict = { type: 'union', refs: [ 'lex:com.atproto.sync.subscribeRepos#commit', + 'lex:com.atproto.sync.subscribeRepos#identity', 'lex:com.atproto.sync.subscribeRepos#handle', 'lex:com.atproto.sync.subscribeRepos#migrate', 'lex:com.atproto.sync.subscribeRepos#tombstone', @@ -4081,11 +4620,15 @@ export const schemaDict = { }, { name: 'ConsumerTooSlow', + description: + 'If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection.', }, ], }, commit: { type: 'object', + description: + 'Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.', required: [ 'seq', 'rebase', @@ -4103,34 +4646,45 @@ export const schemaDict = { properties: { seq: { type: 'integer', + description: 'The stream sequence number of this message.', }, rebase: { type: 'boolean', + description: 'DEPRECATED -- unused', }, tooBig: { type: 'boolean', + description: + 'Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data.', }, repo: { type: 'string', format: 'did', + description: 'The repo this event comes from.', }, commit: { type: 'cid-link', + description: 'Repo commit object CID.', }, prev: { type: 'cid-link', + description: + 'DEPRECATED -- unused. WARNING -- nullable and optional; stick with optional to ensure golang interoperability.', }, rev: { type: 'string', - description: 'The rev of the emitted commit.', + description: + 'The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event.', }, since: { type: 'string', - description: 'The rev of the last emitted commit from this repo.', + description: + 'The rev of the last emitted commit from this repo (if any).', }, blocks: { type: 'bytes', - description: 'CAR file containing relevant blocks.', + description: + 'CAR file containing relevant blocks, as a diff since the previous repo state.', maxLength: 1000000, }, ops: { @@ -4138,6 +4692,8 @@ export const schemaDict = { items: { type: 'ref', ref: 'lex:com.atproto.sync.subscribeRepos#repoOp', + description: + 'List of repo mutation operations in this commit (eg, records created, updated, or deleted).', }, maxLength: 200, }, @@ -4145,8 +4701,31 @@ export const schemaDict = { type: 'array', items: { type: 'cid-link', + description: + 'List of new blobs (by CID) referenced by records in this commit.', }, }, + time: { + type: 'string', + format: 'datetime', + description: + 'Timestamp of when this message was originally broadcast.', + }, + }, + }, + identity: { + type: 'object', + description: + "Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.", + required: ['seq', 'did', 'time'], + properties: { + seq: { + type: 'integer', + }, + did: { + type: 'string', + format: 'did', + }, time: { type: 'string', format: 'datetime', @@ -4155,6 +4734,8 @@ export const schemaDict = { }, handle: { type: 'object', + description: + "Represents an update of the account's handle, or transition to/from invalid state. NOTE: Will be deprecated in favor of #identity.", required: ['seq', 'did', 'handle', 'time'], properties: { seq: { @@ -4176,6 +4757,8 @@ export const schemaDict = { }, migrate: { type: 'object', + description: + 'Represents an account moving from one PDS instance to another. NOTE: not implemented; account migration uses #identity instead', required: ['seq', 'did', 'migrateTo', 'time'], nullable: ['migrateTo'], properties: { @@ -4197,6 +4780,8 @@ export const schemaDict = { }, tombstone: { type: 'object', + description: + 'Indicates that an account has been deleted. NOTE: may be deprecated in favor of #identity or a future #account event', required: ['seq', 'did', 'time'], properties: { seq: { @@ -4227,8 +4812,7 @@ 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.", + description: 'A repo operation, ie a mutation of a single record.', required: ['action', 'path', 'cid'], nullable: ['cid'], properties: { @@ -4241,6 +4825,8 @@ export const schemaDict = { }, cid: { type: 'cid-link', + description: + 'For creates and updates, the new record CID. For deletions, null.', }, }, }, @@ -4281,7 +4867,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Fetch all labels from a labeler created after a certain date.', + 'DEPRECATED: use queryLabels or subscribeLabels instead -- Fetch all labels from a labeler created after a certain date.', parameters: { type: 'params', properties: { @@ -4315,59 +4901,6 @@ export const schemaDict = { }, }, }, - ComAtprotoTempImportRepo: { - lexicon: 1, - id: 'com.atproto.temp.importRepo', - defs: { - main: { - type: 'procedure', - description: - "Gets the did's repo, optionally catching up from a specific revision.", - parameters: { - type: 'params', - required: ['did'], - properties: { - did: { - type: 'string', - format: 'did', - description: 'The DID of the repo.', - }, - }, - }, - input: { - encoding: 'application/vnd.ipld.car', - }, - output: { - encoding: 'text/plain', - }, - }, - }, - }, - ComAtprotoTempPushBlob: { - lexicon: 1, - id: 'com.atproto.temp.pushBlob', - defs: { - main: { - type: 'procedure', - description: - "Gets the did's repo, optionally catching up from a specific revision.", - parameters: { - type: 'params', - required: ['did'], - properties: { - did: { - type: 'string', - format: 'did', - description: 'The DID of the repo.', - }, - }, - }, - input: { - encoding: '*/*', - }, - }, - }, - }, ComAtprotoTempRequestPhoneVerification: { lexicon: 1, id: 'com.atproto.temp.requestPhoneVerification', @@ -4391,86 +4924,9 @@ export const schemaDict = { }, }, }, - ComAtprotoTempTransferAccount: { - lexicon: 1, - id: 'com.atproto.temp.transferAccount', - defs: { - main: { - type: 'procedure', - description: 'Transfer an account.', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['handle', 'did', 'plcOp'], - properties: { - handle: { - type: 'string', - format: 'handle', - }, - did: { - type: 'string', - format: 'did', - }, - plcOp: { - type: 'unknown', - }, - }, - }, - }, - output: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['accessJwt', 'refreshJwt', 'handle', 'did'], - properties: { - accessJwt: { - type: 'string', - }, - refreshJwt: { - type: 'string', - }, - handle: { - type: 'string', - format: 'handle', - }, - did: { - type: 'string', - format: 'did', - }, - }, - }, - }, - errors: [ - { - name: 'InvalidHandle', - }, - { - name: 'InvalidPassword', - }, - { - name: 'InvalidInviteCode', - }, - { - name: 'HandleNotAvailable', - }, - { - name: 'UnsupportedDomain', - }, - { - name: 'UnresolvableDid', - }, - { - name: 'IncompatibleDidDoc', - }, - ], - }, - }, - }, AppBskyActorDefs: { lexicon: 1, id: 'app.bsky.actor.defs', - description: 'A reference to an actor in the network.', defs: { profileViewBasic: { type: 'object', @@ -4603,6 +5059,8 @@ export const schemaDict = { }, viewerState: { type: 'object', + description: + "Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.", properties: { muted: { type: 'boolean', @@ -4644,6 +5102,8 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#feedViewPref', 'lex:app.bsky.actor.defs#threadViewPref', 'lex:app.bsky.actor.defs#interestsPref', + 'lex:app.bsky.actor.defs#mutedWordsPref', + 'lex:app.bsky.actor.defs#hiddenPostsPref', ], }, }, @@ -4688,6 +5148,9 @@ export const schemaDict = { format: 'at-uri', }, }, + timelineIndex: { + type: 'integer', + }, }, }, personalDetailsPref: { @@ -4764,6 +5227,62 @@ export const schemaDict = { }, }, }, + mutedWordTarget: { + type: 'string', + knownValues: ['content', 'tag'], + maxLength: 640, + maxGraphemes: 64, + }, + mutedWord: { + type: 'object', + description: 'A word that the account owner has muted.', + required: ['value', 'targets'], + properties: { + value: { + type: 'string', + description: 'The muted word itself.', + maxLength: 10000, + maxGraphemes: 1000, + }, + targets: { + type: 'array', + description: 'The intended targets of the muted word.', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWordTarget', + }, + }, + }, + }, + mutedWordsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWord', + }, + description: 'A list of words the account owner has muted.', + }, + }, + }, + hiddenPostsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'string', + format: 'at-uri', + }, + description: + 'A list of URIs of posts the account owner has hidden.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { @@ -4772,7 +5291,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get private preferences attached to the account.', + description: + 'Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth.', parameters: { type: 'params', properties: {}, @@ -4799,7 +5319,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get detailed profile view of an actor.', + description: + 'Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.', parameters: { type: 'params', required: ['actor'], @@ -4807,6 +5328,7 @@ export const schemaDict = { actor: { type: 'string', format: 'at-identifier', + description: 'Handle or DID of account to fetch profile of.', }, }, }, @@ -4866,7 +5388,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of suggested actors, used for discovery.', + description: + 'Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding.', parameters: { type: 'params', properties: { @@ -4909,7 +5432,7 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a profile.', + description: 'A declaration of a Bluesky account profile.', key: 'literal:self', record: { type: 'object', @@ -4921,21 +5444,28 @@ export const schemaDict = { }, description: { type: 'string', + description: 'Free-form profile description text.', maxGraphemes: 256, maxLength: 2560, }, avatar: { type: 'blob', + description: + "Small image to be displayed next to posts from account. AKA, 'profile picture'", accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, banner: { type: 'blob', + description: + 'Larger horizontal image to display behind profile view.', accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, labels: { type: 'union', + description: + 'Self-label values, specific to the Bluesky application, on the overall account.', refs: ['lex:com.atproto.label.defs#selfLabels'], }, }, @@ -4972,7 +5502,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Find actors (profiles) matching search criteria.', + description: + 'Find actors (profiles) matching search criteria. Does not require auth.', parameters: { type: 'params', properties: { @@ -5024,7 +5555,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Find actor suggestions for a prefix search term.', + description: + 'Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth.', parameters: { type: 'params', properties: { @@ -5066,11 +5598,11 @@ export const schemaDict = { AppBskyEmbedExternal: { lexicon: 1, id: 'app.bsky.embed.external', - description: - 'A representation of some externally linked content, embedded in another form of content.', defs: { main: { type: 'object', + description: + "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).", required: ['external'], properties: { external: { @@ -5134,7 +5666,7 @@ export const schemaDict = { AppBskyEmbedImages: { lexicon: 1, id: 'app.bsky.embed.images', - description: 'A set of images embedded in some other form of content.', + description: 'A set of images embedded in a Bluesky record (eg, a post).', defs: { main: { type: 'object', @@ -5161,6 +5693,8 @@ export const schemaDict = { }, alt: { type: 'string', + description: + 'Alt text description of the image, for accessibility.', }, aspectRatio: { type: 'ref', @@ -5204,12 +5738,18 @@ export const schemaDict = { properties: { thumb: { type: 'string', + description: + 'Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View.', }, fullsize: { type: 'string', + description: + 'Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View.', }, alt: { type: 'string', + description: + 'Alt text description of the image, for accessibility.', }, aspectRatio: { type: 'ref', @@ -5223,7 +5763,7 @@ export const schemaDict = { lexicon: 1, id: 'app.bsky.embed.record', description: - 'A representation of a record embedded in another form of content.', + 'A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.', defs: { main: { type: 'object', @@ -5269,6 +5809,7 @@ export const schemaDict = { }, value: { type: 'unknown', + description: 'The record data itself.', }, labels: { type: 'array', @@ -5333,7 +5874,7 @@ export const schemaDict = { lexicon: 1, id: 'app.bsky.embed.recordWithMedia', description: - 'A representation of a record embedded in another form of content, alongside other compatible embeds.', + 'A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.', defs: { main: { type: 'object', @@ -5432,6 +5973,8 @@ export const schemaDict = { }, viewerState: { type: 'object', + description: + "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.", properties: { repost: { type: 'string', @@ -5692,7 +6235,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Get information about a feed generator, including policies and offered feed URIs.', + 'Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View).', output: { encoding: 'application/json', schema: { @@ -5747,7 +6290,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of the existence of a feed generator.', + description: + 'Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository.', key: 'any', record: { type: 'object', @@ -5781,6 +6325,7 @@ export const schemaDict = { }, labels: { type: 'union', + description: 'Self-label values', refs: ['lex:com.atproto.label.defs#selfLabels'], }, createdAt: { @@ -5798,7 +6343,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of feeds created by the actor.', + description: + "Get a list of feeds (feed generator records) created by the actor (in the actor's repo).", parameters: { type: 'params', required: ['actor'], @@ -5846,7 +6392,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of posts liked by an actor.', + description: + 'Get a list of posts liked by an actor. Does not require auth.', parameters: { type: 'params', required: ['actor'], @@ -5902,7 +6449,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: "Get a view of an actor's feed.", + description: + "Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.", parameters: { type: 'params', required: ['actor'], @@ -5922,6 +6470,8 @@ export const schemaDict = { }, filter: { type: 'string', + description: + 'Combinations of post/repost types to include in response.', knownValues: [ 'posts_with_replies', 'posts_no_replies', @@ -5969,7 +6519,7 @@ export const schemaDict = { main: { type: 'query', description: - "Get a hydrated feed from an actor's selected feed generator.", + "Get a hydrated feed from an actor's selected feed generator. Implemented by App View.", parameters: { type: 'params', required: ['feed'], @@ -6022,7 +6572,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get information about a feed generator.', + description: + 'Get information about a feed generator. Implemented by AppView.', parameters: { type: 'params', required: ['feed'], @@ -6030,6 +6581,7 @@ export const schemaDict = { feed: { type: 'string', format: 'at-uri', + description: 'AT-URI of the feed generator record.', }, }, }, @@ -6045,9 +6597,13 @@ export const schemaDict = { }, isOnline: { type: 'boolean', + description: + 'Indicates whether the feed generator service has been online recently, or else seems to be inactive.', }, isValid: { type: 'boolean', + description: + 'Indicates whether the feed generator service is compatible with the record declaration.', }, }, }, @@ -6100,7 +6656,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a skeleton of a feed provided by a feed generator.', + description: + 'Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service.', parameters: { type: 'params', required: ['feed'], @@ -6108,6 +6665,8 @@ export const schemaDict = { feed: { type: 'string', format: 'at-uri', + description: + 'Reference to feed generator record describing the specific feed being requested.', }, limit: { type: 'integer', @@ -6153,7 +6712,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get the list of likes.', + description: + 'Get like records which reference a subject (by AT-URI and CID).', parameters: { type: 'params', required: ['uri'], @@ -6161,10 +6721,13 @@ export const schemaDict = { uri: { type: 'string', format: 'at-uri', + description: 'AT-URI of the subject (eg, a post record).', }, cid: { type: 'string', format: 'cid', + description: + 'CID of the subject record (aka, specific version of record), to filter likes.', }, limit: { type: 'integer', @@ -6231,7 +6794,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a view of a recent posts from actors in a list.', + description: + 'Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth.', parameters: { type: 'params', required: ['list'], @@ -6239,6 +6803,7 @@ export const schemaDict = { list: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) to the list record.', }, limit: { type: 'integer', @@ -6284,7 +6849,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get posts in a thread.', + description: + 'Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests.', parameters: { type: 'params', required: ['uri'], @@ -6292,15 +6858,20 @@ export const schemaDict = { uri: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) to post record.', }, depth: { type: 'integer', + description: + 'How many levels of reply depth should be included in response.', default: 6, minimum: 0, maximum: 1000, }, parentHeight: { type: 'integer', + description: + 'How many levels of parent (and grandparent, etc) post to include.', default: 80, minimum: 0, maximum: 1000, @@ -6338,13 +6909,15 @@ export const schemaDict = { defs: { main: { type: 'query', - description: "Get a view of an actor's feed.", + description: + "Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'.", parameters: { type: 'params', required: ['uris'], properties: { uris: { type: 'array', + description: 'List of post AT-URIs to return hydrated views for.', items: { type: 'string', format: 'at-uri', @@ -6378,7 +6951,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of reposts.', + description: 'Get a list of reposts for a given post.', parameters: { type: 'params', required: ['uri'], @@ -6386,10 +6959,13 @@ export const schemaDict = { uri: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) of post record', }, cid: { type: 'string', format: 'cid', + description: + 'If supplied, filters to reposts of specific version (by CID) of the post record.', }, limit: { type: 'integer', @@ -6438,7 +7014,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of suggested feeds for the viewer.', + description: + 'Get a list of suggested feeds (feed generators) for the requesting account.', parameters: { type: 'params', properties: { @@ -6481,12 +7058,15 @@ export const schemaDict = { defs: { main: { type: 'query', - description: "Get a view of the actor's home timeline.", + description: + "Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed.", parameters: { type: 'params', properties: { algorithm: { type: 'string', + description: + "Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism.", }, limit: { type: 'integer', @@ -6527,7 +7107,7 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a like.', + description: "Record declaring a 'like' of a piece of subject content.", key: 'tid', record: { type: 'object', @@ -6552,7 +7132,7 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a post.', + description: 'Record containing a Bluesky post.', key: 'tid', record: { type: 'object', @@ -6562,10 +7142,12 @@ export const schemaDict = { type: 'string', maxLength: 3000, maxGraphemes: 300, + description: + 'The primary post content. May be an empty string, if there are embeds.', }, entities: { type: 'array', - description: 'Deprecated: replaced by app.bsky.richtext.facet.', + description: 'DEPRECATED: replaced by app.bsky.richtext.facet.', items: { type: 'ref', ref: 'lex:app.bsky.feed.post#entity', @@ -6573,6 +7155,8 @@ export const schemaDict = { }, facets: { type: 'array', + description: + 'Annotations of text (mentions, URLs, hashtags, etc)', items: { type: 'ref', ref: 'lex:app.bsky.richtext.facet', @@ -6593,6 +7177,8 @@ export const schemaDict = { }, langs: { type: 'array', + description: + 'Indicates human language of post primary text content.', maxLength: 3, items: { type: 'string', @@ -6601,21 +7187,26 @@ export const schemaDict = { }, labels: { type: 'union', + description: + 'Self-label values for this post. Effectively content warnings.', refs: ['lex:com.atproto.label.defs#selfLabels'], }, tags: { type: 'array', + description: + 'Additional hashtags, in addition to any included in post text and facets.', maxLength: 8, items: { type: 'string', maxLength: 640, maxGraphemes: 64, }, - description: 'Additional non-inline tags describing this post.', }, createdAt: { type: 'string', format: 'datetime', + description: + 'Client-declared timestamp when this post was originally created.', }, }, }, @@ -6675,7 +7266,8 @@ export const schemaDict = { id: 'app.bsky.feed.repost', defs: { main: { - description: 'A declaration of a repost.', + description: + "Record representing a 'repost' of an existing Bluesky post.", type: 'record', key: 'tid', record: { @@ -6701,7 +7293,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Find posts matching search criteria.', + description: + 'Find posts matching search criteria, returning views of those posts.', parameters: { type: 'params', required: ['q'], @@ -6764,7 +7357,7 @@ export const schemaDict = { type: 'record', key: 'tid', description: - "Defines interaction gating rules for a thread. The rkey of the threadgate record should match the rkey of the thread's root post.", + "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository..", record: { type: 'object', required: ['post', 'createdAt'], @@ -6772,6 +7365,7 @@ export const schemaDict = { post: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) to the post record.', }, allow: { type: 'array', @@ -6821,7 +7415,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a block.', + description: + "Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details.", key: 'tid', record: { type: 'object', @@ -6830,6 +7425,7 @@ export const schemaDict = { subject: { type: 'string', format: 'did', + description: 'DID of the account to be blocked.', }, createdAt: { type: 'string', @@ -7018,7 +7614,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a social follow.', + description: + "Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.", key: 'tid', record: { type: 'object', @@ -7043,7 +7640,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of who the actor is blocking.', + description: + 'Enumerates which accounts the requesting account is currently blocking. Requires auth.', parameters: { type: 'params', properties: { @@ -7086,7 +7684,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: "Get a list of an actor's followers.", + description: + 'Enumerates accounts which follow a specified account (actor).', parameters: { type: 'params', required: ['actor'], @@ -7138,7 +7737,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of who the actor follows.', + description: + 'Enumerates accounts which a specified account (actor) follows.', parameters: { type: 'params', required: ['actor'], @@ -7190,7 +7790,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of actors.', + description: + "Gets a 'view' (with additional context) of a specified list.", parameters: { type: 'params', required: ['list'], @@ -7198,6 +7799,7 @@ export const schemaDict = { list: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) of the list record to hydrate.', }, limit: { type: 'integer', @@ -7242,7 +7844,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get lists that the actor is blocking.', + description: + 'Get mod lists that the requesting account (actor) is blocking. Requires auth.', parameters: { type: 'params', properties: { @@ -7285,7 +7888,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get lists that the actor is muting.', + description: + 'Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth.', parameters: { type: 'params', properties: { @@ -7328,7 +7932,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of lists that belong to an actor.', + description: + 'Enumerates the lists created by a specified account (actor).', parameters: { type: 'params', required: ['actor'], @@ -7336,6 +7941,7 @@ export const schemaDict = { actor: { type: 'string', format: 'at-identifier', + description: 'The account (actor) to enumerate lists from.', }, limit: { type: 'integer', @@ -7376,7 +7982,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of who the actor mutes.', + description: + 'Enumerates accounts that the requesting account (actor) currently has muted. Requires auth.', parameters: { type: 'params', properties: { @@ -7420,7 +8027,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Enumerates public relationships between one account, and a list of other accounts', + 'Enumerates public relationships between one account, and a list of other accounts. Does not require auth.', parameters: { type: 'params', required: ['actor'], @@ -7428,9 +8035,12 @@ export const schemaDict = { actor: { type: 'string', format: 'at-identifier', + description: 'Primary account requesting relationships for.', }, others: { type: 'array', + description: + "List of 'other' accounts to be related back to the primary.", maxLength: 30, items: { type: 'string', @@ -7478,7 +8088,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get suggested follows related to a given actor.', + description: + 'Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account.', parameters: { type: 'params', required: ['actor'], @@ -7514,7 +8125,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a list of actors.', + description: + 'Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists.', key: 'tid', record: { type: 'object', @@ -7522,12 +8134,15 @@ export const schemaDict = { properties: { purpose: { type: 'ref', + description: + 'Defines the purpose of the list (aka, moderation-oriented or curration-oriented)', ref: 'lex:app.bsky.graph.defs#listPurpose', }, name: { type: 'string', maxLength: 64, minLength: 1, + description: 'Display name for list; can not be empty.', }, description: { type: 'string', @@ -7565,7 +8180,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A block of an entire list of actors.', + description: + 'Record representing a block relationship against an entire an entire list of accounts (actors).', key: 'tid', record: { type: 'object', @@ -7574,6 +8190,7 @@ export const schemaDict = { subject: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) to the mod list record.', }, createdAt: { type: 'string', @@ -7590,7 +8207,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'An item under a declared list of actors.', + description: + "Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records.", key: 'tid', record: { type: 'object', @@ -7599,10 +8217,13 @@ export const schemaDict = { subject: { type: 'string', format: 'did', + description: 'The account which is included on the list.', }, list: { type: 'string', format: 'at-uri', + description: + 'Reference (AT-URI) to the list record (app.bsky.graph.list).', }, createdAt: { type: 'string', @@ -7619,7 +8240,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Mute an actor by DID or handle.', + description: + 'Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7642,7 +8264,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Mute a list of actors.', + description: + 'Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7665,7 +8288,7 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Unmute an actor by DID or handle.', + description: 'Unmutes the specified account. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7688,7 +8311,7 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Unmute a list of actors.', + description: 'Unmutes the specified list of accounts. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7711,7 +8334,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get the count of unread notifications.', + description: + 'Count the number of unread notifications for the requesting account. Requires auth.', parameters: { type: 'params', properties: { @@ -7742,7 +8366,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of notifications.', + description: + 'Enumerate notifications for the requesting account. Requires auth.', parameters: { type: 'params', properties: { @@ -7853,7 +8478,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Register for push notifications with a service.', + description: + 'Register to receive push notifications, via a specified service, for the requesting account. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7886,7 +8512,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Notify server that the user has seen notifications.', + description: + 'Notify server that the requesting account has seen notifications. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7909,6 +8536,7 @@ export const schemaDict = { defs: { main: { type: 'object', + description: 'Annotation of a sub-string within rich text.', required: ['index', 'features'], properties: { index: { @@ -7930,7 +8558,8 @@ export const schemaDict = { }, mention: { type: 'object', - description: 'A facet feature for actor mentions.', + description: + "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.", required: ['did'], properties: { did: { @@ -7941,7 +8570,8 @@ export const schemaDict = { }, link: { type: 'object', - description: 'A facet feature for links.', + description: + 'Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.', required: ['uri'], properties: { uri: { @@ -7952,7 +8582,8 @@ export const schemaDict = { }, tag: { type: 'object', - description: 'A hashtag.', + description: + "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').", required: ['tag'], properties: { tag: { @@ -7965,7 +8596,7 @@ export const schemaDict = { byteSlice: { type: 'object', description: - 'A text segment. Start is inclusive, end is exclusive. Indices are for utf8-encoded strings.', + 'Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.', required: ['byteStart', 'byteEnd'], properties: { byteStart: { @@ -8258,10 +8889,19 @@ export const ids = { ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail', ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', + ComAtprotoAdminUpdateAccountPassword: + 'com.atproto.admin.updateAccountPassword', ComAtprotoAdminUpdateCommunicationTemplate: 'com.atproto.admin.updateCommunicationTemplate', ComAtprotoAdminUpdateSubjectStatus: 'com.atproto.admin.updateSubjectStatus', + ComAtprotoIdentityGetRecommendedDidCredentials: + 'com.atproto.identity.getRecommendedDidCredentials', + ComAtprotoIdentityRequestPlcOperationSignature: + 'com.atproto.identity.requestPlcOperationSignature', ComAtprotoIdentityResolveHandle: 'com.atproto.identity.resolveHandle', + ComAtprotoIdentitySignPlcOperation: 'com.atproto.identity.signPlcOperation', + ComAtprotoIdentitySubmitPlcOperation: + 'com.atproto.identity.submitPlcOperation', ComAtprotoIdentityUpdateHandle: 'com.atproto.identity.updateHandle', ComAtprotoLabelDefs: 'com.atproto.label.defs', ComAtprotoLabelQueryLabels: 'com.atproto.label.queryLabels', @@ -8273,22 +8913,28 @@ export const ids = { ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord', ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo', ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord', + ComAtprotoRepoImportRepo: 'com.atproto.repo.importRepo', + ComAtprotoRepoListMissingBlobs: 'com.atproto.repo.listMissingBlobs', ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords', ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', + ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount', + ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus', ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword', ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode', ComAtprotoServerCreateInviteCodes: 'com.atproto.server.createInviteCodes', ComAtprotoServerCreateSession: 'com.atproto.server.createSession', + ComAtprotoServerDeactivateAccount: 'com.atproto.server.deactivateAccount', ComAtprotoServerDefs: 'com.atproto.server.defs', ComAtprotoServerDeleteAccount: 'com.atproto.server.deleteAccount', ComAtprotoServerDeleteSession: 'com.atproto.server.deleteSession', ComAtprotoServerDescribeServer: 'com.atproto.server.describeServer', ComAtprotoServerGetAccountInviteCodes: 'com.atproto.server.getAccountInviteCodes', + ComAtprotoServerGetServiceAuth: 'com.atproto.server.getServiceAuth', ComAtprotoServerGetSession: 'com.atproto.server.getSession', ComAtprotoServerListAppPasswords: 'com.atproto.server.listAppPasswords', ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession', @@ -8317,11 +8963,8 @@ export const ids = { ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', ComAtprotoTempCheckSignupQueue: 'com.atproto.temp.checkSignupQueue', ComAtprotoTempFetchLabels: 'com.atproto.temp.fetchLabels', - ComAtprotoTempImportRepo: 'com.atproto.temp.importRepo', - ComAtprotoTempPushBlob: 'com.atproto.temp.pushBlob', ComAtprotoTempRequestPhoneVerification: 'com.atproto.temp.requestPhoneVerification', - ComAtprotoTempTransferAccount: 'com.atproto.temp.transferAccount', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', AppBskyActorGetProfile: 'app.bsky.actor.getProfile', 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 5c1791e6130..630f2454056 100644 --- a/packages/api/src/client/types/app/bsky/actor/defs.ts +++ b/packages/api/src/client/types/app/bsky/actor/defs.ts @@ -82,6 +82,7 @@ export function validateProfileViewDetailed(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#profileViewDetailed', v) } +/** Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests. */ export interface ViewerState { muted?: boolean mutedByList?: AppBskyGraphDefs.ListViewBasic @@ -113,6 +114,8 @@ export type Preferences = ( | FeedViewPref | ThreadViewPref | InterestsPref + | MutedWordsPref + | HiddenPostsPref | { $type: string; [k: string]: unknown } )[] @@ -154,6 +157,7 @@ export function validateContentLabelPref(v: unknown): ValidationResult { export interface SavedFeedsPref { pinned: string[] saved: string[] + timelineIndex?: number [k: string]: unknown } @@ -252,3 +256,62 @@ export function isInterestsPref(v: unknown): v is InterestsPref { export function validateInterestsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#interestsPref', v) } + +export type MutedWordTarget = 'content' | 'tag' | (string & {}) + +/** A word that the account owner has muted. */ +export interface MutedWord { + /** The muted word itself. */ + value: string + /** The intended targets of the muted word. */ + targets: MutedWordTarget[] + [k: string]: unknown +} + +export function isMutedWord(v: unknown): v is MutedWord { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWord' + ) +} + +export function validateMutedWord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWord', v) +} + +export interface MutedWordsPref { + /** A list of words the account owner has muted. */ + items: MutedWord[] + [k: string]: unknown +} + +export function isMutedWordsPref(v: unknown): v is MutedWordsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWordsPref' + ) +} + +export function validateMutedWordsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWordsPref', v) +} + +export interface HiddenPostsPref { + /** A list of URIs of posts the account owner has hidden. */ + items: string[] + [k: string]: unknown +} + +export function isHiddenPostsPref(v: unknown): v is HiddenPostsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#hiddenPostsPref' + ) +} + +export function validateHiddenPostsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#hiddenPostsPref', v) +} diff --git a/packages/api/src/client/types/app/bsky/actor/getProfile.ts b/packages/api/src/client/types/app/bsky/actor/getProfile.ts index 47e36fe974a..bbd88c30a7b 100644 --- a/packages/api/src/client/types/app/bsky/actor/getProfile.ts +++ b/packages/api/src/client/types/app/bsky/actor/getProfile.ts @@ -9,6 +9,7 @@ import { CID } from 'multiformats/cid' import * as AppBskyActorDefs from './defs' export interface QueryParams { + /** Handle or DID of account to fetch profile of. */ actor: string } 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 fa36f4298f1..a0c51e060c5 100644 --- a/packages/api/src/client/types/app/bsky/actor/profile.ts +++ b/packages/api/src/client/types/app/bsky/actor/profile.ts @@ -9,8 +9,11 @@ import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' export interface Record { displayName?: string + /** Free-form profile description text. */ description?: string + /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ avatar?: BlobRef + /** Larger horizontal image to display behind profile view. */ banner?: BlobRef labels?: | ComAtprotoLabelDefs.SelfLabels diff --git a/packages/api/src/client/types/app/bsky/embed/external.ts b/packages/api/src/client/types/app/bsky/embed/external.ts index 271c103dbba..5832cbb3987 100644 --- a/packages/api/src/client/types/app/bsky/embed/external.ts +++ b/packages/api/src/client/types/app/bsky/embed/external.ts @@ -6,6 +6,7 @@ import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' +/** A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post). */ export interface Main { external: External [k: string]: unknown 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 77909a4b3b0..ddfdf4c156c 100644 --- a/packages/api/src/client/types/app/bsky/embed/images.ts +++ b/packages/api/src/client/types/app/bsky/embed/images.ts @@ -26,6 +26,7 @@ export function validateMain(v: unknown): ValidationResult { export interface Image { image: BlobRef + /** Alt text description of the image, for accessibility. */ alt: string aspectRatio?: AspectRatio [k: string]: unknown @@ -76,8 +77,11 @@ export function validateView(v: unknown): ValidationResult { } export interface ViewImage { + /** Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. */ thumb: string + /** Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. */ fullsize: string + /** Alt text description of the image, for accessibility. */ alt: string aspectRatio?: AspectRatio [k: string]: unknown 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 caee8f08cdd..388687fd665 100644 --- a/packages/api/src/client/types/app/bsky/embed/record.ts +++ b/packages/api/src/client/types/app/bsky/embed/record.ts @@ -57,6 +57,7 @@ export interface ViewRecord { uri: string cid: string author: AppBskyActorDefs.ProfileViewBasic + /** The record data itself. */ value: {} labels?: ComAtprotoLabelDefs.Label[] embeds?: ( 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 82cbfd9951a..949b8fb975e 100644 --- a/packages/api/src/client/types/app/bsky/feed/defs.ts +++ b/packages/api/src/client/types/app/bsky/feed/defs.ts @@ -45,6 +45,7 @@ export function validatePostView(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#postView', v) } +/** Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests. */ export interface ViewerState { repost?: string like?: string 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 a070dad6ff7..3f498e49514 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 + /** Combinations of post/repost types to include in response. */ filter?: | 'posts_with_replies' | 'posts_no_replies' diff --git a/packages/api/src/client/types/app/bsky/feed/getFeedGenerator.ts b/packages/api/src/client/types/app/bsky/feed/getFeedGenerator.ts index a2f9b405c97..f08c9b59340 100644 --- a/packages/api/src/client/types/app/bsky/feed/getFeedGenerator.ts +++ b/packages/api/src/client/types/app/bsky/feed/getFeedGenerator.ts @@ -9,6 +9,7 @@ import { CID } from 'multiformats/cid' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** AT-URI of the feed generator record. */ feed: string } @@ -16,7 +17,9 @@ export type InputSchema = undefined export interface OutputSchema { view: AppBskyFeedDefs.GeneratorView + /** Indicates whether the feed generator service has been online recently, or else seems to be inactive. */ isOnline: boolean + /** Indicates whether the feed generator service is compatible with the record declaration. */ isValid: boolean [k: string]: unknown } 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 0aa325d7fec..1426469c84d 100644 --- a/packages/api/src/client/types/app/bsky/feed/getFeedSkeleton.ts +++ b/packages/api/src/client/types/app/bsky/feed/getFeedSkeleton.ts @@ -9,6 +9,7 @@ import { CID } from 'multiformats/cid' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** Reference to feed generator record describing the specific feed being requested. */ feed: string limit?: number cursor?: string diff --git a/packages/api/src/client/types/app/bsky/feed/getLikes.ts b/packages/api/src/client/types/app/bsky/feed/getLikes.ts index d78047feb6e..9725ef065d9 100644 --- a/packages/api/src/client/types/app/bsky/feed/getLikes.ts +++ b/packages/api/src/client/types/app/bsky/feed/getLikes.ts @@ -9,7 +9,9 @@ import { CID } from 'multiformats/cid' import * as AppBskyActorDefs from '../actor/defs' export interface QueryParams { + /** AT-URI of the subject (eg, a post record). */ uri: string + /** CID of the subject record (aka, specific version of record), to filter likes. */ cid?: string limit?: number cursor?: string diff --git a/packages/api/src/client/types/app/bsky/feed/getListFeed.ts b/packages/api/src/client/types/app/bsky/feed/getListFeed.ts index 511e9526c6d..6b4156ddda9 100644 --- a/packages/api/src/client/types/app/bsky/feed/getListFeed.ts +++ b/packages/api/src/client/types/app/bsky/feed/getListFeed.ts @@ -9,6 +9,7 @@ import { CID } from 'multiformats/cid' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** Reference (AT-URI) to the list record. */ list: string limit?: number cursor?: string 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 d3865db9ee2..d03ad7de127 100644 --- a/packages/api/src/client/types/app/bsky/feed/getPostThread.ts +++ b/packages/api/src/client/types/app/bsky/feed/getPostThread.ts @@ -9,8 +9,11 @@ import { CID } from 'multiformats/cid' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** Reference (AT-URI) to post record. */ uri: string + /** How many levels of reply depth should be included in response. */ depth?: number + /** How many levels of parent (and grandparent, etc) post to include. */ parentHeight?: number } diff --git a/packages/api/src/client/types/app/bsky/feed/getPosts.ts b/packages/api/src/client/types/app/bsky/feed/getPosts.ts index 933919bdcc1..cd932d88047 100644 --- a/packages/api/src/client/types/app/bsky/feed/getPosts.ts +++ b/packages/api/src/client/types/app/bsky/feed/getPosts.ts @@ -9,6 +9,7 @@ import { CID } from 'multiformats/cid' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** List of post AT-URIs to return hydrated views for. */ uris: string[] } diff --git a/packages/api/src/client/types/app/bsky/feed/getRepostedBy.ts b/packages/api/src/client/types/app/bsky/feed/getRepostedBy.ts index 30a1a109aaa..d27aa1dec0a 100644 --- a/packages/api/src/client/types/app/bsky/feed/getRepostedBy.ts +++ b/packages/api/src/client/types/app/bsky/feed/getRepostedBy.ts @@ -9,7 +9,9 @@ import { CID } from 'multiformats/cid' import * as AppBskyActorDefs from '../actor/defs' export interface QueryParams { + /** Reference (AT-URI) of post record */ uri: string + /** If supplied, filters to reposts of specific version (by CID) of the post record. */ cid?: string limit?: number cursor?: string diff --git a/packages/api/src/client/types/app/bsky/feed/getTimeline.ts b/packages/api/src/client/types/app/bsky/feed/getTimeline.ts index 6d8dacff99a..5ab2c7c4b1f 100644 --- a/packages/api/src/client/types/app/bsky/feed/getTimeline.ts +++ b/packages/api/src/client/types/app/bsky/feed/getTimeline.ts @@ -9,6 +9,7 @@ import { CID } from 'multiformats/cid' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism. */ algorithm?: string limit?: number cursor?: string 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 a3299e19035..0de5192af77 100644 --- a/packages/api/src/client/types/app/bsky/feed/post.ts +++ b/packages/api/src/client/types/app/bsky/feed/post.ts @@ -14,9 +14,11 @@ import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' export interface Record { + /** The primary post content. May be an empty string, if there are embeds. */ text: string - /** Deprecated: replaced by app.bsky.richtext.facet. */ + /** DEPRECATED: replaced by app.bsky.richtext.facet. */ entities?: Entity[] + /** Annotations of text (mentions, URLs, hashtags, etc) */ facets?: AppBskyRichtextFacet.Main[] reply?: ReplyRef embed?: @@ -25,12 +27,14 @@ export interface Record { | AppBskyEmbedRecord.Main | AppBskyEmbedRecordWithMedia.Main | { $type: string; [k: string]: unknown } + /** Indicates human language of post primary text content. */ langs?: string[] labels?: | ComAtprotoLabelDefs.SelfLabels | { $type: string; [k: string]: unknown } - /** Additional non-inline tags describing this post. */ + /** Additional hashtags, in addition to any included in post text and facets. */ tags?: string[] + /** Client-declared timestamp when this post was originally created. */ createdAt: string [k: string]: unknown } diff --git a/packages/api/src/client/types/app/bsky/feed/threadgate.ts b/packages/api/src/client/types/app/bsky/feed/threadgate.ts index a1afec85673..cc8c05a78ec 100644 --- a/packages/api/src/client/types/app/bsky/feed/threadgate.ts +++ b/packages/api/src/client/types/app/bsky/feed/threadgate.ts @@ -7,6 +7,7 @@ import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' export interface Record { + /** Reference (AT-URI) to the post record. */ post: string allow?: ( | MentionRule diff --git a/packages/api/src/client/types/app/bsky/graph/block.ts b/packages/api/src/client/types/app/bsky/graph/block.ts index c35258d979a..f2455fc08a2 100644 --- a/packages/api/src/client/types/app/bsky/graph/block.ts +++ b/packages/api/src/client/types/app/bsky/graph/block.ts @@ -7,6 +7,7 @@ import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' export interface Record { + /** DID of the account to be blocked. */ subject: string createdAt: string [k: string]: unknown diff --git a/packages/api/src/client/types/app/bsky/graph/getList.ts b/packages/api/src/client/types/app/bsky/graph/getList.ts index 13ebd9d3ae6..36c4cf0aa86 100644 --- a/packages/api/src/client/types/app/bsky/graph/getList.ts +++ b/packages/api/src/client/types/app/bsky/graph/getList.ts @@ -9,6 +9,7 @@ import { CID } from 'multiformats/cid' import * as AppBskyGraphDefs from './defs' export interface QueryParams { + /** Reference (AT-URI) of the list record to hydrate. */ list: string limit?: number cursor?: string diff --git a/packages/api/src/client/types/app/bsky/graph/getLists.ts b/packages/api/src/client/types/app/bsky/graph/getLists.ts index 80a7edfb759..644aeea3b4b 100644 --- a/packages/api/src/client/types/app/bsky/graph/getLists.ts +++ b/packages/api/src/client/types/app/bsky/graph/getLists.ts @@ -9,6 +9,7 @@ import { CID } from 'multiformats/cid' import * as AppBskyGraphDefs from './defs' export interface QueryParams { + /** The account (actor) to enumerate lists from. */ actor: string limit?: number cursor?: string diff --git a/packages/api/src/client/types/app/bsky/graph/getRelationships.ts b/packages/api/src/client/types/app/bsky/graph/getRelationships.ts index 5fce53f635c..9aa58ad2699 100644 --- a/packages/api/src/client/types/app/bsky/graph/getRelationships.ts +++ b/packages/api/src/client/types/app/bsky/graph/getRelationships.ts @@ -9,7 +9,9 @@ import { CID } from 'multiformats/cid' import * as AppBskyGraphDefs from './defs' export interface QueryParams { + /** Primary account requesting relationships for. */ actor: string + /** List of 'other' accounts to be related back to the primary. */ others?: string[] } 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 4fe6dd8ed8b..fec652ccb12 100644 --- a/packages/api/src/client/types/app/bsky/graph/list.ts +++ b/packages/api/src/client/types/app/bsky/graph/list.ts @@ -11,6 +11,7 @@ import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' export interface Record { purpose: AppBskyGraphDefs.ListPurpose + /** Display name for list; can not be empty. */ name: string description?: string descriptionFacets?: AppBskyRichtextFacet.Main[] diff --git a/packages/api/src/client/types/app/bsky/graph/listblock.ts b/packages/api/src/client/types/app/bsky/graph/listblock.ts index 770dfbb0775..e0f02be268f 100644 --- a/packages/api/src/client/types/app/bsky/graph/listblock.ts +++ b/packages/api/src/client/types/app/bsky/graph/listblock.ts @@ -7,6 +7,7 @@ import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' export interface Record { + /** Reference (AT-URI) to the mod list record. */ subject: string createdAt: string [k: string]: unknown diff --git a/packages/api/src/client/types/app/bsky/graph/listitem.ts b/packages/api/src/client/types/app/bsky/graph/listitem.ts index 5059ef69c10..d4fb5631e84 100644 --- a/packages/api/src/client/types/app/bsky/graph/listitem.ts +++ b/packages/api/src/client/types/app/bsky/graph/listitem.ts @@ -7,7 +7,9 @@ import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' export interface Record { + /** The account which is included on the list. */ subject: string + /** Reference (AT-URI) to the list record (app.bsky.graph.list). */ list: string createdAt: string [k: string]: unknown diff --git a/packages/api/src/client/types/app/bsky/richtext/facet.ts b/packages/api/src/client/types/app/bsky/richtext/facet.ts index 96573bb06fe..836136b7dac 100644 --- a/packages/api/src/client/types/app/bsky/richtext/facet.ts +++ b/packages/api/src/client/types/app/bsky/richtext/facet.ts @@ -6,6 +6,7 @@ import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' +/** Annotation of a sub-string within rich text. */ export interface Main { index: ByteSlice features: (Mention | Link | Tag | { $type: string; [k: string]: unknown })[] @@ -25,7 +26,7 @@ export function validateMain(v: unknown): ValidationResult { return lexicons.validate('app.bsky.richtext.facet#main', v) } -/** A facet feature for actor mentions. */ +/** Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. */ export interface Mention { did: string [k: string]: unknown @@ -43,7 +44,7 @@ export function validateMention(v: unknown): ValidationResult { return lexicons.validate('app.bsky.richtext.facet#mention', v) } -/** A facet feature for links. */ +/** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. */ export interface Link { uri: string [k: string]: unknown @@ -61,7 +62,7 @@ export function validateLink(v: unknown): ValidationResult { return lexicons.validate('app.bsky.richtext.facet#link', v) } -/** A hashtag. */ +/** Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). */ export interface Tag { tag: string [k: string]: unknown @@ -77,7 +78,7 @@ export function validateTag(v: unknown): ValidationResult { return lexicons.validate('app.bsky.richtext.facet#tag', v) } -/** A text segment. Start is inclusive, end is exclusive. Indices are for utf8-encoded strings. */ +/** Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. */ export interface ByteSlice { byteStart: number byteEnd: number 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 da154f8a845..4e3d35a869f 100644 --- a/packages/api/src/client/types/com/atproto/admin/defs.ts +++ b/packages/api/src/client/types/com/atproto/admin/defs.ts @@ -40,6 +40,7 @@ export interface ModEventView { | ModEventEscalate | ModEventMute | ModEventEmail + | ModEventResolveAppeal | { $type: string; [k: string]: unknown } subject: | RepoRef @@ -76,6 +77,7 @@ export interface ModEventViewDetail { | ModEventAcknowledge | ModEventEscalate | ModEventMute + | ModEventEmail | ModEventResolveAppeal | { $type: string; [k: string]: unknown } subject: @@ -154,6 +156,7 @@ export interface SubjectStatusView { /** True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators. */ appealed?: boolean suspendUntil?: string + tags?: string[] [k: string]: unknown } @@ -718,6 +721,29 @@ export function validateModEventEmail(v: unknown): ValidationResult { return lexicons.validate('com.atproto.admin.defs#modEventEmail', v) } +/** Add/Remove a tag on a subject */ +export interface ModEventTag { + /** Tags to be added to the subject. If already exists, won't be duplicated. */ + add: string[] + /** Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated. */ + remove: string[] + /** Additional comment about added/removed tags. */ + comment?: string + [k: string]: unknown +} + +export function isModEventTag(v: unknown): v is ModEventTag { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventTag' + ) +} + +export function validateModEventTag(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventTag', v) +} + export interface CommunicationTemplateView { id: string /** Name of the template. */ diff --git a/packages/api/src/client/types/com/atproto/admin/emitModerationEvent.ts b/packages/api/src/client/types/com/atproto/admin/emitModerationEvent.ts index 77b460ed1ff..6e7827bdc6a 100644 --- a/packages/api/src/client/types/com/atproto/admin/emitModerationEvent.ts +++ b/packages/api/src/client/types/com/atproto/admin/emitModerationEvent.ts @@ -23,6 +23,7 @@ export interface InputSchema { | ComAtprotoAdminDefs.ModEventReverseTakedown | ComAtprotoAdminDefs.ModEventUnmute | ComAtprotoAdminDefs.ModEventEmail + | ComAtprotoAdminDefs.ModEventTag | { $type: string; [k: string]: unknown } subject: | ComAtprotoAdminDefs.RepoRef diff --git a/packages/api/src/client/types/com/atproto/admin/queryModerationEvents.ts b/packages/api/src/client/types/com/atproto/admin/queryModerationEvents.ts index ed21c739bcb..2dd3081ef8f 100644 --- a/packages/api/src/client/types/com/atproto/admin/queryModerationEvents.ts +++ b/packages/api/src/client/types/com/atproto/admin/queryModerationEvents.ts @@ -14,10 +14,27 @@ export interface QueryParams { createdBy?: string /** Sort direction for the events. Defaults to descending order of created at timestamp. */ sortDirection?: 'asc' | 'desc' + /** Retrieve events created after a given timestamp */ + createdAfter?: string + /** Retrieve events created before a given timestamp */ + createdBefore?: string subject?: string /** If true, events on all record types (posts, lists, profile etc.) owned by the did are returned */ includeAllUserRecords?: boolean limit?: number + /** If true, only events with comments are returned */ + hasComment?: boolean + /** If specified, only events with comments containing the keyword are returned */ + comment?: string + /** If specified, only events where all of these labels were added are returned */ + addedLabels?: string[] + /** If specified, only events where all of these labels were removed are returned */ + removedLabels?: string[] + /** If specified, only events where all of these tags were added are returned */ + addedTags?: string[] + /** If specified, only events where all of these tags were removed are returned */ + removedTags?: string[] + reportTypes?: string[] cursor?: string } diff --git a/packages/api/src/client/types/com/atproto/admin/queryModerationStatuses.ts b/packages/api/src/client/types/com/atproto/admin/queryModerationStatuses.ts index 0039016a353..c16bb94fec3 100644 --- a/packages/api/src/client/types/com/atproto/admin/queryModerationStatuses.ts +++ b/packages/api/src/client/types/com/atproto/admin/queryModerationStatuses.ts @@ -34,6 +34,8 @@ export interface QueryParams { /** Get subjects in unresolved appealed status */ appealed?: boolean limit?: number + tags?: string[] + excludeTags?: string[] cursor?: string } diff --git a/packages/api/src/client/types/com/atproto/temp/pushBlob.ts b/packages/api/src/client/types/com/atproto/admin/updateAccountPassword.ts similarity index 79% rename from packages/api/src/client/types/com/atproto/temp/pushBlob.ts rename to packages/api/src/client/types/com/atproto/admin/updateAccountPassword.ts index 32165bc8014..99ef3881c37 100644 --- a/packages/api/src/client/types/com/atproto/temp/pushBlob.ts +++ b/packages/api/src/client/types/com/atproto/admin/updateAccountPassword.ts @@ -7,17 +7,18 @@ import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' -export interface QueryParams { - /** The DID of the repo. */ +export interface QueryParams {} + +export interface InputSchema { did: string + password: string + [k: string]: unknown } -export type InputSchema = string | Uint8Array - export interface CallOptions { headers?: Headers qp?: QueryParams - encoding: string + encoding: 'application/json' } export interface Response { diff --git a/packages/api/src/client/types/com/atproto/identity/getRecommendedDidCredentials.ts b/packages/api/src/client/types/com/atproto/identity/getRecommendedDidCredentials.ts new file mode 100644 index 00000000000..dfa143e4ab3 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/identity/getRecommendedDidCredentials.ts @@ -0,0 +1,37 @@ +/** + * 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 type InputSchema = undefined + +export interface OutputSchema { + /** Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs. */ + rotationKeys?: string[] + alsoKnownAs?: string[] + verificationMethods?: {} + services?: {} + [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/com/atproto/identity/requestPlcOperationSignature.ts b/packages/api/src/client/types/com/atproto/identity/requestPlcOperationSignature.ts new file mode 100644 index 00000000000..ef2ed1ac47c --- /dev/null +++ b/packages/api/src/client/types/com/atproto/identity/requestPlcOperationSignature.ts @@ -0,0 +1,28 @@ +/** + * 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 type InputSchema = undefined + +export interface CallOptions { + headers?: Headers + qp?: QueryParams +} + +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/identity/signPlcOperation.ts b/packages/api/src/client/types/com/atproto/identity/signPlcOperation.ts new file mode 100644 index 00000000000..3060c1e3f4d --- /dev/null +++ b/packages/api/src/client/types/com/atproto/identity/signPlcOperation.ts @@ -0,0 +1,44 @@ +/** + * 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 { + /** A token received through com.atproto.identity.requestPlcOperationSignature */ + token?: string + rotationKeys?: string[] + alsoKnownAs?: string[] + verificationMethods?: {} + services?: {} + [k: string]: unknown +} + +export interface OutputSchema { + /** A signed DID PLC operation. */ + operation: {} + [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/api/src/client/types/com/atproto/identity/submitPlcOperation.ts b/packages/api/src/client/types/com/atproto/identity/submitPlcOperation.ts new file mode 100644 index 00000000000..4ba52f74fc7 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/identity/submitPlcOperation.ts @@ -0,0 +1,32 @@ +/** + * 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 { + operation: {} + [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/identity/updateHandle.ts b/packages/api/src/client/types/com/atproto/identity/updateHandle.ts index 4c01e105c28..2bd2c4c9d6a 100644 --- a/packages/api/src/client/types/com/atproto/identity/updateHandle.ts +++ b/packages/api/src/client/types/com/atproto/identity/updateHandle.ts @@ -10,6 +10,7 @@ import { CID } from 'multiformats/cid' export interface QueryParams {} export interface InputSchema { + /** The new handle. */ handle: string [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/moderation/createReport.ts b/packages/api/src/client/types/com/atproto/moderation/createReport.ts index 826b32ff67c..7bf3cc1a380 100644 --- a/packages/api/src/client/types/com/atproto/moderation/createReport.ts +++ b/packages/api/src/client/types/com/atproto/moderation/createReport.ts @@ -14,6 +14,7 @@ export interface QueryParams {} export interface InputSchema { reasonType: ComAtprotoModerationDefs.ReasonType + /** Additional context about the content and violation. */ reason?: string subject: | ComAtprotoAdminDefs.RepoRef 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 f4a8a269201..df35ec7dcbf 100644 --- a/packages/api/src/client/types/com/atproto/repo/applyWrites.ts +++ b/packages/api/src/client/types/com/atproto/repo/applyWrites.ts @@ -10,11 +10,12 @@ import { CID } from 'multiformats/cid' export interface QueryParams {} export interface InputSchema { - /** The handle or DID of the repo. */ + /** The handle or DID of the repo (aka, current account). */ repo: string - /** Flag for validating the records. */ + /** Can be set to 'false' to skip Lexicon schema validation of record data, for all operations. */ validate?: boolean writes: (Create | Update | Delete)[] + /** If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations. */ swapCommit?: string [k: string]: unknown } @@ -43,7 +44,7 @@ export function toKnownErr(e: any) { return e } -/** Create a new record. */ +/** Operation which creates a new record. */ export interface Create { collection: string rkey?: string @@ -63,7 +64,7 @@ export function validateCreate(v: unknown): ValidationResult { return lexicons.validate('com.atproto.repo.applyWrites#create', v) } -/** Update an existing record. */ +/** Operation which updates an existing record. */ export interface Update { collection: string rkey: string @@ -83,7 +84,7 @@ export function validateUpdate(v: unknown): ValidationResult { return lexicons.validate('com.atproto.repo.applyWrites#update', v) } -/** Delete an existing record. */ +/** Operation which deletes an existing record. */ export interface Delete { collection: string rkey: string 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 2056778c71c..6b13f67db7f 100644 --- a/packages/api/src/client/types/com/atproto/repo/createRecord.ts +++ b/packages/api/src/client/types/com/atproto/repo/createRecord.ts @@ -10,15 +10,15 @@ import { CID } from 'multiformats/cid' export interface QueryParams {} export interface InputSchema { - /** The handle or DID of the repo. */ + /** The handle or DID of the repo (aka, current account). */ repo: string /** The NSID of the record collection. */ collection: string - /** The key of the record. */ + /** The Record Key. */ rkey?: string - /** Flag for validating the record. */ + /** Can be set to 'false' to skip Lexicon schema validation of record data. */ validate?: boolean - /** The record to create. */ + /** The record itself. Must contain a $type field. */ record: {} /** Compare and swap with the previous commit by CID. */ swapCommit?: string 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 5bf9237abbb..54109b62f31 100644 --- a/packages/api/src/client/types/com/atproto/repo/deleteRecord.ts +++ b/packages/api/src/client/types/com/atproto/repo/deleteRecord.ts @@ -10,11 +10,11 @@ import { CID } from 'multiformats/cid' export interface QueryParams {} export interface InputSchema { - /** The handle or DID of the repo. */ + /** The handle or DID of the repo (aka, current account). */ repo: string /** The NSID of the record collection. */ collection: string - /** The key of the record. */ + /** The Record Key. */ rkey: string /** Compare and swap with the previous record by CID. */ swapRecord?: string diff --git a/packages/api/src/client/types/com/atproto/repo/describeRepo.ts b/packages/api/src/client/types/com/atproto/repo/describeRepo.ts index e6ecedb3297..f17a8410782 100644 --- a/packages/api/src/client/types/com/atproto/repo/describeRepo.ts +++ b/packages/api/src/client/types/com/atproto/repo/describeRepo.ts @@ -17,8 +17,11 @@ export type InputSchema = undefined export interface OutputSchema { handle: string did: string + /** The complete DID document for this account. */ didDoc: {} + /** List of all the collections (NSIDs) for which this repo contains at least one record. */ collections: string[] + /** Indicates if handle is currently valid (resolves bi-directionally) */ handleIsCorrect: boolean [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/repo/getRecord.ts b/packages/api/src/client/types/com/atproto/repo/getRecord.ts index 56338d016ee..a6d2bd39e8c 100644 --- a/packages/api/src/client/types/com/atproto/repo/getRecord.ts +++ b/packages/api/src/client/types/com/atproto/repo/getRecord.ts @@ -12,7 +12,7 @@ export interface QueryParams { repo: string /** The NSID of the record collection. */ collection: string - /** The key of the record. */ + /** The Record Key. */ rkey: string /** The CID of the version of the record. If not specified, then return the most recent version. */ cid?: string diff --git a/packages/api/src/client/types/com/atproto/temp/importRepo.ts b/packages/api/src/client/types/com/atproto/repo/importRepo.ts similarity index 86% rename from packages/api/src/client/types/com/atproto/temp/importRepo.ts rename to packages/api/src/client/types/com/atproto/repo/importRepo.ts index 6f9f99f2b9d..040cca671bf 100644 --- a/packages/api/src/client/types/com/atproto/temp/importRepo.ts +++ b/packages/api/src/client/types/com/atproto/repo/importRepo.ts @@ -7,10 +7,7 @@ import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' -export interface QueryParams { - /** The DID of the repo. */ - did: string -} +export interface QueryParams {} export type InputSchema = string | Uint8Array @@ -23,7 +20,6 @@ export interface CallOptions { export interface Response { success: boolean headers: Headers - data: Uint8Array } export function toKnownErr(e: any) { diff --git a/packages/api/src/client/types/com/atproto/repo/listMissingBlobs.ts b/packages/api/src/client/types/com/atproto/repo/listMissingBlobs.ts new file mode 100644 index 00000000000..b66f617eea7 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/repo/listMissingBlobs.ts @@ -0,0 +1,55 @@ +/** + * 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 { + limit?: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + blobs: RecordBlob[] + [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 +} + +export interface RecordBlob { + cid: string + recordUri: string + [k: string]: unknown +} + +export function isRecordBlob(v: unknown): v is RecordBlob { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.listMissingBlobs#recordBlob' + ) +} + +export function validateRecordBlob(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.listMissingBlobs#recordBlob', v) +} 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 269ef759401..7421ee19780 100644 --- a/packages/api/src/client/types/com/atproto/repo/putRecord.ts +++ b/packages/api/src/client/types/com/atproto/repo/putRecord.ts @@ -10,17 +10,17 @@ import { CID } from 'multiformats/cid' export interface QueryParams {} export interface InputSchema { - /** The handle or DID of the repo. */ + /** The handle or DID of the repo (aka, current account). */ repo: string /** The NSID of the record collection. */ collection: string - /** The key of the record. */ + /** The Record Key. */ rkey: string - /** Flag for validating the record. */ + /** Can be set to 'false' to skip Lexicon schema validation of record data. */ validate?: boolean /** The record to write. */ record: {} - /** Compare and swap with the previous record by CID. */ + /** Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation */ swapRecord?: string | null /** Compare and swap with the previous commit by CID. */ swapCommit?: string diff --git a/packages/api/src/client/types/com/atproto/server/activateAccount.ts b/packages/api/src/client/types/com/atproto/server/activateAccount.ts new file mode 100644 index 00000000000..ef2ed1ac47c --- /dev/null +++ b/packages/api/src/client/types/com/atproto/server/activateAccount.ts @@ -0,0 +1,28 @@ +/** + * 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 type InputSchema = undefined + +export interface CallOptions { + headers?: Headers + qp?: QueryParams +} + +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/server/checkAccountStatus.ts b/packages/api/src/client/types/com/atproto/server/checkAccountStatus.ts new file mode 100644 index 00000000000..86a942f81f3 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/server/checkAccountStatus.ts @@ -0,0 +1,41 @@ +/** + * 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 type InputSchema = undefined + +export interface OutputSchema { + activated: boolean + validDid: boolean + repoCommit: string + repoRev: string + repoBlocks: number + indexedRecords: number + privateStateValues: number + expectedBlobs: number + importedBlobs: number + [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/com/atproto/server/createAccount.ts b/packages/api/src/client/types/com/atproto/server/createAccount.ts index b62adf97cb1..5e36eca0ee3 100644 --- a/packages/api/src/client/types/com/atproto/server/createAccount.ts +++ b/packages/api/src/client/types/com/atproto/server/createAccount.ts @@ -11,22 +11,30 @@ export interface QueryParams {} export interface InputSchema { email?: string + /** Requested handle for the account. */ handle: string + /** Pre-existing atproto DID, being imported to a new account. */ did?: string inviteCode?: string verificationCode?: string verificationPhone?: string + /** Initial account password. May need to meet instance-specific password strength requirements. */ password?: string + /** DID PLC rotation key (aka, recovery key) to be included in PLC creation operation. */ recoveryKey?: string + /** A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented. */ plcOp?: {} [k: string]: unknown } +/** Account login session returned on successful account creation. */ export interface OutputSchema { accessJwt: string refreshJwt: string handle: string + /** The DID of the new account. */ did: string + /** Complete DID document. */ didDoc?: {} [k: string]: unknown } 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 d6e9ce3ddf5..8b9a5d53a7c 100644 --- a/packages/api/src/client/types/com/atproto/server/createAppPassword.ts +++ b/packages/api/src/client/types/com/atproto/server/createAppPassword.ts @@ -10,6 +10,7 @@ import { CID } from 'multiformats/cid' export interface QueryParams {} export interface InputSchema { + /** A short name for the App Password, to help distinguish them. */ name: string [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/server/deactivateAccount.ts b/packages/api/src/client/types/com/atproto/server/deactivateAccount.ts new file mode 100644 index 00000000000..c88bd548243 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/server/deactivateAccount.ts @@ -0,0 +1,33 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export interface InputSchema { + /** A recommendation to server as to how long they should hold onto the deactivated account before deleting. */ + deleteAfter?: 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/server/describeServer.ts b/packages/api/src/client/types/com/atproto/server/describeServer.ts index fb6c9d5c662..c4b749c7ada 100644 --- a/packages/api/src/client/types/com/atproto/server/describeServer.ts +++ b/packages/api/src/client/types/com/atproto/server/describeServer.ts @@ -12,10 +12,14 @@ export interface QueryParams {} export type InputSchema = undefined export interface OutputSchema { + /** If true, an invite code must be supplied to create an account on this instance. */ inviteCodeRequired?: boolean + /** If true, a phone verification token must be supplied to create an account on this instance. */ phoneVerificationRequired?: boolean + /** List of domain suffixes that can be used in account handles. */ availableUserDomains: string[] links?: Links + did: string [k: string]: unknown } 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 d019ed3fa23..5438cbc96d6 100644 --- a/packages/api/src/client/types/com/atproto/server/getAccountInviteCodes.ts +++ b/packages/api/src/client/types/com/atproto/server/getAccountInviteCodes.ts @@ -10,6 +10,7 @@ import * as ComAtprotoServerDefs from './defs' export interface QueryParams { includeUsed?: boolean + /** Controls whether any new 'earned' but not 'created' invites should be created. */ createAvailable?: boolean } diff --git a/packages/api/src/client/types/com/atproto/server/getServiceAuth.ts b/packages/api/src/client/types/com/atproto/server/getServiceAuth.ts new file mode 100644 index 00000000000..6056960effc --- /dev/null +++ b/packages/api/src/client/types/com/atproto/server/getServiceAuth.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' + +export interface QueryParams { + /** The DID of the service that the token will be used to authenticate with */ + aud: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + token: string + [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/com/atproto/server/reserveSigningKey.ts b/packages/api/src/client/types/com/atproto/server/reserveSigningKey.ts index f5e515ff5cf..324dee9665a 100644 --- a/packages/api/src/client/types/com/atproto/server/reserveSigningKey.ts +++ b/packages/api/src/client/types/com/atproto/server/reserveSigningKey.ts @@ -10,13 +10,13 @@ import { CID } from 'multiformats/cid' export interface QueryParams {} export interface InputSchema { - /** The did to reserve a new did:key for */ + /** The DID to reserve a key for. */ did?: string [k: string]: unknown } export interface OutputSchema { - /** Public signing key in the form of a did:key. */ + /** The public key for the reserved signing key, in did:key serialization. */ signingKey: string [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/sync/getBlob.ts b/packages/api/src/client/types/com/atproto/sync/getBlob.ts index 57bc271ce5a..83d8b79ca08 100644 --- a/packages/api/src/client/types/com/atproto/sync/getBlob.ts +++ b/packages/api/src/client/types/com/atproto/sync/getBlob.ts @@ -8,7 +8,7 @@ import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' export interface QueryParams { - /** The DID of the repo. */ + /** The DID of the account. */ did: string /** The CID of the blob to fetch */ cid: string diff --git a/packages/api/src/client/types/com/atproto/sync/getRecord.ts b/packages/api/src/client/types/com/atproto/sync/getRecord.ts index e7bbcf36343..1fc9a94b406 100644 --- a/packages/api/src/client/types/com/atproto/sync/getRecord.ts +++ b/packages/api/src/client/types/com/atproto/sync/getRecord.ts @@ -11,6 +11,7 @@ export interface QueryParams { /** The DID of the repo. */ did: string collection: string + /** Record Key */ rkey: string /** An optional past commit CID. */ commit?: string 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 0a45536779e..53e0883d74e 100644 --- a/packages/api/src/client/types/com/atproto/sync/getRepo.ts +++ b/packages/api/src/client/types/com/atproto/sync/getRepo.ts @@ -10,7 +10,7 @@ import { CID } from 'multiformats/cid' export interface QueryParams { /** The DID of the repo. */ did: string - /** The revision of the repo to catch up from. */ + /** The revision ('rev') of the repo to create a diff from. */ since?: string } diff --git a/packages/api/src/client/types/com/atproto/sync/listRepos.ts b/packages/api/src/client/types/com/atproto/sync/listRepos.ts index 669dba37e85..eccf796acb6 100644 --- a/packages/api/src/client/types/com/atproto/sync/listRepos.ts +++ b/packages/api/src/client/types/com/atproto/sync/listRepos.ts @@ -38,6 +38,7 @@ export function toKnownErr(e: any) { export interface Repo { did: string + /** Current repo commit CID */ head: string rev: string [k: string]: unknown diff --git a/packages/api/src/client/types/com/atproto/sync/notifyOfUpdate.ts b/packages/api/src/client/types/com/atproto/sync/notifyOfUpdate.ts index 2b098982684..f53e4a55385 100644 --- a/packages/api/src/client/types/com/atproto/sync/notifyOfUpdate.ts +++ b/packages/api/src/client/types/com/atproto/sync/notifyOfUpdate.ts @@ -10,7 +10,7 @@ import { CID } from 'multiformats/cid' export interface QueryParams {} export interface InputSchema { - /** Hostname of the service that is notifying of update. */ + /** Hostname of the current service (usually a PDS) that is notifying of update. */ hostname: string [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/sync/requestCrawl.ts b/packages/api/src/client/types/com/atproto/sync/requestCrawl.ts index c07330a6fb1..089eb84e089 100644 --- a/packages/api/src/client/types/com/atproto/sync/requestCrawl.ts +++ b/packages/api/src/client/types/com/atproto/sync/requestCrawl.ts @@ -10,7 +10,7 @@ import { CID } from 'multiformats/cid' export interface QueryParams {} export interface InputSchema { - /** Hostname of the service that is requesting to be crawled. */ + /** Hostname of the current service (eg, PDS) that is requesting to be crawled. */ hostname: 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 a4fec035874..f4a362f755f 100644 --- a/packages/api/src/client/types/com/atproto/sync/subscribeRepos.ts +++ b/packages/api/src/client/types/com/atproto/sync/subscribeRepos.ts @@ -7,21 +7,29 @@ import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' +/** Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature. */ export interface Commit { + /** The stream sequence number of this message. */ seq: number + /** DEPRECATED -- unused */ rebase: boolean + /** Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data. */ tooBig: boolean + /** The repo this event comes from. */ repo: string + /** Repo commit object CID. */ commit: CID + /** DEPRECATED -- unused. WARNING -- nullable and optional; stick with optional to ensure golang interoperability. */ prev?: CID | null - /** The rev of the emitted commit. */ + /** The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event. */ rev: string - /** The rev of the last emitted commit from this repo. */ + /** The rev of the last emitted commit from this repo (if any). */ since: string | null - /** CAR file containing relevant blocks. */ + /** CAR file containing relevant blocks, as a diff since the previous repo state. */ blocks: Uint8Array ops: RepoOp[] blobs: CID[] + /** Timestamp of when this message was originally broadcast. */ time: string [k: string]: unknown } @@ -38,6 +46,27 @@ export function validateCommit(v: unknown): ValidationResult { return lexicons.validate('com.atproto.sync.subscribeRepos#commit', v) } +/** Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache. */ +export interface Identity { + seq: number + did: string + time: string + [k: string]: unknown +} + +export function isIdentity(v: unknown): v is Identity { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.sync.subscribeRepos#identity' + ) +} + +export function validateIdentity(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.sync.subscribeRepos#identity', v) +} + +/** Represents an update of the account's handle, or transition to/from invalid state. NOTE: Will be deprecated in favor of #identity. */ export interface Handle { seq: number did: string @@ -58,6 +87,7 @@ export function validateHandle(v: unknown): ValidationResult { return lexicons.validate('com.atproto.sync.subscribeRepos#handle', v) } +/** Represents an account moving from one PDS instance to another. NOTE: not implemented; account migration uses #identity instead */ export interface Migrate { seq: number did: string @@ -78,6 +108,7 @@ export function validateMigrate(v: unknown): ValidationResult { return lexicons.validate('com.atproto.sync.subscribeRepos#migrate', v) } +/** Indicates that an account has been deleted. NOTE: may be deprecated in favor of #identity or a future #account event */ export interface Tombstone { seq: number did: string @@ -115,10 +146,11 @@ 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. */ +/** A repo operation, ie a mutation of a single record. */ export interface RepoOp { action: 'create' | 'update' | 'delete' | (string & {}) path: string + /** For creates and updates, the new record CID. For deletions, null. */ cid: CID | null [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/temp/transferAccount.ts b/packages/api/src/client/types/com/atproto/temp/transferAccount.ts deleted file mode 100644 index 7ae16c01290..00000000000 --- a/packages/api/src/client/types/com/atproto/temp/transferAccount.ts +++ /dev/null @@ -1,92 +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 { - handle: string - did: string - plcOp: {} - [k: string]: unknown -} - -export interface OutputSchema { - accessJwt: string - refreshJwt: string - handle: string - did: string - [k: string]: unknown -} - -export interface CallOptions { - headers?: Headers - qp?: QueryParams - encoding: 'application/json' -} - -export interface Response { - success: boolean - headers: Headers - data: OutputSchema -} - -export class InvalidHandleError extends XRPCError { - constructor(src: XRPCError) { - super(src.status, src.error, src.message, src.headers) - } -} - -export class InvalidPasswordError extends XRPCError { - constructor(src: XRPCError) { - super(src.status, src.error, src.message, src.headers) - } -} - -export class InvalidInviteCodeError extends XRPCError { - constructor(src: XRPCError) { - super(src.status, src.error, src.message, src.headers) - } -} - -export class HandleNotAvailableError extends XRPCError { - constructor(src: XRPCError) { - super(src.status, src.error, src.message, src.headers) - } -} - -export class UnsupportedDomainError extends XRPCError { - constructor(src: XRPCError) { - super(src.status, src.error, src.message, src.headers) - } -} - -export class UnresolvableDidError extends XRPCError { - constructor(src: XRPCError) { - super(src.status, src.error, src.message, src.headers) - } -} - -export class IncompatibleDidDocError 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 === 'InvalidHandle') return new InvalidHandleError(e) - if (e.error === 'InvalidPassword') return new InvalidPasswordError(e) - if (e.error === 'InvalidInviteCode') return new InvalidInviteCodeError(e) - if (e.error === 'HandleNotAvailable') return new HandleNotAvailableError(e) - if (e.error === 'UnsupportedDomain') return new UnsupportedDomainError(e) - if (e.error === 'UnresolvableDid') return new UnresolvableDidError(e) - if (e.error === 'IncompatibleDidDoc') return new IncompatibleDidDocError(e) - } - return e -} diff --git a/packages/api/src/rich-text/detection.ts b/packages/api/src/rich-text/detection.ts index 25edcd9e57b..7b5444a68a5 100644 --- a/packages/api/src/rich-text/detection.ts +++ b/packages/api/src/rich-text/detection.ts @@ -70,27 +70,25 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined { } } { - const re = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/g + const re = /(^|\s)#((?!\ufe0f)[^\d\s]\S*)(?=\s)?/g while ((match = re.exec(text.utf16))) { - let [tag] = match - const hasLeadingSpace = /^\s/.test(tag) + let [, leading, tag] = match tag = tag.trim().replace(/\p{P}+$/gu, '') // strip ending punctuation - // inclusive of #, max of 64 chars - if (tag.length > 66) continue + if (tag.length === 0 || tag.length > 64) continue - const index = match.index + (hasLeadingSpace ? 1 : 0) + const index = match.index + leading.length facets.push({ index: { byteStart: text.utf16IndexToUtf8Index(index), - byteEnd: text.utf16IndexToUtf8Index(index + tag.length), // inclusive of last char + byteEnd: text.utf16IndexToUtf8Index(index + 1 + tag.length), }, features: [ { $type: 'app.bsky.richtext.facet#tag', - tag: tag.replace(/^#/, ''), + tag: tag, }, ], }) diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 3d6f73baa33..7e36e77de58 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -1,3 +1,4 @@ +import { AppBskyActorNS, AppBskyActorDefs } from './client' import { LabelPreference } from './moderation/types' /** @@ -119,4 +120,6 @@ export interface BskyPreferences { contentLabels: Record birthDate: Date | undefined interests: BskyInterestsPreference + mutedWords: AppBskyActorDefs.MutedWord[] + hiddenPosts: string[] } diff --git a/packages/api/tests/agent.test.ts b/packages/api/tests/agent.test.ts index cff4e3517a8..7bebb8a1bcf 100644 --- a/packages/api/tests/agent.test.ts +++ b/packages/api/tests/agent.test.ts @@ -439,6 +439,7 @@ describe('agent', () => { expect(originalHandlerCallCount).toEqual(1) agent.setPersistSessionHandler(newPersistSession) + agent.session = undefined await agent.createAccount({ handle: 'user8.test', diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 5f850b19e91..db8ee0a1567 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -239,6 +239,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setAdultContentEnabled(true) @@ -263,6 +265,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setAdultContentEnabled(false) @@ -287,6 +291,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setContentLabelPref('impersonation', 'warn') @@ -313,6 +319,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setContentLabelPref('spam', 'show') // will convert to 'ignore' @@ -341,6 +349,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -371,6 +381,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -401,6 +413,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -431,6 +445,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -461,6 +477,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -491,6 +509,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2') @@ -527,6 +547,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -557,6 +579,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) @@ -587,6 +611,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setFeedViewPrefs('home', { hideReplies: true }) @@ -617,6 +643,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setFeedViewPrefs('home', { hideReplies: false }) @@ -647,6 +675,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setFeedViewPrefs('other', { hideReplies: true }) @@ -684,6 +714,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setThreadViewPrefs({ sort: 'random' }) @@ -721,6 +753,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setThreadViewPrefs({ sort: 'oldest' }) @@ -758,6 +792,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setInterestsPref({ tags: ['foo', 'bar'] }) @@ -795,6 +831,8 @@ describe('agent', () => { interests: { tags: ['foo', 'bar'], }, + mutedWords: [], + hiddenPosts: [], }) }) @@ -921,6 +959,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setAdultContentEnabled(false) @@ -950,6 +990,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setContentLabelPref('nsfw', 'hide') @@ -979,6 +1021,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -1008,6 +1052,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) @@ -1037,6 +1083,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) await agent.setFeedViewPrefs('home', { @@ -1077,6 +1125,8 @@ describe('agent', () => { interests: { tags: [], }, + mutedWords: [], + hiddenPosts: [], }) const res = await agent.app.bsky.actor.getPreferences() @@ -1118,6 +1168,146 @@ describe('agent', () => { ].sort(byType), ) }) + + describe('muted words', () => { + let agent: BskyAgent + const mutedWords = [ + { value: 'both', targets: ['content', 'tag'] }, + { value: 'content', targets: ['content'] }, + { value: 'tag', targets: ['tag'] }, + { value: 'tag_then_both', targets: ['tag'] }, + { value: 'tag_then_content', targets: ['tag'] }, + { value: 'tag_then_none', targets: ['tag'] }, + ] + + beforeAll(async () => { + agent = new BskyAgent({ service: network.pds.url }) + await agent.createAccount({ + handle: 'user7.test', + email: 'user7@test.com', + password: 'password', + }) + }) + + it('upsertMutedWords', async () => { + await agent.upsertMutedWords(mutedWords) + await agent.upsertMutedWords(mutedWords) // double + await expect(agent.getPreferences()).resolves.toHaveProperty( + 'mutedWords', + mutedWords, + ) + }) + + it('upsertMutedWords with #', async () => { + await agent.upsertMutedWords([ + { value: 'hashtag', targets: ['content'] }, + ]) + await agent.upsertMutedWords([{ value: '#hashtag', targets: ['tag'] }]) + const { mutedWords } = await agent.getPreferences() + expect(mutedWords.find((m) => m.value === '#hashtag')).toBeFalsy() + expect(mutedWords.find((m) => m.value === 'hashtag')).toStrictEqual({ + value: 'hashtag', + targets: ['content', 'tag'], + }) + expect(mutedWords.filter((m) => m.value === 'hashtag').length).toBe(1) + }) + + it('updateMutedWord', async () => { + await agent.updateMutedWord({ + value: 'tag_then_content', + targets: ['content'], + }) + await agent.updateMutedWord({ + value: 'tag_then_both', + targets: ['content', 'tag'], + }) + await agent.updateMutedWord({ value: 'tag_then_none', targets: [] }) + await agent.updateMutedWord({ value: 'no_exist', targets: ['tag'] }) + const { mutedWords } = await agent.getPreferences() + + expect( + mutedWords.find((m) => m.value === 'tag_then_content'), + ).toHaveProperty('targets', ['content']) + expect( + mutedWords.find((m) => m.value === 'tag_then_both'), + ).toHaveProperty('targets', ['content', 'tag']) + expect( + mutedWords.find((m) => m.value === 'tag_then_none'), + ).toHaveProperty('targets', []) + expect(mutedWords.find((m) => m.value === 'no_exist')).toBeFalsy() + }) + + it('updateMutedWord with #', async () => { + await agent.updateMutedWord({ + value: 'hashtag', + targets: ['tag', 'content'], + }) + const { mutedWords } = await agent.getPreferences() + expect(mutedWords.find((m) => m.value === 'hashtag')).toStrictEqual({ + value: 'hashtag', + targets: ['tag', 'content'], + }) + }) + + it('removeMutedWord', async () => { + await agent.removeMutedWord({ value: 'tag_then_content', targets: [] }) + await agent.removeMutedWord({ value: 'tag_then_both', targets: [] }) + await agent.removeMutedWord({ value: 'tag_then_none', targets: [] }) + const { mutedWords } = await agent.getPreferences() + + expect( + mutedWords.find((m) => m.value === 'tag_then_content'), + ).toBeFalsy() + expect(mutedWords.find((m) => m.value === 'tag_then_both')).toBeFalsy() + expect(mutedWords.find((m) => m.value === 'tag_then_none')).toBeFalsy() + }) + + it('removeMutedWord with #', async () => { + await agent.removeMutedWord({ value: '#hashtag', targets: [] }) + const { mutedWords } = await agent.getPreferences() + + expect(mutedWords.find((m) => m.value === 'hashtag')).toBeFalsy() + }) + }) + + describe('hidden posts', () => { + let agent: BskyAgent + const postUri = 'at://did:plc:fake/app.bsky.feed.post/fake' + + beforeAll(async () => { + agent = new BskyAgent({ service: network.pds.url }) + await agent.createAccount({ + handle: 'user8.test', + email: 'user8@test.com', + password: 'password', + }) + }) + + it('hidePost', async () => { + await agent.hidePost(postUri) + await agent.hidePost(postUri) // double, should dedupe + await expect(agent.getPreferences()).resolves.toHaveProperty( + 'hiddenPosts', + [postUri], + ) + }) + + it('unhidePost', async () => { + await agent.unhidePost(postUri) + await expect(agent.getPreferences()).resolves.toHaveProperty( + 'hiddenPosts', + [], + ) + // no issues calling a second time + await agent.unhidePost(postUri) + await expect(agent.getPreferences()).resolves.toHaveProperty( + 'hiddenPosts', + [], + ) + }) + }) + + // end }) }) diff --git a/packages/api/tests/rich-text-detection.test.ts b/packages/api/tests/rich-text-detection.test.ts index 9498005076c..b83a841405b 100644 --- a/packages/api/tests/rich-text-detection.test.ts +++ b/packages/api/tests/rich-text-detection.test.ts @@ -241,15 +241,16 @@ describe('detectFacets', () => { ['body #1', [], []], ['body #a1', ['a1'], [{ byteStart: 5, byteEnd: 8 }]], ['#', [], []], + ['#?', [], []], ['text #', [], []], ['text # text', [], []], [ - 'body #thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - ['thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], - [{ byteStart: 5, byteEnd: 71 }], + 'body #thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + ['thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], + [{ byteStart: 5, byteEnd: 70 }], ], [ - 'body #thisisa65characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab', + 'body #thisisa65characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab', [], [], ], @@ -297,6 +298,17 @@ describe('detectFacets', () => { { byteStart: 17, byteEnd: 22 }, ], ], + ['this #️⃣tag should not be a tag', [], []], + [ + 'this ##️⃣tag should be a tag', + ['#️⃣tag'], + [ + { + byteStart: 5, + byteEnd: 16, + }, + ], + ], ] for (const [input, tags, indices] of inputs) { diff --git a/packages/aws/CHANGELOG.md b/packages/aws/CHANGELOG.md index b804e0719e4..f8f0c521f18 100644 --- a/packages/aws/CHANGELOG.md +++ b/packages/aws/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/aws +## 0.1.7 + +### Patch Changes + +- Updated dependencies [[`fcf8e3faf`](https://github.com/bluesky-social/atproto/commit/fcf8e3faf311559162c3aa0d9af36f84951914bc)]: + - @atproto/repo@0.3.7 + ## 0.1.6 ### Patch Changes diff --git a/packages/aws/package.json b/packages/aws/package.json index 949cfaa845e..55638b88552 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/aws", - "version": "0.1.6", + "version": "0.1.7", "license": "MIT", "description": "Shared AWS cloud API helpers for atproto services", "keywords": [ diff --git a/packages/bsky/CHANGELOG.md b/packages/bsky/CHANGELOG.md index 8ce5294eb24..91a5493cb3f 100644 --- a/packages/bsky/CHANGELOG.md +++ b/packages/bsky/CHANGELOG.md @@ -1,5 +1,41 @@ # @atproto/bsky +## 0.0.33 + +### Patch Changes + +- Updated dependencies [[`514aab92d`](https://github.com/bluesky-social/atproto/commit/514aab92d26acd43859285f46318e386846522b1)]: + - @atproto/api@0.10.1 + +## 0.0.32 + +### Patch Changes + +- Updated dependencies [[`b60719480`](https://github.com/bluesky-social/atproto/commit/b60719480f5f00bffd074a40e8ddc03aa93d137d), [`4c511b3d9`](https://github.com/bluesky-social/atproto/commit/4c511b3d9de41ffeae3fc11db941e7df04f4468a)]: + - @atproto/api@0.10.0 + +## 0.0.31 + +### Patch Changes + +- Updated dependencies [[`f79cc6339`](https://github.com/bluesky-social/atproto/commit/f79cc63390ae9dbd47a4ff5d694eec25b78b788e)]: + - @atproto/api@0.9.8 + +## 0.0.30 + +### Patch Changes + +- Updated dependencies [[`fcf8e3faf`](https://github.com/bluesky-social/atproto/commit/fcf8e3faf311559162c3aa0d9af36f84951914bc), [`8c94979f7`](https://github.com/bluesky-social/atproto/commit/8c94979f73fc5057449e24e66ef2e09b0e17e55b)]: + - @atproto/repo@0.3.7 + - @atproto/api@0.9.7 + +## 0.0.29 + +### Patch Changes + +- Updated dependencies [[`e4ec7af03`](https://github.com/bluesky-social/atproto/commit/e4ec7af03608949fc3b00a845f547a77599b5ad0)]: + - @atproto/api@0.9.6 + ## 0.0.28 ### Patch Changes diff --git a/packages/bsky/bin/migration-create.ts b/packages/bsky/bin/migration-create.ts index b51c536c4f2..27edc5e0979 100644 --- a/packages/bsky/bin/migration-create.ts +++ b/packages/bsky/bin/migration-create.ts @@ -14,7 +14,15 @@ export async function main() { ) } const filename = `${prefix}-${name}` - const dir = path.join(__dirname, '..', 'src', 'db', 'migrations') + const dir = path.join( + __dirname, + '..', + 'src', + 'data-plane', + 'server', + 'db', + 'migrations', + ) await fs.writeFile(path.join(dir, `${filename}.ts`), template, { flag: 'wx' }) await fs.writeFile( diff --git a/packages/bsky/build.js b/packages/bsky/build.js index 3822d9bc98f..85c4a88243b 100644 --- a/packages/bsky/build.js +++ b/packages/bsky/build.js @@ -5,7 +5,7 @@ const buildShallow = require('esbuild').build({ logLevel: 'info', - entryPoints: ['src/index.ts', 'src/db/index.ts'], + entryPoints: ['src/index.ts'], bundle: true, sourcemap: true, outdir: 'dist', diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 7187e17f1bd..099501296ed 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.28", + "version": "0.0.33", "license": "MIT", "description": "Reference implementation of app.bsky App View (Bluesky API)", "keywords": [ @@ -42,6 +42,7 @@ "@atproto/xrpc-server": "workspace:^", "@bufbuild/protobuf": "^1.5.0", "@connectrpc/connect": "^1.1.4", + "@connectrpc/connect-express": "^1.1.4", "@connectrpc/connect-node": "^1.1.4", "@did-plc/lib": "^0.0.1", "@isaacs/ttlcache": "^1.4.1", @@ -80,6 +81,7 @@ "@types/express-serve-static-core": "^4.17.36", "@types/pg": "^8.6.6", "@types/qs": "^6.9.7", - "axios": "^0.27.2" + "axios": "^0.27.2", + "http2-express-bridge": "^1.0.7" } } diff --git a/packages/bsky/proto/bsky.proto b/packages/bsky/proto/bsky.proto new file mode 100644 index 00000000000..ffff8efa043 --- /dev/null +++ b/packages/bsky/proto/bsky.proto @@ -0,0 +1,1164 @@ +syntax = "proto3"; + +package bsky; +option go_package = "./;bsky"; + +import "google/protobuf/timestamp.proto"; + +// +// Read Path +// + +message Record { + bytes record = 1; + string cid = 2; + google.protobuf.Timestamp indexed_at = 4; + bool taken_down = 5; + google.protobuf.Timestamp created_at = 6; + google.protobuf.Timestamp sorted_at = 7; + string takedown_ref = 8; +} + +message GetBlockRecordsRequest { + repeated string uris = 1; +} + +message GetBlockRecordsResponse { + repeated Record records = 1; +} + +message GetFeedGeneratorRecordsRequest { + repeated string uris = 1; +} + +message GetFeedGeneratorRecordsResponse { + repeated Record records = 1; +} + +message GetFollowRecordsRequest { + repeated string uris = 1; +} + +message GetFollowRecordsResponse { + repeated Record records = 1; +} + +message GetLikeRecordsRequest { + repeated string uris = 1; +} + +message GetLikeRecordsResponse { + repeated Record records = 1; +} + +message GetListBlockRecordsRequest { + repeated string uris = 1; +} + +message GetListBlockRecordsResponse { + repeated Record records = 1; +} + +message GetListItemRecordsRequest { + repeated string uris = 1; +} + +message GetListItemRecordsResponse { + repeated Record records = 1; +} + +message GetListRecordsRequest { + repeated string uris = 1; +} + +message GetListRecordsResponse { + repeated Record records = 1; +} + +message PostRecordMeta { + bool violates_thread_gate = 1; + bool has_media = 2; + bool is_reply = 3; +} + +message GetPostRecordsRequest { + repeated string uris = 1; +} + +message GetPostRecordsResponse { + repeated Record records = 1; + repeated PostRecordMeta meta = 2; +} + +message GetProfileRecordsRequest { + repeated string uris = 1; +} + +message GetProfileRecordsResponse { + repeated Record records = 1; +} + +message GetRepostRecordsRequest { + repeated string uris = 1; +} + +message GetRepostRecordsResponse { + repeated Record records = 1; +} + +message GetThreadGateRecordsRequest { + repeated string uris = 1; +} + +message GetThreadGateRecordsResponse { + repeated Record records = 1; +} + + +// +// Follows +// + +// - Return follow uris where user A follows users B, C, D, … +// - E.g. for viewer state on `getProfiles` +message GetActorFollowsActorsRequest { + string actor_did = 1; + repeated string target_dids = 2; +} + +message GetActorFollowsActorsResponse { + repeated string uris = 1; +} + +// - Return follow uris of users who follows user A +// - For `getFollowers` list +message GetFollowersRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message FollowInfo { + string uri = 1; + string actor_did = 2; + string subject_did = 3; +} + +message GetFollowersResponse { + repeated FollowInfo followers = 1; + string cursor = 2; +} + +// - Return follow uris of users A follows +// - For `getFollows` list +message GetFollowsRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetFollowsResponse { + repeated FollowInfo follows = 1; + string cursor = 2; +} + +// +// Likes +// + +// - return like uris where subject uri is subject A +// - `getLikes` list for a post +message GetLikesBySubjectRequest { + RecordRef subject = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetLikesBySubjectResponse { + repeated string uris = 1; + string cursor = 2; +} + +// - return like uris for user A on subject B, C, D... +// - viewer state on posts +message GetLikesByActorAndSubjectsRequest { + string actor_did = 1; + repeated RecordRef refs = 2; +} + +message GetLikesByActorAndSubjectsResponse { + repeated string uris = 1; +} + +// - return recent like uris for user A +// - `getActorLikes` list for a user +message GetActorLikesRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message LikeInfo { + string uri = 1; + string subject = 2; +} + +message GetActorLikesResponse { + repeated LikeInfo likes = 1; + string cursor = 2; +} + +// +// Interactions +// +message GetInteractionCountsRequest { + repeated RecordRef refs = 1; +} + +message GetInteractionCountsResponse { + repeated int32 likes = 1; + repeated int32 reposts = 2; + repeated int32 replies = 3; +} + +message GetCountsForUsersRequest { + repeated string dids = 1; +} + +message GetCountsForUsersResponse { + repeated int32 posts = 1; + repeated int32 reposts = 2; + repeated int32 following = 3; + repeated int32 followers = 4; +} + +// +// Reposts +// + +// - return repost uris where subject uri is subject A +// - `getReposts` list for a post +message GetRepostsBySubjectRequest { + RecordRef subject = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetRepostsBySubjectResponse { + repeated string uris = 1; + string cursor = 2; +} + +// - return repost uris for user A on subject B, C, D... +// - viewer state on posts +message GetRepostsByActorAndSubjectsRequest { + string actor_did = 1; + repeated RecordRef refs = 2; +} + +message RecordRef { + string uri = 1; + string cid = 2; +} + +message GetRepostsByActorAndSubjectsResponse { + repeated string uris = 1; +} + +// - return recent repost uris for user A +// - `getActorReposts` list for a user +message GetActorRepostsRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetActorRepostsResponse { + repeated string uris = 1; + string cursor = 2; +} + +// +// Profile +// + +// - return actor information for dids A, B, C… +// - profile hydration +// - should this include handles? apply repo takedown? +message GetActorsRequest { + repeated string dids = 1; +} + +message ActorInfo { + bool exists = 1; + string handle = 2; + Record profile = 3; + bool taken_down = 4; + string takedown_ref = 5; + google.protobuf.Timestamp tombstoned_at = 6; +} + +message GetActorsResponse { + repeated ActorInfo actors = 1; +} + +// - return did for handle A +// - `resolveHandle` +// - answering queries where the query param is a handle +message GetDidsByHandlesRequest { + repeated string handles = 1; +} + +message GetDidsByHandlesResponse { + repeated string dids = 1; +} + +// +// Relationships +// + +// - return relationships between user A and users B, C, D... +// - profile hydration +// - block application +message GetRelationshipsRequest { + string actor_did = 1; + repeated string target_dids = 2; +} + +message Relationships { + bool muted = 1; + string muted_by_list = 2; + string blocked_by = 3; + string blocking = 4; + string blocked_by_list = 5; + string blocking_by_list = 6; + string following = 7; + string followed_by = 8; +} + +message GetRelationshipsResponse { + repeated Relationships relationships = 1; +} + +// - return whether a block (bidrectionally and either direct or through a list) exists between two dids +// - enforcing 3rd party block violations +message RelationshipPair { + string a = 1; + string b = 2; +} + +message GetBlockExistenceRequest { + repeated RelationshipPair pairs = 1; +} + +message GetBlockExistenceResponse { + repeated bool exists = 1; +} + + +// +// Lists +// + +message ListItemInfo { + string uri = 1; + string did = 2; +} + +// - Return dids of users in list A +// - E.g. to view items in one of your mute lists +message GetListMembersRequest { + string list_uri = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetListMembersResponse { + repeated ListItemInfo listitems = 1; + string cursor = 2; +} + +// - Return list uris where user A in list B, C, D… +// - Used in thread reply gates +message GetListMembershipRequest { + string actor_did = 1; + repeated string list_uris = 2; +} + +message GetListMembershipResponse { + repeated string listitem_uris = 1; +} + +// - Return number of items in list A +// - For aggregate +message GetListCountRequest { + string list_uri = 1; +} + +message GetListCountResponse { + int32 count = 1; +} + + +// - return list of uris of lists created by A +// - `getLists` +message GetActorListsRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetActorListsResponse { + repeated string list_uris = 1; + string cursor = 2; +} + +// +// Mutes +// + +// - return boolean if user A has muted user B +// - hydrating mute state onto profiles +message GetActorMutesActorRequest { + string actor_did = 1; + string target_did = 2; +} + +message GetActorMutesActorResponse { + bool muted = 1; +} + +// - return list of user dids of users who A mutes +// - `getMutes` +message GetMutesRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetMutesResponse { + repeated string dids = 1; + string cursor = 2; +} + +// +// Mutelists +// + +// - return list uri of *any* list through which user A has muted user B +// - hydrating mute state onto profiles +// - note: we only need *one* uri even if a user is muted by multiple lists +message GetActorMutesActorViaListRequest { + string actor_did = 1; + string target_did = 2; +} + +message GetActorMutesActorViaListResponse { + string list_uri = 1; +} + +// - return boolean if actor A has subscribed to mutelist B +// - list view hydration +message GetMutelistSubscriptionRequest { + string actor_did = 1; + string list_uri = 2; +} + +message GetMutelistSubscriptionResponse { + bool subscribed = 1; +} + +// - return list of list uris of mutelists that A subscribes to +// - `getListMutes` +message GetMutelistSubscriptionsRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetMutelistSubscriptionsResponse { + repeated string list_uris = 1; + string cursor = 2; +} + +// +// Blocks +// + +// - Return block uri if there is a block between users A & B (bidirectional) +// - hydrating (& actioning) block state on profiles +// - handling 3rd party blocks +message GetBidirectionalBlockRequest { + string actor_did = 1; + string target_did = 2; +} + +message GetBidirectionalBlockResponse { + string block_uri = 1; +} + +// - Return list of block uris and user dids of users who A blocks +// - `getBlocks` +message GetBlocksRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetBlocksResponse { + repeated string block_uris = 1; + string cursor = 2; +} + +// +// Blocklists +// + +// - Return list uri of ***any*** list through which users A & B have a block (bidirectional) +// - hydrating (& actioning) block state on profiles +// - handling 3rd party blocks +message GetBidirectionalBlockViaListRequest { + string actor_did = 1; + string target_did = 2; +} + +message GetBidirectionalBlockViaListResponse { + string list_uri = 1; +} + +// - return boolean if user A has subscribed to blocklist B +// - list view hydration +message GetBlocklistSubscriptionRequest { + string actor_did = 1; + string list_uri = 2; +} + +message GetBlocklistSubscriptionResponse { + string listblock_uri = 1; +} + +// - return list of list uris of Blockslists that A subscribes to +// - `getListBlocks` +message GetBlocklistSubscriptionsRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetBlocklistSubscriptionsResponse { + repeated string list_uris = 1; + string cursor = 2; +} + +// +// Notifications +// + +// - list recent notifications for a user +// - notifications should include a uri for the record that caused the notif & a “reason” for the notification (reply, like, quotepost, etc) +// - this should include both read & unread notifs +message GetNotificationsRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message Notification { + string recipient_did = 1; + string uri = 2; + string reason = 3; + string reason_subject = 4; + google.protobuf.Timestamp timestamp = 5; +} + +message GetNotificationsResponse { + repeated Notification notifications = 1; + string cursor = 2; +} + +// - update a user’s “last seen time” +// - `updateSeen` +message UpdateNotificationSeenRequest { + string actor_did = 1; + google.protobuf.Timestamp timestamp = 2; +} + +message UpdateNotificationSeenResponse {} + +// - get a user’s “last seen time” +// - hydrating read state onto notifications +message GetNotificationSeenRequest { + string actor_did = 1; +} + +message GetNotificationSeenResponse { + google.protobuf.Timestamp timestamp = 1; +} + +// - get a count of all unread notifications (notifications after `updateSeen`) +// - `getUnreadCount` +message GetUnreadNotificationCountRequest { + string actor_did = 1; +} + +message GetUnreadNotificationCountResponse { + int32 count = 1; +} + +// +// FeedGenerators +// + +// - Return uris of feed generator records created by user A +// - `getActorFeeds` +message GetActorFeedsRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetActorFeedsResponse { + repeated string uris = 1; + string cursor = 2; +} + +// - Returns a list of suggested feed generator uris for an actor, paginated +// - `getSuggestedFeeds` +// - This is currently just hardcoded in the Appview DB +message GetSuggestedFeedsRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetSuggestedFeedsResponse { + repeated string uris = 1; + string cursor = 2; +} + +message SearchFeedGeneratorsRequest { + string query = 1; + int32 limit = 2; +} + +message SearchFeedGeneratorsResponse { + repeated string uris = 1; +} + +// - Returns feed generator validity and online status with uris A, B, C… +// - Not currently being used, but could be worhthwhile. +message GetFeedGeneratorStatusRequest { + repeated string uris = 1; +} + +message GetFeedGeneratorStatusResponse { + repeated string status = 1; +} + +// +// Feeds +// + +enum FeedType { + FEED_TYPE_UNSPECIFIED = 0; + FEED_TYPE_POSTS_AND_AUTHOR_THREADS = 1; + FEED_TYPE_POSTS_NO_REPLIES = 2; + FEED_TYPE_POSTS_WITH_MEDIA = 3; +} + +// - Returns recent posts authored by a given DID, paginated +// - `getAuthorFeed` +// - Optionally: filter by if a post is/isn’t a reply and if a post has a media object in it +message GetAuthorFeedRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; + FeedType feed_type = 4; +} + +message AuthorFeedItem { + string uri = 1; + string cid = 2; + string repost = 3; + string repost_cid = 4; + bool posts_and_author_threads = 5; + bool posts_no_replies = 6; + bool posts_with_media = 7; + bool is_reply = 8; + bool is_repost = 9; + bool is_quote_post = 10; +} + +message GetAuthorFeedResponse { + repeated AuthorFeedItem items = 1; + string cursor = 2; +} + +// - Returns recent posts authored by users followed by a given DID, paginated +// - `getTimeline` +message GetTimelineRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; + bool exclude_replies = 4; + bool exclude_reposts = 5; + bool exclude_quotes = 6; +} + +message GetTimelineResponse { + repeated TimelineFeedItem items = 1; + string cursor = 2; +} + +message TimelineFeedItem { + string uri = 1; + string cid = 2; + string repost = 3; + string repost_cid = 4; + bool is_reply = 5; + bool is_repost = 6; + bool is_quote_post = 7; +} + +// - Return recent post uris from users in list A +// - `getListFeed` +// - (This is essentially the same as `getTimeline` but instead of follows of a did, it is list items of a list) +message GetListFeedRequest { + string list_uri = 1; + int32 limit = 2; + string cursor = 3; + bool exclude_replies = 4; + bool exclude_reposts = 5; + bool exclude_quotes = 6; +} + +message GetListFeedResponse { + repeated TimelineFeedItem items = 1; + string cursor = 2; +} + +// +// Threads +// + +// Return posts uris of any replies N levels above or M levels below post A +message GetThreadRequest { + string post_uri = 1; + int32 above = 2; + int32 below = 3; +} + +message GetThreadResponse { + repeated string uris = 1; +} + +// +// Search +// + +// - Return DIDs of actors matching term, paginated +// - `searchActors` skeleton +message SearchActorsRequest { + string term = 1; + int32 limit = 2; + string cursor = 3; +} + +message SearchActorsResponse { + repeated string dids = 1; + string cursor = 2; +} + +// - Return uris of posts matching term, paginated +// - `searchPosts` skeleton +message SearchPostsRequest { + string term = 1; + int32 limit = 2; + string cursor = 3; +} + +message SearchPostsResponse { + repeated string uris = 1; + string cursor = 2; +} + +// +// Suggestions +// + +// - Return DIDs of suggested follows for a user, excluding anyone they already follow +// - `getSuggestions`, `getSuggestedFollowsByActor` +message GetFollowSuggestionsRequest { + string actor_did = 1; + string relative_to_did = 2; + int32 limit = 3; + string cursor = 4; +} + +message GetFollowSuggestionsResponse { + repeated string dids = 1; + string cursor = 2; +} + +message SuggestedEntity { + string tag = 1; + string subject = 2; + string subject_type = 3; + int64 priority = 4; +} + +message GetSuggestedEntitiesRequest { + int32 limit = 1; + string cursor = 2; +} + +message GetSuggestedEntitiesResponse { + repeated SuggestedEntity entities = 1; + string cursor = 2; +} + +// +// Posts +// + +// - Return post reply count with uris A, B, C… +// - All feed hydration +message GetPostReplyCountsRequest { + repeated RecordRef refs = 1; +} + +message GetPostReplyCountsResponse { + repeated int32 counts = 1; +} + +// +// Labels +// + +// - Get all labels on a subjects A, B, C (uri or did) issued by dids D, E, F… +// - label hydration on nearly every view +message GetLabelsRequest { + repeated string subjects = 1; + repeated string issuers = 2; +} + +message GetLabelsResponse { + repeated bytes labels = 1; +} + +// +// Sync +// + +// - Latest repo rev of user w/ DID +// - Read-after-write header in`getProfile`, `getProfiles`, `getActorLikes`, `getAuthorFeed`, `getListFeed`, `getPostThread`, `getTimeline`. Could it be view dependent? +message GetLatestRevRequest { + string actor_did = 1; +} + +message GetLatestRevResponse { + string rev = 1; +} + + +message GetIdentityByDidRequest { + string did = 1; +} +message GetIdentityByDidResponse { + string did = 1; + string handle = 2; + bytes keys = 3; + bytes services = 4; + google.protobuf.Timestamp updated = 5; +} + +message GetIdentityByHandleRequest { + string handle = 1; +} +message GetIdentityByHandleResponse { + string handle = 1; + string did = 2; + bytes keys = 3; + bytes services = 4; + google.protobuf.Timestamp updated = 5; +} + + + +// +// Moderation +// + +message GetBlobTakedownRequest { + string did = 1; + string cid = 2; +} + +message GetBlobTakedownResponse { + bool taken_down = 1; + string takedown_ref = 2; +} + + + +message GetActorTakedownRequest { + string did = 1; +} + +message GetActorTakedownResponse { + bool taken_down = 1; + string takedown_ref = 2; +} + +message GetRecordTakedownRequest { + string record_uri = 1; +} + +message GetRecordTakedownResponse { + bool taken_down = 1; + string takedown_ref = 2; +} + + +// Ping +message PingRequest {} +message PingResponse {} + + + + +service Service { + // + // Read Path + // + + // Records + rpc GetBlockRecords(GetBlockRecordsRequest) returns (GetBlockRecordsResponse); + rpc GetFeedGeneratorRecords(GetFeedGeneratorRecordsRequest) returns (GetFeedGeneratorRecordsResponse); + rpc GetFollowRecords(GetFollowRecordsRequest) returns (GetFollowRecordsResponse); + rpc GetLikeRecords(GetLikeRecordsRequest) returns (GetLikeRecordsResponse); + rpc GetListBlockRecords(GetListBlockRecordsRequest) returns (GetListBlockRecordsResponse); + rpc GetListItemRecords(GetListItemRecordsRequest) returns (GetListItemRecordsResponse); + rpc GetListRecords(GetListRecordsRequest) returns (GetListRecordsResponse); + rpc GetPostRecords(GetPostRecordsRequest) returns (GetPostRecordsResponse); + rpc GetProfileRecords(GetProfileRecordsRequest) returns (GetProfileRecordsResponse); + rpc GetRepostRecords(GetRepostRecordsRequest) returns (GetRepostRecordsResponse); + rpc GetThreadGateRecords(GetThreadGateRecordsRequest) returns (GetThreadGateRecordsResponse); + + // Follows + rpc GetActorFollowsActors(GetActorFollowsActorsRequest) returns (GetActorFollowsActorsResponse); + rpc GetFollowers(GetFollowersRequest) returns (GetFollowersResponse); + rpc GetFollows(GetFollowsRequest) returns (GetFollowsResponse); + + // Likes + rpc GetLikesBySubject(GetLikesBySubjectRequest) returns (GetLikesBySubjectResponse); + rpc GetLikesByActorAndSubjects(GetLikesByActorAndSubjectsRequest) returns (GetLikesByActorAndSubjectsResponse); + rpc GetActorLikes(GetActorLikesRequest) returns (GetActorLikesResponse); + + // Reposts + rpc GetRepostsBySubject(GetRepostsBySubjectRequest) returns (GetRepostsBySubjectResponse); + rpc GetRepostsByActorAndSubjects(GetRepostsByActorAndSubjectsRequest) returns (GetRepostsByActorAndSubjectsResponse); + rpc GetActorReposts(GetActorRepostsRequest) returns (GetActorRepostsResponse); + + // Interaction Counts + rpc GetInteractionCounts(GetInteractionCountsRequest) returns (GetInteractionCountsResponse); + rpc GetCountsForUsers(GetCountsForUsersRequest) returns (GetCountsForUsersResponse); + + // Profile + rpc GetActors(GetActorsRequest) returns (GetActorsResponse); + rpc GetDidsByHandles(GetDidsByHandlesRequest) returns (GetDidsByHandlesResponse); + + // Relationships + rpc GetRelationships(GetRelationshipsRequest) returns (GetRelationshipsResponse); + rpc GetBlockExistence(GetBlockExistenceRequest) returns (GetBlockExistenceResponse); + + // Lists + rpc GetActorLists(GetActorListsRequest) returns (GetActorListsResponse); + rpc GetListMembers(GetListMembersRequest) returns (GetListMembersResponse); + rpc GetListMembership(GetListMembershipRequest) returns (GetListMembershipResponse); + rpc GetListCount(GetListCountRequest) returns (GetListCountResponse); + + // Mutes + rpc GetActorMutesActor(GetActorMutesActorRequest) returns (GetActorMutesActorResponse); + rpc GetMutes(GetMutesRequest) returns (GetMutesResponse); + + // Mutelists + rpc GetActorMutesActorViaList(GetActorMutesActorViaListRequest) returns (GetActorMutesActorViaListResponse); + rpc GetMutelistSubscription(GetMutelistSubscriptionRequest) returns (GetMutelistSubscriptionResponse); + rpc GetMutelistSubscriptions(GetMutelistSubscriptionsRequest) returns (GetMutelistSubscriptionsResponse); + + // Blocks + rpc GetBidirectionalBlock(GetBidirectionalBlockRequest) returns (GetBidirectionalBlockResponse); + rpc GetBlocks(GetBlocksRequest) returns (GetBlocksResponse); + + // Blocklists + rpc GetBidirectionalBlockViaList(GetBidirectionalBlockViaListRequest) returns (GetBidirectionalBlockViaListResponse); + rpc GetBlocklistSubscription(GetBlocklistSubscriptionRequest) returns (GetBlocklistSubscriptionResponse); + rpc GetBlocklistSubscriptions(GetBlocklistSubscriptionsRequest) returns (GetBlocklistSubscriptionsResponse); + + // Notifications + rpc GetNotifications(GetNotificationsRequest) returns (GetNotificationsResponse); + rpc GetNotificationSeen(GetNotificationSeenRequest) returns (GetNotificationSeenResponse); + rpc GetUnreadNotificationCount(GetUnreadNotificationCountRequest) returns (GetUnreadNotificationCountResponse); + rpc UpdateNotificationSeen(UpdateNotificationSeenRequest) returns (UpdateNotificationSeenResponse); + + // FeedGenerators + rpc GetActorFeeds(GetActorFeedsRequest) returns (GetActorFeedsResponse); + rpc GetSuggestedFeeds(GetSuggestedFeedsRequest) returns (GetSuggestedFeedsResponse); + rpc GetFeedGeneratorStatus(GetFeedGeneratorStatusRequest) returns (GetFeedGeneratorStatusResponse); + rpc SearchFeedGenerators(SearchFeedGeneratorsRequest) returns (SearchFeedGeneratorsResponse); + + // Feeds + rpc GetAuthorFeed(GetAuthorFeedRequest) returns (GetAuthorFeedResponse); + rpc GetTimeline(GetTimelineRequest) returns (GetTimelineResponse); + rpc GetListFeed(GetListFeedRequest) returns (GetListFeedResponse); + + // Threads + rpc GetThread(GetThreadRequest) returns (GetThreadResponse); + + // Search + rpc SearchActors(SearchActorsRequest) returns (SearchActorsResponse); + rpc SearchPosts(SearchPostsRequest) returns (SearchPostsResponse); + + // Suggestions + rpc GetFollowSuggestions(GetFollowSuggestionsRequest) returns (GetFollowSuggestionsResponse); + rpc GetSuggestedEntities(GetSuggestedEntitiesRequest) returns (GetSuggestedEntitiesResponse); + + // Posts + rpc GetPostReplyCounts(GetPostReplyCountsRequest) returns (GetPostReplyCountsResponse); + + // Labels + rpc GetLabels(GetLabelsRequest) returns (GetLabelsResponse); + + // Sync + rpc GetLatestRev(GetLatestRevRequest) returns (GetLatestRevResponse); + + // Moderation + rpc GetBlobTakedown(GetBlobTakedownRequest) returns (GetBlobTakedownResponse); + rpc GetRecordTakedown(GetRecordTakedownRequest) returns (GetRecordTakedownResponse); + rpc GetActorTakedown(GetActorTakedownRequest) returns (GetActorTakedownResponse); + + // Identity + rpc GetIdentityByDid(GetIdentityByDidRequest) returns (GetIdentityByDidResponse); + rpc GetIdentityByHandle(GetIdentityByHandleRequest) returns (GetIdentityByHandleResponse); + + // Ping + rpc Ping(PingRequest) returns (PingResponse); + + + + + // + // Write Path + // + + // Moderation + rpc TakedownBlob(TakedownBlobRequest) returns (TakedownBlobResponse); + rpc TakedownRecord(TakedownRecordRequest) returns (TakedownRecordResponse); + rpc TakedownActor(TakedownActorRequest) returns (TakedownActorResponse); + + rpc UntakedownBlob(UntakedownBlobRequest) returns (UntakedownBlobResponse); + rpc UntakedownRecord(UntakedownRecordRequest) returns (UntakedownRecordResponse); + rpc UntakedownActor(UntakedownActorRequest) returns (UntakedownActorResponse); + + // Ingestion + rpc CreateActorMute(CreateActorMuteRequest) returns (CreateActorMuteResponse); + rpc DeleteActorMute(DeleteActorMuteRequest) returns (DeleteActorMuteResponse); + rpc ClearActorMutes(ClearActorMutesRequest) returns (ClearActorMutesResponse); + + rpc CreateActorMutelistSubscription(CreateActorMutelistSubscriptionRequest) returns (CreateActorMutelistSubscriptionResponse); + rpc DeleteActorMutelistSubscription(DeleteActorMutelistSubscriptionRequest) returns (DeleteActorMutelistSubscriptionResponse); + rpc ClearActorMutelistSubscriptions(ClearActorMutelistSubscriptionsRequest) returns (ClearActorMutelistSubscriptionsResponse); +} + + +message TakedownActorRequest { + string did = 1; + string ref = 2; + google.protobuf.Timestamp seen = 3; +} + +message TakedownActorResponse { +} + +message UntakedownActorRequest { + string did = 1; + google.protobuf.Timestamp seen = 2; +} + +message UntakedownActorResponse { +} + +message TakedownBlobRequest { + string did = 1; + string cid = 2; + string ref = 3; + google.protobuf.Timestamp seen = 4; +} + +message TakedownBlobResponse {} + +message UntakedownBlobRequest { + string did = 1; + string cid = 2; + google.protobuf.Timestamp seen = 3; +} + +message UntakedownBlobResponse {} + +message TakedownRecordRequest { + string record_uri = 1; + string ref = 2; + google.protobuf.Timestamp seen = 3; +} + +message TakedownRecordResponse { +} + +message UntakedownRecordRequest { + string record_uri = 1; + google.protobuf.Timestamp seen = 2; +} + +message UntakedownRecordResponse { +} + +message CreateActorMuteRequest { + string actor_did = 1; + string subject_did = 2; +} + +message CreateActorMuteResponse {} + +message DeleteActorMuteRequest { + string actor_did = 1; + string subject_did = 2; +} + +message DeleteActorMuteResponse {} + +message ClearActorMutesRequest { + string actor_did = 1; +} + +message ClearActorMutesResponse {} + +message CreateActorMutelistSubscriptionRequest { + string actor_did = 1; + string subject_uri = 2; +} + +message CreateActorMutelistSubscriptionResponse {} + +message DeleteActorMutelistSubscriptionRequest { + string actor_did = 1; + string subject_uri = 2; +} + +message DeleteActorMutelistSubscriptionResponse {} + +message ClearActorMutelistSubscriptionsRequest { + string actor_did = 1; +} + +message ClearActorMutelistSubscriptionsResponse {} diff --git a/packages/bsky/src/api/app/bsky/actor/getProfile.ts b/packages/bsky/src/api/app/bsky/actor/getProfile.ts index 47c2f8f8ca7..386313aa5cb 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfile.ts @@ -1,108 +1,85 @@ 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' -import { ModerationService } from '../../../../services/moderation' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { const getProfile = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.actor.getProfile({ auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ auth, params, res }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const modService = ctx.services.moderation(ctx.db.getPrimary()) const { viewer, canViewTakedowns } = ctx.authVerifier.parseCreds(auth) - const [result, repoRev] = await Promise.allSettled([ - getProfile( - { ...params, viewer, canViewTakedowns }, - { db, actorService, modService }, - ), - actorService.getRepoRev(viewer), - ]) + const result = await getProfile( + { ...params, viewer, canViewTakedowns }, + ctx, + ) - if (repoRev.status === 'fulfilled') { - setRepoRev(res, repoRev.value) - } - if (result.status === 'rejected') { - throw result.reason - } + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer) + setRepoRev(res, repoRev) return { encoding: 'application/json', - body: result.value, + body: result, } }, }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { actorService } = ctx - const { canViewTakedowns } = params - const actor = await actorService.getActor(params.actor, true) - if (!actor) { +const skeleton = async (input: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = input + const [did] = await ctx.hydrator.actor.getDids([params.actor]) + if (!did) { throw new InvalidRequestError('Profile not found') } - if (!canViewTakedowns && softDeleted(actor)) { - if (actor.takedownRef?.includes('SUSPEND')) { - throw new InvalidRequestError( - 'Account has been temporarily suspended', - 'AccountTakedown', - ) - } else { - throw new InvalidRequestError( - 'Account has been taken down', - 'AccountTakedown', - ) - } - } - return { params, actor } + return { did } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { actorService } = ctx - const { params, actor } = state - const { viewer, canViewTakedowns } = params - const hydration = await actorService.views.profileDetailHydration( - [actor.did], - { viewer, includeSoftDeleted: canViewTakedowns }, +const hydration = async (input: { + ctx: Context + params: Params + skeleton: SkeletonState +}) => { + const { ctx, params, skeleton } = input + return ctx.hydrator.hydrateProfilesDetailed( + [skeleton.did], + params.viewer, + true, ) - 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] +const presentation = (input: { + ctx: Context + params: Params + skeleton: SkeletonState + hydration: HydrationState +}) => { + const { ctx, params, skeleton, hydration } = input + const profile = ctx.views.profileDetailed(skeleton.did, hydration) if (!profile) { throw new InvalidRequestError('Profile not found') + } else if ( + !params.canViewTakedowns && + ctx.views.actorIsTakendown(skeleton.did, hydration) + ) { + throw new InvalidRequestError( + 'Account has been suspended', + 'AccountTakedown', + ) } return profile } type Context = { - db: Database - actorService: ActorService - modService: ModerationService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -110,6 +87,4 @@ type Params = QueryParams & { canViewTakedowns: boolean } -type SkeletonState = { params: Params; actor: Actor } - -type HydrationState = SkeletonState & ProfileDetailHydrationState +type SkeletonState = { did: string } diff --git a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts index 21ca13949d2..7a71340d892 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts @@ -2,28 +2,21 @@ 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' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { const getProfile = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.actor.getProfiles({ auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params, res }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) const viewer = auth.credentials.iss - const [result, repoRev] = await Promise.all([ - getProfile({ ...params, viewer }, { db, actorService }), - actorService.getRepoRev(viewer), - ]) + const result = await getProfile({ ...params, viewer }, ctx) + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer) setRepoRev(res, repoRev) return { @@ -34,45 +27,44 @@ export default function (server: Server, ctx: AppContext) { }) } -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 skeleton = async (input: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = input + const dids = await ctx.hydrator.actor.getDidsDefined(params.actors) + return { dids } } -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 hydration = async (input: { + ctx: Context + params: Params + skeleton: SkeletonState +}) => { + const { ctx, params, skeleton } = input + return ctx.hydrator.hydrateProfilesDetailed(skeleton.dids, params.viewer) } -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 } +const presentation = (input: { + ctx: Context + params: Params + skeleton: SkeletonState + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = input + const profiles = mapDefined(skeleton.dids, (did) => + ctx.views.profileDetailed(did, hydration), + ) + return { profiles } } type Context = { - db: Database - actorService: ActorService + hydrator: Hydrator + views: Views } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { params: Params; dids: string[] } - -type HydrationState = SkeletonState & ProfileDetailHydrationState +type SkeletonState = { dids: string[] } diff --git a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts index 3bfeccfa28e..622fcd3891c 100644 --- a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts @@ -1,13 +1,12 @@ 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' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' export default function (server: Server, ctx: AppContext) { const getSuggestions = createPipeline( @@ -19,15 +18,8 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getSuggestions({ auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) const viewer = auth.credentials.iss - - const result = await getSuggestions( - { ...params, viewer }, - { db, actorService, graphService }, - ) + const result = await getSuggestions({ ...params, viewer }, ctx) return { encoding: 'application/json', @@ -37,114 +29,80 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db } = ctx - const { viewer } = params - const alreadyIncluded = parseCursor(params.cursor) // @NOTE handles bad cursor e.g. on appview swap - const { ref } = db.db.dynamic - const suggestions = await db.db - .selectFrom('suggested_follow') - .innerJoin('actor', 'actor.did', 'suggested_follow.did') - .where(notSoftDeletedClause(ref('actor'))) - .where('suggested_follow.did', '!=', viewer ?? '') - .whereNotExists((qb) => - qb - .selectFrom('follow') - .selectAll() - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('actor.did')), - ) - .if(alreadyIncluded.length > 0, (qb) => - qb.where('suggested_follow.order', 'not in', alreadyIncluded), - ) - .selectAll() - .orderBy('suggested_follow.order', 'asc') - .execute() - - // always include first two - const firstTwo = suggestions.filter( - (row) => row.order === 1 || row.order === 2, - ) - const rest = suggestions.filter((row) => row.order !== 1 && row.order !== 2) - const limited = firstTwo.concat(shuffle(rest)).slice(0, params.limit) - - // if the result set ends up getting larger, consider using a seed included in the cursor for for the randomized shuffle - const cursor = - limited.length > 0 - ? limited - .map((row) => row.order.toString()) - .concat(alreadyIncluded.map((id) => id.toString())) - .join(':') - : undefined - - return { params, suggestions: limited, cursor } -} - -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 skeleton = async (input: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = input + // @NOTE for appview swap moving to rkey-based cursors which are somewhat permissive, should not hard-break pagination + const suggestions = await ctx.dataplane.getFollowSuggestions({ + actorDid: params.viewer ?? undefined, + cursor: params.cursor, + limit: params.limit, + }) + let dids = suggestions.dids + if (params.viewer !== null) { + const follows = await ctx.dataplane.getActorFollowsActors({ + actorDid: params.viewer, + targetDids: dids, + }) + dids = dids.filter((did, i) => !follows.uris[i] && did !== params.viewer) + } + return { dids, cursor: parseString(suggestions.cursor) } } -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]), +const hydration = async (input: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = input + return ctx.hydrator.hydrateProfilesDetailed( + skeleton.dids, + params.viewer, + true, ) - return state } -const presentation = (state: HydrationState) => { - const { suggestions, actors, cursor } = state - const suggestedActors = mapDefined(suggestions, (sug) => actors[sug.did]) - return { actors: suggestedActors, cursor } +const noBlocksOrMutes = (input: { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = input + skeleton.dids = skeleton.dids.filter( + (did) => + !ctx.views.viewerBlockExists(did, hydration) && + !ctx.views.viewerMuteExists(did, hydration), + ) + return skeleton } -const parseCursor = (cursor?: string): number[] => { - if (!cursor) { - return [] - } - try { - return cursor - .split(':') - .map((id) => parseInt(id, 10)) - .filter((id) => !isNaN(id)) - } catch { - return [] +const presentation = (input: { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = input + const actors = mapDefined(skeleton.dids, (did) => + ctx.views.profile(did, hydration), + ) + return { + actors, + cursor: skeleton.cursor, } } -const shuffle = (arr: T[]): T[] => { - return arr - .map((value) => ({ value, sort: Math.random() })) - .sort((a, b) => a.sort - b.sort) - .map(({ value }) => value) -} - type Context = { - db: Database - actorService: ActorService - graphService: GraphService + dataplane: DataPlaneClient + hydrator: Hydrator + views: Views } -type Params = QueryParams & { viewer: string | null } - -type SkeletonState = { params: Params; suggestions: Actor[]; cursor?: string } - -type HydrationState = SkeletonState & { - bam: BlockAndMuteState - actors: ActorInfoMap +type Params = QueryParams & { + viewer: string | null } + +type Skeleton = { dids: string[]; cursor?: string } diff --git a/packages/bsky/src/api/app/bsky/actor/searchActors.ts b/packages/bsky/src/api/app/bsky/actor/searchActors.ts index 82f0327ef89..403be45892a 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActors.ts @@ -1,56 +1,111 @@ import AppContext from '../../../../context' import { Server } from '../../../../lexicon' -import { cleanQuery } from '../../../../services/util/search' +import { mapDefined } from '@atproto/common' +import AtpAgent from '@atproto/api' +import { QueryParams } from '../../../../lexicon/types/app/bsky/actor/searchActors' +import { + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' export default function (server: Server, ctx: AppContext) { + const searchActors = createPipeline( + skeleton, + hydration, + noBlocks, + presentation, + ) server.app.bsky.actor.searchActors({ auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { - const { cursor, limit } = params - const requester = auth.credentials.iss - const rawQuery = params.q ?? params.term - const query = cleanQuery(rawQuery || '') - const db = ctx.db.getReplica('search') - - let results: string[] - let resCursor: string | undefined - if (ctx.searchAgent) { - // @NOTE cursors wont change on appview swap - const res = - await ctx.searchAgent.api.app.bsky.unspecced.searchActorsSkeleton({ - q: query, - cursor, - limit, - }) - results = res.data.actors.map((a) => a.did) - resCursor = res.data.cursor - } else { - const res = await ctx.services - .actor(ctx.db.getReplica('search')) - .getSearchResults({ query, limit, cursor }) - results = res.results.map((a) => a.did) - resCursor = res.cursor + const viewer = auth.credentials.iss + const results = await searchActors({ ...params, viewer }, ctx) + return { + encoding: 'application/json', + body: results, } + }, + }) +} + +const skeleton = async (inputs: SkeletonFnInput) => { + const { ctx, params } = inputs + const term = params.q ?? params.term ?? '' - const actors = await ctx.services - .actor(db) - .views.profiles(results, requester) + // @TODO + // add hits total - const SKIP = [] - const filtered = results.flatMap((did) => { - const actor = actors[did] - if (!actor) return SKIP - if (actor.viewer?.blocking || actor.viewer?.blockedBy) return SKIP - return actor + if (ctx.searchAgent) { + // @NOTE cursors wont change on appview swap + const { data: res } = + await ctx.searchAgent.api.app.bsky.unspecced.searchActorsSkeleton({ + q: term, + cursor: params.cursor, + limit: params.limit, }) + return { + dids: res.actors.map(({ did }) => did), + cursor: parseString(res.cursor), + } + } - return { - encoding: 'application/json', - body: { - cursor: resCursor, - actors: filtered, - }, - } - }, + const res = await ctx.dataplane.searchActors({ + term, + limit: params.limit, + cursor: params.cursor, }) + return { + dids: res.dids, + cursor: parseString(res.cursor), + } +} + +const hydration = async ( + inputs: HydrationFnInput, +) => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydrateProfiles(skeleton.dids, params.viewer) +} + +const noBlocks = (inputs: RulesFnInput) => { + const { ctx, skeleton, hydration } = inputs + skeleton.dids = skeleton.dids.filter( + (did) => !ctx.views.viewerBlockExists(did, hydration), + ) + return skeleton +} + +const presentation = ( + inputs: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = inputs + const actors = mapDefined(skeleton.dids, (did) => + ctx.views.profile(did, hydration), + ) + return { + actors, + cursor: skeleton.cursor, + } +} + +type Context = { + dataplane: DataPlaneClient + hydrator: Hydrator + views: Views + searchAgent?: AtpAgent +} + +type Params = QueryParams & { viewer: string | null } + +type Skeleton = { + dids: string[] + hitsTotal?: number + cursor?: string } diff --git a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts index 6a3167fd2d0..8c7507961a7 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -1,56 +1,107 @@ import AppContext from '../../../../context' import { Server } from '../../../../lexicon' +import AtpAgent from '@atproto/api' +import { mapDefined } from '@atproto/common' +import { QueryParams } from '../../../../lexicon/types/app/bsky/actor/searchActorsTypeahead' import { - cleanQuery, - getUserSearchQuerySimple, -} from '../../../../services/util/search' + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' export default function (server: Server, ctx: AppContext) { + const searchActorsTypeahead = createPipeline( + skeleton, + hydration, + noBlocks, + presentation, + ) server.app.bsky.actor.searchActorsTypeahead({ auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { - const { limit } = params - const requester = auth.credentials.iss - const rawQuery = params.q ?? params.term - const query = cleanQuery(rawQuery || '') - const db = ctx.db.getReplica('search') - - let results: string[] - if (ctx.searchAgent) { - const res = - await ctx.searchAgent.api.app.bsky.unspecced.searchActorsSkeleton({ - q: query, - typeahead: true, - limit, - }) - results = res.data.actors.map((a) => a.did) - } else { - const res = query - ? await getUserSearchQuerySimple(db, { query, limit }) - .selectAll('actor') - .execute() - : [] - results = res.map((a) => a.did) + const viewer = auth.credentials.iss + const results = await searchActorsTypeahead({ ...params, viewer }, ctx) + return { + encoding: 'application/json', + body: results, } + }, + }) +} + +const skeleton = async (inputs: SkeletonFnInput) => { + const { ctx, params } = inputs + const term = params.q ?? params.term ?? '' - const actors = await ctx.services - .actor(db) - .views.profilesBasic(results, requester) + // @TODO + // add typeahead option + // add hits total - const SKIP = [] - const filtered = results.flatMap((did) => { - const actor = actors[did] - if (!actor) return SKIP - if (actor.viewer?.blocking || actor.viewer?.blockedBy) return SKIP - return actor + if (ctx.searchAgent) { + const { data: res } = + await ctx.searchAgent.api.app.bsky.unspecced.searchActorsSkeleton({ + typeahead: true, + q: term, + limit: params.limit, }) + return { + dids: res.actors.map(({ did }) => did), + cursor: parseString(res.cursor), + } + } - return { - encoding: 'application/json', - body: { - actors: filtered, - }, - } - }, + const res = await ctx.dataplane.searchActors({ + term, + limit: params.limit, }) + return { + dids: res.dids, + cursor: parseString(res.cursor), + } +} + +const hydration = async ( + inputs: HydrationFnInput, +) => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydrateProfilesBasic(skeleton.dids, params.viewer) +} + +const noBlocks = (inputs: RulesFnInput) => { + const { ctx, skeleton, hydration } = inputs + skeleton.dids = skeleton.dids.filter( + (did) => !ctx.views.viewerBlockExists(did, hydration), + ) + return skeleton +} + +const presentation = ( + inputs: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = inputs + const actors = mapDefined(skeleton.dids, (did) => + ctx.views.profileBasic(did, hydration), + ) + return { + actors, + } +} + +type Context = { + dataplane: DataPlaneClient + hydrator: Hydrator + views: Views + searchAgent?: AtpAgent +} + +type Params = QueryParams & { viewer: string | null } + +type Skeleton = { + dids: string[] } diff --git a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts index 266839c7711..b138ae1acad 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts @@ -1,69 +1,91 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getActorFeeds' import AppContext from '../../../../context' -import { TimeCidKeyset, paginate } from '../../../../db/pagination' +import { createPipeline, noRules } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' +import { clearlyBadCursor } from '../../../util' export default function (server: Server, ctx: AppContext) { + const getActorFeeds = createPipeline( + skeleton, + hydration, + noRules, + presentation, + ) server.app.bsky.feed.getActorFeeds({ auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { - const { actor, limit, cursor } = params const viewer = auth.credentials.iss - if (TimeCidKeyset.clearlyBad(cursor)) { - return { - encoding: 'application/json', - body: { feeds: [] }, - } + const result = await getActorFeeds({ ...params, viewer }, ctx) + return { + encoding: 'application/json', + body: result, } + }, + }) +} - 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 skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + if (clearlyBadCursor(params.cursor)) { + return { feedUris: [] } + } + const [did] = await ctx.hydrator.actor.getDids([params.actor]) + if (!did) { + throw new InvalidRequestError('Profile not found') + } + const feedsRes = await ctx.dataplane.getActorFeeds({ + actorDid: did, + cursor: params.cursor, + limit: params.limit, + }) + return { + feedUris: feedsRes.uris, + cursor: parseString(feedsRes.cursor), + } +} - const { ref } = db.db.dynamic - let feedsQb = feedService - .selectFeedGeneratorQb(viewer) - .where('feed_generator.creator', '=', creatorRes.did) +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return await ctx.hydrator.hydrateFeedGens(skeleton.feedUris, params.viewer) +} - const keyset = new TimeCidKeyset( - ref('feed_generator.createdAt'), - ref('feed_generator.cid'), - ) - feedsQb = paginate(feedsQb, { - limit, - cursor, - keyset, - }) +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feeds = mapDefined(skeleton.feedUris, (uri) => + ctx.views.feedGenerator(uri, hydration), + ) + return { + feeds, + cursor: skeleton.cursor, + } +} - const [feedsRes, profiles] = await Promise.all([ - feedsQb.execute(), - actorService.views.profiles([creatorRes], viewer), - ]) - if (!profiles[creatorRes.did]) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } +type Context = { + hydrator: Hydrator + views: Views + dataplane: DataPlaneClient +} - const feeds = mapDefined(feedsRes, (row) => { - const feed = { - ...row, - viewer: viewer ? { like: row.viewerLike } : undefined, - } - return feedService.views.formatFeedGeneratorView(feed, profiles) - }) +type Params = QueryParams & { viewer: string | null } - return { - encoding: 'application/json', - body: { - cursor: keyset.packFromResult(feedsRes), - feeds, - }, - } - }, - }) +type Skeleton = { + feedUris: string[] + cursor?: string } diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 48d6437e494..4f026418bc4 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -1,19 +1,16 @@ import { InvalidRequestError } from '@atproto/xrpc-server' +import { mapDefined } from '@atproto/common' 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 { 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 { clearlyBadCursor, setRepoRev } from '../../../util' import { createPipeline } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' +import { creatorFromUri } from '../../../../views/util' +import { FeedItem } from '../../../../hydration/feed' export default function (server: Server, ctx: AppContext) { const getActorLikes = createPipeline( @@ -26,19 +23,10 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth, res }) => { const viewer = auth.credentials.iss - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const feedService = ctx.services.feed(db) - const graphService = ctx.services.graph(db) - const [result, repoRev] = await Promise.all([ - getActorLikes( - { ...params, viewer }, - { db, actorService, feedService, graphService }, - ), - actorService.getRepoRev(viewer), - ]) + const result = await getActorLikes({ ...params, viewer }, ctx) + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer) setRepoRev(res, repoRev) return { @@ -49,81 +37,80 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db, actorService, feedService } = ctx +const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs const { actor, limit, cursor, viewer } = params - const { ref } = db.db.dynamic - - const actorRes = await actorService.getActor(actor) - if (!actorRes) { - throw new InvalidRequestError('Profile not found') + if (clearlyBadCursor(cursor)) { + return { items: [] } } - const actorDid = actorRes.did - - if (!viewer || viewer !== actorDid) { + const [actorDid] = await ctx.hydrator.actor.getDids([actor]) + if (!actorDid || !viewer || viewer !== actorDid) { throw new InvalidRequestError('Profile not found') } - if (FeedKeyset.clearlyBad(cursor)) { - return { params, feedItems: [] } - } - - let feedItemsQb = feedService - .selectFeedItemQb() - .innerJoin('like', 'like.subject', 'feed_item.uri') - .where('like.creator', '=', actorDid) - - const keyset = new FeedKeyset(ref('like.sortAt'), ref('like.cid')) - - feedItemsQb = paginate(feedItemsQb, { + const likesRes = await ctx.dataplane.getActorLikes({ + actorDid, limit, cursor, - keyset, }) - const feedItems = await feedItemsQb.execute() + const items = likesRes.likes.map((l) => ({ post: { uri: l.subject } })) - return { params, feedItems, cursor: keyset.packFromResult(feedItems) } + return { + items, + cursor: parseString(likesRes.cursor), + } } -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 hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return await ctx.hydrator.hydrateFeedItems(skeleton.items, params.viewer) } -const noPostBlocks = (state: HydrationState) => { - const { viewer } = state.params - state.feedItems = state.feedItems.filter( - (item) => !viewer || !state.bam.block([viewer, item.postAuthorDid]), - ) - return state +const noPostBlocks = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + skeleton.items = skeleton.items.filter((item) => { + const creator = creatorFromUri(item.post.uri) + return !ctx.views.viewerBlockExists(creator, hydration) + }) + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const { feedItems, cursor, params } = state - const feed = feedService.views.formatFeed(feedItems, state, params.viewer) - return { feed, cursor } +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feed = mapDefined(skeleton.items, (item) => + ctx.views.feedViewPost(item, hydration), + ) + return { + feed, + cursor: skeleton.cursor, + } } type Context = { - db: Database - feedService: FeedService - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views + dataplane: DataPlaneClient } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { params: Params; feedItems: FeedRow[]; cursor?: string } - -type HydrationState = SkeletonState & FeedHydrationState +type Skeleton = { + items: FeedItem[] + cursor?: string +} diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 6c783efdd0c..02e2240828f 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -1,19 +1,21 @@ +import { mapDefined } from '@atproto/common' 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 { 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 { clearlyBadCursor, setRepoRev } from '../../../util' import { createPipeline } from '../../../../pipeline' +import { + HydrationState, + Hydrator, + mergeStates, +} from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' +import { Actor } from '../../../../hydration/actor' +import { FeedItem } from '../../../../hydration/feed' +import { FeedType } from '../../../../proto/bsky_pb' export default function (server: Server, ctx: AppContext) { const getAuthorFeed = createPipeline( @@ -25,20 +27,14 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getAuthorFeed({ auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth, res }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const feedService = ctx.services.feed(db) - const graphService = ctx.services.graph(db) - const { viewer } = ctx.authVerifier.parseCreds(auth) + const { viewer, canViewTakedowns } = ctx.authVerifier.parseCreds(auth) - const [result, repoRev] = await Promise.all([ - getAuthorFeed( - { ...params, viewer }, - { db, actorService, feedService, graphService }, - ), - actorService.getRepoRev(viewer), - ]) + const result = await getAuthorFeed( + { ...params, viewer, includeTakedowns: canViewTakedowns }, + ctx, + ) + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer) setRepoRev(res, repoRev) return { @@ -49,131 +45,122 @@ export default function (server: Server, ctx: AppContext) { }) } -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 +const FILTER_TO_FEED_TYPE = { + posts_with_replies: undefined, // default: all posts, replies, and reposts + posts_no_replies: FeedType.POSTS_NO_REPLIES, + posts_with_media: FeedType.POSTS_WITH_MEDIA, + posts_and_author_threads: FeedType.POSTS_AND_AUTHOR_THREADS, +} - // maybe resolve did first - const actorRes = await actorService.getActor(actor) - if (!actorRes) { +export const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + const [did] = await ctx.hydrator.actor.getDids([params.actor]) + if (!did) { 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', - ) - } - } - - if (FeedKeyset.clearlyBad(cursor)) { - return { params, feedItems: [] } + const actors = await ctx.hydrator.actor.getActors( + [did], + params.includeTakedowns, + ) + const actor = actors.get(did) + if (!actor) { + throw new InvalidRequestError('Profile not found') } - - // defaults to posts, reposts, and replies - let feedItemsQb = feedService - .selectFeedItemQb() - .where('originatorDid', '=', actorDid) - - if (filter === 'posts_with_media') { - feedItemsQb = feedItemsQb - // only your own posts - .where('type', '=', 'post') - // 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'), - ) - } else if (filter === 'posts_and_author_threads') { - feedItemsQb = feedItemsQb.where((qb) => - qb - .where('type', '=', 'repost') - .orWhere('post.replyParent', 'is', null) - .orWhere('post.replyRoot', 'like', `at://${actorDid}/%`), - ) + if (clearlyBadCursor(params.cursor)) { + return { actor, items: [] } } - - const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid')) - - feedItemsQb = paginate(feedItemsQb, { - limit, - cursor, - keyset, + const res = await ctx.dataplane.getAuthorFeed({ + actorDid: did, + limit: params.limit, + cursor: params.cursor, + feedType: FILTER_TO_FEED_TYPE[params.filter], }) - - const feedItems = await feedItemsQb.execute() - return { - params, - feedItems, - cursor: keyset.packFromResult(feedItems), + actor, + items: res.items.map((item) => ({ + post: { uri: item.uri, cid: item.cid || undefined }, + repost: item.repost + ? { uri: item.repost, cid: item.repostCid || undefined } + : undefined, + })), + cursor: parseString(res.cursor), } } -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 hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}): Promise => { + const { ctx, params, skeleton } = inputs + const [feedPostState, profileViewerState = {}] = await Promise.all([ + ctx.hydrator.hydrateFeedItems( + skeleton.items, + params.viewer, + params.includeTakedowns, + ), + params.viewer + ? ctx.hydrator.hydrateProfileViewers([skeleton.actor.did], params.viewer) + : undefined, + ]) + return mergeStates(feedPostState, profileViewerState) } -const noBlocksOrMutedReposts = (state: HydrationState) => { - const { viewer } = state.params - state.feedItems = state.feedItems.filter((item) => { - if (!viewer) return true +const noBlocksOrMutedReposts = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}): Skeleton => { + const { ctx, skeleton, hydration } = inputs + const relationship = hydration.profileViewers?.get(skeleton.actor.did) + if (relationship?.blocking || relationship?.blockingByList) { + throw new InvalidRequestError( + `Requester has blocked actor: ${skeleton.actor.did}`, + 'BlockedActor', + ) + } + if (relationship?.blockedBy || relationship?.blockedByList) { + throw new InvalidRequestError( + `Requester is blocked by actor: ${skeleton.actor.did}`, + 'BlockedByActor', + ) + } + skeleton.items = skeleton.items.filter((item) => { + const bam = ctx.views.feedItemBlocksAndMutes(item, hydration) return ( - !state.bam.block([viewer, item.postAuthorDid]) && - (item.type === 'post' || !state.bam.mute([viewer, item.postAuthorDid])) + !bam.authorBlocked && + !bam.originatorBlocked && + !(bam.authorMuted && !bam.originatorMuted) ) }) - return state + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const { feedItems, cursor, params } = state - const feed = feedService.views.formatFeed(feedItems, state, params.viewer) - return { feed, cursor } +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feed = mapDefined(skeleton.items, (item) => + ctx.views.feedViewPost(item, hydration), + ) + return { feed, cursor: skeleton.cursor } } type Context = { - db: Database - actorService: ActorService - feedService: FeedService - graphService: GraphService + hydrator: Hydrator + views: Views + dataplane: DataPlaneClient } -type Params = QueryParams & { viewer: string | null } +type Params = QueryParams & { viewer: string | null; includeTakedowns: boolean } -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + actor: Actor + items: FeedItem[] 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 66461cd3bbb..aae633e4f2a 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -1,3 +1,4 @@ +import { mapDefined } from '@atproto/common' import { InvalidRequestError, UpstreamFailureError, @@ -5,25 +6,27 @@ import { serverTimingHeader, } from '@atproto/xrpc-server' import { ResponseType, XRPCError } from '@atproto/xrpc' -import { - DidDocument, - PoorlyFormattedDidDocumentError, - getFeedGen, -} from '@atproto/identity' import { AtpAgent, AppBskyFeedGetFeedSkeleton } from '@atproto/api' import { noUndefinedVals } from '@atproto/common' 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 { Database } from '../../../../db' import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' -import { createPipeline } from '../../../../pipeline' + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { FeedItem } from '../../../../hydration/feed' +import { GetIdentityByDidResponse } from '../../../../proto/bsky_pb' +import { + Code, + getServiceEndpoint, + isDataplaneError, + unpackIdentityServices, +} from '../../../../data-plane' export default function (server: Server, ctx: AppContext) { const getFeed = createPipeline( @@ -35,8 +38,6 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeed({ auth: ctx.authVerifier.standardOptionalAnyAud, handler: async ({ params, auth, req }) => { - const db = ctx.db.getReplica() - const feedService = ctx.services.feed(db) const viewer = auth.credentials.iss const headers = noUndefinedVals({ authorization: req.headers['authorization'], @@ -44,13 +45,8 @@ export default function (server: Server, ctx: AppContext) { }) // @NOTE feed cursors should not be affected by appview swap const { timerSkele, timerHydr, resHeaders, ...result } = await getFeed( - { ...params, viewer }, - { - db, - feedService, - appCtx: ctx, - headers, - }, + { ...params, viewer, headers }, + ctx, ) return { @@ -66,125 +62,113 @@ export default function (server: Server, ctx: AppContext) { } const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { + inputs: SkeletonFnInput, +): Promise => { + const { ctx, params } = inputs const timerSkele = new ServerTimer('skele').start() - const feedParams: GetFeedParams = { - feed: params.feed, - limit: params.limit, - cursor: params.cursor, - } - const { feedItems, cursor, resHeaders, ...passthrough } = - await skeletonFromFeedGen(ctx, feedParams) + const { + feedItems: algoItems, + cursor, + resHeaders, + ...passthrough + } = await skeletonFromFeedGen(ctx, params) + return { - params, cursor, - feedItems, + items: algoItems.map(toFeedItem), timerSkele: timerSkele.stop(), + timerHydr: new ServerTimer('hydr').start(), resHeaders, passthrough, } } -const hydration = async (state: SkeletonState, ctx: Context) => { +const hydration = async ( + inputs: HydrationFnInput, +) => { + const { ctx, params, skeleton } = inputs 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 hydration = await ctx.hydrator.hydrateFeedItems( + skeleton.items, + params.viewer, + ) + skeleton.timerHydr = timerHydr.stop() + return hydration } -const noBlocksOrMutes = (state: HydrationState) => { - const { viewer } = state.params - state.feedItems = state.feedItems.filter((item) => { - if (!viewer) return true +const noBlocksOrMutes = (inputs: RulesFnInput) => { + const { ctx, skeleton, hydration } = inputs + skeleton.items = skeleton.items.filter((item) => { + const bam = ctx.views.feedItemBlocksAndMutes(item, hydration) return ( - !state.bam.block([viewer, item.postAuthorDid]) && - !state.bam.block([viewer, item.originatorDid]) && - !state.bam.mute([viewer, item.postAuthorDid]) && - !state.bam.mute([viewer, item.originatorDid]) + !bam.authorBlocked && + !bam.authorMuted && + !bam.originatorBlocked && + !bam.originatorMuted ) }) - return state + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const { feedItems, cursor, passthrough, params } = state - const feed = feedService.views.formatFeed(feedItems, state, params.viewer) +const presentation = ( + inputs: PresentationFnInput, +) => { + const { ctx, params, skeleton, hydration } = inputs + const feed = mapDefined(skeleton.items, (item) => { + return ctx.views.feedViewPost(item, hydration) + }).slice(0, params.limit) return { feed, - cursor, - timerSkele: state.timerSkele, - timerHydr: state.timerHydr, - resHeaders: state.resHeaders, - ...passthrough, + cursor: skeleton.cursor, + timerSkele: skeleton.timerSkele, + timerHydr: skeleton.timerHydr, + resHeaders: skeleton.resHeaders, + ...skeleton.passthrough, } } -type Context = { - db: Database - feedService: FeedService - appCtx: AppContext +type Context = AppContext + +type Params = GetFeedParams & { + viewer: string | null headers: Record } -type Params = GetFeedParams & { viewer: string | null } - -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + items: FeedItem[] passthrough: Record // pass through additional items in feedgen response resHeaders?: Record cursor?: string timerSkele: ServerTimer -} - -type HydrationState = SkeletonState & - FeedHydrationState & { feedItems: FeedRow[]; timerHydr: ServerTimer } - -type AlgoResponse = { - feedItems: FeedRow[] - resHeaders?: Record - cursor?: string + timerHydr: ServerTimer } const skeletonFromFeedGen = async ( ctx: Context, - params: GetFeedParams, + params: Params, ): Promise => { - const { db, appCtx, headers } = ctx - const { feed } = params - // Resolve and fetch feed skeleton - const found = await db.db - .selectFrom('feed_generator') - .where('uri', '=', feed) - .select('feedDid') - .executeTakeFirst() - if (!found) { + const { feed, headers } = params + const found = await ctx.hydrator.feed.getFeedGens([feed], true) + const feedDid = await found.get(feed)?.record.did + if (!feedDid) { throw new InvalidRequestError('could not find feed') } - const feedDid = found.feedDid - let resolved: DidDocument | null + let identity: GetIdentityByDidResponse try { - resolved = await appCtx.idResolver.did.resolve(feedDid) + identity = await ctx.dataplane.getIdentityByDid({ did: feedDid }) } catch (err) { - if (err instanceof PoorlyFormattedDidDocumentError) { - throw new InvalidRequestError(`invalid did document: ${feedDid}`) + if (isDataplaneError(err, Code.NotFound)) { + throw new InvalidRequestError(`could not resolve identity: ${feedDid}`) } throw err } - if (!resolved) { - throw new InvalidRequestError(`could not resolve did document: ${feedDid}`) - } - const fgEndpoint = getFeedGen(resolved) + const services = unpackIdentityServices(identity.services) + const fgEndpoint = getServiceEndpoint(services, { + id: 'bsky_fg', + type: 'BskyFeedGenerator', + }) if (!fgEndpoint) { throw new InvalidRequestError( `invalid feed generator service details in did document: ${feedDid}`, @@ -197,9 +181,16 @@ const skeletonFromFeedGen = async ( let resHeaders: Record | undefined = undefined try { // @TODO currently passthrough auth headers from pds - const result = await agent.api.app.bsky.feed.getFeedSkeleton(params, { - headers, - }) + const result = await agent.api.app.bsky.feed.getFeedSkeleton( + { + feed: params.feed, + limit: params.limit, + cursor: params.cursor, + }, + { + headers, + }, + ) skeleton = result.data if (result.headers['content-language']) { resHeaders = { @@ -225,33 +216,30 @@ const skeletonFromFeedGen = async ( } const { feed: feedSkele, ...skele } = skeleton - const feedItems = await skeletonToFeedItems( - feedSkele.slice(0, params.limit), - ctx, - ) + const feedItems = feedSkele.map((item) => ({ + itemUri: + typeof item.reason?.repost === 'string' ? item.reason.repost : item.post, + postUri: item.post, + })) return { ...skele, resHeaders, 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 +export type AlgoResponse = { + feedItems: AlgoResponseItem[] + resHeaders?: Record + cursor?: string } -const getSkeleFeedItemUri = (item: SkeletonFeedPost) => { - return typeof item.reason?.repost === 'string' - ? item.reason.repost - : item.post +export type AlgoResponseItem = { + itemUri: string + postUri: string } + +export const toFeedItem = (feedItem: AlgoResponseItem): FeedItem => ({ + post: { uri: feedItem.postUri }, + repost: + feedItem.itemUri === feedItem.postUri + ? undefined + : { uri: feedItem.itemUri }, +}) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts index 125af1db9b9..57f86af9a28 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts @@ -1,11 +1,13 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { - DidDocument, - PoorlyFormattedDidDocumentError, - getFeedGen, -} from '@atproto/identity' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { GetIdentityByDidResponse } from '../../../../proto/bsky_pb' +import { + Code, + getServiceEndpoint, + isDataplaneError, + unpackIdentityServices, +} from '../../../../data-plane' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedGenerator({ @@ -14,47 +16,37 @@ export default function (server: Server, ctx: AppContext) { const { feed } = params const viewer = auth.credentials.iss - 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] + const hydration = await ctx.hydrator.hydrateFeedGens([feed], viewer) + const feedInfo = hydration.feedgens?.get(feed) if (!feedInfo) { throw new InvalidRequestError('could not find feed') } - const feedDid = feedInfo.feedDid - let resolved: DidDocument | null + const feedDid = feedInfo.record.did + let identity: GetIdentityByDidResponse try { - resolved = await ctx.idResolver.did.resolve(feedDid) + identity = await ctx.dataplane.getIdentityByDid({ did: feedDid }) } catch (err) { - if (err instanceof PoorlyFormattedDidDocumentError) { - throw new InvalidRequestError(`invalid did document: ${feedDid}`) + if (isDataplaneError(err, Code.NotFound)) { + throw new InvalidRequestError( + `could not resolve identity: ${feedDid}`, + ) } throw err } - if (!resolved) { - throw new InvalidRequestError( - `could not resolve did document: ${feedDid}`, - ) - } - const fgEndpoint = getFeedGen(resolved) + const services = unpackIdentityServices(identity.services) + const fgEndpoint = getServiceEndpoint(services, { + id: 'bsky_fg', + type: 'BskyFeedGenerator', + }) if (!fgEndpoint) { throw new InvalidRequestError( `invalid feed generator service details in did document: ${feedDid}`, ) } - const profiles = await actorService.views.profilesBasic( - [feedInfo.creator], - viewer, - ) - const feedView = feedService.views.formatFeedGeneratorView( - feedInfo, - profiles, - ) + const feedView = ctx.views.feedGenerator(feed, hydration) if (!feedView) { throw new InvalidRequestError('could not find feed') } diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts index ed6df5760cb..34547948204 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts @@ -1,10 +1,10 @@ import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getFeedGenerators' 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' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { const getFeedGenerators = createPipeline( @@ -16,17 +16,8 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedGenerators({ auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { - const { feeds } = params const viewer = auth.credentials.iss - const db = ctx.db.getReplica() - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) - - const view = await getFeedGenerators( - { feeds, viewer }, - { db, feedService, actorService }, - ) - + const view = await getFeedGenerators({ ...params, viewer }, ctx) return { encoding: 'application/json', body: view, @@ -35,46 +26,42 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async (params: Params, ctx: Context) => { - const { feedService } = ctx - const genInfos = await feedService.getFeedGeneratorInfos( - params.feeds, - params.viewer, - ) +const skeleton = async (inputs: { params: Params }): Promise => { return { - params, - generators: Object.values(genInfos), + feedUris: inputs.params.feeds, } } -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 hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return await ctx.hydrator.hydrateFeedGens(skeleton.feedUris, params.viewer) } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const feeds = mapDefined(state.generators, (gen) => - feedService.views.formatFeedGeneratorView(gen, state.profiles), +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feeds = mapDefined(skeleton.feedUris, (uri) => + ctx.views.feedGenerator(uri, hydration), ) - return { feeds } + return { + feeds, + } } type Context = { - db: Database - feedService: FeedService - actorService: ActorService + hydrator: Hydrator + views: Views } -type Params = { viewer: string | null; feeds: string[] } +type Params = QueryParams & { viewer: string | null } -type SkeletonState = { params: Params; generators: FeedGenInfo[] } - -type HydrationState = SkeletonState & { profiles: ActorInfoMap } +type Skeleton = { + feedUris: string[] +} diff --git a/packages/bsky/src/api/app/bsky/feed/getLikes.ts b/packages/bsky/src/api/app/bsky/feed/getLikes.ts index 2d59656a517..582dd78a073 100644 --- a/packages/bsky/src/api/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getLikes.ts @@ -1,29 +1,22 @@ import { mapDefined } from '@atproto/common' +import { normalizeDatetimeAlways } from '@atproto/syntax' 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' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { parseString } from '../../../../hydration/util' +import { creatorFromUri } from '../../../../views/util' +import { clearlyBadCursor } from '../../../util' export default function (server: Server, ctx: AppContext) { const getLikes = createPipeline(skeleton, hydration, noBlocks, presentation) server.app.bsky.feed.getLikes({ auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) const viewer = auth.credentials.iss - - const result = await getLikes( - { ...params, viewer }, - { db, actorService, graphService }, - ) + const result = await getLikes({ ...params, viewer }, ctx) return { encoding: 'application/json', @@ -33,99 +26,86 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db } = ctx - const { uri, cid, limit, cursor } = params - const { ref } = db.db.dynamic - - if (TimeCidKeyset.clearlyBad(cursor)) { - return { params, likes: [] } - } - - 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 skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + if (clearlyBadCursor(params.cursor)) { + return { likes: [] } } - - const keyset = new TimeCidKeyset(ref('like.sortAt'), ref('like.cid')) - builder = paginate(builder, { - limit, - cursor, - keyset, + const likesRes = await ctx.hydrator.dataplane.getLikesBySubject({ + subject: { uri: params.uri, cid: params.cid }, + cursor: params.cursor, + limit: params.limit, }) - - const likes = await builder.execute() - - return { params, likes, cursor: keyset.packFromResult(likes) } + return { + likes: likesRes.uris, + cursor: parseString(likesRes.cursor), + } } -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 hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return await ctx.hydrator.hydrateLikes(skeleton.likes, params.viewer) } -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 noBlocks = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + skeleton.likes = skeleton.likes.filter((uri) => { + const creator = creatorFromUri(uri) + return !ctx.views.viewerBlockExists(creator, hydration) + }) + return skeleton } -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 } +const presentation = (inputs: { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, params, skeleton, hydration } = inputs + const likeViews = mapDefined(skeleton.likes, (uri) => { + const like = hydration.likes?.get(uri) + if (!like || !like.record) { + return + } + const creatorDid = creatorFromUri(uri) + const actor = ctx.views.profile(creatorDid, hydration) + if (!actor) { + return + } + return { + actor, + createdAt: normalizeDatetimeAlways(like.record.createdAt), + indexedAt: like.sortedAt.toISOString(), + } + }) + return { + likes: likeViews, + cursor: skeleton.cursor, + uri: params.uri, + cid: params.cid, + } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - likes: (Actor & { createdAt: string })[] +type Skeleton = { + likes: string[] cursor?: string } - -type HydrationState = SkeletonState & { - bam: BlockAndMuteState - actors: ActorInfoMap -} diff --git a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts index 478d9b08efa..ec182e39ac5 100644 --- a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts @@ -1,18 +1,14 @@ import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getListFeed' -import { FeedKeyset, getFeedDateThreshold } from '../util/feed' -import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' -import { setRepoRev } from '../../../util' -import { Database } from '../../../../db' -import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' -import { ActorService } from '../../../../services/actor' -import { GraphService } from '../../../../services/graph' +import { clearlyBadCursor, setRepoRev } from '../../../util' import { createPipeline } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { mapDefined } from '@atproto/common' +import { parseString } from '../../../../hydration/util' +import { FeedItem } from '../../../../hydration/feed' export default function (server: Server, ctx: AppContext) { const getListFeed = createPipeline( @@ -25,19 +21,10 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth, res }) => { const viewer = auth.credentials.iss - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const feedService = ctx.services.feed(db) - const graphService = ctx.services.graph(db) - const [result, repoRev] = await Promise.all([ - getListFeed( - { ...params, viewer }, - { db, actorService, feedService, graphService }, - ), - actorService.getRepoRev(viewer), - ]) + const result = await getListFeed({ ...params, viewer }, ctx) + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer) setRepoRev(res, repoRev) return { @@ -48,84 +35,78 @@ export default function (server: Server, ctx: AppContext) { }) } -export const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { list, cursor, limit } = params - const { db } = ctx - const { ref } = db.db.dynamic - - if (FeedKeyset.clearlyBad(cursor)) { - return { params, feedItems: [] } +export const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + if (clearlyBadCursor(params.cursor)) { + return { items: [] } } - - const keyset = new FeedKeyset(ref('post.sortAt'), ref('post.cid')) - const sortFrom = keyset.unpack(cursor)?.primary - - let builder = ctx.feedService - .selectPostQb() - .innerJoin('list_item', 'list_item.subjectDid', 'post.creator') - .where('list_item.listUri', '=', list) - .where('post.sortAt', '>', getFeedDateThreshold(sortFrom, 3)) - - builder = paginate(builder, { - limit, - cursor, - keyset, - tryIndex: true, + const res = await ctx.dataplane.getListFeed({ + listUri: params.list, + limit: params.limit, + cursor: params.cursor, }) - const feedItems = await builder.execute() - return { - params, - feedItems, - cursor: keyset.packFromResult(feedItems), + items: res.items.map((item) => ({ + post: { uri: item.uri, cid: item.cid || undefined }, + repost: item.repost + ? { uri: item.repost, cid: item.repostCid || undefined } + : undefined, + })), + cursor: parseString(res.cursor), } } -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 hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}): Promise => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydrateFeedItems(skeleton.items, params.viewer) } -const noBlocksOrMutes = (state: HydrationState) => { - const { viewer } = state.params - if (!viewer) return state - state.feedItems = state.feedItems.filter( - (item) => - !state.bam.block([viewer, item.postAuthorDid]) && - !state.bam.mute([viewer, item.postAuthorDid]), - ) - return state +const noBlocksOrMutes = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}): Skeleton => { + const { ctx, skeleton, hydration } = inputs + skeleton.items = skeleton.items.filter((item) => { + const bam = ctx.views.feedItemBlocksAndMutes(item, hydration) + return ( + !bam.authorBlocked && + !bam.authorMuted && + !bam.originatorBlocked && + !bam.originatorMuted + ) + }) + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const { feedItems, cursor, params } = state - const feed = feedService.views.formatFeed(feedItems, state, params.viewer) - return { feed, cursor } +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feed = mapDefined(skeleton.items, (item) => + ctx.views.feedViewPost(item, hydration), + ) + return { feed, cursor: skeleton.cursor } } type Context = { - db: Database - actorService: ActorService - feedService: FeedService - graphService: GraphService + hydrator: Hydrator + views: Views + dataplane: DataPlaneClient } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + items: FeedItem[] cursor?: string } - -type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 18d9d3124d0..cfe59e290fb 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -1,27 +1,22 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { AtUri } from '@atproto/syntax' import { Server } from '../../../../lexicon' +import { isNotFoundPost } from '../../../../lexicon/types/app/bsky/feed/defs' import { - BlockedPost, - NotFoundPost, - ThreadViewPost, - isNotFoundPost, -} from '../../../../lexicon/types/app/bsky/feed/defs' -import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getPostThread' + QueryParams, + OutputSchema, +} 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' -import { ActorInfoMap, ActorService } from '../../../../services/actor' -import { createPipeline, noRules } from '../../../../pipeline' +import { + HydrationFnInput, + PresentationFnInput, + SkeletonFnInput, + createPipeline, + noRules, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient, isDataplaneError, Code } from '../../../../data-plane' export default function (server: Server, ctx: AppContext) { const getPostThread = createPipeline( @@ -34,299 +29,85 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth, res }) => { const { viewer } = ctx.authVerifier.parseCreds(auth) - const db = ctx.db.getReplica('thread') - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) - const [result, repoRev] = await Promise.allSettled([ - getPostThread({ ...params, viewer }, { db, feedService, actorService }), - actorService.getRepoRev(viewer), - ]) - - if (repoRev.status === 'fulfilled') { - setRepoRev(res, repoRev.value) - } - if (result.status === 'rejected') { - throw result.reason + let result: OutputSchema + try { + result = await getPostThread({ ...params, viewer }, ctx) + } catch (err) { + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer) + setRepoRev(res, repoRev) + throw err } + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer) + setRepoRev(res, repoRev) + return { encoding: 'application/json', - body: result.value, + body: result, } }, }) } -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, - params.viewer, - ) - const thread = composeThread( - state.threadData, - actors, - state, - ctx, - params.viewer, - ) - 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, - actors: ActorInfoMap, - state: HydrationState, - ctx: Context, - viewer: string | null, -) => { - const { feedService } = ctx - const { posts, threadgates, embeds, blocks, labels, lists } = state - - const post = feedService.views.formatPostView( - threadData.post.postUri, - actors, - posts, - threadgates, - embeds, - labels, - lists, - viewer, - ) - - // replies that are invalid due to reply-gating: - // a. may appear as the anchor post, but without any parent or replies. - // b. may not appear anywhere else in the thread. - const isAnchorPost = state.threadData.post.uri === threadData.post.postUri - const info = posts[threadData.post.postUri] - // @TODO re-enable invalidReplyRoot check - // const badReply = !!info?.invalidReplyRoot || !!info?.violatesThreadGate - const badReply = !!info?.violatesThreadGate - const violatesBlock = (post && blocks[post.uri]?.reply) ?? false - const omitBadReply = !isAnchorPost && (badReply || violatesBlock) - - if (!post || omitBadReply) { - return { - $type: 'app.bsky.feed.defs#notFoundPost', - uri: threadData.post.postUri, - notFound: true, - } - } - - if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { +const skeleton = async (inputs: SkeletonFnInput) => { + const { ctx, params } = inputs + try { + const res = await ctx.dataplane.getThread({ + postUri: params.uri, + above: params.parentHeight, + below: params.depth, + }) return { - $type: 'app.bsky.feed.defs#blockedPost', - uri: threadData.post.postUri, - blocked: true, - author: { - did: post.author.did, - viewer: post.author.viewer - ? { - blockedBy: post.author.viewer?.blockedBy, - blocking: post.author.viewer?.blocking, - } - : undefined, - }, + anchor: params.uri, + uris: res.uris, } - } - - let parent - if (threadData.parent && !badReply && !violatesBlock) { - if (threadData.parent instanceof ParentNotFoundError) { - parent = { - $type: 'app.bsky.feed.defs#notFoundPost', - uri: threadData.parent.uri, - notFound: true, + } catch (err) { + if (isDataplaneError(err, Code.NotFound)) { + return { + anchor: params.uri, + uris: [], } } else { - parent = composeThread(threadData.parent, actors, state, ctx, viewer) + throw err } } - - let replies: (ThreadViewPost | NotFoundPost | BlockedPost)[] | undefined - if (threadData.replies && !badReply) { - replies = threadData.replies.flatMap((reply) => { - const thread = composeThread(reply, actors, state, ctx, viewer) - // e.g. don't bother including #postNotFound reply placeholders for takedowns. either way matches api contract. - const skip = [] - return isNotFoundPost(thread) ? skip : thread - }) - } - - return { - $type: 'app.bsky.feed.defs#threadViewPost', - post, - parent, - replies, - } } -const getRelevantIds = ( - thread: PostThread, -): { dids: Set; uris: Set } => { - const dids = new Set() - const uris = new Set() - if (thread.parent && !(thread.parent instanceof ParentNotFoundError)) { - const fromParent = getRelevantIds(thread.parent) - fromParent.dids.forEach((did) => dids.add(did)) - fromParent.uris.forEach((uri) => uris.add(uri)) - } - if (thread.replies) { - for (const reply of thread.replies) { - const fromChild = getRelevantIds(reply) - fromChild.dids.forEach((did) => dids.add(did)) - fromChild.uris.forEach((uri) => uris.add(uri)) - } - } - dids.add(thread.post.postAuthorDid) - uris.add(thread.post.postUri) - if (thread.post.replyRoot) { - // ensure root is included for checking interactions - uris.add(thread.post.replyRoot) - dids.add(new AtUri(thread.post.replyRoot).hostname) - } - return { dids, uris } -} - -const getThreadData = async ( - params: Params, - ctx: Context, -): Promise => { - const { db, feedService } = ctx - const { uri, depth, parentHeight } = params - - const [parents, children] = await Promise.all([ - getAncestorsAndSelfQb(db.db, { uri, parentHeight }) - .selectFrom('ancestor') - .innerJoin( - feedService.selectPostQb().as('post'), - 'post.uri', - 'ancestor.uri', - ) - .selectAll('post') - .execute(), - getDescendentsQb(db.db, { uri, depth }) - .selectFrom('descendent') - .innerJoin( - feedService.selectPostQb().as('post'), - 'post.uri', - 'descendent.uri', - ) - .selectAll('post') - .orderBy('sortAt', 'desc') - .execute(), - ]) - // prevent self-referential loops - const includedPosts = new Set([uri]) - const parentsByUri = parents.reduce((acc, post) => { - return Object.assign(acc, { [post.uri]: post }) - }, {} as Record) - const childrenByParentUri = children.reduce((acc, child) => { - if (!child.replyParent) return acc - if (includedPosts.has(child.uri)) return acc - includedPosts.add(child.uri) - acc[child.replyParent] ??= [] - acc[child.replyParent].push(child) - return acc - }, {} as Record) - const post = parentsByUri[uri] - if (!post) return null - return { - post, - parent: post.replyParent - ? getParentData( - parentsByUri, - includedPosts, - post.replyParent, - parentHeight, - ) - : undefined, - replies: getChildrenData(childrenByParentUri, uri, depth), - } -} - -const getParentData = ( - postsByUri: Record, - includedPosts: Set, - uri: string, - depth: number, -): PostThread | ParentNotFoundError | undefined => { - if (depth < 1) return undefined - if (includedPosts.has(uri)) return undefined - includedPosts.add(uri) - const post = postsByUri[uri] - if (!post) return new ParentNotFoundError(uri) - return { - post, - parent: post.replyParent - ? getParentData(postsByUri, includedPosts, post.replyParent, depth - 1) - : undefined, - replies: [], - } -} - -const getChildrenData = ( - childrenByParentUri: Record, - uri: string, - depth: number, -): PostThread[] | undefined => { - if (depth === 0) return undefined - const children = childrenByParentUri[uri] ?? [] - return children.map((row) => ({ - post: row, - replies: getChildrenData(childrenByParentUri, row.postUri, depth - 1), - })) +const hydration = async ( + inputs: HydrationFnInput, +) => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydrateThreadPosts( + skeleton.uris.map((uri) => ({ uri })), + params.viewer, + ) } -class ParentNotFoundError extends Error { - constructor(public uri: string) { - super(`Parent not found: ${uri}`) +const presentation = ( + inputs: PresentationFnInput, +) => { + const { ctx, params, skeleton, hydration } = inputs + const thread = ctx.views.thread(skeleton, hydration, { + height: params.parentHeight, + depth: params.depth, + }) + if (isNotFoundPost(thread)) { + // @TODO technically this could be returned as a NotFoundPost based on lexicon + throw new InvalidRequestError(`Post not found: ${params.uri}`, 'NotFound') } -} - -type PostThread = { - post: FeedRow - parent?: PostThread | ParentNotFoundError - replies?: PostThread[] + return { thread } } type Context = { - db: Database - feedService: FeedService - actorService: ActorService + dataplane: DataPlaneClient + hydrator: Hydrator + views: Views } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - threadData: PostThread +type Skeleton = { + anchor: string + uris: string[] } - -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 9db7cf0a252..fc9592ad7f7 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -1,30 +1,19 @@ -import { dedupeStrs } from '@atproto/common' +import { dedupeStrs, mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getPosts' import AppContext from '../../../../context' -import { Database } from '../../../../db' -import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' import { createPipeline } from '../../../../pipeline' -import { ActorService } from '../../../../services/actor' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { creatorFromUri } from '../../../../views/util' export default function (server: Server, ctx: AppContext) { const getPosts = createPipeline(skeleton, hydration, noBlocks, presentation) server.app.bsky.feed.getPosts({ auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) const viewer = auth.credentials.iss - - const results = await getPosts( - { ...params, viewer }, - { db, feedService, actorService }, - ) + const results = await getPosts({ ...params, viewer }, ctx) return { encoding: 'application/json', @@ -34,68 +23,55 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async (params: Params, ctx: Context) => { - const deduped = dedupeStrs(params.uris) - const feedItems = await ctx.feedService.postUrisToFeedItems(deduped) - return { params, feedItems } +const skeleton = async (inputs: { params: Params }) => { + return { posts: dedupeStrs(inputs.params.uris) } } -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 hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydratePosts( + skeleton.posts.map((uri) => ({ uri })), + params.viewer, + ) } -const noBlocks = (state: HydrationState) => { - const { viewer } = state.params - state.feedItems = state.feedItems.filter((item) => { - if (!viewer) return true - return !state.bam.block([viewer, item.postAuthorDid]) +const noBlocks = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + skeleton.posts = skeleton.posts.filter((uri) => { + const creator = creatorFromUri(uri) + return !ctx.views.viewerBlockExists(creator, hydration) }) - return state + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService, actorService } = ctx - const { feedItems, profiles, params } = state - const SKIP = [] - const actors = actorService.views.profileBasicPresentation( - Object.keys(profiles), - state, - params.viewer, +const presentation = (inputs: { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const posts = mapDefined(skeleton.posts, (uri) => + ctx.views.post(uri, hydration), ) - const postViews = feedItems.flatMap((item) => { - const postView = feedService.views.formatPostView( - item.postUri, - actors, - state.posts, - state.threadgates, - state.embeds, - state.labels, - state.lists, - params.viewer, - ) - return postView ?? SKIP - }) - return { posts: postViews } + return { posts } } type Context = { - db: Database - feedService: FeedService - actorService: ActorService + hydrator: Hydrator + views: Views } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + posts: 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 7d28014a7b7..fe33e305774 100644 --- a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts @@ -1,14 +1,13 @@ 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' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { parseString } from '../../../../hydration/util' +import { creatorFromUri } from '../../../../views/util' +import { clearlyBadCursor } from '../../../util' export default function (server: Server, ctx: AppContext) { const getRepostedBy = createPipeline( @@ -20,15 +19,8 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getRepostedBy({ auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) const viewer = auth.credentials.iss - - const result = await getRepostedBy( - { ...params, viewer }, - { db, actorService, graphService }, - ) + const result = await getRepostedBy({ ...params, viewer }, ctx) return { encoding: 'application/json', @@ -38,85 +30,78 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db } = ctx - const { limit, cursor, uri, cid } = params - const { ref } = db.db.dynamic - - if (TimeCidKeyset.clearlyBad(cursor)) { - return { params, repostedBy: [] } - } - - 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 skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + if (clearlyBadCursor(params.cursor)) { + return { reposts: [] } } - - const keyset = new TimeCidKeyset(ref('repost.sortAt'), ref('repost.cid')) - builder = paginate(builder, { - limit, - cursor, - keyset, + const res = await ctx.hydrator.dataplane.getRepostsBySubject({ + subject: { uri: params.uri, cid: params.cid }, + cursor: params.cursor, + limit: params.limit, }) - - const repostedBy = await builder.execute() - return { params, repostedBy, cursor: keyset.packFromResult(repostedBy) } + return { + reposts: res.uris, + cursor: parseString(res.cursor), + } } -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 hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return await ctx.hydrator.hydrateReposts(skeleton.reposts, params.viewer) } -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 noBlocks = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + skeleton.reposts = skeleton.reposts.filter((uri) => { + const creator = creatorFromUri(uri) + return !ctx.views.viewerBlockExists(creator, hydration) + }) + return skeleton } -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 } +const presentation = (inputs: { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, params, skeleton, hydration } = inputs + const repostViews = mapDefined(skeleton.reposts, (uri) => { + const repost = hydration.reposts?.get(uri) + if (!repost?.record) { + return + } + const creatorDid = creatorFromUri(uri) + return ctx.views.profile(creatorDid, hydration) + }) + return { + repostedBy: repostViews, + cursor: skeleton.cursor, + uri: params.uri, + cid: params.cid, + } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - repostedBy: Actor[] +type Skeleton = { + reposts: string[] 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 b96ae2722fa..0524f33b0c0 100644 --- a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -1,37 +1,31 @@ import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { parseString } from '../../../../hydration/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getSuggestedFeeds({ auth: ctx.authVerifier.standardOptional, - handler: async ({ auth }) => { - // @NOTE ignores cursor, doesn't matter for appview swap + handler: async ({ auth, params }) => { const viewer = auth.credentials.iss - 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') - .selectAll() - .execute() - const genInfos = await feedService.getFeedGeneratorInfos( - feedsRes.map((r) => r.uri), - viewer, - ) - const genList = feedsRes.map((r) => genInfos[r.uri]).filter(Boolean) - const creators = genList.map((gen) => gen.creator) - const profiles = await actorService.views.profilesBasic(creators, viewer) - const feedViews = mapDefined(genList, (gen) => - feedService.views.formatFeedGeneratorView(gen, profiles), + // @NOTE no need to coordinate the cursor for appview swap, as v1 doesn't use the cursor + const suggestedRes = await ctx.dataplane.getSuggestedFeeds({ + actorDid: viewer ?? undefined, + limit: params.limit, + cursor: params.cursor, + }) + const uris = suggestedRes.uris + const hydration = await ctx.hydrator.hydrateFeedGens(uris, viewer) + const feedViews = mapDefined(uris, (uri) => + ctx.views.feedGenerator(uri, hydration), ) return { encoding: 'application/json', body: { feeds: feedViews, + cursor: parseString(suggestedRes.cursor), }, } }, diff --git a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts index 05ef505ea04..e7e4bed20a9 100644 --- a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts @@ -1,18 +1,14 @@ -import { sql } from 'kysely' -import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { FeedAlgorithm, FeedKeyset, getFeedDateThreshold } from '../util/feed' -import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' -import { Database } from '../../../../db' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getTimeline' -import { setRepoRev } from '../../../util' -import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' +import { clearlyBadCursor, setRepoRev } from '../../../util' import { createPipeline } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' +import { mapDefined } from '@atproto/common' +import { FeedItem } from '../../../../hydration/feed' export default function (server: Server, ctx: AppContext) { const getTimeline = createPipeline( @@ -25,15 +21,10 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.standard, handler: async ({ params, auth, res }) => { const viewer = auth.credentials.iss - const db = ctx.db.getReplica('timeline') - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) - const [result, repoRev] = await Promise.all([ - getTimeline({ ...params, viewer }, { db, feedService }), - actorService.getRepoRev(viewer), - ]) + const result = await getTimeline({ ...params, viewer }, ctx) + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer) setRepoRev(res, repoRev) return { @@ -44,181 +35,78 @@ export default function (server: Server, ctx: AppContext) { }) } -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}`) - } - - if (limit === 1 && !cursor) { - // special case for limit=1, which is often used to check if there are new items at the top of the timeline. - return skeletonLimit1(params, ctx) - } - - if (FeedKeyset.clearlyBad(cursor)) { - return { params, feedItems: [] } +export const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + if (clearlyBadCursor(params.cursor)) { + return { items: [] } } - - 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, 2)) - .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, 2)) - .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 res = await ctx.dataplane.getTimeline({ + actorDid: params.viewer, + limit: params.limit, + cursor: params.cursor, }) - - const [followRes, selfRes] = await Promise.all([ - followQb.execute(), - selfQb.execute(), - ]) - - 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) - return { - params, - feedItems, - cursor: keyset.packFromResult(feedItems), + items: res.items.map((item) => ({ + post: { uri: item.uri, cid: item.cid || undefined }, + repost: item.repost + ? { uri: item.repost, cid: item.repostCid || undefined } + : undefined, + })), + cursor: parseString(res.cursor), } } -// The limit=1 case is used commonly to check if there are new items at the top of the timeline. -// Since it's so common, it's optimized here. The most common strategy that postgres takes to -// build a timeline is to grab all recent content from each of the user's follow, then paginate it. -// The downside here is that it requires grabbing all recent content from all follows, even if you -// only want a single result. The approach here instead takes the single most recent post from -// each of the user's follows, then sorts only those and takes the top item. -const skeletonLimit1 = async (params: Params, ctx: Context) => { - const { viewer } = params - const { db } = ctx - const { ref } = db.db.dynamic - const creatorsQb = db.db - .selectFrom('follow') - .where('creator', '=', viewer) - .select('subjectDid as did') - .unionAll(sql`select ${viewer} as did`) - const feedItemsQb = db.db - .selectFrom(creatorsQb.as('creator')) - .innerJoinLateral( - (eb) => { - const keyset = new FeedKeyset( - ref('feed_item.sortAt'), - ref('feed_item.cid'), - ) - const creatorFeedItemQb = eb - .selectFrom('feed_item') - .innerJoin('post', 'post.uri', 'feed_item.postUri') - .whereRef('feed_item.originatorDid', '=', 'creator.did') - .where('feed_item.sortAt', '>', getFeedDateThreshold(undefined, 2)) - .selectAll('feed_item') - .select([ - 'post.replyRoot', - 'post.replyParent', - 'post.creator as postAuthorDid', - ]) - return paginate(creatorFeedItemQb, { limit: 1, keyset }).as('result') - }, - (join) => join.onTrue(), - ) - .selectAll('result') - const keyset = new FeedKeyset(ref('result.sortAt'), ref('result.cid')) - const feedItems = await paginate(feedItemsQb, { limit: 1, keyset }).execute() - return { - params, - feedItems, - cursor: keyset.packFromResult(feedItems), - } +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}): Promise => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydrateFeedItems(skeleton.items, params.viewer) } -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, +const noBlocksOrMutes = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}): Skeleton => { + const { ctx, skeleton, hydration } = inputs + skeleton.items = skeleton.items.filter((item) => { + const bam = ctx.views.feedItemBlocksAndMutes(item, hydration) + return ( + !bam.authorBlocked && + !bam.authorMuted && + !bam.originatorBlocked && + !bam.originatorMuted + ) }) - return { ...state, ...hydrated } + return skeleton } -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]), +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feed = mapDefined(skeleton.items, (item) => + ctx.views.feedViewPost(item, hydration), ) - return state -} - -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const { feedItems, cursor, params } = state - const feed = feedService.views.formatFeed(feedItems, state, params.viewer) - return { feed, cursor } + return { feed, cursor: skeleton.cursor } } type Context = { - db: Database - feedService: FeedService + hydrator: Hydrator + views: Views + dataplane: DataPlaneClient } type Params = QueryParams & { viewer: string } -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + items: FeedItem[] cursor?: string } - -type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts index 3ac0b3f9477..876b3113b9f 100644 --- a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts @@ -1,17 +1,20 @@ import AppContext from '../../../../context' import { Server } from '../../../../lexicon' -import { InvalidRequestError } from '@atproto/xrpc-server' import AtpAgent from '@atproto/api' import { mapDefined } from '@atproto/common' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/searchPosts' -import { Database } from '../../../../db' import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' -import { ActorService } from '../../../../services/actor' -import { createPipeline } from '../../../../pipeline' + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' +import { creatorFromUri } from '../../../../views/util' export default function (server: Server, ctx: AppContext) { const searchPosts = createPipeline( @@ -24,19 +27,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { const viewer = auth.credentials.iss - const db = ctx.db.getReplica('search') - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) - const searchAgent = ctx.searchAgent - if (!searchAgent) { - throw new InvalidRequestError('Search not available') - } - - const results = await searchPosts( - { ...params, viewer }, - { db, feedService, actorService, searchAgent }, - ) - + const results = await searchPosts({ ...params, viewer }, ctx) return { encoding: 'application/json', body: results, @@ -45,87 +36,78 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - // @NOTE cursors wont change on appview swap - const res = await ctx.searchAgent.api.app.bsky.unspecced.searchPostsSkeleton({ - q: params.q, - cursor: params.cursor, +const skeleton = async (inputs: SkeletonFnInput) => { + const { ctx, params } = inputs + + if (ctx.searchAgent) { + // @NOTE cursors wont change on appview swap + const { data: res } = + await ctx.searchAgent.api.app.bsky.unspecced.searchPostsSkeleton({ + q: params.q, + cursor: params.cursor, + limit: params.limit, + }) + return { + posts: res.posts.map(({ uri }) => uri), + cursor: parseString(res.cursor), + } + } + + const res = await ctx.dataplane.searchPosts({ + term: params.q, limit: params.limit, + cursor: params.cursor, }) - const postUris = res.data.posts.map((a) => a.uri) - const feedItems = await ctx.feedService.postUrisToFeedItems(postUris) return { - params, - feedItems, - cursor: res.data.cursor, - hitsTotal: res.data.hitsTotal, + posts: res.uris, + cursor: parseString(res.cursor), } } 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 } + inputs: HydrationFnInput, +) => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydratePosts( + skeleton.posts.map((uri) => ({ uri })), + params.viewer, + ) } -const noBlocks = (state: HydrationState): HydrationState => { - const { viewer } = state.params - state.feedItems = state.feedItems.filter((item) => { - if (!viewer) return true - return !state.bam.block([viewer, item.postAuthorDid]) +const noBlocks = (inputs: RulesFnInput) => { + const { ctx, skeleton, hydration } = inputs + skeleton.posts = skeleton.posts.filter((uri) => { + const creator = creatorFromUri(uri) + return !ctx.views.viewerBlockExists(creator, hydration) }) - return state + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService, actorService } = ctx - const { feedItems, profiles, params } = state - const actors = actorService.views.profileBasicPresentation( - Object.keys(profiles), - state, - params.viewer, - ) - - const postViews = mapDefined(feedItems, (item) => - feedService.views.formatPostView( - item.postUri, - actors, - state.posts, - state.threadgates, - state.embeds, - state.labels, - state.lists, - params.viewer, - ), +const presentation = ( + inputs: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = inputs + const posts = mapDefined(skeleton.posts, (uri) => + ctx.views.post(uri, hydration), ) - return { posts: postViews, cursor: state.cursor, hitsTotal: state.hitsTotal } + return { + posts, + cursor: skeleton.cursor, + hitsTotal: skeleton.hitsTotal, + } } type Context = { - db: Database - feedService: FeedService - actorService: ActorService - searchAgent: AtpAgent + dataplane: DataPlaneClient + hydrator: Hydrator + views: Views + searchAgent?: AtpAgent } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + posts: string[] hitsTotal?: number 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 adc28752a25..1df10bf5a23 100644 --- a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts @@ -1,54 +1,84 @@ +import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getBlocks' import AppContext from '../../../../context' -import { notSoftDeletedClause } from '../../../../db/util' +import { + createPipeline, + HydrationFnInput, + noRules, + PresentationFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { clearlyBadCursor } from '../../../util' export default function (server: Server, ctx: AppContext) { + const getBlocks = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.graph.getBlocks({ auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { - const { limit, cursor } = params - const requester = auth.credentials.iss - if (TimeCidKeyset.clearlyBad(cursor)) { - return { - encoding: 'application/json', - body: { blocks: [] }, - } + const viewer = auth.credentials.iss + const result = await getBlocks({ ...params, viewer }, ctx) + return { + encoding: 'application/json', + body: result, } + }, + }) +} - const db = ctx.db.getReplica() - const { ref } = db.db.dynamic +const skeleton = async (input: SkeletonFnInput) => { + const { params, ctx } = input + if (clearlyBadCursor(params.cursor)) { + return { blockedDids: [] } + } + const { blockUris, cursor } = await ctx.hydrator.dataplane.getBlocks({ + actorDid: params.viewer, + cursor: params.cursor, + limit: params.limit, + }) + const blocks = await ctx.hydrator.graph.getBlocks(blockUris) + const blockedDids = mapDefined( + blockUris, + (uri) => blocks.get(uri)?.record.subject, + ) + return { + blockedDids, + cursor: cursor || undefined, + } +} - let blocksReq = db.db - .selectFrom('actor_block') - .where('actor_block.creator', '=', requester) - .innerJoin('actor as subject', 'subject.did', 'actor_block.subjectDid') - .where(notSoftDeletedClause(ref('subject'))) - .selectAll('subject') - .select(['actor_block.cid as cid', 'actor_block.sortAt as sortAt']) +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + const { viewer } = params + const { blockedDids } = skeleton + return ctx.hydrator.hydrateProfiles(blockedDids, viewer) +} - const keyset = new TimeCidKeyset( - ref('actor_block.sortAt'), - ref('actor_block.cid'), - ) - blocksReq = paginate(blocksReq, { - limit, - cursor, - keyset, - }) +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, hydration, skeleton } = input + const { blockedDids, cursor } = skeleton + const blocks = mapDefined(blockedDids, (did) => { + return ctx.views.profile(did, hydration) + }) + return { blocks, cursor } +} - const blocksRes = await blocksReq.execute() +type Context = { + hydrator: Hydrator + views: Views +} - const actorService = ctx.services.actor(db) - const blocks = await actorService.views.profilesList(blocksRes, requester) +type Params = QueryParams & { + viewer: string +} - return { - encoding: 'application/json', - body: { - blocks, - cursor: keyset.packFromResult(blocksRes), - }, - } - }, - }) +type SkeletonState = { + blockedDids: string[] + cursor?: string } diff --git a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts index bf22b2be6cb..c771d034cfd 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts @@ -3,32 +3,33 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' 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' +import { + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { didFromUri } from '../../../../hydration/util' +import { Hydrator, mergeStates } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { clearlyBadCursor } from '../../../util' export default function (server: Server, ctx: AppContext) { const getFollowers = createPipeline( skeleton, hydration, - noBlocksInclInvalid, + noBlocks, presentation, ) server.app.bsky.graph.getFollowers({ auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) const { viewer, canViewTakedowns } = ctx.authVerifier.parseCreds(auth) const result = await getFollowers( { ...params, viewer, canViewTakedowns }, - { db, actorService, graphService }, + ctx, ) return { @@ -39,95 +40,86 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db, actorService } = ctx - const { limit, cursor, actor, canViewTakedowns } = params - const { ref } = db.db.dynamic - - const subject = await actorService.getActor(actor, canViewTakedowns) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) +const skeleton = async (input: SkeletonFnInput) => { + const { params, ctx } = input + const [subjectDid] = await ctx.hydrator.actor.getDidsDefined([params.actor]) + if (!subjectDid) { + throw new InvalidRequestError(`Actor not found: ${params.actor}`) } - - if (TimeCidKeyset.clearlyBad(cursor)) { - return { params, followers: [], subject } + if (clearlyBadCursor(params.cursor)) { + return { subjectDid, followUris: [] } } - - let followersReq = db.db - .selectFrom('follow') - .where('follow.subjectDid', '=', subject.did) - .innerJoin('actor as creator', 'creator.did', 'follow.creator') - .if(!canViewTakedowns, (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, cursor } = await ctx.hydrator.graph.getActorFollowers({ + did: subjectDid, + cursor: params.cursor, + limit: params.limit, }) - - const followers = await followersReq.execute() return { - params, - followers, - subject, - cursor: keyset.packFromResult(followers), + subjectDid, + followUris: followers.map((f) => f.uri), + cursor: cursor || undefined, } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { graphService, actorService } = ctx - const { params, followers, subject } = state +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input 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 { followUris, subjectDid } = skeleton + const followState = await ctx.hydrator.hydrateFollows(followUris) + const dids = [subjectDid] + if (followState.follows) { + for (const [uri, follow] of followState.follows) { + if (follow) { + dids.push(didFromUri(uri)) + } + } + } + const profileState = await ctx.hydrator.hydrateProfiles(dids, viewer) + return mergeStates(followState, profileState) } -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 noBlocks = (input: RulesFnInput) => { + const { skeleton, params, hydration, ctx } = input + const { viewer } = params + skeleton.followUris = skeleton.followUris.filter((followUri) => { + const followerDid = didFromUri(followUri) + return ( + !hydration.followBlocks?.get(followUri) && + (!viewer || !ctx.views.viewerBlockExists(followerDid, hydration)) + ) + }) + return skeleton } -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) { +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, hydration, skeleton, params } = input + const { subjectDid, followUris, cursor } = skeleton + const isTakendown = (did: string) => + ctx.views.actorIsTakendown(did, hydration) + + const subject = ctx.views.profile(subjectDid, hydration) + if (!subject || (!params.canViewTakedowns && isTakendown(subjectDid))) { throw new InvalidRequestError(`Actor not found: ${params.actor}`) } - return { followers: followersView, subject: subjectView, cursor } + + const followers = mapDefined(followUris, (followUri) => { + const followerDid = didFromUri(followUri) + if (!params.canViewTakedowns && isTakendown(followerDid)) { + return + } + return ctx.views.profile(didFromUri(followUri), hydration) + }) + + return { followers, subject, cursor } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -136,13 +128,7 @@ type Params = QueryParams & { } type SkeletonState = { - params: Params - followers: Actor[] - subject: Actor + subjectDid: string + followUris: string[] 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 8d294b27354..81df38e453e 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollows.ts @@ -1,34 +1,30 @@ import { mapDefined } from '@atproto/common' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getFollows' +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' +import { + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { Hydrator, mergeStates } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { clearlyBadCursor } from '../../../util' export default function (server: Server, ctx: AppContext) { - const getFollows = createPipeline( - skeleton, - hydration, - noBlocksInclInvalid, - presentation, - ) + const getFollows = createPipeline(skeleton, hydration, noBlocks, presentation) server.app.bsky.graph.getFollows({ auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) const { viewer, canViewTakedowns } = ctx.authVerifier.parseCreds(auth) + // @TODO ensure canViewTakedowns gets threaded through and applied properly const result = await getFollows( { ...params, viewer, canViewTakedowns }, - { db, actorService, graphService }, + ctx, ) return { @@ -39,96 +35,89 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db, actorService } = ctx - const { limit, cursor, actor, canViewTakedowns } = params - const { ref } = db.db.dynamic - - const creator = await actorService.getActor(actor, canViewTakedowns) - if (!creator) { - throw new InvalidRequestError(`Actor not found: ${actor}`) +const skeleton = async (input: SkeletonFnInput) => { + const { params, ctx } = input + const [subjectDid] = await ctx.hydrator.actor.getDidsDefined([params.actor]) + if (!subjectDid) { + throw new InvalidRequestError(`Actor not found: ${params.actor}`) } - - if (TimeCidKeyset.clearlyBad(cursor)) { - return { params, follows: [], creator } + if (clearlyBadCursor(params.cursor)) { + return { subjectDid, followUris: [] } } - - let followsReq = db.db - .selectFrom('follow') - .where('follow.creator', '=', creator.did) - .innerJoin('actor as subject', 'subject.did', 'follow.subjectDid') - .if(!canViewTakedowns, (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, cursor } = await ctx.hydrator.graph.getActorFollows({ + did: subjectDid, + cursor: params.cursor, + limit: params.limit, }) - - const follows = await followsReq.execute() - return { - params, - follows, - creator, - cursor: keyset.packFromResult(follows), + subjectDid, + followUris: follows.map((f) => f.uri), + cursor: cursor || undefined, } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { graphService, actorService } = ctx - const { params, follows, creator } = state +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input 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 { followUris, subjectDid } = skeleton + const followState = await ctx.hydrator.hydrateFollows(followUris) + const dids = [subjectDid] + if (followState.follows) { + for (const follow of followState.follows.values()) { + if (follow) { + dids.push(follow.record.subject) + } + } + } + const profileState = await ctx.hydrator.hydrateProfiles(dids, viewer) + return mergeStates(followState, profileState) } -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 noBlocks = (input: RulesFnInput) => { + const { skeleton, params, hydration, ctx } = input + const { viewer } = params + skeleton.followUris = skeleton.followUris.filter((followUri) => { + const follow = hydration.follows?.get(followUri) + if (!follow) return false + return ( + !hydration.followBlocks?.get(followUri) && + (!viewer || + !ctx.views.viewerBlockExists(follow.record.subject, hydration)) + ) + }) + return skeleton } -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) { +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, hydration, skeleton, params } = input + const { subjectDid, followUris, cursor } = skeleton + const isTakendown = (did: string) => + ctx.views.actorIsTakendown(did, hydration) + + const subject = ctx.views.profile(subjectDid, hydration) + if (!subject || (!params.canViewTakedowns && isTakendown(subjectDid))) { throw new InvalidRequestError(`Actor not found: ${params.actor}`) } - return { follows: followsView, subject: creatorView, cursor } + + const follows = mapDefined(followUris, (followUri) => { + const followDid = hydration.follows?.get(followUri)?.record.subject + if (!followDid) return + if (!params.canViewTakedowns && isTakendown(followDid)) { + return + } + return ctx.views.profile(followDid, hydration) + }) + + return { follows, subject, cursor } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -137,13 +126,7 @@ type Params = QueryParams & { } type SkeletonState = { - params: Params - follows: Actor[] - creator: Actor + subjectDid: string + followUris: string[] 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 82007b45388..e8204d4ac0d 100644 --- a/packages/bsky/src/api/app/bsky/graph/getList.ts +++ b/packages/bsky/src/api/app/bsky/graph/getList.ts @@ -3,28 +3,25 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' 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' +import { + createPipeline, + HydrationFnInput, + noRules, + PresentationFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator, mergeStates } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { clearlyBadCursor } from '../../../util' +import { ListItemInfo } from '../../../../proto/bsky_pb' export default function (server: Server, ctx: AppContext) { const getList = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.graph.getList({ auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const graphService = ctx.services.graph(db) - const actorService = ctx.services.actor(db) const viewer = auth.credentials.iss - - const result = await getList( - { ...params, viewer }, - { db, graphService, actorService }, - ) - + const result = await getList({ ...params, viewer }, ctx) return { encoding: 'application/json', body: result, @@ -34,89 +31,60 @@ export default function (server: Server, ctx: AppContext) { } const skeleton = async ( - params: Params, - ctx: Context, + input: SkeletonFnInput, ): Promise => { - const { db, graphService } = ctx - const { list, limit, cursor, viewer } = params - const { ref } = db.db.dynamic - - const listRes = await graphService - .getListsQb(viewer) - .where('list.uri', '=', list) - .executeTakeFirst() - if (!listRes) { - throw new InvalidRequestError(`List not found: ${list}`) - } - - if (TimeCidKeyset.clearlyBad(cursor)) { - return { params, list: listRes, listItems: [] } + const { ctx, params } = input + if (clearlyBadCursor(params.cursor)) { + return { listUri: params.list, listitems: [] } } - - let itemsReq = graphService - .getListItemsQb() - .where('list_item.listUri', '=', list) - .where('list_item.creator', '=', listRes.creator) - - const keyset = new TimeCidKeyset( - ref('list_item.sortAt'), - ref('list_item.cid'), - ) - - itemsReq = paginate(itemsReq, { - limit, - cursor, - keyset, + const { listitems, cursor } = await ctx.hydrator.dataplane.getListMembers({ + listUri: params.list, + limit: params.limit, + cursor: params.cursor, }) - - const listItems = await itemsReq.execute() - return { - params, - list: listRes, - listItems, - cursor: keyset.packFromResult(listItems), + listUri: params.list, + listitems, + cursor: cursor || undefined, } } -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 hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + const { viewer } = params + const { listUri, listitems } = skeleton + const [listState, profileState] = await Promise.all([ + ctx.hydrator.hydrateLists([listUri], viewer), + ctx.hydrator.hydrateProfiles( + listitems.map(({ did }) => did), + viewer, + ), + ]) + return mergeStates(listState, 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, - params.viewer, - ) - const creator = actors[list.creator] - if (!creator) { - throw new InvalidRequestError(`Actor not found: ${list.handle}`) - } - const listView = graphService.formatListView(list, actors) - if (!listView) { - throw new InvalidRequestError('List not found') - } - const items = mapDefined(listItems, (item) => { - const subject = actors[item.did] +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = input + const { listUri, listitems, cursor } = skeleton + const list = ctx.views.list(listUri, hydration) + const items = mapDefined(listitems, ({ uri, did }) => { + const subject = ctx.views.profile(did, hydration) if (!subject) return - return { uri: item.uri, subject } + return { uri, subject } }) - return { list: listView, items, cursor } + if (!list) { + throw new InvalidRequestError('List not found') + } + return { list, items, cursor } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -124,10 +92,7 @@ type Params = QueryParams & { } type SkeletonState = { - params: Params - list: Actor & ListInfo - listItems: (Actor & { uri: string; cid: string; sortAt: string })[] + listUri: string + listitems: ListItemInfo[] 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 index 6fb6df55e82..ae4a9a27b59 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts @@ -1,13 +1,17 @@ import { mapDefined } from '@atproto/common' 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' +import { + createPipeline, + HydrationFnInput, + noRules, + PresentationFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { clearlyBadCursor } from '../../../util' export default function (server: Server, ctx: AppContext) { const getListBlocks = createPipeline( @@ -19,16 +23,8 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getListBlocks({ auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const graphService = ctx.services.graph(db) - const actorService = ctx.services.actor(db) const viewer = auth.credentials.iss - - const result = await getListBlocks( - { ...params, viewer }, - { db, actorService, graphService }, - ) - + const result = await getListBlocks({ ...params, viewer }, ctx) return { encoding: 'application/json', body: result, @@ -38,72 +34,40 @@ export default function (server: Server, ctx: AppContext) { } const skeleton = async ( - params: Params, - ctx: Context, + input: SkeletonFnInput, ): Promise => { - const { db, graphService } = ctx - const { limit, cursor, viewer } = params - const { ref } = db.db.dynamic - - if (TimeCidKeyset.clearlyBad(cursor)) { - return { params, listInfos: [] } - } - - 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 { ctx, params } = input + if (clearlyBadCursor(params.cursor)) { + return { listUris: [] } } + const { listUris, cursor } = + await ctx.hydrator.dataplane.getBlocklistSubscriptions({ + actorDid: params.viewer, + cursor: params.cursor, + limit: params.limit, + }) + return { listUris, cursor: cursor || undefined } } -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 hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + return await ctx.hydrator.hydrateLists(skeleton.listUris, params.viewer) } -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, - params.viewer, - ) - const lists = mapDefined(listInfos, (list) => - graphService.formatListView(list, actors), - ) +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = input + const { listUris, cursor } = skeleton + const lists = mapDefined(listUris, (uri) => ctx.views.list(uri, hydration)) return { lists, cursor } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -111,9 +75,6 @@ type Params = QueryParams & { } type SkeletonState = { - params: Params - listInfos: (Actor & ListInfo)[] + listUris: string[] cursor?: string } - -type HydrationState = SkeletonState & ProfileHydrationState diff --git a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts index 8baa509ae47..20060a5b9a2 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts @@ -1,58 +1,80 @@ import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getListBlocks' import AppContext from '../../../../context' +import { + createPipeline, + HydrationFnInput, + noRules, + PresentationFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { clearlyBadCursor } from '../../../util' export default function (server: Server, ctx: AppContext) { + const getListMutes = createPipeline( + skeleton, + hydration, + noRules, + presentation, + ) server.app.bsky.graph.getListMutes({ auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { - const { limit, cursor } = params - const requester = auth.credentials.iss - if (TimeCidKeyset.clearlyBad(cursor)) { - return { - encoding: 'application/json', - body: { lists: [] }, - } + const viewer = auth.credentials.iss + const result = await getListMutes({ ...params, viewer }, ctx) + return { + encoding: 'application/json', + body: result, } + }, + }) +} - const db = ctx.db.getReplica() - const { ref } = db.db.dynamic - - const graphService = ctx.services.graph(db) +const skeleton = async ( + input: SkeletonFnInput, +): Promise => { + const { ctx, params } = input + if (clearlyBadCursor(params.cursor)) { + return { listUris: [] } + } + const { listUris, cursor } = + await ctx.hydrator.dataplane.getMutelistSubscriptions({ + actorDid: params.viewer, + cursor: params.cursor, + limit: params.limit, + }) + return { listUris, cursor: cursor || undefined } +} - let listsReq = graphService - .getListsQb(requester) - .whereExists( - db.db - .selectFrom('list_mute') - .where('list_mute.mutedByDid', '=', requester) - .whereRef('list_mute.listUri', '=', ref('list.uri')) - .selectAll(), - ) +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + return await ctx.hydrator.hydrateLists(skeleton.listUris, params.viewer) +} - const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid')) - listsReq = paginate(listsReq, { - limit, - cursor, - keyset, - }) - const listsRes = await listsReq.execute() +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = input + const { listUris, cursor } = skeleton + const lists = mapDefined(listUris, (uri) => ctx.views.list(uri, hydration)) + return { lists, cursor } +} - const actorService = ctx.services.actor(db) - const profiles = await actorService.views.profiles(listsRes, requester) +type Context = { + hydrator: Hydrator + views: Views +} - const lists = mapDefined(listsRes, (row) => - graphService.formatListView(row, profiles), - ) +type Params = QueryParams & { + viewer: string +} - return { - encoding: 'application/json', - body: { - lists, - cursor: keyset.packFromResult(listsRes), - }, - } - }, - }) +type SkeletonState = { + listUris: string[] + cursor?: string } diff --git a/packages/bsky/src/api/app/bsky/graph/getLists.ts b/packages/bsky/src/api/app/bsky/graph/getLists.ts index 40bb903f5b4..0ff90f5c4bf 100644 --- a/packages/bsky/src/api/app/bsky/graph/getLists.ts +++ b/packages/bsky/src/api/app/bsky/graph/getLists.ts @@ -1,63 +1,79 @@ 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/getLists' import AppContext from '../../../../context' +import { + createPipeline, + HydrationFnInput, + noRules, + PresentationFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { clearlyBadCursor } from '../../../util' export default function (server: Server, ctx: AppContext) { + const getLists = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.graph.getLists({ auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { - const { actor, limit, cursor } = params - const requester = auth.credentials.iss - if (TimeCidKeyset.clearlyBad(cursor)) { - return { - encoding: 'application/json', - body: { lists: [] }, - } + const viewer = auth.credentials.iss + const result = await getLists({ ...params, viewer }, ctx) + + return { + encoding: 'application/json', + body: result, } + }, + }) +} - const db = ctx.db.getReplica() - const { ref } = db.db.dynamic +const skeleton = async ( + input: SkeletonFnInput, +): Promise => { + const { ctx, params } = input + if (clearlyBadCursor(params.cursor)) { + return { listUris: [] } + } + const { listUris, cursor } = await ctx.hydrator.dataplane.getActorLists({ + actorDid: params.actor, + cursor: params.cursor, + limit: params.limit, + }) + return { listUris, cursor: cursor || undefined } +} - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + const { viewer } = params + const { listUris } = skeleton + return ctx.hydrator.hydrateLists(listUris, viewer) +} - const creatorRes = await actorService.getActor(actor) - if (!creatorRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = input + const { listUris, cursor } = skeleton + const lists = mapDefined(listUris, (uri) => { + return ctx.views.list(uri, hydration) + }) + return { lists, cursor } +} - let listsReq = graphService - .getListsQb(requester) - .where('list.creator', '=', creatorRes.did) - - const keyset = new TimeCidKeyset(ref('list.sortAt'), ref('list.cid')) - listsReq = paginate(listsReq, { - limit, - cursor, - keyset, - }) - - const [listsRes, profiles] = await Promise.all([ - listsReq.execute(), - actorService.views.profiles([creatorRes], requester), - ]) - if (!profiles[creatorRes.did]) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } +type Context = { + hydrator: Hydrator + views: Views +} - const lists = mapDefined(listsRes, (row) => - graphService.formatListView(row, profiles), - ) +type Params = QueryParams & { + viewer: string | null +} - return { - encoding: 'application/json', - body: { - lists, - cursor: keyset.packFromResult(listsRes), - }, - } - }, - }) +type SkeletonState = { + listUris: string[] + cursor?: string } diff --git a/packages/bsky/src/api/app/bsky/graph/getMutes.ts b/packages/bsky/src/api/app/bsky/graph/getMutes.ts index 827573258bd..3abd417eb87 100644 --- a/packages/bsky/src/api/app/bsky/graph/getMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getMutes.ts @@ -1,62 +1,79 @@ +import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getMutes' import AppContext from '../../../../context' -import { notSoftDeletedClause } from '../../../../db/util' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { + HydrationFnInput, + PresentationFnInput, + SkeletonFnInput, + createPipeline, + noRules, +} from '../../../../pipeline' +import { clearlyBadCursor } from '../../../util' export default function (server: Server, ctx: AppContext) { + const getMutes = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.graph.getMutes({ auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { - const { limit, cursor } = params - const requester = auth.credentials.iss - if (TimeCidKeyset.clearlyBad(cursor)) { - return { - encoding: 'application/json', - body: { mutes: [] }, - } - } - - const db = ctx.db.getReplica() - const { ref } = db.db.dynamic - - let mutesReq = db.db - .selectFrom('mute') - .innerJoin('actor', 'actor.did', 'mute.subjectDid') - .where(notSoftDeletedClause(ref('actor'))) - .where('mute.mutedByDid', '=', requester) - .selectAll('actor') - .select('mute.createdAt as createdAt') - - const keyset = new CreatedAtDidKeyset( - ref('mute.createdAt'), - ref('mute.subjectDid'), - ) - mutesReq = paginate(mutesReq, { - limit, - cursor, - keyset, - }) - - const mutesRes = await mutesReq.execute() - - const actorService = ctx.services.actor(db) - + const viewer = auth.credentials.iss + const result = await getMutes({ ...params, viewer }, ctx) return { encoding: 'application/json', - body: { - cursor: keyset.packFromResult(mutesRes), - mutes: await actorService.views.profilesList(mutesRes, requester), - }, + body: result, } }, }) } -export class CreatedAtDidKeyset extends TimeCidKeyset<{ - createdAt: string - did: string // dids are treated identically to cids in TimeCidKeyset -}> { - labelResult(result: { createdAt: string; did: string }) { - return { primary: result.createdAt, secondary: result.did } +const skeleton = async (input: SkeletonFnInput) => { + const { params, ctx } = input + if (clearlyBadCursor(params.cursor)) { + return { mutedDids: [] } } + const { dids, cursor } = await ctx.hydrator.dataplane.getMutes({ + actorDid: params.viewer, + cursor: params.cursor, + limit: params.limit, + }) + return { + mutedDids: dids, + cursor: cursor || undefined, + } +} + +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + const { viewer } = params + const { mutedDids } = skeleton + return ctx.hydrator.hydrateProfiles(mutedDids, viewer) +} + +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, hydration, skeleton } = input + const { mutedDids, cursor } = skeleton + const mutes = mapDefined(mutedDids, (did) => { + return ctx.views.profile(did, hydration) + }) + return { mutes, cursor } +} + +type Context = { + hydrator: Hydrator + views: Views +} + +type Params = QueryParams & { + viewer: string +} + +type SkeletonState = { + mutedDids: string[] + cursor?: string } diff --git a/packages/bsky/src/api/app/bsky/graph/getRelationships.ts b/packages/bsky/src/api/app/bsky/graph/getRelationships.ts index 5ac4f3107c4..47aaa6cd083 100644 --- a/packages/bsky/src/api/app/bsky/graph/getRelationships.ts +++ b/packages/bsky/src/api/app/bsky/graph/getRelationships.ts @@ -1,6 +1,5 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { Relationship } from '../../../../lexicon/types/app/bsky/graph/defs' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getRelationships({ @@ -15,42 +14,18 @@ export default function (server: Server, ctx: AppContext) { }, } } - const db = ctx.db.getPrimary() - const { ref } = db.db.dynamic - const res = await db.db - .selectFrom('actor') - .select([ - 'actor.did', - db.db - .selectFrom('follow') - .where('creator', '=', actor) - .whereRef('subjectDid', '=', ref('actor.did')) - .select('uri') - .as('following'), - db.db - .selectFrom('follow') - .whereRef('creator', '=', ref('actor.did')) - .where('subjectDid', '=', actor) - .select('uri') - .as('followedBy'), - ]) - .where('actor.did', 'in', others) - .execute() - - const relationshipsMap = res.reduce((acc, cur) => { - return acc.set(cur.did, { - did: cur.did, - following: cur.following ?? undefined, - followedBy: cur.followedBy ?? undefined, - }) - }, new Map()) - + const res = await ctx.hydrator.actor.getProfileViewerStatesNaive( + others, + actor, + ) const relationships = others.map((did) => { - const relationship = relationshipsMap.get(did) - return relationship + const subject = res.get(did) + return subject ? { $type: 'app.bsky.graph.defs#relationship', - ...relationship, + did, + following: subject.following, + followedBy: subject.followedBy, } : { $type: 'app.bsky.graph.defs#notFoundActor', @@ -58,7 +33,6 @@ export default function (server: Server, ctx: AppContext) { notFound: true, } }) - return { encoding: 'application/json', body: { diff --git a/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index 3aec8ded48e..49c4a61c1c8 100644 --- a/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -1,139 +1,98 @@ -import { sql } from 'kysely' +import { mapDefined } from '@atproto/common' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getSuggestedFollowsByActor' import AppContext from '../../../../context' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Database } from '../../../../db' -import { ActorService } from '../../../../services/actor' - -const RESULT_LENGTH = 10 +import { + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { + const getSuggestedFollowsByActor = createPipeline( + skeleton, + hydration, + noBlocksOrMutes, + presentation, + ) server.app.bsky.graph.getSuggestedFollowsByActor({ auth: ctx.authVerifier.standard, handler: async ({ auth, params }) => { - const { actor } = params const viewer = auth.credentials.iss - - 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 result = await getSuggestedFollowsByActor( + { ...params, viewer }, + ctx, ) - 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 }, + body: result, } }, }) } -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')) - .orderBy('sortAt', 'desc') - .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 - }[] +const skeleton = async (input: SkeletonFnInput) => { + const { params, ctx } = input + const [relativeToDid] = await ctx.hydrator.actor.getDids([params.actor]) + if (!relativeToDid) { + throw new InvalidRequestError('Actor not found') + } + const { dids, cursor } = await ctx.hydrator.dataplane.getFollowSuggestions({ + actorDid: params.viewer, + relativeToDid, + }) + return { + suggestedDids: dids, + cursor: cursor || undefined, + } +} + +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + const { viewer } = params + const { suggestedDids } = skeleton + return ctx.hydrator.hydrateProfilesDetailed(suggestedDids, viewer) +} - 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() +const noBlocksOrMutes = ( + input: RulesFnInput, +) => { + const { ctx, skeleton, hydration } = input + skeleton.suggestedDids = skeleton.suggestedDids.filter( + (did) => + !ctx.views.viewerBlockExists(did, hydration) && + !ctx.views.viewerMuteExists(did, hydration), + ) + return skeleton +} - resultDids.push(...mostPopularAccountsActorFollows) - } +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, hydration, skeleton } = input + const { suggestedDids } = skeleton + const suggestions = mapDefined(suggestedDids, (did) => + ctx.views.profileDetailed(did, hydration), + ) + return { suggestions } +} - 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() +type Context = { + hydrator: Hydrator + views: Views +} - resultDids.push(...additional) - } +type Params = QueryParams & { + viewer: string +} - return resultDids +type SkeletonState = { + suggestedDids: string[] } diff --git a/packages/bsky/src/api/app/bsky/graph/muteActor.ts b/packages/bsky/src/api/app/bsky/graph/muteActor.ts index 72a1635c55d..65600344a8d 100644 --- a/packages/bsky/src/api/app/bsky/graph/muteActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/muteActor.ts @@ -1,9 +1,7 @@ -import assert from 'node:assert' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { MuteOperation_Type } from '../../../../proto/bsync_pb' -import { BsyncClient } from '../../../../bsync' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.muteActor({ @@ -11,44 +9,13 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, auth, input }) => { const { actor } = input.body const requester = auth.credentials.iss - const db = ctx.db.getPrimary() - - const subjectDid = await ctx.services.actor(db).getActorDid(actor) - if (!subjectDid) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - if (subjectDid === requester) { - throw new InvalidRequestError('Cannot mute oneself') - } - - const muteActor = async () => { - await ctx.services.graph(db).muteActor({ - subjectDid, - mutedByDid: requester, - }) - } - - const addBsyncMuteOp = async (bsyncClient: BsyncClient) => { - await bsyncClient.addMuteOperation({ - type: MuteOperation_Type.ADD, - actorDid: requester, - subject: subjectDid, - }) - } - - if (ctx.cfg.bsyncOnlyMutes) { - assert(ctx.bsyncClient) - await addBsyncMuteOp(ctx.bsyncClient) - } else { - await muteActor() - if (ctx.bsyncClient) { - try { - await addBsyncMuteOp(ctx.bsyncClient) - } catch (err) { - req.log.warn(err, 'failed to sync mute op to bsync') - } - } - } + const [did] = await ctx.hydrator.actor.getDids([actor]) + if (!did) throw new InvalidRequestError('Actor not found') + await ctx.bsyncClient.addMuteOperation({ + type: MuteOperation_Type.ADD, + actorDid: requester, + subject: did, + }) }, }) } diff --git a/packages/bsky/src/api/app/bsky/graph/muteActorList.ts b/packages/bsky/src/api/app/bsky/graph/muteActorList.ts index f2ee8bfeea0..2f9f8c7573f 100644 --- a/packages/bsky/src/api/app/bsky/graph/muteActorList.ts +++ b/packages/bsky/src/api/app/bsky/graph/muteActorList.ts @@ -1,55 +1,18 @@ -import assert from 'node:assert' -import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import * as lex from '../../../../lexicon/lexicons' import AppContext from '../../../../context' -import { AtUri } from '@atproto/syntax' import { MuteOperation_Type } from '../../../../proto/bsync_pb' -import { BsyncClient } from '../../../../bsync' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.muteActorList({ auth: ctx.authVerifier.standard, - handler: async ({ req, auth, input }) => { + handler: async ({ auth, input }) => { const { list } = input.body const requester = auth.credentials.iss - - 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}`) - } - - const muteActorList = async () => { - await ctx.services.graph(db).muteActorList({ - list, - mutedByDid: requester, - }) - } - - const addBsyncMuteOp = async (bsyncClient: BsyncClient) => { - await bsyncClient.addMuteOperation({ - type: MuteOperation_Type.ADD, - actorDid: requester, - subject: list, - }) - } - - if (ctx.cfg.bsyncOnlyMutes) { - assert(ctx.bsyncClient) - await addBsyncMuteOp(ctx.bsyncClient) - } else { - await muteActorList() - if (ctx.bsyncClient) { - try { - await addBsyncMuteOp(ctx.bsyncClient) - } catch (err) { - req.log.warn(err, 'failed to sync mute op to bsync') - } - } - } + await ctx.bsyncClient.addMuteOperation({ + type: MuteOperation_Type.ADD, + actorDid: requester, + subject: list, + }) }, }) } diff --git a/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts b/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts index e35e00202ef..5462d7a7117 100644 --- a/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts @@ -1,54 +1,21 @@ -import assert from 'node:assert' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { MuteOperation_Type } from '../../../../proto/bsync_pb' -import { BsyncClient } from '../../../../bsync' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.unmuteActor({ auth: ctx.authVerifier.standard, - handler: async ({ req, auth, input }) => { + handler: async ({ auth, input }) => { const { actor } = input.body const requester = auth.credentials.iss - const db = ctx.db.getPrimary() - - const subjectDid = await ctx.services.actor(db).getActorDid(actor) - if (!subjectDid) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - if (subjectDid === requester) { - throw new InvalidRequestError('Cannot mute oneself') - } - - const unmuteActor = async () => { - await ctx.services.graph(db).unmuteActor({ - subjectDid, - mutedByDid: requester, - }) - } - - const addBsyncMuteOp = async (bsyncClient: BsyncClient) => { - await bsyncClient.addMuteOperation({ - type: MuteOperation_Type.REMOVE, - actorDid: requester, - subject: subjectDid, - }) - } - - if (ctx.cfg.bsyncOnlyMutes) { - assert(ctx.bsyncClient) - await addBsyncMuteOp(ctx.bsyncClient) - } else { - await unmuteActor() - if (ctx.bsyncClient) { - try { - await addBsyncMuteOp(ctx.bsyncClient) - } catch (err) { - req.log.warn(err, 'failed to sync mute op to bsync') - } - } - } + const [did] = await ctx.hydrator.actor.getDids([actor]) + if (!did) throw new InvalidRequestError('Actor not found') + await ctx.bsyncClient.addMuteOperation({ + type: MuteOperation_Type.REMOVE, + actorDid: requester, + subject: did, + }) }, }) } diff --git a/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts b/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts index 18d612d7c95..2c80e42187f 100644 --- a/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts +++ b/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts @@ -1,45 +1,18 @@ -import assert from 'node:assert' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { MuteOperation_Type } from '../../../../proto/bsync_pb' -import { BsyncClient } from '../../../../bsync' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.unmuteActorList({ auth: ctx.authVerifier.standard, - handler: async ({ req, auth, input }) => { + handler: async ({ auth, input }) => { const { list } = input.body const requester = auth.credentials.iss - const db = ctx.db.getPrimary() - - const unmuteActorList = async () => { - await ctx.services.graph(db).unmuteActorList({ - list, - mutedByDid: requester, - }) - } - - const addBsyncMuteOp = async (bsyncClient: BsyncClient) => { - await bsyncClient.addMuteOperation({ - type: MuteOperation_Type.REMOVE, - actorDid: requester, - subject: list, - }) - } - - if (ctx.cfg.bsyncOnlyMutes) { - assert(ctx.bsyncClient) - await addBsyncMuteOp(ctx.bsyncClient) - } else { - await unmuteActorList() - if (ctx.bsyncClient) { - try { - await addBsyncMuteOp(ctx.bsyncClient) - } catch (err) { - req.log.warn(err, 'failed to sync mute op to bsync') - } - } - } + await ctx.bsyncClient.addMuteOperation({ + type: MuteOperation_Type.REMOVE, + actorDid: requester, + subject: list, + }) }, }) } diff --git a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts index 71391457902..afbb5b06780 100644 --- a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts @@ -1,43 +1,74 @@ -import { sql } from 'kysely' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { countAll, notSoftDeletedClause } from '../../../../db/util' +import { QueryParams } from '../../../../lexicon/types/app/bsky/notification/getUnreadCount' import AppContext from '../../../../context' +import { + HydrationFnInput, + PresentationFnInput, + SkeletonFnInput, + createPipeline, + noRules, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { + const getUnreadCount = createPipeline( + skeleton, + hydration, + noRules, + presentation, + ) server.app.bsky.notification.getUnreadCount({ auth: ctx.authVerifier.standard, handler: async ({ auth, params }) => { - const requester = auth.credentials.iss - if (params.seenAt) { - throw new InvalidRequestError('The seenAt parameter is unsupported') - } - - 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') - .leftJoin('actor_state', 'actor_state.did', 'actor.did') - .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( - 'notification.sortAt', - '>', - sql`coalesce(${ref('actor_state.lastSeenNotifs')}, ${''})`, - ) - .executeTakeFirst() - - const count = result?.count ?? 0 - + const viewer = auth.credentials.iss + const result = await getUnreadCount({ ...params, viewer }, ctx) return { encoding: 'application/json', - body: { count }, + body: result, } }, }) } + +const skeleton = async ( + input: SkeletonFnInput, +): Promise => { + const { params, ctx } = input + if (params.seenAt) { + throw new InvalidRequestError('The seenAt parameter is unsupported') + } + const res = await ctx.hydrator.dataplane.getUnreadNotificationCount({ + actorDid: params.viewer, + }) + return { + count: res.count, + } +} + +const hydration = async ( + _input: HydrationFnInput, +) => { + return {} +} + +const presentation = ( + input: PresentationFnInput, +) => { + const { skeleton } = input + return { count: skeleton.count } +} + +type Context = { + hydrator: Hydrator + views: Views +} + +type Params = QueryParams & { + viewer: string +} + +type SkeletonState = { + count: number +} diff --git a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts index ce5274a9da7..6f738fb4cca 100644 --- a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts @@ -1,16 +1,20 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { jsonStringToLex } from '@atproto/lexicon' import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/notification/listNotifications' import AppContext from '../../../../context' -import { Database } from '../../../../db' -import { notSoftDeletedClause } from '../../../../db/util' -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' +import { + createPipeline, + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { Notification } from '../../../../proto/bsky_pb' +import { didFromUri } from '../../../../hydration/util' +import { clearlyBadCursor } from '../../../util' export default function (server: Server, ctx: AppContext) { const listNotifications = createPipeline( @@ -22,17 +26,8 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.listNotifications({ auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) - const labelService = ctx.services.label(db) const viewer = auth.credentials.iss - - const result = await listNotifications( - { ...params, viewer }, - { db, actorService, graphService, labelService }, - ) - + const result = await listNotifications({ ...params, viewer }, ctx) return { encoding: 'application/json', body: result, @@ -42,141 +37,73 @@ export default function (server: Server, ctx: AppContext) { } const skeleton = async ( - params: Params, - ctx: Context, + input: SkeletonFnInput, ): Promise => { - const { db } = ctx - const { limit, cursor, viewer } = params - const { ref } = db.db.dynamic + const { params, ctx } = input if (params.seenAt) { throw new InvalidRequestError('The seenAt parameter is unsupported') } - if (NotifsKeyset.clearlyBad(cursor)) { - return { params, notifs: [] } + if (clearlyBadCursor(params.cursor)) { + return { notifs: [] } } - let notifBuilder = db.db - .selectFrom('notification as notif') - .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.author as authorDid', - 'notif.recordUri as uri', - 'notif.recordCid as cid', - 'notif.reason as reason', - 'notif.reasonSubject as reasonSubject', - 'notif.sortAt as indexedAt', - ]) - - const keyset = new NotifsKeyset(ref('notif.sortAt'), ref('notif.recordCid')) - notifBuilder = paginate(notifBuilder, { - cursor, - limit, - keyset, - tryIndex: true, - }) - - const actorStateQuery = db.db - .selectFrom('actor_state') - .selectAll() - .where('did', '=', viewer) - - const [notifs, actorState] = await Promise.all([ - notifBuilder.execute(), - actorStateQuery.executeTakeFirst(), + const [res, lastSeenRes] = await Promise.all([ + ctx.hydrator.dataplane.getNotifications({ + actorDid: params.viewer, + cursor: params.cursor, + limit: params.limit, + }), + ctx.hydrator.dataplane.getNotificationSeen({ + actorDid: params.viewer, + }), ]) - + // @NOTE for the first page of results if there's no last-seen time, consider top notification unread + // rather than all notifications. bit of a hack to be more graceful when seen times are out of sync. + let lastSeenDate = lastSeenRes.timestamp?.toDate() + if (!lastSeenDate && !params.cursor) { + lastSeenDate = res.notifications.at(0)?.timestamp?.toDate() + } return { - params, - notifs, - cursor: keyset.packFromResult(notifs), - lastSeenNotifs: actorState?.lastSeenNotifs, + notifs: res.notifications, + cursor: res.cursor || undefined, + lastSeenNotifs: lastSeenDate?.toISOString(), } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { graphService, actorService, labelService, db } = ctx - const { params, notifs } = state - const { viewer } = params - const dids = notifs.map((notif) => notif.authorDid) - const uris = notifs.map((notif) => notif.uri) - const [actors, records, labels, bam] = await Promise.all([ - actorService.views.profiles(dids, viewer), - getRecordMap(db, uris), - labelService.getLabelsForUris(uris), - graphService.getBlockAndMuteState(dids.map((did) => [viewer, did])), - ]) - return { ...state, actors, records, labels, bam } +const hydration = async ( + input: HydrationFnInput, +) => { + const { skeleton, params, ctx } = input + return ctx.hydrator.hydrateNotifications(skeleton.notifs, params.viewer) } -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, records, labels, lastSeenNotifs } = state - const notifications = mapDefined(notifs, (notif) => { - const author = actors[notif.authorDid] - const record = records[notif.uri] - if (!author || !record) return undefined - 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], - } +const noBlockOrMutes = ( + input: RulesFnInput, +) => { + const { skeleton, hydration, ctx } = input + skeleton.notifs = skeleton.notifs.filter((item) => { + const did = didFromUri(item.uri) + return ( + !ctx.views.viewerBlockExists(did, hydration) && + !ctx.views.viewerMuteExists(did, hydration) + ) }) - return { notifications, cursor, seenAt: lastSeenNotifs } + return skeleton } -const getRecordMap = async ( - db: Database, - uris: string[], -): Promise => { - if (!uris.length) return {} - const { ref } = db.db.dynamic - const recordRows = await db.db - .selectFrom('record') - .select(['uri', 'json']) - .where('uri', 'in', uris) - .where(notSoftDeletedClause(ref('record'))) - .execute() - return recordRows.reduce((acc, { uri, json }) => { - acc[uri] = jsonStringToLex(json) as Record - return acc - }, {} as RecordMap) +const presentation = ( + input: PresentationFnInput, +) => { + const { skeleton, hydration, ctx } = input + const { notifs, lastSeenNotifs, cursor } = skeleton + const notifications = mapDefined(notifs, (notif) => + ctx.views.notification(notif, lastSeenNotifs, hydration), + ) + return { notifications, cursor, seenAt: skeleton.lastSeenNotifs } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService - labelService: LabelService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -184,32 +111,7 @@ type Params = QueryParams & { } type SkeletonState = { - params: Params - notifs: NotifRow[] + notifs: Notification[] lastSeenNotifs?: string cursor?: string } - -type HydrationState = SkeletonState & { - bam: BlockAndMuteState - actors: ActorInfoMap - records: RecordMap - labels: Labels -} - -type RecordMap = { [uri: string]: Record } - -type NotifRow = { - authorDid: string - uri: string - cid: string - reason: string - reasonSubject: string | null - indexedAt: string -} - -class NotifsKeyset extends TimeCidKeyset { - labelResult(result: NotifRow) { - return { primary: result.indexedAt, secondary: result.cid } - } -} diff --git a/packages/bsky/src/api/app/bsky/notification/registerPush.ts b/packages/bsky/src/api/app/bsky/notification/registerPush.ts index abce1cd096c..bd4bcec1f2b 100644 --- a/packages/bsky/src/api/app/bsky/notification/registerPush.ts +++ b/packages/bsky/src/api/app/bsky/notification/registerPush.ts @@ -1,15 +1,12 @@ -import assert from 'node:assert' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { Platform } from '../../../../notifications' -import { CourierClient } from '../../../../courier' import { AppPlatform } from '../../../../proto/courier_pb' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.registerPush({ auth: ctx.authVerifier.standard, - handler: async ({ req, auth, input }) => { + handler: async ({ auth, input }) => { const { token, platform, serviceDid, appId } = input.body const did = auth.credentials.iss if (serviceDid !== auth.credentials.aud) { @@ -20,44 +17,17 @@ export default function (server: Server, ctx: AppContext) { 'Unsupported platform: must be "ios", "android", or "web".', ) } - - const db = ctx.db.getPrimary() - - const registerDeviceWithAppview = async () => { - await ctx.services - .actor(db) - .registerPushDeviceToken(did, token, platform as Platform, appId) - } - - const registerDeviceWithCourier = async ( - courierClient: CourierClient, - ) => { - await courierClient.registerDeviceToken({ - did, - token, - platform: - platform === 'ios' - ? AppPlatform.IOS - : platform === 'android' - ? AppPlatform.ANDROID - : AppPlatform.WEB, - appId, - }) - } - - if (ctx.cfg.courierOnlyRegistration) { - assert(ctx.courierClient) - await registerDeviceWithCourier(ctx.courierClient) - } else { - await registerDeviceWithAppview() - if (ctx.courierClient) { - try { - await registerDeviceWithCourier(ctx.courierClient) - } catch (err) { - req.log.warn(err, 'failed to register device token with courier') - } - } - } + await ctx.courierClient.registerDeviceToken({ + did, + token, + platform: + platform === 'ios' + ? AppPlatform.IOS + : platform === 'android' + ? AppPlatform.ANDROID + : AppPlatform.WEB, + appId, + }) }, }) } diff --git a/packages/bsky/src/api/app/bsky/notification/updateSeen.ts b/packages/bsky/src/api/app/bsky/notification/updateSeen.ts index 4b8b614fbad..4c9e66113d6 100644 --- a/packages/bsky/src/api/app/bsky/notification/updateSeen.ts +++ b/packages/bsky/src/api/app/bsky/notification/updateSeen.ts @@ -1,7 +1,6 @@ +import { Timestamp } from '@bufbuild/protobuf' import { Server } from '../../../../lexicon' -import { InvalidRequestError } from '@atproto/xrpc-server' import AppContext from '../../../../context' -import { excluded } from '../../../../db/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.updateSeen({ @@ -9,25 +8,10 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ input, auth }) => { const { seenAt } = input.body const viewer = auth.credentials.iss - - let parsed: string - try { - parsed = new Date(seenAt).toISOString() - } catch (_err) { - throw new InvalidRequestError('Invalid date') - } - - const db = ctx.db.getPrimary() - - await db.db - .insertInto('actor_state') - .values({ did: viewer, lastSeenNotifs: parsed }) - .onConflict((oc) => - oc.column('did').doUpdateSet({ - lastSeenNotifs: excluded(db.db, 'lastSeenNotifs'), - }), - ) - .executeTakeFirst() + await ctx.dataplane.updateNotificationSeen({ + actorDid: viewer, + timestamp: Timestamp.fromDate(new Date(seenAt)), + }) }, }) } diff --git a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index c15a5242b0f..a5a3d8a8cef 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -1,107 +1,57 @@ +import { mapDefined } from '@atproto/common' 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' +import { parseString } from '../../../../hydration/util' +import { clearlyBadCursor } from '../../../util' // THIS IS A TEMPORARY UNSPECCED ROUTE +// @TODO currently mirrors getSuggestedFeeds and ignores the "query" param. +// In the future may take into consideration popularity via likes w/ its own dataplane endpoint. export default function (server: Server, ctx: AppContext) { server.app.bsky.unspecced.getPopularFeedGenerators({ auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { - const { limit, cursor, query } = params - const requester = auth.credentials.iss - if (LikeCountKeyset.clearlyBad(cursor)) { + const viewer = auth.credentials.iss + + if (clearlyBadCursor(params.cursor)) { return { encoding: 'application/json', body: { feeds: [] }, } } - 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') - .select([ - 'uri', - 'cid', - db.db - .selectFrom('like') - .whereRef('like.subject', '=', ref('feed_generator.uri')) - .select(countAll.as('count')) - .as('likeCount'), - ]) + let uris: string[] + let cursor: string | undefined + const query = params.query?.trim() ?? '' if (query) { - inner = inner.where((qb) => - qb - .where('feed_generator.displayName', 'ilike', `%${query}%`) - .orWhere('feed_generator.description', 'ilike', `%${query}%`), - ) + const res = await ctx.dataplane.searchFeedGenerators({ + query, + limit: params.limit, + }) + uris = res.uris + } else { + const res = await ctx.dataplane.getSuggestedFeeds({ + actorDid: viewer ?? undefined, + limit: params.limit, + cursor: params.cursor, + }) + uris = res.uris + cursor = parseString(res.cursor) } - 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' }) - - const res = await builder.execute() - - const genInfos = await feedService.getFeedGeneratorInfos( - res.map((feed) => feed.uri), - requester, + const hydration = await ctx.hydrator.hydrateFeedGens(uris, viewer) + const feedViews = mapDefined(uris, (uri) => + ctx.views.feedGenerator(uri, hydration), ) - const creators = Object.values(genInfos).map((gen) => gen.creator) - const profiles = await actorService.views.profiles(creators, requester) - - const genViews: GeneratorView[] = [] - for (const row of res) { - const gen = genInfos[row.uri] - if (!gen) continue - const view = feedService.views.formatFeedGeneratorView(gen, profiles) - if (view) { - genViews.push(view) - } - } - return { encoding: 'application/json', body: { - cursor: keyset.packFromResult(res), - feeds: genViews, + feeds: feedViews, + cursor, }, } }, }) } - -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/api/app/bsky/unspecced/getTaggedSuggestions.ts b/packages/bsky/src/api/app/bsky/unspecced/getTaggedSuggestions.ts index 4fd248e87ff..79df4857945 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getTaggedSuggestions.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getTaggedSuggestions.ts @@ -5,11 +5,12 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.unspecced.getTaggedSuggestions({ handler: async () => { - const suggestions = await ctx.db - .getReplica() - .db.selectFrom('tagged_suggestion') - .selectAll() - .execute() + const res = await ctx.dataplane.getSuggestedEntities({}) + const suggestions = res.entities.map((entity) => ({ + tag: entity.tag, + subjectType: entity.subjectType, + subject: entity.subject, + })) return { encoding: 'application/json', body: { diff --git a/packages/bsky/src/api/app/bsky/util/feed.ts b/packages/bsky/src/api/app/bsky/util/feed.ts deleted file mode 100644 index 769b2d7e833..00000000000 --- a/packages/bsky/src/api/app/bsky/util/feed.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TimeCidKeyset } from '../../../../db/pagination' -import { FeedRow } from '../../../../services/feed/types' - -export enum FeedAlgorithm { - ReverseChronological = 'reverse-chronological', -} - -export class FeedKeyset extends TimeCidKeyset { - labelResult(result: FeedRow) { - return { primary: result.sortAt, secondary: result.cid } - } -} - -// For users with sparse feeds, avoid scanning more than one week for a single page -export const getFeedDateThreshold = (from: string | undefined, days = 1) => { - const timelineDateThreshold = from ? new Date(from) : new Date() - timelineDateThreshold.setDate(timelineDateThreshold.getDate() - days) - return timelineDateThreshold.toISOString() -} diff --git a/packages/bsky/src/api/blob-resolver.ts b/packages/bsky/src/api/blob-resolver.ts index c307152c43a..1a2d1ee560d 100644 --- a/packages/bsky/src/api/blob-resolver.ts +++ b/packages/bsky/src/api/blob-resolver.ts @@ -5,11 +5,16 @@ import axios, { AxiosError } from 'axios' import { CID } from 'multiformats/cid' import { ensureValidDid } from '@atproto/syntax' import { forwardStreamErrors, VerifyCidTransform } from '@atproto/common' -import { IdResolver, DidNotFoundError } from '@atproto/identity' +import { DidNotFoundError } from '@atproto/identity' import AppContext from '../context' import { httpLogger as log } from '../logger' import { retryHttp } from '../util/retry' -import { Database } from '../db' +import { + Code, + getServiceEndpoint, + isDataplaneError, + unpackIdentityServices, +} from '../data-plane' // Resolve and verify blob from its origin host @@ -31,8 +36,7 @@ export const createRouter = (ctx: AppContext): express.Router => { return next(createError(400, 'Invalid cid')) } - const db = ctx.db.getReplica() - const verifiedImage = await resolveBlob(did, cid, db, ctx.idResolver) + const verifiedImage = await resolveBlob(ctx, did, cid) // Send chunked response, destroying stream early (before // closing chunk) if the bytes don't match the expected cid. @@ -76,24 +80,29 @@ export const createRouter = (ctx: AppContext): express.Router => { return router } -export async function resolveBlob( - did: string, - cid: CID, - db: Database, - idResolver: IdResolver, -) { +export async function resolveBlob(ctx: AppContext, did: string, cid: CID) { const cidStr = cid.toString() - const [{ pds }, takedown] = await Promise.all([ - idResolver.did.resolveAtprotoData(did), // @TODO cache did info - db.db - .selectFrom('blob_takedown') - .select('takedownRef') - .where('did', '=', did) - .where('cid', '=', cid.toString()) - .executeTakeFirst(), + const [identity, { takenDown }] = await Promise.all([ + ctx.dataplane.getIdentityByDid({ did }).catch((err) => { + if (isDataplaneError(err, Code.NotFound)) { + return undefined + } + throw err + }), + ctx.dataplane.getBlobTakedown({ did, cid: cid.toString() }), ]) - if (takedown) { + const services = identity && unpackIdentityServices(identity.services) + const pds = + services && + getServiceEndpoint(services, { + id: 'atproto_pds', + type: 'AtprotoPersonalDataServer', + }) + if (!pds) { + throw createError(404, 'Origin not found') + } + if (takenDown) { throw createError(404, 'Blob not found') } diff --git a/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts b/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts index 9ef66c94c9b..e01be2d6383 100644 --- a/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts +++ b/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts @@ -1,6 +1,5 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { Actor } from '../../../../db/tables/actor' import { mapDefined } from '@atproto/common' import { INVALID_HANDLE } from '@atproto/syntax' @@ -9,25 +8,16 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.roleOrAdminService, handler: async ({ params }) => { const { dids } = params - const db = ctx.db.getPrimary() - const actorService = ctx.services.actor(db) - const [actors, profiles] = await Promise.all([ - actorService.getActors(dids, true), - actorService.getProfileRecords(dids, true), - ]) - const actorByDid = actors.reduce((acc, cur) => { - return acc.set(cur.did, cur) - }, new Map()) + const actors = await ctx.hydrator.actor.getActors(dids, true) const infos = mapDefined(dids, (did) => { - const info = actorByDid.get(did) + const info = actors.get(did) if (!info) return - const profile = profiles.get(did) return { did, handle: info.handle ?? INVALID_HANDLE, - relatedRecords: profile ? [profile] : undefined, - indexedAt: info.indexedAt, + relatedRecords: info.profile ? [info.profile] : undefined, + indexedAt: (info.sortedAt ?? new Date(0)).toISOString(), } }) diff --git a/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts b/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts index 8ac237240f9..2ca7bcdc2c9 100644 --- a/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts +++ b/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.roleOrAdminService, handler: async ({ params }) => { const { did, uri, blob } = params - const modService = ctx.services.moderation(ctx.db.getPrimary()) + let body: OutputSchema | null = null if (blob) { if (!did) { @@ -16,46 +16,48 @@ export default function (server: Server, ctx: AppContext) { 'Must provide a did to request blob state', ) } - const takedown = await modService.getBlobTakedownRef(did, blob) - if (takedown) { - body = { - subject: { - $type: 'com.atproto.admin.defs#repoBlobRef', - did: did, - cid: blob, - }, - takedown, - } + const res = await ctx.dataplane.getBlobTakedown({ + did, + cid: blob, + }) + body = { + subject: { + $type: 'com.atproto.admin.defs#repoBlobRef', + did: did, + cid: blob, + }, + takedown: { + applied: res.takenDown, + ref: res.takedownRef ? 'TAKEDOWN' : undefined, + }, } } else if (uri) { - const [takedown, cidRes] = await Promise.all([ - modService.getRecordTakedownRef(uri), - ctx.db - .getPrimary() - .db.selectFrom('record') - .where('uri', '=', uri) - .select('cid') - .executeTakeFirst(), - ]) - if (cidRes && takedown) { + const res = await ctx.hydrator.getRecord(uri, true) + if (res) { body = { subject: { $type: 'com.atproto.repo.strongRef', uri, - cid: cidRes.cid, + cid: res.cid, + }, + takedown: { + applied: !!res.takedownRef, + ref: res.takedownRef || undefined, }, - takedown, } } } else if (did) { - const takedown = await modService.getRepoTakedownRef(did) - if (takedown) { + const res = (await ctx.hydrator.actor.getActors([did], true)).get(did) + if (res) { body = { subject: { $type: 'com.atproto.admin.defs#repoRef', did: did, }, - takedown, + takedown: { + applied: !!res.takedownRef, + ref: res.takedownRef || undefined, + }, } } } else { diff --git a/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts b/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts index a7875280137..bb78832aa93 100644 --- a/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts +++ b/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts @@ -1,4 +1,5 @@ -import { AtUri } from '@atproto/syntax' +import { Timestamp } from '@bufbuild/protobuf' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { @@ -6,8 +7,6 @@ import { isRepoBlobRef, } from '../../../../lexicon/types/com/atproto/admin/defs' import { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/repo/strongRef' -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' -import { CID } from 'multiformats/cid' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateSubjectStatus({ @@ -19,43 +18,49 @@ export default function (server: Server, ctx: AppContext) { 'Must be a full moderator to update subject state', ) } - - const modService = ctx.services.moderation(ctx.db.getPrimary()) - + const now = new Date() const { subject, takedown } = input.body if (takedown) { if (isRepoRef(subject)) { - const did = subject.did if (takedown.applied) { - await modService.takedownRepo({ - takedownRef: takedown.ref ?? new Date().toISOString(), - did, + await ctx.dataplane.takedownActor({ + did: subject.did, + ref: takedown.ref, + seen: Timestamp.fromDate(now), }) } else { - await modService.reverseTakedownRepo({ did }) + await ctx.dataplane.untakedownActor({ + did: subject.did, + seen: Timestamp.fromDate(now), + }) } } else if (isStrongRef(subject)) { - const uri = new AtUri(subject.uri) - const cid = CID.parse(subject.cid) if (takedown.applied) { - await modService.takedownRecord({ - takedownRef: takedown.ref ?? new Date().toISOString(), - uri, - cid, + await ctx.dataplane.takedownRecord({ + recordUri: subject.uri, + ref: takedown.ref, + seen: Timestamp.fromDate(now), }) } else { - await modService.reverseTakedownRecord({ uri }) + await ctx.dataplane.untakedownRecord({ + recordUri: subject.uri, + seen: Timestamp.fromDate(now), + }) } } else if (isRepoBlobRef(subject)) { - const { did, cid } = subject if (takedown.applied) { - await modService.takedownBlob({ - takedownRef: takedown.ref ?? new Date().toISOString(), - did, - cid, + await ctx.dataplane.takedownBlob({ + did: subject.did, + cid: subject.cid, + ref: takedown.ref, + seen: Timestamp.fromDate(now), }) } else { - await modService.reverseTakedownBlob({ did, cid }) + await ctx.dataplane.untakedownBlob({ + did: subject.did, + cid: subject.cid, + seen: Timestamp.fromDate(now), + }) } } else { throw new InvalidRequestError('Invalid subject') diff --git a/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts b/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts index 30c1d7f8a6f..6cb524c6ec2 100644 --- a/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts @@ -7,12 +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(db).getActor(handle, true) - if (user) { - did = user.did - } else { + let [did] = await ctx.hydrator.actor.getDids([handle]) + + if (!did) { const publicHostname = ctx.cfg.publicUrl ? new URL(ctx.cfg.publicUrl).hostname : null diff --git a/packages/bsky/src/api/com/atproto/repo/getRecord.ts b/packages/bsky/src/api/com/atproto/repo/getRecord.ts index c42c1fd6b4c..95f2c5eda81 100644 --- a/packages/bsky/src/api/com/atproto/repo/getRecord.ts +++ b/packages/bsky/src/api/com/atproto/repo/getRecord.ts @@ -2,37 +2,28 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { AtUri } from '@atproto/syntax' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { jsonStringToLex } from '@atproto/lexicon' export default function (server: Server, ctx: AppContext) { server.com.atproto.repo.getRecord(async ({ params }) => { const { repo, collection, rkey, cid } = params - const db = ctx.db.getReplica() - const did = await ctx.services.actor(db).getActorDid(repo) + const [did] = await ctx.hydrator.actor.getDids([repo]) if (!did) { throw new InvalidRequestError(`Could not find repo: ${repo}`) } - const uri = AtUri.make(did, collection, rkey) + const uri = AtUri.make(did, collection, rkey).toString() + const result = await ctx.hydrator.getRecord(uri, true) - let builder = db.db - .selectFrom('record') - .selectAll() - .where('uri', '=', uri.toString()) - if (cid) { - builder = builder.where('cid', '=', cid) - } - - const record = await builder.executeTakeFirst() - if (!record) { + if (!result || (cid && result.cid !== cid)) { throw new InvalidRequestError(`Could not locate record: ${uri}`) } + return { - encoding: 'application/json', + encoding: 'application/json' as const, body: { - uri: record.uri, - cid: record.cid, - value: jsonStringToLex(record.json) as Record, + uri: uri, + cid: result.cid, + value: result.record, }, } }) diff --git a/packages/bsky/src/api/com/atproto/temp/fetchLabels.ts b/packages/bsky/src/api/com/atproto/temp/fetchLabels.ts index 8a6cacc2fbd..044a8d4dfd4 100644 --- a/packages/bsky/src/api/com/atproto/temp/fetchLabels.ts +++ b/packages/bsky/src/api/com/atproto/temp/fetchLabels.ts @@ -1,30 +1,9 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { InvalidRequestError } from '@atproto/xrpc-server' -export default function (server: Server, ctx: AppContext) { - server.com.atproto.temp.fetchLabels(async ({ params }) => { - const { limit } = params - const db = ctx.db.getReplica() - const since = - params.since !== undefined ? new Date(params.since).toISOString() : '' - const labelRes = await db.db - .selectFrom('label') - .selectAll() - .orderBy('label.cts', 'asc') - .where('cts', '>', since) - .limit(limit) - .execute() - - const labels = labelRes.map((l) => ({ - ...l, - cid: l.cid === '' ? undefined : l.cid, - })) - - return { - encoding: 'application/json', - body: { - labels, - }, - } +export default function (server: Server, _ctx: AppContext) { + server.com.atproto.temp.fetchLabels(async (_reqCtx) => { + throw new InvalidRequestError('not implemented on dataplane') }) } diff --git a/packages/bsky/src/api/health.ts b/packages/bsky/src/api/health.ts index b8ebadd4b71..20190cd030c 100644 --- a/packages/bsky/src/api/health.ts +++ b/packages/bsky/src/api/health.ts @@ -1,5 +1,4 @@ import express from 'express' -import { sql } from 'kysely' import AppContext from '../context' export const createRouter = (ctx: AppContext): express.Router => { @@ -21,9 +20,8 @@ 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(db.db) + await ctx.dataplane.ping({}) } catch (err) { req.log.error(err, 'failed health check') return res.status(503).send({ version, error: 'Service Unavailable' }) diff --git a/packages/bsky/src/api/util.ts b/packages/bsky/src/api/util.ts index ef7e51bc95e..2fe54a8a7be 100644 --- a/packages/bsky/src/api/util.ts +++ b/packages/bsky/src/api/util.ts @@ -5,3 +5,8 @@ export const setRepoRev = (res: express.Response, rev: string | null) => { res.setHeader('Atproto-Repo-Rev', rev) } } + +export const clearlyBadCursor = (cursor?: string) => { + // hallmark of v1 cursor, highly unlikely in v2 cursors based on time or rkeys + return !!cursor?.includes('::') +} diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 5a2bf753072..7798efa99b2 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -2,9 +2,16 @@ import { AuthRequiredError, verifyJwt as verifyServiceJwt, } from '@atproto/xrpc-server' -import { IdResolver } from '@atproto/identity' import * as ui8 from 'uint8arrays' import express from 'express' +import { + Code, + DataPlaneClient, + getKeyAsDidKey, + isDataplaneError, + unpackIdentityKeys, +} from './data-plane' +import { GetIdentityByDidResponse } from './proto/bsky_pb' type ReqCtx = { req: express.Request @@ -35,8 +42,6 @@ type RoleOutput = { credentials: { type: 'role' admin: boolean - moderator: boolean - triage: boolean } } @@ -51,29 +56,35 @@ type AdminServiceOutput = { export type AuthVerifierOpts = { ownDid: string adminDid: string - adminPass: string - moderatorPass: string - triagePass: string + adminPasses: string[] } export class AuthVerifier { - private _adminPass: string - private _moderatorPass: string - private _triagePass: string public ownDid: string public adminDid: string + private adminPasses: Set - constructor(public idResolver: IdResolver, opts: AuthVerifierOpts) { - this._adminPass = opts.adminPass - this._moderatorPass = opts.moderatorPass - this._triagePass = opts.triagePass + constructor(public dataplane: DataPlaneClient, opts: AuthVerifierOpts) { this.ownDid = opts.ownDid this.adminDid = opts.adminDid + this.adminPasses = new Set(opts.adminPasses) } // verifiers (arrow fns to preserve scope) standard = async (ctx: ReqCtx): Promise => { + // @TODO remove! basic auth + did supported just for testing. + if (isBasicToken(ctx.req)) { + const aud = this.ownDid + const iss = ctx.req.headers['appview-as-did'] + if (typeof iss !== 'string' || !iss.startsWith('did:')) { + throw new AuthRequiredError('bad issuer') + } + if (!this.parseRoleCreds(ctx.req).admin) { + throw new AuthRequiredError('bad credentials') + } + return { credentials: { type: 'standard', iss, aud } } + } const { iss, aud } = await this.verifyServiceJwt(ctx, { aud: this.ownDid, iss: null, @@ -84,7 +95,7 @@ export class AuthVerifier { standardOptional = async ( ctx: ReqCtx, ): Promise => { - if (isBearerToken(ctx.req)) { + if (isBearerToken(ctx.req) || isBasicToken(ctx.req)) { return this.standard(ctx) } return this.nullCreds() @@ -173,16 +184,10 @@ export class AuthVerifier { return { status: Missing, admin: false, moderator: false, triage: false } } const { username, password } = parsed - if (username === 'admin' && password === this._adminPass) { - return { status: Valid, admin: true, moderator: true, triage: true } - } - if (username === 'admin' && password === this._moderatorPass) { - return { status: Valid, admin: false, moderator: true, triage: true } + if (username === 'admin' && this.adminPasses.has(password)) { + return { status: Valid, admin: true } } - if (username === 'admin' && password === this._triagePass) { - return { status: Valid, admin: false, moderator: false, triage: true } - } - return { status: Invalid, admin: false, moderator: false, triage: false } + return { status: Invalid, admin: false } } async verifyServiceJwt( @@ -191,12 +196,26 @@ export class AuthVerifier { ) { const getSigningKey = async ( did: string, - forceRefresh: boolean, + _forceRefresh: boolean, // @TODO consider propagating to dataplane ): Promise => { if (opts.iss !== null && !opts.iss.includes(did)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - return this.idResolver.did.resolveAtprotoKey(did, forceRefresh) + let identity: GetIdentityByDidResponse + try { + identity = await this.dataplane.getIdentityByDid({ did }) + } catch (err) { + if (isDataplaneError(err, Code.NotFound)) { + throw new AuthRequiredError('identity unknown') + } + throw err + } + const keys = unpackIdentityKeys(identity.keys) + const didKey = getKeyAsDidKey(keys, { id: 'atproto' }) + if (!didKey) { + throw new AuthRequiredError('missing or bad key') + } + return didKey } const jwtStr = bearerTokenFromReq(reqCtx.req) @@ -222,10 +241,10 @@ export class AuthVerifier { const viewer = creds.credentials.type === 'standard' ? creds.credentials.iss : null const canViewTakedowns = - (creds.credentials.type === 'role' && creds.credentials.triage) || + (creds.credentials.type === 'role' && creds.credentials.admin) || creds.credentials.type === 'admin_service' const canPerformTakedown = - (creds.credentials.type === 'role' && creds.credentials.moderator) || + (creds.credentials.type === 'role' && creds.credentials.admin) || creds.credentials.type === 'admin_service' return { viewer, @@ -245,6 +264,10 @@ const isBearerToken = (req: express.Request): boolean => { return req.headers.authorization?.startsWith(BEARER) ?? false } +const isBasicToken = (req: express.Request): boolean => { + return req.headers.authorization?.startsWith(BASIC) ?? false +} + const bearerTokenFromReq = (req: express.Request) => { const header = req.headers.authorization || '' if (!header.startsWith(BEARER)) return null diff --git a/packages/bsky/src/auto-moderator/hive.ts b/packages/bsky/src/auto-moderator/hive.ts deleted file mode 100644 index 51d67c1c783..00000000000 --- a/packages/bsky/src/auto-moderator/hive.ts +++ /dev/null @@ -1,187 +0,0 @@ -import axios from 'axios' -import FormData from 'form-data' -import { CID } from 'multiformats/cid' -import { IdResolver } from '@atproto/identity' -import { PrimaryDatabase } from '../db' -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 interface ImgLabeler { - labelImg(did: string, cid: CID): Promise -} - -export class HiveLabeler implements ImgLabeler { - constructor( - public hiveApiKey: string, - protected ctx: { - db: PrimaryDatabase - idResolver: IdResolver - }, - ) {} - - 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) - } - - async makeHiveReq(did: string, cid: CID): Promise { - 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, { - headers: { - 'Content-Type': 'multipart/form-data', - authorization: `token ${this.hiveApiKey}`, - accept: 'application/json', - }, - }) - return data - } -} - -export const respToClasses = (res: HiveResp): HiveRespClass[] => { - const classes: HiveRespClass[] = [] - for (const status of res.status) { - for (const out of status.response.output) { - for (const cls of out.classes) { - classes.push(cls) - } - } - } - return classes -} - -// Matches only one (or none) of: porn, sexual, nudity -// -// porn: sexual and nudity. including both explicit activity or full-frontal and suggestive/intent -// sexual: sexually suggestive, not explicit; may include some forms of nudity -// nudity: non-sexual nudity (eg, artistic, possibly some photographic) -// -// hive docs/definitions: https://docs.thehive.ai/docs/sexual-content -export const sexualLabels = (classes: HiveRespClass[]): string[] => { - const scores = {} - - for (const cls of classes) { - scores[cls.class] = cls.score - } - - // first check if porn... - for (const pornClass of [ - 'yes_sexual_activity', - 'animal_genitalia_and_human', - 'yes_realistic_nsfw', - ]) { - if (scores[pornClass] >= 0.9) { - return ['porn'] - } - } - if (scores['general_nsfw'] >= 0.9) { - // special case for some anime examples - if (scores['animated_animal_genitalia'] >= 0.5) { - return ['porn'] - } - // special case for some pornographic/explicit classic drawings - if (scores['yes_undressed'] >= 0.9 && scores['yes_sexual_activity'] > 0.9) { - return ['porn'] - } - } - - // then check for sexual suggestive (which may include nudity)... - for (const sexualClass of ['yes_sexual_intent', 'yes_sex_toy']) { - if (scores[sexualClass] >= 0.9) { - return ['sexual'] - } - } - if (scores['yes_undressed'] >= 0.9) { - // special case for bondage examples - if (scores['yes_sex_toy'] > 0.75) { - return ['sexual'] - } - } - - // then non-sexual nudity... - for (const nudityClass of [ - 'yes_male_nudity', - 'yes_female_nudity', - 'yes_undressed', - ]) { - if (scores[nudityClass] >= 0.9) { - return ['nudity'] - } - } - - // then finally flag remaining "underwear" images in to sexually suggestive - // (after non-sexual content already labeled above) - for (const nudityClass of ['yes_male_underwear', 'yes_female_underwear']) { - if (scores[nudityClass] >= 0.9) { - // TODO: retaining 'underwear' label for a short time to help understand - // the impact of labeling all "underwear" as "sexual". This *will* be - // pulling in somewhat non-sexual content in to "sexual" label. - return ['sexual'] - } - } - - return [] -} - -// gore and violence: https://docs.thehive.ai/docs/class-descriptions-violence-gore -const labelForClass = { - very_bloody: 'gore', - human_corpse: 'corpse', - hanging: 'corpse', -} -const labelForClassLessSensitive = { - yes_self_harm: 'self-harm', -} - -export const summarizeLabels = (classes: HiveRespClass[]): string[] => { - const labels: string[] = sexualLabels(classes) - for (const cls of classes) { - if (labelForClass[cls.class] && cls.score >= 0.9) { - labels.push(labelForClass[cls.class]) - } - } - for (const cls of classes) { - if (labelForClassLessSensitive[cls.class] && cls.score >= 0.96) { - labels.push(labelForClassLessSensitive[cls.class]) - } - } - return labels -} - -type HiveResp = { - status: HiveRespStatus[] -} - -type HiveRespStatus = { - response: { - output: HiveRespOutput[] - } -} - -type HiveRespOutput = { - time: number - classes: HiveRespClass[] -} - -type HiveRespClass = { - class: string - score: number -} diff --git a/packages/bsky/src/auto-moderator/index.ts b/packages/bsky/src/auto-moderator/index.ts deleted file mode 100644 index 7883a26d9e4..00000000000 --- a/packages/bsky/src/auto-moderator/index.ts +++ /dev/null @@ -1,91 +0,0 @@ -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-verifier' -import { CID } from 'multiformats/cid' -import { HiveLabeler, ImgLabeler } from './hive' -import { KeywordLabeler, TextLabeler } from './keyword' -import { ids } from '../lexicon/lexicons' - -export class AutoModerator { - public pushAgent: AtpAgent - public imgLabeler?: ImgLabeler - public textLabeler?: TextLabeler - - constructor( - public ctx: { - db: PrimaryDatabase - idResolver: IdResolver - cfg: IndexerConfig - backgroundQueue: BackgroundQueue - }, - ) { - const { hiveApiKey } = ctx.cfg - this.imgLabeler = hiveApiKey ? new HiveLabeler(hiveApiKey, ctx) : undefined - this.textLabeler = new KeywordLabeler(ctx.cfg.labelerKeywords) - - 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, uri) - await this.labelRecord(uri, cid, text, imgs).catch((err) => { - log.error( - { err, uri: uri.toString(), record: obj }, - 'failed to label record', - ) - }) - }) - } - - processHandle(_handle: string, _did: string) { - // no-op since this functionality moved to auto-mod service - } - - async labelRecord(uri: AtUri, recordCid: CID, text: string[], imgs: CID[]) { - if (uri.collection !== ids.AppBskyFeedPost) { - // @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.pushLabels(uri, recordCid, labels) - } - - async pushLabels(uri: AtUri, cid: CID, labels: string[]): Promise { - if (labels.length < 1) return - - await this.pushAgent.com.atproto.admin.emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventLabel', - comment: '[AutoModerator]: Applying labels', - createLabelVals: labels, - negateLabelVals: [], - }, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: uri.toString(), - cid: cid.toString(), - }, - createdBy: this.ctx.cfg.serverDid, - }) - } - - async processAll() { - await this.ctx.backgroundQueue.processAll() - } -} diff --git a/packages/bsky/src/auto-moderator/keyword.ts b/packages/bsky/src/auto-moderator/keyword.ts deleted file mode 100644 index 6bc504aa142..00000000000 --- a/packages/bsky/src/auto-moderator/keyword.ts +++ /dev/null @@ -1,25 +0,0 @@ -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/auto-moderator/util.ts b/packages/bsky/src/auto-moderator/util.ts deleted file mode 100644 index ab1467a07f2..00000000000 --- a/packages/bsky/src/auto-moderator/util.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/syntax' -import * as lex from '../lexicon/lexicons' -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' - -type RecordFields = { - text: string[] - imgs: CID[] -} - -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: [] } - } -} - -export const getFieldsFromPost = (record: PostRecord): RecordFields => { - const text: string[] = [] - const imgs: CID[] = [] - text.push(record.text) - const embeds = separateEmbeds(record.embed) - for (const embed of embeds) { - if (isEmbedImage(embed)) { - for (const img of embed.images) { - imgs.push(img.image.ref) - text.push(img.alt) - } - } else if (isEmbedExternal(embed)) { - if (embed.external.thumb) { - imgs.push(embed.external.thumb.ref) - } - text.push(embed.external.title) - text.push(embed.external.description) - } - } - return { text, imgs } -} - -export const getFieldsFromProfile = (record: ProfileRecord): RecordFields => { - const text: string[] = [] - const imgs: CID[] = [] - if (record.displayName) { - text.push(record.displayName) - } - if (record.description) { - text.push(record.description) - } - if (record.avatar) { - imgs.push(record.avatar.ref) - } - if (record.banner) { - imgs.push(record.banner.ref) - } - return { text, imgs } -} - -export const 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) { - if (str !== undefined) { - set.add(str) - } - } - return [...set] -} - -const separateEmbeds = (embed: PostRecord['embed']) => { - if (!embed) { - return [] - } - if (isEmbedRecordWithMedia(embed)) { - return [{ $type: lex.ids.AppBskyEmbedRecord, ...embed.record }, embed.media] - } - return [embed] -} diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index 1d88abb588a..6f9c96776f4 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -1,53 +1,35 @@ -import assert from 'assert' -import { - DAY, - HOUR, - MINUTE, - SECOND, - parseIntWithFallback, -} from '@atproto/common' +import assert from 'node:assert' export interface ServerConfigValues { - version: string + // service + version?: string debugMode?: boolean port?: number publicUrl?: string serverDid: string - feedGenDid?: string - dbPrimaryPostgresUrl: string - dbReplicaPostgresUrls?: string[] - dbReplicaTags?: Record // E.g. { timeline: [0], thread: [1] } - 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 - labelCacheStaleTTL: number - labelCacheMaxTTL: number - handleResolveNameservers?: string[] - imgUriEndpoint?: string - blobCacheLocation?: string - searchEndpoint?: string - bsyncUrl?: string + // external services + dataplaneUrls: string[] + dataplaneHttpVersion?: '1.1' | '2' + dataplaneIgnoreBadTls?: boolean + bsyncUrl: string bsyncApiKey?: string bsyncHttpVersion?: '1.1' | '2' bsyncIgnoreBadTls?: boolean - bsyncOnlyMutes?: boolean - courierUrl?: string + courierUrl: string courierApiKey?: string courierHttpVersion?: '1.1' | '2' courierIgnoreBadTls?: boolean - courierOnlyRegistration?: boolean - adminPassword: string - moderatorPassword: string - triagePassword: string + searchUrl?: string + cdnUrl?: string + // identity + didPlcUrl: string + handleResolveNameservers?: string[] + // moderation and administration modServiceDid: string - rateLimitsEnabled: boolean - rateLimitBypassKey?: string - rateLimitBypassIps?: string[] + adminPasswords: string[] + labelsFromIssuerDids?: string[] + // misc/dev + blobCacheLocation?: string } export class ServerConfig { @@ -55,149 +37,77 @@ export class ServerConfig { constructor(private cfg: ServerConfigValues) {} static readEnv(overrides?: Partial) { - const version = process.env.BSKY_VERSION || '0.0.0' + const version = process.env.BSKY_VERSION || undefined const debugMode = process.env.NODE_ENV !== 'production' - const publicUrl = process.env.PUBLIC_URL || undefined - const serverDid = process.env.SERVER_DID || 'did:example:test' - const feedGenDid = process.env.FEED_GEN_DID - const envPort = parseInt(process.env.PORT || '', 10) + const publicUrl = process.env.BSKY_PUBLIC_URL || undefined + const serverDid = process.env.BSKY_SERVER_DID || 'did:example:test' + const envPort = parseInt(process.env.BSKY_PORT || '', 10) const port = isNaN(envPort) ? 2584 : envPort - const redisHost = - overrides?.redisHost || process.env.REDIS_HOST || undefined - const redisSentinelName = - overrides?.redisSentinelName || - process.env.REDIS_SENTINEL_NAME || + const didPlcUrl = process.env.BSKY_DID_PLC_URL || 'http://localhost:2582' + const handleResolveNameservers = process.env.BSKY_HANDLE_RESOLVE_NAMESERVERS + ? process.env.BSKY_HANDLE_RESOLVE_NAMESERVERS.split(',') + : [] + const cdnUrl = process.env.BSKY_CDN_URL || process.env.BSKY_IMG_URI_ENDPOINT + const blobCacheLocation = process.env.BSKY_BLOB_CACHE_LOC + const searchUrl = + process.env.BSKY_SEARCH_URL || + process.env.BSKY_SEARCH_ENDPOINT || 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 labelCacheStaleTTL = parseIntWithFallback( - process.env.LABEL_CACHE_STALE_TTL, - 30 * SECOND, - ) - const labelCacheMaxTTL = parseIntWithFallback( - process.env.LABEL_CACHE_MAX_TTL, - MINUTE, - ) - const handleResolveNameservers = process.env.HANDLE_RESOLVE_NAMESERVERS - ? process.env.HANDLE_RESOLVE_NAMESERVERS.split(',') + let dataplaneUrls = overrides?.dataplaneUrls + dataplaneUrls ??= process.env.BSKY_DATAPLANE_URLS + ? process.env.BSKY_DATAPLANE_URLS.split(',') + : [] + const dataplaneHttpVersion = process.env.BSKY_DATAPLANE_HTTP_VERSION || '2' + const dataplaneIgnoreBadTls = + process.env.BSKY_DATAPLANE_IGNORE_BAD_TLS === 'true' + const labelsFromIssuerDids = process.env.BSKY_LABELS_FROM_ISSUER_DIDS + ? process.env.BSKY_LABELS_FROM_ISSUER_DIDS.split(',') : [] - const imgUriEndpoint = process.env.IMG_URI_ENDPOINT - const blobCacheLocation = process.env.BLOB_CACHE_LOC - const searchEndpoint = process.env.SEARCH_ENDPOINT const bsyncUrl = process.env.BSKY_BSYNC_URL || undefined + assert(bsyncUrl) const bsyncApiKey = process.env.BSKY_BSYNC_API_KEY || undefined const bsyncHttpVersion = process.env.BSKY_BSYNC_HTTP_VERSION || '2' const bsyncIgnoreBadTls = process.env.BSKY_BSYNC_IGNORE_BAD_TLS === 'true' - const bsyncOnlyMutes = process.env.BSKY_BSYNC_ONLY_MUTES === 'true' - assert(!bsyncOnlyMutes || bsyncUrl, 'bsync-only mutes requires a bsync url') assert(bsyncHttpVersion === '1.1' || bsyncHttpVersion === '2') const courierUrl = process.env.BSKY_COURIER_URL || undefined + assert(courierUrl) const courierApiKey = process.env.BSKY_COURIER_API_KEY || undefined const courierHttpVersion = process.env.BSKY_COURIER_HTTP_VERSION || '2' const courierIgnoreBadTls = process.env.BSKY_COURIER_IGNORE_BAD_TLS === 'true' - const courierOnlyRegistration = - process.env.BSKY_COURIER_ONLY_REGISTRATION === 'true' - assert( - !courierOnlyRegistration || courierUrl, - 'courier-only registration requires a courier url', - ) assert(courierHttpVersion === '1.1' || courierHttpVersion === '2') - 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 adminPasswords = envList( + process.env.BSKY_ADMIN_PASSWORDS || process.env.BSKY_ADMIN_PASSWORD, ) - const dbPostgresSchema = process.env.DB_POSTGRES_SCHEMA - assert(dbPrimaryPostgresUrl) - const adminPassword = process.env.ADMIN_PASSWORD || undefined - assert(adminPassword) - const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined - assert(moderatorPassword) - const triagePassword = process.env.TRIAGE_PASSWORD || undefined - assert(triagePassword) - const modServiceDid = - overrides?.modServiceDid || - process.env.MODERATION_SERVICE_DID || - undefined + const modServiceDid = process.env.MOD_SERVICE_DID assert(modServiceDid) - const rateLimitsEnabled = process.env.RATE_LIMITS_ENABLED === 'true' - const rateLimitBypassKey = process.env.RATE_LIMIT_BYPASS_KEY - const rateLimitBypassIps = process.env.RATE_LIMIT_BYPASS_IPS - ? process.env.RATE_LIMIT_BYPASS_IPS.split(',').map((ipOrCidr) => - ipOrCidr.split('/')[0]?.trim(), - ) - : undefined - + assert(dataplaneUrls.length) + assert(dataplaneHttpVersion === '1.1' || dataplaneHttpVersion === '2') return new ServerConfig({ version, debugMode, port, publicUrl, serverDid, - feedGenDid, - dbPrimaryPostgresUrl, - dbReplicaPostgresUrls, - dbReplicaTags, - dbPostgresSchema, - redisHost, - redisSentinelName, - redisSentinelHosts, - redisPassword, + dataplaneUrls, + dataplaneHttpVersion, + dataplaneIgnoreBadTls, + searchUrl, didPlcUrl, - didCacheStaleTTL, - didCacheMaxTTL, - labelCacheStaleTTL, - labelCacheMaxTTL, + labelsFromIssuerDids, handleResolveNameservers, - imgUriEndpoint, + cdnUrl, blobCacheLocation, - searchEndpoint, bsyncUrl, bsyncApiKey, bsyncHttpVersion, bsyncIgnoreBadTls, - bsyncOnlyMutes, courierUrl, courierApiKey, courierHttpVersion, courierIgnoreBadTls, - courierOnlyRegistration, - adminPassword, - moderatorPassword, - triagePassword, + adminPasswords, modServiceDid, - rateLimitsEnabled, - rateLimitBypassKey, - rateLimitBypassIps, ...stripUndefineds(overrides ?? {}), }) } @@ -235,76 +145,16 @@ export class ServerConfig { return this.cfg.serverDid } - get feedGenDid() { - return this.cfg.feedGenDid - } - - get dbPrimaryPostgresUrl() { - return this.cfg.dbPrimaryPostgresUrl - } - - get dbReplicaPostgresUrl() { - return this.cfg.dbReplicaPostgresUrls - } - - get dbReplicaTags() { - return this.cfg.dbReplicaTags - } - - get dbPostgresSchema() { - return this.cfg.dbPostgresSchema + get dataplaneUrls() { + return this.cfg.dataplaneUrls } - get redisHost() { - return this.cfg.redisHost + get dataplaneHttpVersion() { + return this.cfg.dataplaneHttpVersion } - get redisSentinelName() { - return this.cfg.redisSentinelName - } - - get redisSentinelHosts() { - return this.cfg.redisSentinelHosts - } - - get redisPassword() { - return this.cfg.redisPassword - } - - get didCacheStaleTTL() { - return this.cfg.didCacheStaleTTL - } - - get didCacheMaxTTL() { - return this.cfg.didCacheMaxTTL - } - - get labelCacheStaleTTL() { - return this.cfg.labelCacheStaleTTL - } - - get labelCacheMaxTTL() { - return this.cfg.labelCacheMaxTTL - } - - get handleResolveNameservers() { - return this.cfg.handleResolveNameservers - } - - get didPlcUrl() { - return this.cfg.didPlcUrl - } - - get imgUriEndpoint() { - return this.cfg.imgUriEndpoint - } - - get blobCacheLocation() { - return this.cfg.blobCacheLocation - } - - get searchEndpoint() { - return this.cfg.searchEndpoint + get dataplaneIgnoreBadTls() { + return this.cfg.dataplaneIgnoreBadTls } get bsyncUrl() { @@ -315,10 +165,6 @@ export class ServerConfig { return this.cfg.bsyncApiKey } - get bsyncOnlyMutes() { - return this.cfg.bsyncOnlyMutes - } - get bsyncHttpVersion() { return this.cfg.bsyncHttpVersion } @@ -343,43 +189,39 @@ export class ServerConfig { return this.cfg.courierIgnoreBadTls } - get courierOnlyRegistration() { - return this.cfg.courierOnlyRegistration + get searchUrl() { + return this.cfg.searchUrl } - get adminPassword() { - return this.cfg.adminPassword + get cdnUrl() { + return this.cfg.cdnUrl } - get moderatorPassword() { - return this.cfg.moderatorPassword + get didPlcUrl() { + return this.cfg.didPlcUrl } - get triagePassword() { - return this.cfg.triagePassword + get handleResolveNameservers() { + return this.cfg.handleResolveNameservers } - get modServiceDid() { - return this.cfg.modServiceDid + get adminPasswords() { + return this.cfg.adminPasswords } - get rateLimitsEnabled() { - return this.cfg.rateLimitsEnabled + get modServiceDid() { + return this.cfg.modServiceDid } - get rateLimitBypassKey() { - return this.cfg.rateLimitBypassKey + get labelsFromIssuerDids() { + return this.cfg.labelsFromIssuerDids ?? [] } - get rateLimitBypassIps() { - return this.cfg.rateLimitBypassIps + get blobCacheLocation() { + return this.cfg.blobCacheLocation } } -function getTagIdxs(str?: string): number[] { - return str ? str.split(',').map((item) => parseInt(item, 10)) : [] -} - function stripUndefineds( obj: Record, ): Record { @@ -391,3 +233,8 @@ function stripUndefineds( }) return result } + +function envList(str: string | undefined): string[] { + if (str === undefined || str.length === 0) return [] + return str.split(',') +} diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index db9779e54b9..3b297caf095 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -1,15 +1,12 @@ import * as plc from '@did-plc/lib' import { IdResolver } from '@atproto/identity' -import { AtpAgent } from '@atproto/api' +import AtpAgent from '@atproto/api' import { Keypair } from '@atproto/crypto' import { createServiceJwt } from '@atproto/xrpc-server' -import { DatabaseCoordinator } from './db' import { ServerConfig } from './config' -import { ImageUriBuilder } from './image/uri' -import { Services } from './services' -import DidRedisCache from './did-cache' -import { BackgroundQueue } from './background' -import { Redis } from './redis' +import { DataPlaneClient } from './data-plane/client' +import { Hydrator } from './hydration/hydrator' +import { Views } from './views' import { AuthVerifier } from './auth-verifier' import { BsyncClient } from './bsync' import { CourierClient } from './courier' @@ -17,36 +14,37 @@ import { CourierClient } from './courier' export class AppContext { constructor( private opts: { - db: DatabaseCoordinator - imgUriBuilder: ImageUriBuilder cfg: ServerConfig - services: Services + dataplane: DataPlaneClient + searchAgent: AtpAgent | undefined + hydrator: Hydrator + views: Views signingKey: Keypair idResolver: IdResolver - didCache: DidRedisCache - redis: Redis - backgroundQueue: BackgroundQueue - searchAgent?: AtpAgent - bsyncClient?: BsyncClient - courierClient?: CourierClient + bsyncClient: BsyncClient + courierClient: CourierClient authVerifier: AuthVerifier }, ) {} - get db(): DatabaseCoordinator { - return this.opts.db + get cfg(): ServerConfig { + return this.opts.cfg } - get imgUriBuilder(): ImageUriBuilder { - return this.opts.imgUriBuilder + get dataplane(): DataPlaneClient { + return this.opts.dataplane } - get cfg(): ServerConfig { - return this.opts.cfg + get searchAgent(): AtpAgent | undefined { + return this.opts.searchAgent + } + + get hydrator(): Hydrator { + return this.opts.hydrator } - get services(): Services { - return this.opts.services + get views(): Views { + return this.opts.views } get signingKey(): Keypair { @@ -61,23 +59,11 @@ export class AppContext { return this.opts.idResolver } - get didCache(): DidRedisCache { - return this.opts.didCache - } - - get redis(): Redis { - return this.opts.redis - } - - get searchAgent(): AtpAgent | undefined { - return this.opts.searchAgent - } - - get bsyncClient(): BsyncClient | undefined { + get bsyncClient(): BsyncClient { return this.opts.bsyncClient } - get courierClient(): CourierClient | undefined { + get courierClient(): CourierClient { return this.opts.courierClient } @@ -93,10 +79,6 @@ export class AppContext { keypair: this.signingKey, }) } - - get backgroundQueue(): BackgroundQueue { - return this.opts.backgroundQueue - } } export default AppContext diff --git a/packages/bsky/src/daemon/config.ts b/packages/bsky/src/daemon/config.ts deleted file mode 100644 index 3dd7d557652..00000000000 --- a/packages/bsky/src/daemon/config.ts +++ /dev/null @@ -1,60 +0,0 @@ -import assert from 'assert' - -export interface DaemonConfigValues { - version: string - dbPostgresUrl: string - dbPostgresSchema?: string - notificationsDaemonFromDid?: string -} - -export class DaemonConfig { - constructor(private cfg: DaemonConfigValues) {} - - static readEnv(overrides?: Partial) { - const version = process.env.BSKY_VERSION || '0.0.0' - const dbPostgresUrl = - overrides?.dbPostgresUrl || process.env.DB_PRIMARY_POSTGRES_URL - const dbPostgresSchema = - overrides?.dbPostgresSchema || process.env.DB_POSTGRES_SCHEMA - const notificationsDaemonFromDid = - overrides?.notificationsDaemonFromDid || - process.env.BSKY_NOTIFS_DAEMON_FROM_DID || - undefined - assert(dbPostgresUrl) - return new DaemonConfig({ - version, - dbPostgresUrl, - dbPostgresSchema, - notificationsDaemonFromDid, - ...stripUndefineds(overrides ?? {}), - }) - } - - get version() { - return this.cfg.version - } - - get dbPostgresUrl() { - return this.cfg.dbPostgresUrl - } - - get dbPostgresSchema() { - return this.cfg.dbPostgresSchema - } - - get notificationsDaemonFromDid() { - return this.cfg.notificationsDaemonFromDid - } -} - -function stripUndefineds( - obj: Record, -): Record { - const result = {} - Object.entries(obj).forEach(([key, val]) => { - if (val !== undefined) { - result[key] = val - } - }) - return result -} diff --git a/packages/bsky/src/daemon/context.ts b/packages/bsky/src/daemon/context.ts deleted file mode 100644 index dd3d5c1114f..00000000000 --- a/packages/bsky/src/daemon/context.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { PrimaryDatabase } from '../db' -import { DaemonConfig } from './config' -import { Services } from './services' - -export class DaemonContext { - constructor( - private opts: { - db: PrimaryDatabase - cfg: DaemonConfig - services: Services - }, - ) {} - - get db(): PrimaryDatabase { - return this.opts.db - } - - get cfg(): DaemonConfig { - return this.opts.cfg - } - - get services(): Services { - return this.opts.services - } -} - -export default DaemonContext diff --git a/packages/bsky/src/daemon/index.ts b/packages/bsky/src/daemon/index.ts deleted file mode 100644 index 80da01edc2f..00000000000 --- a/packages/bsky/src/daemon/index.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { PrimaryDatabase } from '../db' -import { dbLogger } from '../logger' -import { DaemonConfig } from './config' -import { DaemonContext } from './context' -import { createServices } from './services' -import { ImageUriBuilder } from '../image/uri' -import { NotificationsDaemon } from './notifications' -import logger from './logger' - -export { DaemonConfig } from './config' -export type { DaemonConfigValues } from './config' - -export class BskyDaemon { - public ctx: DaemonContext - public notifications: NotificationsDaemon - private dbStatsInterval: NodeJS.Timer - private notifStatsInterval: NodeJS.Timer - - constructor(opts: { - ctx: DaemonContext - notifications: NotificationsDaemon - }) { - this.ctx = opts.ctx - this.notifications = opts.notifications - } - - static create(opts: { db: PrimaryDatabase; cfg: DaemonConfig }): BskyDaemon { - const { db, cfg } = opts - const imgUriBuilder = new ImageUriBuilder('https://daemon.invalid') // will not be used by daemon - const services = createServices({ - imgUriBuilder, - }) - const ctx = new DaemonContext({ - db, - cfg, - services, - }) - const notifications = new NotificationsDaemon(ctx) - return new BskyDaemon({ ctx, notifications }) - } - - async start() { - const { db, cfg } = this.ctx - const pool = db.pool - this.notifications.run({ - startFromDid: cfg.notificationsDaemonFromDid, - }) - this.dbStatsInterval = setInterval(() => { - dbLogger.info( - { - idleCount: pool.idleCount, - totalCount: pool.totalCount, - waitingCount: pool.waitingCount, - }, - 'db pool stats', - ) - }, 10000) - this.notifStatsInterval = setInterval(() => { - logger.info( - { - count: this.notifications.count, - lastDid: this.notifications.lastDid, - }, - 'notifications daemon stats', - ) - }, 10000) - return this - } - - async destroy(): Promise { - await this.notifications.destroy() - await this.ctx.db.close() - clearInterval(this.dbStatsInterval) - clearInterval(this.notifStatsInterval) - } -} - -export default BskyDaemon diff --git a/packages/bsky/src/daemon/logger.ts b/packages/bsky/src/daemon/logger.ts deleted file mode 100644 index 8599acc315e..00000000000 --- a/packages/bsky/src/daemon/logger.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { subsystemLogger } from '@atproto/common' - -const logger: ReturnType = - subsystemLogger('bsky:daemon') - -export default logger diff --git a/packages/bsky/src/daemon/notifications.ts b/packages/bsky/src/daemon/notifications.ts deleted file mode 100644 index 96431af8c1f..00000000000 --- a/packages/bsky/src/daemon/notifications.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { tidyNotifications } from '../services/util/notification' -import DaemonContext from './context' -import logger from './logger' - -export class NotificationsDaemon { - ac = new AbortController() - running: Promise | undefined - count = 0 - lastDid: string | null = null - - constructor(private ctx: DaemonContext) {} - - run(opts?: RunOptions) { - if (this.running) return - this.count = 0 - this.lastDid = null - this.ac = new AbortController() - this.running = this.tidyNotifications({ - ...opts, - forever: opts?.forever !== false, // run forever by default - }) - .catch((err) => { - // allow this to cause an unhandled rejection, let deployment handle the crash. - logger.error({ err }, 'notifications daemon crashed') - throw err - }) - .finally(() => (this.running = undefined)) - } - - private async tidyNotifications(opts: RunOptions) { - const actorService = this.ctx.services.actor(this.ctx.db) - for await (const { did } of actorService.all(opts)) { - if (this.ac.signal.aborted) return - try { - await tidyNotifications(this.ctx.db, did) - this.count++ - this.lastDid = did - } catch (err) { - logger.warn({ err, did }, 'failed to tidy notifications for actor') - } - } - } - - async destroy() { - this.ac.abort() - await this.running - } -} - -type RunOptions = { - forever?: boolean - batchSize?: number - startFromDid?: string -} diff --git a/packages/bsky/src/daemon/services.ts b/packages/bsky/src/daemon/services.ts deleted file mode 100644 index 93141d13a08..00000000000 --- a/packages/bsky/src/daemon/services.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PrimaryDatabase } from '../db' -import { ActorService } from '../services/actor' -import { ImageUriBuilder } from '../image/uri' -import { GraphService } from '../services/graph' -import { LabelService } from '../services/label' - -export function createServices(resources: { - imgUriBuilder: ImageUriBuilder -}): Services { - const { imgUriBuilder } = resources - const graph = GraphService.creator(imgUriBuilder) - const label = LabelService.creator(null) - return { - actor: ActorService.creator(imgUriBuilder, graph, label), - } -} - -export type Services = { - actor: FromDbPrimary -} - -type FromDbPrimary = (db: PrimaryDatabase) => T diff --git a/packages/bsky/src/data-plane/bsync/index.ts b/packages/bsky/src/data-plane/bsync/index.ts new file mode 100644 index 00000000000..a1c82bc1126 --- /dev/null +++ b/packages/bsky/src/data-plane/bsync/index.ts @@ -0,0 +1,98 @@ +import http from 'http' +import events from 'events' +import express from 'express' +import { ConnectRouter } from '@connectrpc/connect' +import { expressConnectMiddleware } from '@connectrpc/connect-express' +import { Database } from '../server/db' +import { Service } from '../../proto/bsync_connect' +import { MuteOperation_Type } from '../../proto/bsync_pb' +import assert from 'assert' + +export class MockBsync { + constructor(public server: http.Server) {} + + static async create(db: Database, port: number) { + const app = express() + const routes = createRoutes(db) + app.use(expressConnectMiddleware({ routes })) + const server = app.listen(port) + await events.once(server, 'listening') + return new MockBsync(server) + } + + async destroy() { + return new Promise((resolve, reject) => { + this.server.close((err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } +} + +const createRoutes = (db: Database) => (router: ConnectRouter) => + router.service(Service, { + async addMuteOperation(req) { + const { type, actorDid, subject } = req + if (type === MuteOperation_Type.ADD) { + if (subject.startsWith('did:')) { + assert(actorDid !== subject, 'cannot mute yourself') // @TODO pass message through in http error + await db.db + .insertInto('mute') + .values({ + mutedByDid: actorDid, + subjectDid: subject, + createdAt: new Date().toISOString(), + }) + .onConflict((oc) => oc.doNothing()) + .execute() + } else { + await db.db + .insertInto('list_mute') + .values({ + mutedByDid: actorDid, + listUri: subject, + createdAt: new Date().toISOString(), + }) + .onConflict((oc) => oc.doNothing()) + .execute() + } + } else if (type === MuteOperation_Type.REMOVE) { + if (subject.startsWith('did:')) { + await db.db + .deleteFrom('mute') + .where('mutedByDid', '=', actorDid) + .where('subjectDid', '=', subject) + .execute() + } else { + await db.db + .deleteFrom('list_mute') + .where('mutedByDid', '=', actorDid) + .where('listUri', '=', subject) + .execute() + } + } else if (type === MuteOperation_Type.CLEAR) { + await db.db + .deleteFrom('mute') + .where('mutedByDid', '=', actorDid) + .execute() + await db.db + .deleteFrom('list_mute') + .where('mutedByDid', '=', actorDid) + .execute() + } + + return {} + }, + + async scanMuteOperations() { + throw new Error('not implemented') + }, + + async ping() { + return {} + }, + }) diff --git a/packages/bsky/src/data-plane/client.ts b/packages/bsky/src/data-plane/client.ts new file mode 100644 index 00000000000..dd525267fe3 --- /dev/null +++ b/packages/bsky/src/data-plane/client.ts @@ -0,0 +1,151 @@ +import assert from 'node:assert' +import { randomInt } from 'node:crypto' +import * as ui8 from 'uint8arrays' +import { + Code, + ConnectError, + PromiseClient, + createPromiseClient, + makeAnyClient, +} from '@connectrpc/connect' +import { createGrpcTransport } from '@connectrpc/connect-node' +import { getDidKeyFromMultibase } from '@atproto/identity' +import { Service } from '../proto/bsky_connect' + +export type DataPlaneClient = PromiseClient +type BaseClient = { lib: DataPlaneClient; url: URL } +type HttpVersion = '1.1' | '2' +const MAX_RETRIES = 3 + +export const createDataPlaneClient = ( + baseUrls: string[], + opts: { httpVersion?: HttpVersion; rejectUnauthorized?: boolean }, +) => { + const clients = baseUrls.map((baseUrl) => createBaseClient(baseUrl, opts)) + assert(clients.length > 0, 'no clients available') + return makeAnyClient(Service, (method) => { + return async (...args) => { + let tries = 0 + let error: unknown + let remainingClients = clients + while (tries < MAX_RETRIES) { + const client = randomElement(remainingClients) + assert(client, 'no clients available') + try { + return await client.lib[method.localName](...args) + } catch (err) { + if (err instanceof ConnectError && err.code === Code.Unavailable) { + tries++ + error = err + remainingClients = getRemainingClients(remainingClients, client) + } else { + throw err + } + } + } + assert(error) + throw error + } + }) as DataPlaneClient +} + +export { Code } + +export const isDataplaneError = ( + err: unknown, + code?: Code, +): err is ConnectError => { + if (err instanceof ConnectError) { + return !code || err.code === code + } + return false +} + +const createBaseClient = ( + baseUrl: string, + opts: { httpVersion?: HttpVersion; rejectUnauthorized?: boolean }, +): BaseClient => { + const { httpVersion = '2', rejectUnauthorized = true } = opts + const transport = createGrpcTransport({ + baseUrl, + httpVersion, + acceptCompression: [], + nodeOptions: { rejectUnauthorized }, + }) + return { + lib: createPromiseClient(Service, transport), + url: new URL(baseUrl), + } +} + +const getRemainingClients = (clients: BaseClient[], lastClient: BaseClient) => { + if (clients.length < 2) return clients // no clients to choose from + if (lastClient.url.port) { + // if the last client had a port, we attempt to exclude its whole host. + const maybeRemaining = clients.filter( + (c) => c.url.hostname !== lastClient.url.hostname, + ) + if (maybeRemaining.length) { + return maybeRemaining + } + } + return clients.filter((c) => c !== lastClient) +} + +const randomElement = (arr: T[]): T | undefined => { + if (arr.length === 0) return + return arr[randomInt(arr.length)] +} + +export const unpackIdentityServices = (servicesBytes: Uint8Array) => { + const servicesStr = ui8.toString(servicesBytes, 'utf8') + if (!servicesStr) return {} + return JSON.parse(servicesStr) as UnpackedServices +} + +export const unpackIdentityKeys = (keysBytes: Uint8Array) => { + const keysStr = ui8.toString(keysBytes, 'utf8') + if (!keysStr) return {} + return JSON.parse(keysStr) as UnpackedKeys +} + +export const getServiceEndpoint = ( + services: UnpackedServices, + opts: { id: string; type: string }, +) => { + const endpoint = + services[opts.id] && + services[opts.id].Type === opts.type && + validateUrl(services[opts.id].URL) + return endpoint || undefined +} + +export const getKeyAsDidKey = (keys: UnpackedKeys, opts: { id: string }) => { + const key = + keys[opts.id] && + getDidKeyFromMultibase({ + type: keys[opts.id].Type, + publicKeyMultibase: keys[opts.id].PublicKeyMultibase, + }) + return key || undefined +} + +type UnpackedServices = Record + +type UnpackedKeys = Record + +const validateUrl = (urlStr: string): string | undefined => { + let url + try { + url = new URL(urlStr) + } catch { + return undefined + } + if (!['http:', 'https:'].includes(url.protocol)) { + return undefined + } else if (!url.hostname) { + return undefined + } else { + return urlStr + } +} diff --git a/packages/bsky/src/data-plane/index.ts b/packages/bsky/src/data-plane/index.ts new file mode 100644 index 00000000000..31fe4b0dde3 --- /dev/null +++ b/packages/bsky/src/data-plane/index.ts @@ -0,0 +1,3 @@ +export * from './server' +export * from './client' +export * from './bsync' diff --git a/packages/bsky/src/background.ts b/packages/bsky/src/data-plane/server/background.ts similarity index 76% rename from packages/bsky/src/background.ts rename to packages/bsky/src/data-plane/server/background.ts index 466bad80a51..59d8ccf0ddf 100644 --- a/packages/bsky/src/background.ts +++ b/packages/bsky/src/data-plane/server/background.ts @@ -1,13 +1,13 @@ import PQueue from 'p-queue' -import { PrimaryDatabase } from './db' -import { dbLogger } from './logger' +import { Database } from './db' +import { dbLogger } from '../../logger' // A simple queue for in-process, out-of-band/backgrounded work export class BackgroundQueue { - queue = new PQueue({ concurrency: 20 }) + queue = new PQueue() destroyed = false - constructor(public db: PrimaryDatabase) {} + constructor(public db: Database) {} add(task: Task) { if (this.destroyed) { @@ -32,4 +32,4 @@ export class BackgroundQueue { } } -type Task = (db: PrimaryDatabase) => Promise +type Task = (db: Database) => Promise diff --git a/packages/bsky/src/db/database-schema.ts b/packages/bsky/src/data-plane/server/db/database-schema.ts similarity index 97% rename from packages/bsky/src/db/database-schema.ts rename to packages/bsky/src/data-plane/server/db/database-schema.ts index df28c8b91d8..e02e07f7ad0 100644 --- a/packages/bsky/src/db/database-schema.ts +++ b/packages/bsky/src/data-plane/server/db/database-schema.ts @@ -24,6 +24,7 @@ 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' import * as algo from './tables/algo' @@ -58,6 +59,7 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB & record.PartialDB & notification.PartialDB & notificationPushToken.PartialDB & + didCache.PartialDB & moderation.PartialDB & label.PartialDB & algo.PartialDB & diff --git a/packages/bsky/src/db/primary.ts b/packages/bsky/src/data-plane/server/db/db.ts similarity index 55% rename from packages/bsky/src/db/primary.ts rename to packages/bsky/src/data-plane/server/db/db.ts index e6e69872fd5..6411938d69d 100644 --- a/packages/bsky/src/db/primary.ts +++ b/packages/bsky/src/data-plane/server/db/db.ts @@ -1,35 +1,77 @@ +import assert from 'assert' import EventEmitter from 'events' import { - Migrator, + Kysely, KyselyPlugin, + Migrator, PluginTransformQueryArgs, PluginTransformResultArgs, - RootOperationNode, + PostgresDialect, QueryResult, + RootOperationNode, 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 { Pool as PgPool, types as pgTypes } from 'pg' import * as migrations from './migrations' -import { CtxMigrationProvider } from './migrations/provider' -import { dbLogger as log } from '../logger' +import DatabaseSchema, { DatabaseSchemaType } from './database-schema' import { PgOptions } from './types' -import { Database } from './db' +import { dbLogger } from '../../../logger' +import { CtxMigrationProvider } from './migrations/provider' -export class PrimaryDatabase extends Database { +export class Database { + pool: PgPool + db: DatabaseSchema migrator: Migrator txEvt = new EventEmitter() as TxnEmitter destroyed = false - isPrimary = true constructor( public opts: PgOptions, - instances?: { db: DatabaseSchema; pool: PgPool }, + instances?: { db: DatabaseSchema; pool: PgPool; migrator: Migrator }, ) { - super(opts, instances) + // if instances are provided, use those + if (instances) { + this.db = instances.db + this.pool = instances.pool + this.migrator = instances.migrator + return + } + + // else create a pool & connect + const { schema, url } = opts + const pool = + opts.pool ?? + new PgPool({ + connectionString: url, + max: opts.poolSize, + maxUses: opts.poolMaxUses, + idleTimeoutMillis: opts.poolIdleTimeoutMs, + }) + + // Select count(*) and other pg bigints as js integer + pgTypes.setTypeParser(pgTypes.builtins.INT8, (n) => parseInt(n, 10)) + + // Setup schema usage, primarily for test parallelism (each test suite runs in its own pg schema) + if (schema && !/^[a-z_]+$/i.test(schema)) { + throw new Error(`Postgres schema must only contain [A-Za-z_]: ${schema}`) + } + + pool.on('error', onPoolError) + pool.on('connect', (client) => { + client.on('error', onClientError) + // Used for trigram indexes, e.g. on actor search + client.query('SET pg_trgm.word_similarity_threshold TO .4;') + if (schema) { + // Shared objects such as extensions will go in the public schema + client.query(`SET search_path TO "${schema}",public;`) + } + }) + + this.pool = pool + this.db = new Kysely({ + dialect: new PostgresDialect({ pool }), + }) this.migrator = new Migrator({ db: this.db, migrationTableSchema: opts.schema, @@ -37,23 +79,20 @@ export class PrimaryDatabase extends Database { }) } - static is(db: Database): db is PrimaryDatabase { - return db.isPrimary - } - - asPrimary(): PrimaryDatabase { - return this + get schema(): string | undefined { + return this.opts.schema } - async transaction(fn: (db: PrimaryDatabase) => Promise): Promise { + 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 PrimaryDatabase(this.opts, { + const dbTxn = new Database(this.opts, { db: txn, pool: this.pool, + migrator: this.migrator, }) const txRes = await fn(dbTxn) .catch(async (err) => { @@ -69,17 +108,23 @@ export class PrimaryDatabase extends Database { return txRes } + get isTransaction() { + return this.db.isTransaction + } + + assertTransaction() { + assert(this.isTransaction, 'Transaction required') + } + + assertNotTransaction() { + assert(!this.isTransaction, 'Cannot be in a transaction') + } + 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() @@ -108,48 +153,17 @@ export class PrimaryDatabase extends Database { 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, - ) + async close(): Promise { + if (this.destroyed) return + await this.db.destroy() + this.destroyed = true } } -export default PrimaryDatabase +export default Database + +const onPoolError = (err: Error) => dbLogger.error({ err }, 'db pool error') +const onClientError = (err: Error) => dbLogger.error({ err }, 'db client error') // utils // ------- diff --git a/packages/bsky/src/data-plane/server/db/index.ts b/packages/bsky/src/data-plane/server/db/index.ts new file mode 100644 index 00000000000..1beb455f5e3 --- /dev/null +++ b/packages/bsky/src/data-plane/server/db/index.ts @@ -0,0 +1 @@ +export * from './db' diff --git a/packages/bsky/src/db/migrations/20230309T045948368Z-init.ts b/packages/bsky/src/data-plane/server/db/migrations/20230309T045948368Z-init.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230309T045948368Z-init.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230309T045948368Z-init.ts diff --git a/packages/bsky/src/db/migrations/20230408T152211201Z-notification-init.ts b/packages/bsky/src/data-plane/server/db/migrations/20230408T152211201Z-notification-init.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230408T152211201Z-notification-init.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230408T152211201Z-notification-init.ts diff --git a/packages/bsky/src/db/migrations/20230417T210628672Z-moderation-init.ts b/packages/bsky/src/data-plane/server/db/migrations/20230417T210628672Z-moderation-init.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230417T210628672Z-moderation-init.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230417T210628672Z-moderation-init.ts diff --git a/packages/bsky/src/db/migrations/20230420T211446071Z-did-cache.ts b/packages/bsky/src/data-plane/server/db/migrations/20230420T211446071Z-did-cache.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230420T211446071Z-did-cache.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230420T211446071Z-did-cache.ts diff --git a/packages/bsky/src/db/migrations/20230427T194702079Z-notif-record-index.ts b/packages/bsky/src/data-plane/server/db/migrations/20230427T194702079Z-notif-record-index.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230427T194702079Z-notif-record-index.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230427T194702079Z-notif-record-index.ts diff --git a/packages/bsky/src/db/migrations/20230605T144730094Z-post-profile-aggs.ts b/packages/bsky/src/data-plane/server/db/migrations/20230605T144730094Z-post-profile-aggs.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230605T144730094Z-post-profile-aggs.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230605T144730094Z-post-profile-aggs.ts diff --git a/packages/bsky/src/db/migrations/20230607T211442112Z-feed-generator-init.ts b/packages/bsky/src/data-plane/server/db/migrations/20230607T211442112Z-feed-generator-init.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230607T211442112Z-feed-generator-init.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230607T211442112Z-feed-generator-init.ts diff --git a/packages/bsky/src/db/migrations/20230608T155101190Z-algo-whats-hot-view.ts b/packages/bsky/src/data-plane/server/db/migrations/20230608T155101190Z-algo-whats-hot-view.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230608T155101190Z-algo-whats-hot-view.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230608T155101190Z-algo-whats-hot-view.ts diff --git a/packages/bsky/src/db/migrations/20230608T201813132Z-mute-lists.ts b/packages/bsky/src/data-plane/server/db/migrations/20230608T201813132Z-mute-lists.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230608T201813132Z-mute-lists.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230608T201813132Z-mute-lists.ts diff --git a/packages/bsky/src/db/migrations/20230608T205147239Z-mutes.ts b/packages/bsky/src/data-plane/server/db/migrations/20230608T205147239Z-mutes.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230608T205147239Z-mutes.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230608T205147239Z-mutes.ts diff --git a/packages/bsky/src/db/migrations/20230609T153623961Z-blocks.ts b/packages/bsky/src/data-plane/server/db/migrations/20230609T153623961Z-blocks.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230609T153623961Z-blocks.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230609T153623961Z-blocks.ts diff --git a/packages/bsky/src/db/migrations/20230609T232122649Z-actor-deletion-indexes.ts b/packages/bsky/src/data-plane/server/db/migrations/20230609T232122649Z-actor-deletion-indexes.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230609T232122649Z-actor-deletion-indexes.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230609T232122649Z-actor-deletion-indexes.ts diff --git a/packages/bsky/src/db/migrations/20230610T203555962Z-suggested-follows.ts b/packages/bsky/src/data-plane/server/db/migrations/20230610T203555962Z-suggested-follows.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230610T203555962Z-suggested-follows.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230610T203555962Z-suggested-follows.ts diff --git a/packages/bsky/src/db/migrations/20230611T215300060Z-actor-state.ts b/packages/bsky/src/data-plane/server/db/migrations/20230611T215300060Z-actor-state.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230611T215300060Z-actor-state.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230611T215300060Z-actor-state.ts diff --git a/packages/bsky/src/db/migrations/20230620T161134972Z-post-langs.ts b/packages/bsky/src/data-plane/server/db/migrations/20230620T161134972Z-post-langs.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230620T161134972Z-post-langs.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230620T161134972Z-post-langs.ts diff --git a/packages/bsky/src/db/migrations/20230627T212437895Z-optional-handle.ts b/packages/bsky/src/data-plane/server/db/migrations/20230627T212437895Z-optional-handle.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230627T212437895Z-optional-handle.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230627T212437895Z-optional-handle.ts diff --git a/packages/bsky/src/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts b/packages/bsky/src/data-plane/server/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts diff --git a/packages/bsky/src/db/migrations/20230703T045536691Z-feed-and-label-indices.ts b/packages/bsky/src/data-plane/server/db/migrations/20230703T045536691Z-feed-and-label-indices.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230703T045536691Z-feed-and-label-indices.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230703T045536691Z-feed-and-label-indices.ts diff --git a/packages/bsky/src/db/migrations/20230720T164800037Z-posts-cursor-idx.ts b/packages/bsky/src/data-plane/server/db/migrations/20230720T164800037Z-posts-cursor-idx.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230720T164800037Z-posts-cursor-idx.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230720T164800037Z-posts-cursor-idx.ts diff --git a/packages/bsky/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts b/packages/bsky/src/data-plane/server/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts diff --git a/packages/bsky/src/db/migrations/20230808T172902639Z-repo-rev.ts b/packages/bsky/src/data-plane/server/db/migrations/20230808T172902639Z-repo-rev.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230808T172902639Z-repo-rev.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230808T172902639Z-repo-rev.ts diff --git a/packages/bsky/src/db/migrations/20230810T203349843Z-action-duration.ts b/packages/bsky/src/data-plane/server/db/migrations/20230810T203349843Z-action-duration.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230810T203349843Z-action-duration.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230810T203349843Z-action-duration.ts diff --git a/packages/bsky/src/db/migrations/20230817T195936007Z-native-notifications.ts b/packages/bsky/src/data-plane/server/db/migrations/20230817T195936007Z-native-notifications.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230817T195936007Z-native-notifications.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230817T195936007Z-native-notifications.ts diff --git a/packages/bsky/src/db/migrations/20230830T205507322Z-suggested-feeds.ts b/packages/bsky/src/data-plane/server/db/migrations/20230830T205507322Z-suggested-feeds.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230830T205507322Z-suggested-feeds.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230830T205507322Z-suggested-feeds.ts diff --git a/packages/bsky/src/db/migrations/20230904T211011773Z-block-lists.ts b/packages/bsky/src/data-plane/server/db/migrations/20230904T211011773Z-block-lists.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230904T211011773Z-block-lists.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230904T211011773Z-block-lists.ts diff --git a/packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts b/packages/bsky/src/data-plane/server/db/migrations/20230906T222220386Z-thread-gating.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230906T222220386Z-thread-gating.ts diff --git a/packages/bsky/src/db/migrations/20230920T213858047Z-add-tags-to-post.ts b/packages/bsky/src/data-plane/server/db/migrations/20230920T213858047Z-add-tags-to-post.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230920T213858047Z-add-tags-to-post.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230920T213858047Z-add-tags-to-post.ts diff --git a/packages/bsky/src/db/migrations/20230929T192920807Z-record-cursor-indexes.ts b/packages/bsky/src/data-plane/server/db/migrations/20230929T192920807Z-record-cursor-indexes.ts similarity index 100% rename from packages/bsky/src/db/migrations/20230929T192920807Z-record-cursor-indexes.ts rename to packages/bsky/src/data-plane/server/db/migrations/20230929T192920807Z-record-cursor-indexes.ts diff --git a/packages/bsky/src/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts b/packages/bsky/src/data-plane/server/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts similarity index 100% rename from packages/bsky/src/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts rename to packages/bsky/src/data-plane/server/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts diff --git a/packages/bsky/src/db/migrations/20231220T225126090Z-blob-takedowns.ts b/packages/bsky/src/data-plane/server/db/migrations/20231220T225126090Z-blob-takedowns.ts similarity index 100% rename from packages/bsky/src/db/migrations/20231220T225126090Z-blob-takedowns.ts rename to packages/bsky/src/data-plane/server/db/migrations/20231220T225126090Z-blob-takedowns.ts diff --git a/packages/bsky/src/db/migrations/20240124T023719200Z-tagged-suggestions.ts b/packages/bsky/src/data-plane/server/db/migrations/20240124T023719200Z-tagged-suggestions.ts similarity index 100% rename from packages/bsky/src/db/migrations/20240124T023719200Z-tagged-suggestions.ts rename to packages/bsky/src/data-plane/server/db/migrations/20240124T023719200Z-tagged-suggestions.ts diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/data-plane/server/db/migrations/index.ts similarity index 97% rename from packages/bsky/src/db/migrations/index.ts rename to packages/bsky/src/data-plane/server/db/migrations/index.ts index e5a5b155e71..03f8795b3f7 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/data-plane/server/db/migrations/index.ts @@ -31,6 +31,5 @@ export * as _20230906T222220386Z from './20230906T222220386Z-thread-gating' export * as _20230920T213858047Z from './20230920T213858047Z-add-tags-to-post' export * as _20230929T192920807Z from './20230929T192920807Z-record-cursor-indexes' export * as _20231003T202833377Z from './20231003T202833377Z-create-moderation-subject-status' -export * as _20231205T000257238Z from './20231205T000257238Z-remove-did-cache' export * as _20231220T225126090Z from './20231220T225126090Z-blob-takedowns' export * as _20240124T023719200Z from './20240124T023719200Z-tagged-suggestions' diff --git a/packages/bsky/src/db/migrations/provider.ts b/packages/bsky/src/data-plane/server/db/migrations/provider.ts similarity index 100% rename from packages/bsky/src/db/migrations/provider.ts rename to packages/bsky/src/data-plane/server/db/migrations/provider.ts diff --git a/packages/bsky/src/db/pagination.ts b/packages/bsky/src/data-plane/server/db/pagination.ts similarity index 88% rename from packages/bsky/src/db/pagination.ts rename to packages/bsky/src/data-plane/server/db/pagination.ts index f08702cb003..d4c5747e2b4 100644 --- a/packages/bsky/src/db/pagination.ts +++ b/packages/bsky/src/data-plane/server/db/pagination.ts @@ -16,7 +16,7 @@ export type LabeledResult = { * - LabeledResult: a Result processed such that the "primary" and "secondary" parts of the cursor are labeled. * - E.g. { primary: '2022-01-01T12:00:00Z', secondary: 'bafyx' } * - Cursor: the two string parts that make-up the packed/string cursor. - * - E.g. packed cursor '1641038400000::bafyx' in parts { primary: '1641038400000', secondary: 'bafyx' } + * - E.g. packed cursor '1641038400000__bafyx' in parts { primary: '1641038400000', secondary: 'bafyx' } * * These types relate as such. Implementers define the relations marked with a *: * Result -*-> LabeledResult <-*-> Cursor <--> packed/string cursor @@ -27,9 +27,6 @@ export abstract class GenericKeyset { abstract labelResult(result: R): LR abstract labeledResultToCursor(labeled: LR): Cursor abstract cursorToLabeledResult(cursor: Cursor): LR - static clearlyBad(cursor?: string) { - return cursor !== undefined && !cursor.includes('::') - } packFromResult(results: R | R[]): string | undefined { const result = Array.isArray(results) ? results.at(-1) : results if (!result) return @@ -47,11 +44,11 @@ export abstract class GenericKeyset { } packCursor(cursor?: Cursor): string | undefined { if (!cursor) return - return `${cursor.primary}::${cursor.secondary}` + return `${cursor.primary}__${cursor.secondary}` } unpackCursor(cursorStr?: string): Cursor | undefined { if (!cursorStr) return - const result = cursorStr.split('::') + const result = cursorStr.split('__') const [primary, secondary, ...others] = result if (!primary || !secondary || others.length > 0) { throw new InvalidRequestError('Malformed cursor') @@ -109,6 +106,24 @@ export class TimeCidKeyset< } } +export class CreatedAtDidKeyset extends TimeCidKeyset<{ + createdAt: string + did: string // dids are treated identically to cids in TimeCidKeyset +}> { + labelResult(result: { createdAt: string; did: string }) { + return { primary: result.createdAt, secondary: result.did } + } +} + +export class IndexedAtDidKeyset extends TimeCidKeyset<{ + indexedAt: string + did: string // dids are treated identically to cids in TimeCidKeyset +}> { + labelResult(result: { indexedAt: string; did: string }) { + return { primary: result.indexedAt, secondary: result.did } + } +} + export const paginate = < QB extends AnyQb, K extends GenericKeyset, diff --git a/packages/bsky/src/db/tables/actor-block.ts b/packages/bsky/src/data-plane/server/db/tables/actor-block.ts similarity index 100% rename from packages/bsky/src/db/tables/actor-block.ts rename to packages/bsky/src/data-plane/server/db/tables/actor-block.ts diff --git a/packages/bsky/src/db/tables/actor-state.ts b/packages/bsky/src/data-plane/server/db/tables/actor-state.ts similarity index 100% rename from packages/bsky/src/db/tables/actor-state.ts rename to packages/bsky/src/data-plane/server/db/tables/actor-state.ts diff --git a/packages/bsky/src/db/tables/actor-sync.ts b/packages/bsky/src/data-plane/server/db/tables/actor-sync.ts similarity index 100% rename from packages/bsky/src/db/tables/actor-sync.ts rename to packages/bsky/src/data-plane/server/db/tables/actor-sync.ts diff --git a/packages/bsky/src/db/tables/actor.ts b/packages/bsky/src/data-plane/server/db/tables/actor.ts similarity index 100% rename from packages/bsky/src/db/tables/actor.ts rename to packages/bsky/src/data-plane/server/db/tables/actor.ts diff --git a/packages/bsky/src/db/tables/algo.ts b/packages/bsky/src/data-plane/server/db/tables/algo.ts similarity index 100% rename from packages/bsky/src/db/tables/algo.ts rename to packages/bsky/src/data-plane/server/db/tables/algo.ts diff --git a/packages/bsky/src/db/tables/blob-takedown.ts b/packages/bsky/src/data-plane/server/db/tables/blob-takedown.ts similarity index 100% rename from packages/bsky/src/db/tables/blob-takedown.ts rename to packages/bsky/src/data-plane/server/db/tables/blob-takedown.ts diff --git a/packages/bsky/src/data-plane/server/db/tables/did-cache.ts b/packages/bsky/src/data-plane/server/db/tables/did-cache.ts new file mode 100644 index 00000000000..b3865548725 --- /dev/null +++ b/packages/bsky/src/data-plane/server/db/tables/did-cache.ts @@ -0,0 +1,13 @@ +import { DidDocument } from '@atproto/identity' + +export interface DidCache { + did: string + doc: DidDocument + updatedAt: number +} + +export const tableName = 'did_cache' + +export type PartialDB = { + [tableName]: DidCache +} diff --git a/packages/bsky/src/db/tables/duplicate-record.ts b/packages/bsky/src/data-plane/server/db/tables/duplicate-record.ts similarity index 100% rename from packages/bsky/src/db/tables/duplicate-record.ts rename to packages/bsky/src/data-plane/server/db/tables/duplicate-record.ts diff --git a/packages/bsky/src/db/tables/feed-generator.ts b/packages/bsky/src/data-plane/server/db/tables/feed-generator.ts similarity index 100% rename from packages/bsky/src/db/tables/feed-generator.ts rename to packages/bsky/src/data-plane/server/db/tables/feed-generator.ts diff --git a/packages/bsky/src/db/tables/feed-item.ts b/packages/bsky/src/data-plane/server/db/tables/feed-item.ts similarity index 100% rename from packages/bsky/src/db/tables/feed-item.ts rename to packages/bsky/src/data-plane/server/db/tables/feed-item.ts diff --git a/packages/bsky/src/db/tables/follow.ts b/packages/bsky/src/data-plane/server/db/tables/follow.ts similarity index 100% rename from packages/bsky/src/db/tables/follow.ts rename to packages/bsky/src/data-plane/server/db/tables/follow.ts diff --git a/packages/bsky/src/db/tables/label.ts b/packages/bsky/src/data-plane/server/db/tables/label.ts similarity index 100% rename from packages/bsky/src/db/tables/label.ts rename to packages/bsky/src/data-plane/server/db/tables/label.ts diff --git a/packages/bsky/src/db/tables/like.ts b/packages/bsky/src/data-plane/server/db/tables/like.ts similarity index 100% rename from packages/bsky/src/db/tables/like.ts rename to packages/bsky/src/data-plane/server/db/tables/like.ts diff --git a/packages/bsky/src/db/tables/list-block.ts b/packages/bsky/src/data-plane/server/db/tables/list-block.ts similarity index 100% rename from packages/bsky/src/db/tables/list-block.ts rename to packages/bsky/src/data-plane/server/db/tables/list-block.ts diff --git a/packages/bsky/src/db/tables/list-item.ts b/packages/bsky/src/data-plane/server/db/tables/list-item.ts similarity index 100% rename from packages/bsky/src/db/tables/list-item.ts rename to packages/bsky/src/data-plane/server/db/tables/list-item.ts diff --git a/packages/bsky/src/db/tables/list-mute.ts b/packages/bsky/src/data-plane/server/db/tables/list-mute.ts similarity index 100% rename from packages/bsky/src/db/tables/list-mute.ts rename to packages/bsky/src/data-plane/server/db/tables/list-mute.ts diff --git a/packages/bsky/src/db/tables/list.ts b/packages/bsky/src/data-plane/server/db/tables/list.ts similarity index 100% rename from packages/bsky/src/db/tables/list.ts rename to packages/bsky/src/data-plane/server/db/tables/list.ts diff --git a/packages/bsky/src/db/tables/moderation.ts b/packages/bsky/src/data-plane/server/db/tables/moderation.ts similarity index 96% rename from packages/bsky/src/db/tables/moderation.ts rename to packages/bsky/src/data-plane/server/db/tables/moderation.ts index f1ac3572785..c483ae20a4c 100644 --- a/packages/bsky/src/db/tables/moderation.ts +++ b/packages/bsky/src/data-plane/server/db/tables/moderation.ts @@ -3,7 +3,7 @@ import { REVIEWCLOSED, REVIEWOPEN, REVIEWESCALATED, -} from '../../lexicon/types/com/atproto/admin/defs' +} from '../../../../lexicon/types/com/atproto/admin/defs' export const eventTableName = 'moderation_event' export const subjectStatusTableName = 'moderation_subject_status' diff --git a/packages/bsky/src/db/tables/mute.ts b/packages/bsky/src/data-plane/server/db/tables/mute.ts similarity index 100% rename from packages/bsky/src/db/tables/mute.ts rename to packages/bsky/src/data-plane/server/db/tables/mute.ts diff --git a/packages/bsky/src/db/tables/notification-push-token.ts b/packages/bsky/src/data-plane/server/db/tables/notification-push-token.ts similarity index 100% rename from packages/bsky/src/db/tables/notification-push-token.ts rename to packages/bsky/src/data-plane/server/db/tables/notification-push-token.ts diff --git a/packages/bsky/src/db/tables/notification.ts b/packages/bsky/src/data-plane/server/db/tables/notification.ts similarity index 100% rename from packages/bsky/src/db/tables/notification.ts rename to packages/bsky/src/data-plane/server/db/tables/notification.ts diff --git a/packages/bsky/src/db/tables/post-agg.ts b/packages/bsky/src/data-plane/server/db/tables/post-agg.ts similarity index 100% rename from packages/bsky/src/db/tables/post-agg.ts rename to packages/bsky/src/data-plane/server/db/tables/post-agg.ts diff --git a/packages/bsky/src/db/tables/post-embed.ts b/packages/bsky/src/data-plane/server/db/tables/post-embed.ts similarity index 100% rename from packages/bsky/src/db/tables/post-embed.ts rename to packages/bsky/src/data-plane/server/db/tables/post-embed.ts diff --git a/packages/bsky/src/db/tables/post.ts b/packages/bsky/src/data-plane/server/db/tables/post.ts similarity index 100% rename from packages/bsky/src/db/tables/post.ts rename to packages/bsky/src/data-plane/server/db/tables/post.ts diff --git a/packages/bsky/src/db/tables/profile-agg.ts b/packages/bsky/src/data-plane/server/db/tables/profile-agg.ts similarity index 100% rename from packages/bsky/src/db/tables/profile-agg.ts rename to packages/bsky/src/data-plane/server/db/tables/profile-agg.ts diff --git a/packages/bsky/src/db/tables/profile.ts b/packages/bsky/src/data-plane/server/db/tables/profile.ts similarity index 100% rename from packages/bsky/src/db/tables/profile.ts rename to packages/bsky/src/data-plane/server/db/tables/profile.ts diff --git a/packages/bsky/src/db/tables/record.ts b/packages/bsky/src/data-plane/server/db/tables/record.ts similarity index 100% rename from packages/bsky/src/db/tables/record.ts rename to packages/bsky/src/data-plane/server/db/tables/record.ts diff --git a/packages/bsky/src/db/tables/repost.ts b/packages/bsky/src/data-plane/server/db/tables/repost.ts similarity index 100% rename from packages/bsky/src/db/tables/repost.ts rename to packages/bsky/src/data-plane/server/db/tables/repost.ts diff --git a/packages/bsky/src/db/tables/subscription.ts b/packages/bsky/src/data-plane/server/db/tables/subscription.ts similarity index 100% rename from packages/bsky/src/db/tables/subscription.ts rename to packages/bsky/src/data-plane/server/db/tables/subscription.ts diff --git a/packages/bsky/src/db/tables/suggested-feed.ts b/packages/bsky/src/data-plane/server/db/tables/suggested-feed.ts similarity index 100% rename from packages/bsky/src/db/tables/suggested-feed.ts rename to packages/bsky/src/data-plane/server/db/tables/suggested-feed.ts diff --git a/packages/bsky/src/db/tables/suggested-follow.ts b/packages/bsky/src/data-plane/server/db/tables/suggested-follow.ts similarity index 100% rename from packages/bsky/src/db/tables/suggested-follow.ts rename to packages/bsky/src/data-plane/server/db/tables/suggested-follow.ts diff --git a/packages/bsky/src/db/tables/tagged-suggestion.ts b/packages/bsky/src/data-plane/server/db/tables/tagged-suggestion.ts similarity index 100% rename from packages/bsky/src/db/tables/tagged-suggestion.ts rename to packages/bsky/src/data-plane/server/db/tables/tagged-suggestion.ts diff --git a/packages/bsky/src/db/tables/thread-gate.ts b/packages/bsky/src/data-plane/server/db/tables/thread-gate.ts similarity index 100% rename from packages/bsky/src/db/tables/thread-gate.ts rename to packages/bsky/src/data-plane/server/db/tables/thread-gate.ts diff --git a/packages/bsky/src/db/tables/view-param.ts b/packages/bsky/src/data-plane/server/db/tables/view-param.ts similarity index 100% rename from packages/bsky/src/db/tables/view-param.ts rename to packages/bsky/src/data-plane/server/db/tables/view-param.ts diff --git a/packages/bsky/src/db/types.ts b/packages/bsky/src/data-plane/server/db/types.ts similarity index 100% rename from packages/bsky/src/db/types.ts rename to packages/bsky/src/data-plane/server/db/types.ts diff --git a/packages/bsky/src/db/util.ts b/packages/bsky/src/data-plane/server/db/util.ts similarity index 100% rename from packages/bsky/src/db/util.ts rename to packages/bsky/src/data-plane/server/db/util.ts diff --git a/packages/bsky/src/data-plane/server/index.ts b/packages/bsky/src/data-plane/server/index.ts new file mode 100644 index 00000000000..f925de83c48 --- /dev/null +++ b/packages/bsky/src/data-plane/server/index.ts @@ -0,0 +1,36 @@ +import http from 'http' +import events from 'events' +import express from 'express' +import { expressConnectMiddleware } from '@connectrpc/connect-express' +import createRoutes from './routes' +import { Database } from './db' +import { IdResolver, MemoryCache } from '@atproto/identity' + +export { RepoSubscription } from './subscription' + +export class DataPlaneServer { + constructor(public server: http.Server, public idResolver: IdResolver) {} + + static async create(db: Database, port: number, plcUrl?: string) { + const app = express() + const didCache = new MemoryCache() + const idResolver = new IdResolver({ plcUrl, didCache }) + const routes = createRoutes(db, idResolver) + app.use(expressConnectMiddleware({ routes })) + const server = app.listen(port) + await events.once(server, 'listening') + return new DataPlaneServer(server, idResolver) + } + + async destroy() { + return new Promise((resolve, reject) => { + this.server.close((err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } +} diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/data-plane/server/indexing/index.ts similarity index 86% rename from packages/bsky/src/services/indexing/index.ts rename to packages/bsky/src/data-plane/server/indexing/index.ts index 44dd9c3c986..68f5ff8b721 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/data-plane/server/indexing/index.ts @@ -13,7 +13,8 @@ import { AtUri } from '@atproto/syntax' import { IdResolver, getPds } from '@atproto/identity' import { DAY, HOUR } from '@atproto/common' import { ValidationError } from '@atproto/lexicon' -import { PrimaryDatabase } from '../../db' +import { Database } from '../db' +import { Actor } from '../db/tables/actor' import * as Post from './plugins/post' import * as Threadgate from './plugins/thread-gate' import * as Like from './plugins/like' @@ -26,12 +27,9 @@ import * as ListBlock from './plugins/list-block' import * as Block from './plugins/block' import * as FeedGenerator from './plugins/feed-generator' import RecordProcessor from './processor' -import { subLogger } from '../../logger' -import { retryHttp } from '../../util/retry' -import { BackgroundQueue } from '../../background' -import { NotificationServer } from '../../notifications' -import { AutoModerator } from '../../auto-moderator' -import { Actor } from '../../db/tables/actor' +import { subLogger } from '../../../logger' +import { retryHttp } from '../../../util/retry' +import { BackgroundQueue } from '../background' export class IndexingService { records: { @@ -49,50 +47,28 @@ export class IndexingService { } constructor( - public db: PrimaryDatabase, + public db: Database, public idResolver: IdResolver, - public autoMod: AutoModerator, - public backgroundQueue: BackgroundQueue, - public notifServer?: NotificationServer, + public background: BackgroundQueue, ) { this.records = { - post: Post.makePlugin(this.db, backgroundQueue, notifServer), - threadGate: Threadgate.makePlugin(this.db, backgroundQueue, notifServer), - like: Like.makePlugin(this.db, backgroundQueue, notifServer), - repost: Repost.makePlugin(this.db, backgroundQueue, notifServer), - follow: Follow.makePlugin(this.db, backgroundQueue, notifServer), - 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, - backgroundQueue, - notifServer, - ), + post: Post.makePlugin(this.db, this.background), + threadGate: Threadgate.makePlugin(this.db, this.background), + like: Like.makePlugin(this.db, this.background), + repost: Repost.makePlugin(this.db, this.background), + follow: Follow.makePlugin(this.db, this.background), + profile: Profile.makePlugin(this.db, this.background), + list: List.makePlugin(this.db, this.background), + listItem: ListItem.makePlugin(this.db, this.background), + listBlock: ListBlock.makePlugin(this.db, this.background), + block: Block.makePlugin(this.db, this.background), + feedGenerator: FeedGenerator.makePlugin(this.db, this.background), } } - transact(txn: PrimaryDatabase) { + transact(txn: Database) { txn.assertTransaction() - return new IndexingService( - txn, - this.idResolver, - this.autoMod, - this.backgroundQueue, - this.notifServer, - ) - } - - static creator( - idResolver: IdResolver, - autoMod: AutoModerator, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, - ) { - return (db: PrimaryDatabase) => - new IndexingService(db, idResolver, autoMod, backgroundQueue, notifServer) + return new IndexingService(txn, this.idResolver, this.background) } async indexRecord( @@ -114,9 +90,6 @@ export class IndexingService { await indexer.updateRecord(uri, cid, obj, timestamp) } }) - if (!opts?.disableLabels) { - this.autoMod.processRecord(uri, cid, obj) - } } async deleteRecord(uri: AtUri, cascading = false) { @@ -170,10 +143,6 @@ 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/bsky/src/services/indexing/plugins/block.ts b/packages/bsky/src/data-plane/server/indexing/plugins/block.ts similarity index 77% rename from packages/bsky/src/services/indexing/plugins/block.ts rename to packages/bsky/src/data-plane/server/indexing/plugins/block.ts index 88e62b6f5ac..ec4956a04f5 100644 --- a/packages/bsky/src/services/indexing/plugins/block.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/block.ts @@ -1,13 +1,12 @@ import { Selectable } from 'kysely' import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax' import { CID } from 'multiformats/cid' -import * as Block from '../../../lexicon/types/app/bsky/graph/block' -import * as lex from '../../../lexicon/lexicons' -import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' +import * as Block from '../../../../lexicon/types/app/bsky/graph/block' +import * as lex from '../../../../lexicon/lexicons' +import { Database } from '../../db' +import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema' import RecordProcessor from '../processor' -import { PrimaryDatabase } from '../../../db' -import { BackgroundQueue } from '../../../background' -import { NotificationServer } from '../../../notifications' +import { BackgroundQueue } from '../../background' const lexId = lex.ids.AppBskyGraphBlock type IndexedBlock = Selectable @@ -72,11 +71,10 @@ const notifsForDelete = () => { export type PluginType = RecordProcessor export const makePlugin = ( - db: PrimaryDatabase, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, + db: Database, + background: BackgroundQueue, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, notifServer, { + return new RecordProcessor(db, background, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/feed-generator.ts b/packages/bsky/src/data-plane/server/indexing/plugins/feed-generator.ts similarity index 77% rename from packages/bsky/src/services/indexing/plugins/feed-generator.ts rename to packages/bsky/src/data-plane/server/indexing/plugins/feed-generator.ts index be5435966f1..f3b82c75567 100644 --- a/packages/bsky/src/services/indexing/plugins/feed-generator.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/feed-generator.ts @@ -1,13 +1,12 @@ import { Selectable } from 'kysely' import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax' import { CID } from 'multiformats/cid' -import * as FeedGenerator from '../../../lexicon/types/app/bsky/feed/generator' -import * as lex from '../../../lexicon/lexicons' -import { PrimaryDatabase } from '../../../db' -import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' -import { BackgroundQueue } from '../../../background' +import * as FeedGenerator from '../../../../lexicon/types/app/bsky/feed/generator' +import * as lex from '../../../../lexicon/lexicons' +import { Database } from '../../db' +import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema' import RecordProcessor from '../processor' -import { NotificationServer } from '../../../notifications' +import { BackgroundQueue } from '../../background' const lexId = lex.ids.AppBskyFeedGenerator type IndexedFeedGenerator = Selectable @@ -71,11 +70,10 @@ export type PluginType = RecordProcessor< > export const makePlugin = ( - db: PrimaryDatabase, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, + db: Database, + background: BackgroundQueue, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, notifServer, { + return new RecordProcessor(db, background, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/follow.ts b/packages/bsky/src/data-plane/server/indexing/plugins/follow.ts similarity index 84% rename from packages/bsky/src/services/indexing/plugins/follow.ts rename to packages/bsky/src/data-plane/server/indexing/plugins/follow.ts index 8655c7eba71..6f238755761 100644 --- a/packages/bsky/src/services/indexing/plugins/follow.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/follow.ts @@ -1,14 +1,13 @@ import { Selectable } from 'kysely' import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax' import { CID } from 'multiformats/cid' -import * as Follow from '../../../lexicon/types/app/bsky/graph/follow' -import * as lex from '../../../lexicon/lexicons' -import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' +import * as Follow from '../../../../lexicon/types/app/bsky/graph/follow' +import * as lex from '../../../../lexicon/lexicons' import RecordProcessor from '../processor' -import { PrimaryDatabase } from '../../../db' -import { countAll, excluded } from '../../../db/util' -import { BackgroundQueue } from '../../../background' -import { NotificationServer } from '../../../notifications' +import { Database } from '../../db' +import { countAll, excluded } from '../../db/util' +import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema' +import { BackgroundQueue } from '../../background' const lexId = lex.ids.AppBskyGraphFollow type IndexedFollow = Selectable @@ -119,11 +118,10 @@ const updateAggregates = async (db: DatabaseSchema, follow: IndexedFollow) => { export type PluginType = RecordProcessor export const makePlugin = ( - db: PrimaryDatabase, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, + db: Database, + background: BackgroundQueue, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, notifServer, { + return new RecordProcessor(db, background, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/like.ts b/packages/bsky/src/data-plane/server/indexing/plugins/like.ts similarity index 83% rename from packages/bsky/src/services/indexing/plugins/like.ts rename to packages/bsky/src/data-plane/server/indexing/plugins/like.ts index 703800f67c8..98e9fc722f8 100644 --- a/packages/bsky/src/services/indexing/plugins/like.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/like.ts @@ -1,14 +1,13 @@ import { Selectable } from 'kysely' import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax' import { CID } from 'multiformats/cid' -import * as Like from '../../../lexicon/types/app/bsky/feed/like' -import * as lex from '../../../lexicon/lexicons' -import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' +import * as Like from '../../../../lexicon/types/app/bsky/feed/like' +import * as lex from '../../../../lexicon/lexicons' import RecordProcessor from '../processor' -import { countAll, excluded } from '../../../db/util' -import { PrimaryDatabase } from '../../../db' -import { BackgroundQueue } from '../../../background' -import { NotificationServer } from '../../../notifications' +import { countAll, excluded } from '../../db/util' +import { Database } from '../../db' +import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema' +import { BackgroundQueue } from '../../background' const lexId = lex.ids.AppBskyFeedLike type IndexedLike = Selectable @@ -109,11 +108,10 @@ const updateAggregates = async (db: DatabaseSchema, like: IndexedLike) => { export type PluginType = RecordProcessor export const makePlugin = ( - db: PrimaryDatabase, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, + db: Database, + background: BackgroundQueue, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, notifServer, { + return new RecordProcessor(db, background, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/list-block.ts b/packages/bsky/src/data-plane/server/indexing/plugins/list-block.ts similarity index 77% rename from packages/bsky/src/services/indexing/plugins/list-block.ts rename to packages/bsky/src/data-plane/server/indexing/plugins/list-block.ts index 3040f1aa3f9..09eabcdb9f4 100644 --- a/packages/bsky/src/services/indexing/plugins/list-block.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/list-block.ts @@ -1,13 +1,12 @@ import { Selectable } from 'kysely' import { AtUri, normalizeDatetimeAlways } 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 * as ListBlock from '../../../../lexicon/types/app/bsky/graph/listblock' +import * as lex from '../../../../lexicon/lexicons' +import { Database } from '../../db' +import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema' import RecordProcessor from '../processor' -import { BackgroundQueue } from '../../../background' -import { NotificationServer } from '../../../notifications' +import { BackgroundQueue } from '../../background' const lexId = lex.ids.AppBskyGraphListblock type IndexedListBlock = Selectable @@ -72,11 +71,10 @@ const notifsForDelete = () => { export type PluginType = RecordProcessor export const makePlugin = ( - db: PrimaryDatabase, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, + db: Database, + background: BackgroundQueue, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, notifServer, { + return new RecordProcessor(db, background, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/list-item.ts b/packages/bsky/src/data-plane/server/indexing/plugins/list-item.ts similarity index 79% rename from packages/bsky/src/services/indexing/plugins/list-item.ts rename to packages/bsky/src/data-plane/server/indexing/plugins/list-item.ts index 9e08145b23e..f2a43cff485 100644 --- a/packages/bsky/src/services/indexing/plugins/list-item.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/list-item.ts @@ -1,14 +1,13 @@ import { Selectable } from 'kysely' +import { InvalidRequestError } from '@atproto/xrpc-server' import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax' import { CID } from 'multiformats/cid' -import * as ListItem from '../../../lexicon/types/app/bsky/graph/listitem' -import * as lex from '../../../lexicon/lexicons' -import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' +import * as ListItem from '../../../../lexicon/types/app/bsky/graph/listitem' +import * as lex from '../../../../lexicon/lexicons' import RecordProcessor from '../processor' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { PrimaryDatabase } from '../../../db' -import { BackgroundQueue } from '../../../background' -import { NotificationServer } from '../../../notifications' +import { Database } from '../../db' +import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema' +import { BackgroundQueue } from '../../background' const lexId = lex.ids.AppBskyGraphListitem type IndexedListItem = Selectable @@ -80,11 +79,10 @@ const notifsForDelete = () => { export type PluginType = RecordProcessor export const makePlugin = ( - db: PrimaryDatabase, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, + db: Database, + background: BackgroundQueue, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, notifServer, { + return new RecordProcessor(db, background, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/list.ts b/packages/bsky/src/data-plane/server/indexing/plugins/list.ts similarity index 77% rename from packages/bsky/src/services/indexing/plugins/list.ts rename to packages/bsky/src/data-plane/server/indexing/plugins/list.ts index 0d078572501..f6deaf0a68e 100644 --- a/packages/bsky/src/services/indexing/plugins/list.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/list.ts @@ -1,13 +1,12 @@ import { Selectable } from 'kysely' import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax' import { CID } from 'multiformats/cid' -import * as List from '../../../lexicon/types/app/bsky/graph/list' -import * as lex from '../../../lexicon/lexicons' -import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' +import * as List from '../../../../lexicon/types/app/bsky/graph/list' +import * as lex from '../../../../lexicon/lexicons' +import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema' import RecordProcessor from '../processor' -import { PrimaryDatabase } from '../../../db' -import { BackgroundQueue } from '../../../background' -import { NotificationServer } from '../../../notifications' +import { Database } from '../../db' +import { BackgroundQueue } from '../../background' const lexId = lex.ids.AppBskyGraphList type IndexedList = Selectable @@ -68,11 +67,10 @@ const notifsForDelete = () => { export type PluginType = RecordProcessor export const makePlugin = ( - db: PrimaryDatabase, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, + db: Database, + background: BackgroundQueue, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, notifServer, { + return new RecordProcessor(db, background, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/post.ts b/packages/bsky/src/data-plane/server/indexing/plugins/post.ts similarity index 89% rename from packages/bsky/src/services/indexing/plugins/post.ts rename to packages/bsky/src/data-plane/server/indexing/plugins/post.ts index af581b3bdff..cc4121ab667 100644 --- a/packages/bsky/src/services/indexing/plugins/post.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/post.ts @@ -5,27 +5,30 @@ import { jsonStringToLex } from '@atproto/lexicon' import { Record as PostRecord, ReplyRef, -} from '../../../lexicon/types/app/bsky/feed/post' -import { Record as GateRecord } from '../../../lexicon/types/app/bsky/feed/threadgate' -import { isMain as isEmbedImage } from '../../../lexicon/types/app/bsky/embed/images' -import { isMain as isEmbedExternal } from '../../../lexicon/types/app/bsky/embed/external' -import { isMain as isEmbedRecord } from '../../../lexicon/types/app/bsky/embed/record' -import { isMain as isEmbedRecordWithMedia } from '../../../lexicon/types/app/bsky/embed/recordWithMedia' +} from '../../../../lexicon/types/app/bsky/feed/post' +import { Record as GateRecord } from '../../../../lexicon/types/app/bsky/feed/threadgate' +import { isMain as isEmbedImage } from '../../../../lexicon/types/app/bsky/embed/images' +import { isMain as isEmbedExternal } from '../../../../lexicon/types/app/bsky/embed/external' +import { isMain as isEmbedRecord } from '../../../../lexicon/types/app/bsky/embed/record' +import { isMain as isEmbedRecordWithMedia } from '../../../../lexicon/types/app/bsky/embed/recordWithMedia' import { isMention, isLink, -} from '../../../lexicon/types/app/bsky/richtext/facet' -import * as lex from '../../../lexicon/lexicons' -import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' +} from '../../../../lexicon/types/app/bsky/richtext/facet' +import * as lex from '../../../../lexicon/lexicons' +import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema' import RecordProcessor from '../processor' -import { Notification } from '../../../db/tables/notification' -import { PrimaryDatabase } from '../../../db' -import { countAll, excluded } from '../../../db/util' -import { BackgroundQueue } from '../../../background' -import { getAncestorsAndSelfQb, getDescendentsQb } from '../../util/post' -import { NotificationServer } from '../../../notifications' -import * as feedutil from '../../feed/util' -import { postToThreadgateUri } from '../../feed/util' +import { Notification } from '../../db/tables/notification' +import { Database } from '../../db' +import { countAll, excluded } from '../../db/util' +import { + getAncestorsAndSelfQb, + getDescendentsQb, + invalidReplyRoot as checkInvalidReplyRoot, + violatesThreadGate as checkViolatesThreadGate, + postToThreadgateUri, +} from '../../util' +import { BackgroundQueue } from '../../background' type Notif = Insertable type Post = Selectable @@ -392,11 +395,10 @@ const updateAggregates = async (db: DatabaseSchema, postIdx: IndexedPost) => { export type PluginType = RecordProcessor export const makePlugin = ( - db: PrimaryDatabase, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, + db: Database, + background: BackgroundQueue, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, notifServer, { + return new RecordProcessor(db, background, { lexId, insertFn, findDuplicate, @@ -427,9 +429,9 @@ async function validateReply( const replyRefs = await getReplyRefs(db, reply) // check reply const invalidReplyRoot = - !replyRefs.parent || feedutil.invalidReplyRoot(reply, replyRefs.parent) + !replyRefs.parent || checkInvalidReplyRoot(reply, replyRefs.parent) // check interaction - const violatesThreadGate = await feedutil.violatesThreadGate( + const violatesThreadGate = await checkViolatesThreadGate( db, creator, new AtUri(reply.root.uri).hostname, diff --git a/packages/bsky/src/services/indexing/plugins/profile.ts b/packages/bsky/src/data-plane/server/indexing/plugins/profile.ts similarity index 75% rename from packages/bsky/src/services/indexing/plugins/profile.ts rename to packages/bsky/src/data-plane/server/indexing/plugins/profile.ts index ea0c8f07f98..18c9b54bbb9 100644 --- a/packages/bsky/src/services/indexing/plugins/profile.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/profile.ts @@ -1,12 +1,11 @@ import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' -import * as Profile from '../../../lexicon/types/app/bsky/actor/profile' -import * as lex from '../../../lexicon/lexicons' -import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' +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 { PrimaryDatabase } from '../../../db' -import { BackgroundQueue } from '../../../background' -import { NotificationServer } from '../../../notifications' +import { Database } from '../../db' +import { BackgroundQueue } from '../../background' const lexId = lex.ids.AppBskyActorProfile type IndexedProfile = DatabaseSchemaType['profile'] @@ -64,11 +63,10 @@ const notifsForDelete = () => { export type PluginType = RecordProcessor export const makePlugin = ( - db: PrimaryDatabase, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, + db: Database, + background: BackgroundQueue, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, notifServer, { + return new RecordProcessor(db, background, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/repost.ts b/packages/bsky/src/data-plane/server/indexing/plugins/repost.ts similarity index 85% rename from packages/bsky/src/services/indexing/plugins/repost.ts rename to packages/bsky/src/data-plane/server/indexing/plugins/repost.ts index ea8d517dc52..ec2e7754fb0 100644 --- a/packages/bsky/src/services/indexing/plugins/repost.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/repost.ts @@ -1,14 +1,13 @@ import { Selectable } from 'kysely' import { CID } from 'multiformats/cid' import { AtUri, normalizeDatetimeAlways } 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' +import * as Repost from '../../../../lexicon/types/app/bsky/feed/repost' +import * as lex from '../../../../lexicon/lexicons' import RecordProcessor from '../processor' -import { PrimaryDatabase } from '../../../db' -import { countAll, excluded } from '../../../db/util' -import { BackgroundQueue } from '../../../background' -import { NotificationServer } from '../../../notifications' +import { Database } from '../../db' +import { countAll, excluded } from '../../db/util' +import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema' +import { BackgroundQueue } from '../../background' const lexId = lex.ids.AppBskyFeedRepost type IndexedRepost = Selectable @@ -134,11 +133,10 @@ const updateAggregates = async (db: DatabaseSchema, repost: IndexedRepost) => { export type PluginType = RecordProcessor export const makePlugin = ( - db: PrimaryDatabase, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, + db: Database, + background: BackgroundQueue, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, notifServer, { + return new RecordProcessor(db, background, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/thread-gate.ts b/packages/bsky/src/data-plane/server/indexing/plugins/thread-gate.ts similarity index 79% rename from packages/bsky/src/services/indexing/plugins/thread-gate.ts rename to packages/bsky/src/data-plane/server/indexing/plugins/thread-gate.ts index 9a58547f2da..0402fe8289f 100644 --- a/packages/bsky/src/services/indexing/plugins/thread-gate.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/thread-gate.ts @@ -1,13 +1,12 @@ import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' import { CID } from 'multiformats/cid' -import * as Threadgate from '../../../lexicon/types/app/bsky/feed/threadgate' -import * as lex from '../../../lexicon/lexicons' -import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' +import * as Threadgate from '../../../../lexicon/types/app/bsky/feed/threadgate' +import * as lex from '../../../../lexicon/lexicons' +import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema' +import { Database } from '../../db' import RecordProcessor from '../processor' -import { PrimaryDatabase } from '../../../db' -import { BackgroundQueue } from '../../../background' -import { NotificationServer } from '../../../notifications' +import { BackgroundQueue } from '../../background' const lexId = lex.ids.AppBskyFeedThreadgate type IndexedGate = DatabaseSchemaType['thread_gate'] @@ -77,11 +76,10 @@ const notifsForDelete = () => { export type PluginType = RecordProcessor export const makePlugin = ( - db: PrimaryDatabase, - backgroundQueue: BackgroundQueue, - notifServer?: NotificationServer, + db: Database, + background: BackgroundQueue, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, notifServer, { + return new RecordProcessor(db, background, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/processor.ts b/packages/bsky/src/data-plane/server/indexing/processor.ts similarity index 80% rename from packages/bsky/src/services/indexing/processor.ts rename to packages/bsky/src/data-plane/server/indexing/processor.ts index 0dad405b9ef..77a8fbdf09f 100644 --- a/packages/bsky/src/services/indexing/processor.ts +++ b/packages/bsky/src/data-plane/server/indexing/processor.ts @@ -1,15 +1,13 @@ import { Insertable } from 'kysely' import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/syntax' -import { jsonStringToLex, stringifyLex } from '@atproto/lexicon' -import DatabaseSchema from '../../db/database-schema' -import { lexicons } from '../../lexicon/lexicons' -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' +import { jsonStringToLex, stringifyLex } from '@atproto/lexicon' +import { lexicons } from '../../../lexicon/lexicons' +import { Database } from '../db' +import DatabaseSchema from '../db/database-schema' +import { Notification } from '../db/tables/notification' +import { BackgroundQueue } from '../background' // @NOTE re: insertions and deletions. Due to how record updates are handled, // (insertFn) should have the same effect as (insertFn -> deleteFn -> insertFn). @@ -42,9 +40,8 @@ export class RecordProcessor { collection: string db: DatabaseSchema constructor( - private appDb: PrimaryDatabase, - private backgroundQueue: BackgroundQueue, - private notifServer: NotificationServer | undefined, + private appDb: Database, + private background: BackgroundQueue, private params: RecordProcessorParams, ) { this.db = appDb.db @@ -228,8 +225,7 @@ export class RecordProcessor { async handleNotifs(op: { deleted?: S; inserted?: S }) { let notifs: Notif[] = [] - const runOnCommit: ((db: PrimaryDatabase) => Promise)[] = [] - const sendOnCommit: (() => Promise)[] = [] + const runOnCommit: ((db: Database) => Promise)[] = [] if (op.deleted) { const forDelete = this.params.notifsForDelete( op.deleted, @@ -253,37 +249,10 @@ 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.prepareNotifications(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. - this.appDb.onCommit(() => { - this.backgroundQueue.add(async (db) => { - for (const fn of runOnCommit) { - await fn(db) - } - }) - }) - } - 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() - } - }) - }) + // Need to ensure notif deletion always happens before creation, otherwise delete may clobber in a race. + for (const fn of runOnCommit) { + await fn(this.appDb) // these could be backgrounded } } @@ -291,7 +260,7 @@ export class RecordProcessor { const { updateAggregates } = this.params if (!updateAggregates) return this.appDb.onCommit(() => { - this.backgroundQueue.add((db) => updateAggregates(db.db, indexed)) + this.background.add((db) => updateAggregates(db.db, indexed)) }) } } diff --git a/packages/bsky/src/data-plane/server/routes/blocks.ts b/packages/bsky/src/data-plane/server/routes/blocks.ts new file mode 100644 index 00000000000..54ec900b280 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/blocks.ts @@ -0,0 +1,121 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { TimeCidKeyset, paginate } from '../db/pagination' + +export default (db: Database): Partial> => ({ + async getBidirectionalBlock(req) { + const { actorDid, targetDid } = req + const res = await db.db + .selectFrom('actor_block') + .where((qb) => + qb + .where('actor_block.creator', '=', actorDid) + .where('actor_block.subjectDid', '=', targetDid), + ) + .orWhere((qb) => + qb + .where('actor_block.creator', '=', targetDid) + .where('actor_block.subjectDid', '=', actorDid), + ) + .limit(1) + .selectAll() + .executeTakeFirst() + + return { + blockUri: res?.uri, + } + }, + + async getBlocks(req) { + const { actorDid, cursor, limit } = req + const { ref } = db.db.dynamic + + let builder = db.db + .selectFrom('actor_block') + .where('actor_block.creator', '=', actorDid) + .selectAll() + + const keyset = new TimeCidKeyset( + ref('actor_block.sortAt'), + ref('actor_block.cid'), + ) + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + + const blocks = await builder.execute() + return { + blockUris: blocks.map((b) => b.uri), + cursor: keyset.packFromResult(blocks), + } + }, + + async getBidirectionalBlockViaList(req) { + const { actorDid, targetDid } = req + const res = await db.db + .selectFrom('list_block') + .innerJoin('list_item', 'list_item.listUri', 'list_block.subjectUri') + .where((qb) => + qb + .where('list_block.creator', '=', actorDid) + .where('list_item.subjectDid', '=', targetDid), + ) + .orWhere((qb) => + qb + .where('list_block.creator', '=', targetDid) + .where('list_item.subjectDid', '=', actorDid), + ) + .limit(1) + .selectAll('list_block') + .executeTakeFirst() + + return { + listUri: res?.subjectUri, + } + }, + + async getBlocklistSubscription(req) { + const { actorDid, listUri } = req + const res = await db.db + .selectFrom('list_block') + .where('creator', '=', actorDid) + .where('subjectUri', '=', listUri) + .selectAll() + .limit(1) + .executeTakeFirst() + return { + listblockUri: res?.uri, + } + }, + + async getBlocklistSubscriptions(req) { + const { actorDid, limit, cursor } = req + const { ref } = db.db.dynamic + let builder = db.db + .selectFrom('list') + .whereExists( + db.db + .selectFrom('list_block') + .where('list_block.creator', '=', actorDid) + .whereRef('list_block.subjectUri', '=', ref('list.uri')) + .selectAll(), + ) + .selectAll('list') + + const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + const lists = await builder.execute() + + return { + listUris: lists.map((l) => l.uri), + cursor: keyset.packFromResult(lists), + } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/feed-gens.ts b/packages/bsky/src/data-plane/server/routes/feed-gens.ts new file mode 100644 index 00000000000..129229f923f --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/feed-gens.ts @@ -0,0 +1,70 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { TimeCidKeyset, paginate } from '../db/pagination' + +export default (db: Database): Partial> => ({ + async getActorFeeds(req) { + const { actorDid, limit, cursor } = req + + const { ref } = db.db.dynamic + let builder = db.db + .selectFrom('feed_generator') + .selectAll() + .where('feed_generator.creator', '=', actorDid) + + const keyset = new TimeCidKeyset( + ref('feed_generator.createdAt'), + ref('feed_generator.cid'), + ) + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + const feeds = await builder.execute() + + return { + uris: feeds.map((f) => f.uri), + cursor: keyset.packFromResult(feeds), + } + }, + + async getSuggestedFeeds(req) { + const feeds = await db.db + .selectFrom('suggested_feed') + .orderBy('suggested_feed.order', 'asc') + .if(!!req.cursor, (q) => q.where('order', '>', parseInt(req.cursor, 10))) + .limit(req.limit || 50) + .selectAll() + .execute() + return { + uris: feeds.map((f) => f.uri), + cursor: feeds.at(-1)?.order.toString(), + } + }, + + async searchFeedGenerators(req) { + const { ref } = db.db.dynamic + const limit = req.limit + const query = req.query.trim() + let builder = db.db + .selectFrom('feed_generator') + .if(!!query, (q) => q.where('displayName', 'ilike', `%${query}%`)) + .selectAll() + const keyset = new TimeCidKeyset( + ref('feed_generator.createdAt'), + ref('feed_generator.cid'), + ) + builder = paginate(builder, { limit, keyset }) + const feeds = await builder.execute() + return { + uris: feeds.map((f) => f.uri), + cursor: keyset.packFromResult(feeds), + } + }, + + async getFeedGeneratorStatus() { + throw new Error('unimplemented') + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/feeds.ts b/packages/bsky/src/data-plane/server/routes/feeds.ts new file mode 100644 index 00000000000..3cb56d57e3f --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/feeds.ts @@ -0,0 +1,148 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { TimeCidKeyset, paginate } from '../db/pagination' +import { FeedType } from '../../../proto/bsky_pb' + +export default (db: Database): Partial> => ({ + async getAuthorFeed(req) { + const { actorDid, limit, cursor, feedType } = req + const { ref } = db.db.dynamic + + // defaults to posts, reposts, and replies + let builder = db.db + .selectFrom('feed_item') + .innerJoin('post', 'post.uri', 'feed_item.postUri') + .selectAll('feed_item') + .where('originatorDid', '=', actorDid) + + if (feedType === FeedType.POSTS_WITH_MEDIA) { + builder = builder + // only your own posts + .where('type', '=', 'post') + // 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 (feedType === FeedType.POSTS_NO_REPLIES) { + builder = builder.where((qb) => + qb.where('post.replyParent', 'is', null).orWhere('type', '=', 'repost'), + ) + } else if (feedType === FeedType.POSTS_AND_AUTHOR_THREADS) { + builder = builder.where((qb) => + qb + .where('type', '=', 'repost') + .orWhere('post.replyParent', 'is', null) + .orWhere('post.replyRoot', 'like', `at://${actorDid}/%`), + ) + } + + const keyset = new TimeCidKeyset( + ref('feed_item.sortAt'), + ref('feed_item.cid'), + ) + + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + + const feedItems = await builder.execute() + + return { + items: feedItems.map(feedItemFromRow), + cursor: keyset.packFromResult(feedItems), + } + }, + + async getTimeline(req) { + const { actorDid, limit, cursor } = req + const { ref } = db.db.dynamic + + const keyset = new TimeCidKeyset( + ref('feed_item.sortAt'), + ref('feed_item.cid'), + ) + + let followQb = db.db + .selectFrom('feed_item') + .innerJoin('follow', 'follow.subjectDid', 'feed_item.originatorDid') + .where('follow.creator', '=', actorDid) + .selectAll('feed_item') + + followQb = paginate(followQb, { + limit, + cursor, + keyset, + tryIndex: true, + }) + + let selfQb = db.db + .selectFrom('feed_item') + .where('feed_item.originatorDid', '=', actorDid) + .selectAll('feed_item') + + 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) + + return { + items: feedItems.map(feedItemFromRow), + cursor: keyset.packFromResult(feedItems), + } + }, + + async getListFeed(req) { + const { listUri, cursor, limit } = req + const { ref } = db.db.dynamic + + let builder = db.db + .selectFrom('post') + .selectAll('post') + .innerJoin('list_item', 'list_item.subjectDid', 'post.creator') + .where('list_item.listUri', '=', listUri) + + const keyset = new TimeCidKeyset(ref('post.sortAt'), ref('post.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + tryIndex: true, + }) + const feedItems = await builder.execute() + + return { + items: feedItems.map((item) => ({ uri: item.uri, cid: item.cid })), + cursor: keyset.packFromResult(feedItems), + } + }, +}) + +// @NOTE does not support additional fields in the protos specific to author feeds +// and timelines. at the time of writing, hydration/view implementations do not rely on them. +const feedItemFromRow = (row: { postUri: string; uri: string }) => { + return { + uri: row.postUri, + repost: row.uri === row.postUri ? undefined : row.uri, + } +} diff --git a/packages/bsky/src/data-plane/server/routes/follows.ts b/packages/bsky/src/data-plane/server/routes/follows.ts new file mode 100644 index 00000000000..1380fa281d6 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/follows.ts @@ -0,0 +1,95 @@ +import { keyBy } from '@atproto/common' +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { TimeCidKeyset, paginate } from '../db/pagination' + +export default (db: Database): Partial> => ({ + async getActorFollowsActors(req) { + const { actorDid, targetDids } = req + if (targetDids.length < 1) { + return { uris: [] } + } + const res = await db.db + .selectFrom('follow') + .where('follow.creator', '=', actorDid) + .where('follow.subjectDid', 'in', targetDids) + .selectAll() + .execute() + const bySubject = keyBy(res, 'subjectDid') + const uris = targetDids.map((did) => bySubject[did]?.uri ?? '') + return { + uris, + } + }, + async getFollowers(req) { + const { actorDid, limit, cursor } = req + const { ref } = db.db.dynamic + let followersReq = db.db + .selectFrom('follow') + .where('follow.subjectDid', '=', actorDid) + .innerJoin('actor as creator', 'creator.did', 'follow.creator') + .selectAll('creator') + .select([ + 'follow.uri as uri', + 'follow.cid as cid', + 'follow.creator as creatorDid', + 'follow.subjectDid as subjectDid', + 'follow.sortAt as sortAt', + ]) + + const keyset = new TimeCidKeyset(ref('follow.sortAt'), ref('follow.cid')) + followersReq = paginate(followersReq, { + limit, + cursor, + keyset, + tryIndex: true, + }) + + const followers = await followersReq.execute() + return { + followers: followers.map((f) => ({ + uri: f.uri, + actorDid: f.creatorDid, + subjectDid: f.subjectDid, + })), + cursor: keyset.packFromResult(followers), + } + }, + async getFollows(req) { + const { actorDid, limit, cursor } = req + const { ref } = db.db.dynamic + + let followsReq = db.db + .selectFrom('follow') + .where('follow.creator', '=', actorDid) + .innerJoin('actor as subject', 'subject.did', 'follow.subjectDid') + .selectAll('subject') + .select([ + 'follow.uri as uri', + 'follow.cid as cid', + 'follow.creator as creatorDid', + 'follow.subjectDid as subjectDid', + 'follow.sortAt as sortAt', + ]) + + const keyset = new TimeCidKeyset(ref('follow.sortAt'), ref('follow.cid')) + followsReq = paginate(followsReq, { + limit, + cursor, + keyset, + tryIndex: true, + }) + + const follows = await followsReq.execute() + + return { + follows: follows.map((f) => ({ + uri: f.uri, + actorDid: f.creatorDid, + subjectDid: f.subjectDid, + })), + cursor: keyset.packFromResult(follows), + } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/identity.ts b/packages/bsky/src/data-plane/server/routes/identity.ts new file mode 100644 index 00000000000..fb8dffc096c --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/identity.ts @@ -0,0 +1,59 @@ +import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { DidDocument, IdResolver, getDid, getHandle } from '@atproto/identity' +import { Timestamp } from '@bufbuild/protobuf' + +export default ( + _db: Database, + idResolver: IdResolver, +): Partial> => ({ + async getIdentityByDid(req) { + const doc = await idResolver.did.resolve(req.did) + if (!doc) { + throw new ConnectError('identity not found', Code.NotFound) + } + return getResultFromDoc(doc) + }, + + async getIdentityByHandle(req) { + const did = await idResolver.handle.resolve(req.handle) + if (!did) { + throw new ConnectError('identity not found', Code.NotFound) + } + const doc = await idResolver.did.resolve(did) + if (!doc || did !== getDid(doc)) { + throw new ConnectError('identity not found', Code.NotFound) + } + return getResultFromDoc(doc) + }, +}) + +const getResultFromDoc = (doc: DidDocument) => { + const keys: Record = {} + doc.verificationMethod?.forEach((method) => { + const id = method.id.split('#').at(1) + if (!id) return + keys[id] = { + Type: method.type, + PublicKeyMultibase: method.publicKeyMultibase || '', + } + }) + const services: Record = {} + doc.service?.forEach((service) => { + const id = service.id.split('#').at(1) + if (!id) return + if (typeof service.serviceEndpoint !== 'string') return + services[id] = { + Type: service.type, + URL: service.serviceEndpoint, + } + }) + return { + did: getDid(doc), + handle: getHandle(doc), + keys: Buffer.from(JSON.stringify(keys)), + services: Buffer.from(JSON.stringify(services)), + updated: Timestamp.fromDate(new Date()), + } +} diff --git a/packages/bsky/src/data-plane/server/routes/index.ts b/packages/bsky/src/data-plane/server/routes/index.ts new file mode 100644 index 00000000000..6169a48a28d --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/index.ts @@ -0,0 +1,55 @@ +import { ConnectRouter } from '@connectrpc/connect' +import { IdResolver } from '@atproto/identity' +import { Service } from '../../../proto/bsky_connect' +import blocks from './blocks' +import feedGens from './feed-gens' +import feeds from './feeds' +import follows from './follows' +import identity from './identity' +import interactions from './interactions' +import labels from './labels' +import likes from './likes' +import lists from './lists' +import moderation from './moderation' +import mutes from './mutes' +import notifs from './notifs' +import posts from './posts' +import profile from './profile' +import records from './records' +import relationships from './relationships' +import reposts from './reposts' +import search from './search' +import suggestions from './suggestions' +import sync from './sync' +import threads from './threads' +import { Database } from '../db' + +export default (db: Database, idResolver: IdResolver) => + (router: ConnectRouter) => + router.service(Service, { + ...blocks(db), + ...feedGens(db), + ...feeds(db), + ...follows(db), + ...identity(db, idResolver), + ...interactions(db), + ...labels(db), + ...likes(db), + ...lists(db), + ...moderation(db), + ...mutes(db), + ...notifs(db), + ...posts(db), + ...profile(db), + ...records(db), + ...relationships(db), + ...reposts(db), + ...search(db), + ...suggestions(db), + ...sync(db), + ...threads(db), + + async ping() { + return {} + }, + }) diff --git a/packages/bsky/src/data-plane/server/routes/interactions.ts b/packages/bsky/src/data-plane/server/routes/interactions.ts new file mode 100644 index 00000000000..73e46372a55 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/interactions.ts @@ -0,0 +1,40 @@ +import { keyBy } from '@atproto/common' +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' + +export default (db: Database): Partial> => ({ + async getInteractionCounts(req) { + const uris = req.refs.map((ref) => ref.uri) + if (uris.length === 0) { + return { likes: [], replies: [], reposts: [] } + } + const res = await db.db + .selectFrom('post_agg') + .where('uri', 'in', uris) + .selectAll() + .execute() + const byUri = keyBy(res, 'uri') + return { + likes: uris.map((uri) => byUri[uri]?.likeCount ?? 0), + replies: uris.map((uri) => byUri[uri]?.replyCount ?? 0), + reposts: uris.map((uri) => byUri[uri]?.repostCount ?? 0), + } + }, + async getCountsForUsers(req) { + if (req.dids.length === 0) { + return { followers: [], following: [], posts: [] } + } + const res = await db.db + .selectFrom('profile_agg') + .selectAll() + .where('did', 'in', req.dids) + .execute() + const byDid = keyBy(res, 'did') + return { + followers: req.dids.map((uri) => byDid[uri]?.followersCount ?? 0), + following: req.dids.map((uri) => byDid[uri]?.followsCount ?? 0), + posts: req.dids.map((uri) => byDid[uri]?.postsCount ?? 0), + } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/labels.ts b/packages/bsky/src/data-plane/server/routes/labels.ts new file mode 100644 index 00000000000..dd58387f579 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/labels.ts @@ -0,0 +1,28 @@ +import * as ui8 from 'uint8arrays' +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' + +export default (db: Database): Partial> => ({ + async getLabels(req) { + const { subjects, issuers } = req + if (subjects.length === 0 || issuers.length === 0) { + return { records: [] } + } + const res = await db.db + .selectFrom('label') + .where('uri', 'in', subjects) + .where('src', 'in', issuers) + .selectAll() + .execute() + + const labels = res.map((l) => { + const formatted = { + ...l, + cid: l.cid === '' ? undefined : l.cid, + } + return ui8.fromString(JSON.stringify(formatted), 'utf8') + }) + return { labels } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/likes.ts b/packages/bsky/src/data-plane/server/routes/likes.ts new file mode 100644 index 00000000000..7f7ace202b7 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/likes.ts @@ -0,0 +1,86 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { TimeCidKeyset, paginate } from '../db/pagination' +import { keyBy } from '@atproto/common' + +export default (db: Database): Partial> => ({ + async getLikesBySubject(req) { + const { subject, cursor, limit } = req + const { ref } = db.db.dynamic + + if (!subject?.uri) { + return { uris: [] } + } + + // @NOTE ignoring subject.cid + let builder = db.db + .selectFrom('like') + .where('like.subject', '=', subject?.uri) + .selectAll('like') + + const keyset = new TimeCidKeyset(ref('like.sortAt'), ref('like.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + + const likes = await builder.execute() + + return { + uris: likes.map((l) => l.uri), + cursor: keyset.packFromResult(likes), + } + }, + + async getLikesByActorAndSubjects(req) { + const { actorDid, refs } = req + if (refs.length === 0) { + return { uris: [] } + } + // @NOTE ignoring ref.cid + const res = await db.db + .selectFrom('like') + .where('creator', '=', actorDid) + .where( + 'subject', + 'in', + refs.map(({ uri }) => uri), + ) + .selectAll() + .execute() + const bySubject = keyBy(res, 'subject') + // @TODO handling undefineds properly, or do we need to turn them into empty strings? + const uris = refs.map(({ uri }) => bySubject[uri]?.uri) + return { uris } + }, + + async getActorLikes(req) { + const { actorDid, limit, cursor } = req + const { ref } = db.db.dynamic + + let builder = db.db + .selectFrom('like') + .where('like.creator', '=', actorDid) + .selectAll() + + const keyset = new TimeCidKeyset(ref('like.sortAt'), ref('like.cid')) + + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + + const likes = await builder.execute() + + return { + likes: likes.map((l) => ({ + uri: l.uri, + subject: l.subject, + })), + cursor: keyset.packFromResult(likes), + } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/lists.ts b/packages/bsky/src/data-plane/server/routes/lists.ts new file mode 100644 index 00000000000..d757b1d39e4 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/lists.ts @@ -0,0 +1,88 @@ +import { keyBy } from '@atproto/common' +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { countAll } from '../db/util' +import { TimeCidKeyset, paginate } from '../db/pagination' + +export default (db: Database): Partial> => ({ + async getActorLists(req) { + const { actorDid, cursor, limit } = req + const { ref } = db.db.dynamic + let builder = db.db + .selectFrom('list') + .where('creator', '=', actorDid) + .selectAll() + const keyset = new TimeCidKeyset(ref('list.sortAt'), ref('list.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + tryIndex: true, + }) + const lists = await builder.execute() + return { + listUris: lists.map((item) => item.uri), + cursor: keyset.packFromResult(lists), + } + }, + + async getListMembers(req) { + const { listUri, cursor, limit } = req + const { ref } = db.db.dynamic + let builder = db.db + .selectFrom('list_item') + .where('listUri', '=', listUri) + .selectAll() + + const keyset = new TimeCidKeyset( + ref('list_item.sortAt'), + ref('list_item.cid'), + ) + + builder = paginate(builder, { + limit, + cursor, + keyset, + tryIndex: true, + }) + + const listItems = await builder.execute() + return { + listitems: listItems.map((item) => ({ + uri: item.uri, + did: item.subjectDid, + })), + cursor: keyset.packFromResult(listItems), + } + }, + + async getListMembership(req) { + const { actorDid, listUris } = req + if (listUris.length === 0) { + return { listitemUris: [] } + } + const res = await db.db + .selectFrom('list_item') + .where('subjectDid', '=', actorDid) + .where('listUri', 'in', listUris) + .selectAll() + .execute() + const byListUri = keyBy(res, 'listUri') + const listitemUris = listUris.map((uri) => byListUri[uri]?.uri ?? '') + return { + listitemUris, + } + }, + + async getListCount(req) { + const res = await db.db + .selectFrom('list_item') + .select(countAll.as('count')) + .where('list_item.listUri', '=', req.listUri) + .executeTakeFirst() + return { + count: res?.count, + } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/moderation.ts b/packages/bsky/src/data-plane/server/routes/moderation.ts new file mode 100644 index 00000000000..d59360847fe --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/moderation.ts @@ -0,0 +1,102 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' + +export default (db: Database): Partial> => ({ + async getActorTakedown(req) { + const { did } = req + const res = await db.db + .selectFrom('actor') + .where('did', '=', did) + .select('takedownRef') + .executeTakeFirst() + return { + takenDown: !!res?.takedownRef, + takedownRef: res?.takedownRef || undefined, + } + }, + + async getBlobTakedown(req) { + const { did, cid } = req + const res = await db.db + .selectFrom('blob_takedown') + .where('did', '=', did) + .where('cid', '=', cid) + .select('takedownRef') + .executeTakeFirst() + return { + takenDown: !!res, + takedownRef: res?.takedownRef || undefined, + } + }, + + async getRecordTakedown(req) { + const { recordUri } = req + const res = await db.db + .selectFrom('record') + .where('uri', '=', recordUri) + .select('takedownRef') + .executeTakeFirst() + return { + takenDown: !!res?.takedownRef, + takedownRef: res?.takedownRef || undefined, + } + }, + + async takedownActor(req) { + const { did, ref } = req + await db.db + .updateTable('actor') + .set({ takedownRef: ref || 'TAKEDOWN' }) + .where('did', '=', did) + .execute() + }, + + async takedownBlob(req) { + const { did, cid, ref } = req + await db.db + .insertInto('blob_takedown') + .values({ + did, + cid, + takedownRef: ref || 'TAKEDOWN', + }) + .execute() + }, + + async takedownRecord(req) { + const { recordUri, ref } = req + await db.db + .updateTable('record') + .set({ takedownRef: ref || 'TAKEDOWN' }) + .where('uri', '=', recordUri) + .execute() + }, + + async untakedownActor(req) { + const { did } = req + await db.db + .updateTable('actor') + .set({ takedownRef: null }) + .where('did', '=', did) + .execute() + }, + + async untakedownBlob(req) { + const { did, cid } = req + await db.db + .deleteFrom('blob_takedown') + .where('did', '=', did) + .where('cid', '=', cid) + .executeTakeFirst() + }, + + async untakedownRecord(req) { + const { recordUri } = req + await db.db + .updateTable('record') + .set({ takedownRef: null }) + .where('uri', '=', recordUri) + .execute() + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/mutes.ts b/packages/bsky/src/data-plane/server/routes/mutes.ts new file mode 100644 index 00000000000..387e0f4d904 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/mutes.ts @@ -0,0 +1,172 @@ +import assert from 'assert' +import { ServiceImpl } from '@connectrpc/connect' +import { AtUri } from '@atproto/syntax' +import { ids } from '../../../lexicon/lexicons' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { CreatedAtDidKeyset, TimeCidKeyset, paginate } from '../db/pagination' + +export default (db: Database): Partial> => ({ + async getActorMutesActor(req) { + const { actorDid, targetDid } = req + const res = await db.db + .selectFrom('mute') + .selectAll() + .where('mutedByDid', '=', actorDid) + .where('subjectDid', '=', targetDid) + .executeTakeFirst() + return { + muted: !!res, + } + }, + + async getMutes(req) { + const { actorDid, limit, cursor } = req + const { ref } = db.db.dynamic + + let builder = db.db + .selectFrom('mute') + .innerJoin('actor', 'actor.did', 'mute.subjectDid') + .where('mute.mutedByDid', '=', actorDid) + .selectAll('actor') + .select('mute.createdAt as createdAt') + + const keyset = new CreatedAtDidKeyset( + ref('mute.createdAt'), + ref('mute.subjectDid'), + ) + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + + const mutes = await builder.execute() + + return { + dids: mutes.map((m) => m.did), + cursor: keyset.packFromResult(mutes), + } + }, + + async getActorMutesActorViaList(req) { + const { actorDid, targetDid } = req + const res = await db.db + .selectFrom('list_mute') + .innerJoin('list_item', 'list_item.listUri', 'list_mute.listUri') + .where('list_mute.mutedByDid', '=', actorDid) + .where('list_item.subjectDid', '=', targetDid) + .select('list_mute.listUri') + .limit(1) + .executeTakeFirst() + return { + listUri: res?.listUri, + } + }, + + async getMutelistSubscription(req) { + const { actorDid, listUri } = req + const res = await db.db + .selectFrom('list_mute') + .where('mutedByDid', '=', actorDid) + .where('listUri', '=', listUri) + .selectAll() + .limit(1) + .executeTakeFirst() + return { + subscribed: !!res, + } + }, + + async getMutelistSubscriptions(req) { + const { actorDid, limit, cursor } = req + const { ref } = db.db.dynamic + let builder = db.db + .selectFrom('list') + .whereExists( + db.db + .selectFrom('list_mute') + .where('list_mute.mutedByDid', '=', actorDid) + .whereRef('list_mute.listUri', '=', ref('list.uri')) + .selectAll(), + ) + .selectAll('list') + + const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + const lists = await builder.execute() + + return { + listUris: lists.map((l) => l.uri), + cursor: keyset.packFromResult(lists), + } + }, + + async createActorMute(req) { + const { actorDid, subjectDid } = req + assert(actorDid !== subjectDid, 'cannot mute yourself') // @TODO pass message through in http error + await db.db + .insertInto('mute') + .values({ + subjectDid, + mutedByDid: actorDid, + createdAt: new Date().toISOString(), + }) + .onConflict((oc) => oc.doNothing()) + .execute() + }, + + async deleteActorMute(req) { + const { actorDid, subjectDid } = req + assert(actorDid !== subjectDid, 'cannot mute yourself') + await db.db + .deleteFrom('mute') + .where('subjectDid', '=', subjectDid) + .where('mutedByDid', '=', actorDid) + .execute() + }, + + async clearActorMutes(req) { + const { actorDid } = req + await db.db.deleteFrom('mute').where('mutedByDid', '=', actorDid).execute() + }, + + async createActorMutelistSubscription(req) { + const { actorDid, subjectUri } = req + assert(isListUri(subjectUri), 'must mute a list') + await db.db + .insertInto('list_mute') + .values({ + listUri: subjectUri, + mutedByDid: actorDid, + createdAt: new Date().toISOString(), + }) + .onConflict((oc) => oc.doNothing()) + .execute() + }, + + async deleteActorMutelistSubscription(req) { + const { actorDid, subjectUri } = req + assert(isListUri(subjectUri), 'must mute a list') + await db.db + .deleteFrom('list_mute') + .where('listUri', '=', subjectUri) + .where('mutedByDid', '=', actorDid) + .execute() + }, + + async clearActorMutelistSubscriptions(req) { + const { actorDid } = req + await db.db + .deleteFrom('list_mute') + .where('mutedByDid', '=', actorDid) + .execute() + }, +}) + +const isListUri = (uri: string) => + new AtUri(uri).collection === ids.AppBskyGraphList diff --git a/packages/bsky/src/data-plane/server/routes/notifs.ts b/packages/bsky/src/data-plane/server/routes/notifs.ts new file mode 100644 index 00000000000..d5289806128 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/notifs.ts @@ -0,0 +1,115 @@ +import { sql } from 'kysely' +import { ServiceImpl } from '@connectrpc/connect' +import { Timestamp } from '@bufbuild/protobuf' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { countAll, excluded, notSoftDeletedClause } from '../db/util' +import { TimeCidKeyset, paginate } from '../db/pagination' + +export default (db: Database): Partial> => ({ + async getNotifications(req) { + const { actorDid, limit, cursor } = req + const { ref } = db.db.dynamic + let builder = db.db + .selectFrom('notification as notif') + .where('notif.did', '=', actorDid) + .where((clause) => + clause + .where('reasonSubject', 'is', null) + .orWhereExists( + db.db + .selectFrom('record as subject') + .selectAll() + .whereRef('subject.uri', '=', ref('notif.reasonSubject')), + ), + ) + .select([ + 'notif.author as authorDid', + 'notif.recordUri as uri', + 'notif.recordCid as cid', + 'notif.reason as reason', + 'notif.reasonSubject as reasonSubject', + 'notif.sortAt as sortAt', + ]) + + const keyset = new TimeCidKeyset( + ref('notif.sortAt'), + ref('notif.recordCid'), + ) + builder = paginate(builder, { + cursor, + limit, + keyset, + tryIndex: true, + }) + + const notifsRes = await builder.execute() + const notifications = notifsRes.map((notif) => ({ + recipientDid: actorDid, + uri: notif.uri, + reason: notif.reason, + reasonSubject: notif.reasonSubject ?? undefined, + timestamp: Timestamp.fromDate(new Date(notif.sortAt)), + })) + return { + notifications, + cursor: keyset.packFromResult(notifsRes), + } + }, + + async getNotificationSeen(req) { + const res = await db.db + .selectFrom('actor_state') + .where('did', '=', req.actorDid) + .selectAll() + .executeTakeFirst() + if (!res) { + return {} + } + return { + timestamp: Timestamp.fromDate(new Date(res.lastSeenNotifs)), + } + }, + + async getUnreadNotificationCount(req) { + const { actorDid } = req + const { ref } = db.db.dynamic + const result = await db.db + .selectFrom('notification') + .select(countAll.as('count')) + .innerJoin('actor', 'actor.did', 'notification.did') + .leftJoin('actor_state', 'actor_state.did', 'actor.did') + .innerJoin('record', 'record.uri', 'notification.recordUri') + .where(notSoftDeletedClause(ref('record'))) + .where(notSoftDeletedClause(ref('actor'))) + // Ensure to hit notification_did_sortat_idx, handling case where lastSeenNotifs is null. + .where('notification.did', '=', actorDid) + .where( + 'notification.sortAt', + '>', + sql`coalesce(${ref('actor_state.lastSeenNotifs')}, ${''})`, + ) + .executeTakeFirst() + + return { + count: result?.count, + } + }, + + async updateNotificationSeen(req) { + const { actorDid, timestamp } = req + if (!timestamp) { + return + } + const lastSeenNotifs = timestamp.toDate().toISOString() + await db.db + .insertInto('actor_state') + .values({ did: actorDid, lastSeenNotifs }) + .onConflict((oc) => + oc.column('did').doUpdateSet({ + lastSeenNotifs: excluded(db.db, 'lastSeenNotifs'), + }), + ) + .executeTakeFirst() + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/posts.ts b/packages/bsky/src/data-plane/server/routes/posts.ts new file mode 100644 index 00000000000..8f90a34b3cc --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/posts.ts @@ -0,0 +1,21 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { keyBy } from '@atproto/common' +import { Database } from '../db' + +export default (db: Database): Partial> => ({ + async getPostReplyCounts(req) { + const uris = req.refs.map((ref) => ref.uri) + if (uris.length === 0) { + return { counts: [] } + } + const res = await db.db + .selectFrom('post_agg') + .select(['uri', 'replyCount']) + .where('uri', 'in', uris) + .execute() + const byUri = keyBy(res, 'uri') + const counts = uris.map((uri) => byUri[uri]?.replyCount ?? 0) + return { counts } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/profile.ts b/packages/bsky/src/data-plane/server/routes/profile.ts new file mode 100644 index 00000000000..20768ed89aa --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/profile.ts @@ -0,0 +1,49 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { keyBy } from '@atproto/common' +import { getRecords } from './records' +import { Database } from '../db' + +export default (db: Database): Partial> => ({ + async getActors(req) { + const { dids } = req + if (dids.length === 0) { + return { actors: [] } + } + const profileUris = dids.map( + (did) => `at://${did}/app.bsky.actor.profile/self`, + ) + const [handlesRes, profiles] = await Promise.all([ + db.db.selectFrom('actor').where('did', 'in', dids).selectAll().execute(), + getRecords(db)({ uris: profileUris }), + ]) + const byDid = keyBy(handlesRes, 'did') + const actors = dids.map((did, i) => { + const row = byDid[did] + return { + exists: !!row, + handle: row?.handle ?? undefined, + profile: profiles.records[i], + takenDown: !!row?.takedownRef, + takedownRef: row?.takedownRef || undefined, + tombstonedAt: undefined, // in current implementation, tombstoned actors are deleted + } + }) + return { actors } + }, + + async getDidsByHandles(req) { + const { handles } = req + if (handles.length === 0) { + return { dids: [] } + } + const res = await db.db + .selectFrom('actor') + .where('handle', 'in', handles) + .selectAll() + .execute() + const byHandle = keyBy(res, 'handle') + const dids = handles.map((handle) => byHandle[handle]?.did ?? '') + return { dids } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/records.ts b/packages/bsky/src/data-plane/server/routes/records.ts new file mode 100644 index 00000000000..47670a24412 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/records.ts @@ -0,0 +1,95 @@ +import { keyBy } from '@atproto/common' +import { AtUri } from '@atproto/syntax' +import { Timestamp } from '@bufbuild/protobuf' +import { ServiceImpl } from '@connectrpc/connect' +import * as ui8 from 'uint8arrays' +import { ids } from '../../../lexicon/lexicons' +import { Service } from '../../../proto/bsky_connect' +import { PostRecordMeta, Record } from '../../../proto/bsky_pb' +import { Database } from '../db' + +export default (db: Database): Partial> => ({ + getBlockRecords: getRecords(db, ids.AppBskyGraphBlock), + getFeedGeneratorRecords: getRecords(db, ids.AppBskyFeedGenerator), + getFollowRecords: getRecords(db, ids.AppBskyGraphFollow), + getLikeRecords: getRecords(db, ids.AppBskyFeedLike), + getListBlockRecords: getRecords(db, ids.AppBskyGraphListblock), + getListItemRecords: getRecords(db, ids.AppBskyGraphListitem), + getListRecords: getRecords(db, ids.AppBskyGraphList), + getPostRecords: getPostRecords(db), + getProfileRecords: getRecords(db, ids.AppBskyActorProfile), + getRepostRecords: getRecords(db, ids.AppBskyFeedRepost), + getThreadGateRecords: getRecords(db, ids.AppBskyFeedThreadgate), +}) + +export const getRecords = + (db: Database, collection?: string) => + async (req: { uris: string[] }): Promise<{ records: Record[] }> => { + const validUris = collection + ? req.uris.filter((uri) => new AtUri(uri).collection === collection) + : req.uris + const res = validUris.length + ? await db.db + .selectFrom('record') + .selectAll() + .where('uri', 'in', validUris) + .execute() + : [] + const byUri = keyBy(res, 'uri') + const records: Record[] = req.uris.map((uri) => { + const row = byUri[uri] + const json = row ? row.json : JSON.stringify(null) + const createdAtRaw = new Date(JSON.parse(json)?.['createdAt']) + const createdAt = !isNaN(createdAtRaw.getTime()) + ? Timestamp.fromDate(createdAtRaw) + : undefined + const indexedAt = row?.indexedAt + ? Timestamp.fromDate(new Date(row?.indexedAt)) + : undefined + const recordBytes = ui8.fromString(json, 'utf8') + return new Record({ + record: recordBytes, + cid: row?.cid, + createdAt, + indexedAt, + sortedAt: compositeTime(createdAt, indexedAt), + takenDown: !!row?.takedownRef, + takedownRef: row?.takedownRef ?? undefined, + }) + }) + return { records } + } + +export const getPostRecords = (db: Database) => { + const getBaseRecords = getRecords(db, ids.AppBskyFeedPost) + return async (req: { + uris: string[] + }): Promise<{ records: Record[]; meta: PostRecordMeta[] }> => { + const [{ records }, details] = await Promise.all([ + getBaseRecords(req), + req.uris.length + ? await db.db + .selectFrom('post') + .where('uri', 'in', req.uris) + .select(['uri', 'violatesThreadGate']) + .execute() + : [], + ]) + const byKey = keyBy(details, 'uri') + const meta = req.uris.map((uri) => { + return new PostRecordMeta({ + violatesThreadGate: !!byKey[uri]?.violatesThreadGate, + }) + }) + return { records, meta } + } +} + +const compositeTime = ( + ts1: Timestamp | undefined, + ts2: Timestamp | undefined, +) => { + if (!ts1) return ts2 + if (!ts2) return ts1 + return ts1.toDate() < ts2.toDate() ? ts1 : ts2 +} diff --git a/packages/bsky/src/data-plane/server/routes/relationships.ts b/packages/bsky/src/data-plane/server/routes/relationships.ts new file mode 100644 index 00000000000..d6029e169e8 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/relationships.ts @@ -0,0 +1,148 @@ +import { sql } from 'kysely' +import { ServiceImpl } from '@connectrpc/connect' +import { keyBy } from '@atproto/common' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { valuesList } from '../db/util' + +export default (db: Database): Partial> => ({ + async getRelationships(req) { + const { actorDid, targetDids } = req + if (targetDids.length === 0) { + return { relationships: [] } + } + const { ref } = db.db.dynamic + const res = await db.db + .selectFrom('actor') + .where('did', 'in', targetDids) + .select([ + 'actor.did', + db.db + .selectFrom('mute') + .where('mute.mutedByDid', '=', actorDid) + .whereRef('mute.subjectDid', '=', ref('actor.did')) + .select(sql`${true}`.as('val')) + .as('muted'), + db.db + .selectFrom('list_item') + .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') + .where('list_mute.mutedByDid', '=', actorDid) + .whereRef('list_item.subjectDid', '=', ref('actor.did')) + .select('list_item.listUri') + .as('mutedByList'), + db.db + .selectFrom('actor_block') + .where('actor_block.creator', '=', actorDid) + .whereRef('actor_block.subjectDid', '=', ref('actor.did')) + .select('uri') + .as('blocking'), + db.db + .selectFrom('actor_block') + .where('actor_block.subjectDid', '=', actorDid) + .whereRef('actor_block.creator', '=', ref('actor.did')) + .select('uri') + .as('blockedBy'), + db.db + .selectFrom('list_item') + .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') + .where('list_block.creator', '=', actorDid) + .whereRef('list_item.subjectDid', '=', ref('actor.did')) + .select('list_item.listUri') + .as('blockingByList'), + db.db + .selectFrom('list_item') + .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') + .where('list_item.subjectDid', '=', actorDid) + .whereRef('list_block.creator', '=', ref('actor.did')) + .select('list_item.listUri') + .as('blockedByList'), + db.db + .selectFrom('follow') + .where('follow.creator', '=', actorDid) + .whereRef('follow.subjectDid', '=', ref('actor.did')) + .select('uri') + .as('following'), + db.db + .selectFrom('follow') + .where('follow.subjectDid', '=', actorDid) + .whereRef('follow.creator', '=', ref('actor.did')) + .select('uri') + .as('followedBy'), + ]) + .execute() + const byDid = keyBy(res, 'did') + const relationships = targetDids.map((did) => { + const row = byDid[did] ?? {} + return { + muted: row.muted ?? false, + mutedByList: row.mutedByList ?? '', + blockedBy: row.blockedBy ?? '', + blocking: row.blocking ?? '', + blockedByList: row.blockedByList ?? '', + blockingByList: row.blockingByList ?? '', + following: row.following ?? '', + followedBy: row.followedBy ?? '', + } + }) + return { relationships } + }, + + async getBlockExistence(req) { + const { pairs } = req + if (pairs.length === 0) { + return { exists: [] } + } + const { ref } = db.db.dynamic + const sourceRef = ref('pair.source') + const targetRef = ref('pair.target') + const values = valuesList(pairs.map((p) => sql`${p.a}, ${p.b}`)) + const res = await db.db + .selectFrom(values.as(sql`pair (source, target)`)) + .select([ + sql`${sourceRef}`.as('source'), + sql`${targetRef}`.as('target'), + ]) + .whereExists((qb) => + qb + .selectFrom('actor_block') + .whereRef('actor_block.creator', '=', sourceRef) + .whereRef('actor_block.subjectDid', '=', targetRef) + .select('uri'), + ) + .orWhereExists((qb) => + qb + .selectFrom('actor_block') + .whereRef('actor_block.creator', '=', targetRef) + .whereRef('actor_block.subjectDid', '=', sourceRef) + .select('uri'), + ) + .orWhereExists((qb) => + qb + .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'), + ) + .orWhereExists((qb) => + qb + .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'), + ) + .execute() + const existMap = res.reduce((acc, cur) => { + const key = [cur.source, cur.target].sort().join(',') + return acc.set(key, true) + }, new Map()) + const exists = pairs.map((pair) => { + const key = [pair.a, pair.b].sort().join(',') + return existMap.get(key) === true + }) + return { + exists, + } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/reposts.ts b/packages/bsky/src/data-plane/server/routes/reposts.ts new file mode 100644 index 00000000000..9c6f72435c0 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/reposts.ts @@ -0,0 +1,77 @@ +import { keyBy } from '@atproto/common' +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { TimeCidKeyset, paginate } from '../db/pagination' + +export default (db: Database): Partial> => ({ + async getRepostsBySubject(req) { + const { subject, cursor, limit } = req + const { ref } = db.db.dynamic + + let builder = db.db + .selectFrom('repost') + .where('repost.subject', '=', subject?.uri ?? '') + .selectAll('repost') + + const keyset = new TimeCidKeyset(ref('repost.sortAt'), ref('repost.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + + const reposts = await builder.execute() + + return { + uris: reposts.map((l) => l.uri), + cursor: keyset.packFromResult(reposts), + } + }, + + async getRepostsByActorAndSubjects(req) { + const { actorDid, refs } = req + if (refs.length === 0) { + return { uris: [] } + } + const res = await db.db + .selectFrom('repost') + .where('creator', '=', actorDid) + .where( + 'subject', + 'in', + refs.map(({ uri }) => uri), + ) + .selectAll() + .execute() + const bySubject = keyBy(res, 'subject') + // @TODO handling undefineds properly, or do we need to turn them into empty strings? + const uris = refs.map(({ uri }) => bySubject[uri]?.uri) + return { uris } + }, + + async getActorReposts(req) { + const { actorDid, limit, cursor } = req + const { ref } = db.db.dynamic + + let builder = db.db + .selectFrom('repost') + .where('repost.creator', '=', actorDid) + .selectAll() + + const keyset = new TimeCidKeyset(ref('repost.sortAt'), ref('repost.cid')) + + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + + const reposts = await builder.execute() + + return { + uris: reposts.map((l) => l.uri), + cursor: keyset.packFromResult(reposts), + } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/search.ts b/packages/bsky/src/data-plane/server/routes/search.ts new file mode 100644 index 00000000000..7b3b12d8c29 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/search.ts @@ -0,0 +1,61 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { IndexedAtDidKeyset, TimeCidKeyset, paginate } from '../db/pagination' + +export default (db: Database): Partial> => ({ + // @TODO actor search endpoints still fall back to search service + async searchActors(req) { + const { term, limit, cursor } = req + const { ref } = db.db.dynamic + let builder = db.db + .selectFrom('actor') + .where('actor.handle', 'like', `%${cleanQuery(term)}%`) + .selectAll() + + const keyset = new IndexedAtDidKeyset( + ref('actor.indexedAt'), + ref('actor.did'), + ) + builder = paginate(builder, { + limit, + cursor, + keyset, + tryIndex: true, + }) + + const res = await builder.execute() + + return { + dids: res.map((row) => row.did), + cursor: keyset.packFromResult(res), + } + }, + + // @TODO post search endpoint still falls back to search service + async searchPosts(req) { + const { term, limit, cursor } = req + const { ref } = db.db.dynamic + let builder = db.db + .selectFrom('post') + .where('post.text', 'like', `%${term}%`) + .selectAll() + + const keyset = new TimeCidKeyset(ref('post.sortAt'), ref('post.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + tryIndex: true, + }) + + const res = await builder.execute() + return { + uris: res.map((row) => row.uri), + cursor: keyset.packFromResult(res), + } + }, +}) + +// Remove leading @ in case a handle is input that way +const cleanQuery = (query: string) => query.trim().replace(/^@/g, '') diff --git a/packages/bsky/src/data-plane/server/routes/suggestions.ts b/packages/bsky/src/data-plane/server/routes/suggestions.ts new file mode 100644 index 00000000000..68b1dc54d5b --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/suggestions.ts @@ -0,0 +1,175 @@ +import { sql } from 'kysely' +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' + +export default (db: Database): Partial> => ({ + async getFollowSuggestions(req) { + const { actorDid, relativeToDid, cursor, limit } = req + if (relativeToDid) { + return getFollowSuggestionsRelativeTo(db, { + actorDid, + relativeToDid, + cursor: cursor || undefined, + limit: limit || undefined, + }) + } else { + return getFollowSuggestionsGlobal(db, { + actorDid, + cursor: cursor || undefined, + limit: limit || undefined, + }) + } + }, + + async getSuggestedEntities() { + const entities = await db.db + .selectFrom('tagged_suggestion') + .selectAll() + .execute() + return { + entities, + } + }, +}) + +const getFollowSuggestionsGlobal = async ( + db: Database, + input: { actorDid: string; cursor?: string; limit?: number }, +) => { + const alreadyIncluded = parseCursor(input.cursor) + const suggestions = await db.db + .selectFrom('suggested_follow') + .innerJoin('actor', 'actor.did', 'suggested_follow.did') + .if(alreadyIncluded.length > 0, (qb) => + qb.where('suggested_follow.order', 'not in', alreadyIncluded), + ) + .selectAll() + .orderBy('suggested_follow.order', 'asc') + .execute() + + // always include first two + const firstTwo = suggestions.filter( + (row) => row.order === 1 || row.order === 2, + ) + const rest = suggestions.filter((row) => row.order !== 1 && row.order !== 2) + const limited = firstTwo.concat(shuffle(rest)).slice(0, input.limit) + + // if the result set ends up getting larger, consider using a seed included in the cursor for for the randomized shuffle + const cursor = + limited.length > 0 + ? limited + .map((row) => row.order.toString()) + .concat(alreadyIncluded.map((id) => id.toString())) + .join(':') + : undefined + + return { + dids: limited.map((s) => s.did), + cursor, + } +} + +const getFollowSuggestionsRelativeTo = async ( + db: Database, + input: { + actorDid: string + relativeToDid: string + cursor?: string + limit?: number + }, +) => { + if (input.cursor) return { dids: [] } + const limit = input.limit ? Math.min(10, input.limit) : 10 + const actorsViewerFollows = db.db + .selectFrom('follow') + .where('creator', '=', input.actorDid) + .select('subjectDid') + const mostLikedAccounts = await db.db + .selectFrom( + db.db + .selectFrom('like') + .where('creator', '=', input.relativeToDid) + .select(sql`split_part(subject, '/', 3)`.as('subjectDid')) + .orderBy('sortAt', 'desc') + .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', [input.actorDid, input.relativeToDid]) + .groupBy('likes.subjectDid') + .orderBy('count', 'desc') + .limit(limit) + .execute() + const resultDids = mostLikedAccounts.map((a) => ({ did: a.did })) as { + did: string + }[] + + if (resultDids.length < limit) { + // backfill with popular accounts followed by actor + const mostPopularAccountsActorFollows = await db.db + .selectFrom('follow') + .innerJoin('profile_agg', 'follow.subjectDid', 'profile_agg.did') + .select('follow.subjectDid as did') + .where('follow.creator', '=', input.actorDid) + .where('follow.subjectDid', '!=', input.relativeToDid) + .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(limit) + .execute() + + resultDids.push(...mostPopularAccountsActorFollows) + } + + if (resultDids.length < limit) { + // backfill with suggested_follow table + const additional = await db.db + .selectFrom('suggested_follow') + .where( + 'did', + 'not in', + // exclude any we already have + resultDids + .map((a) => a.did) + .concat([input.actorDid, input.relativeToDid]), + ) + // and aren't already followed by viewer + .where('did', 'not in', actorsViewerFollows) + .selectAll() + .execute() + + resultDids.push(...additional) + } + + return { dids: resultDids.map((x) => x.did) } +} + +const parseCursor = (cursor?: string): number[] => { + if (!cursor) { + return [] + } + try { + return cursor + .split(':') + .map((id) => parseInt(id, 10)) + .filter((id) => !isNaN(id)) + } catch { + return [] + } +} + +const shuffle = (arr: T[]): T[] => { + return arr + .map((value) => ({ value, sort: Math.random() })) + .sort((a, b) => a.sort - b.sort) + .map(({ value }) => value) +} diff --git a/packages/bsky/src/data-plane/server/routes/sync.ts b/packages/bsky/src/data-plane/server/routes/sync.ts new file mode 100644 index 00000000000..90222b628e7 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/sync.ts @@ -0,0 +1,16 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' + +export default (db: Database): Partial> => ({ + async getLatestRev(req) { + const res = await db.db + .selectFrom('actor_sync') + .where('did', '=', req.actorDid) + .select('repoRev') + .executeTakeFirst() + return { + rev: res?.repoRev ?? undefined, + } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/threads.ts b/packages/bsky/src/data-plane/server/routes/threads.ts new file mode 100644 index 00000000000..5e19451d465 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/threads.ts @@ -0,0 +1,33 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { getAncestorsAndSelfQb, getDescendentsQb } from '../util' + +export default (db: Database): Partial> => ({ + async getThread(req) { + const { postUri, above, below } = req + const [ancestors, descendents] = await Promise.all([ + getAncestorsAndSelfQb(db.db, { + uri: postUri, + parentHeight: above, + }) + .selectFrom('ancestor') + .selectAll() + .execute(), + getDescendentsQb(db.db, { + uri: postUri, + depth: below, + }) + .selectFrom('descendent') + .innerJoin('post', 'post.uri', 'descendent.uri') + .orderBy('post.sortAt', 'desc') + .selectAll() + .execute(), + ]) + const uris = [ + ...ancestors.map((p) => p.uri), + ...descendents.map((p) => p.uri), + ] + return { uris } + }, +}) diff --git a/packages/bsky/src/indexer/subscription.ts b/packages/bsky/src/data-plane/server/subscription/index.ts similarity index 55% rename from packages/bsky/src/indexer/subscription.ts rename to packages/bsky/src/data-plane/server/subscription/index.ts index abc672db3b0..a020eb0542d 100644 --- a/packages/bsky/src/indexer/subscription.ts +++ b/packages/bsky/src/data-plane/server/subscription/index.ts @@ -1,8 +1,10 @@ import assert from 'node:assert' import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/syntax' -import { cborDecode, wait, handleAllSettledErrors } from '@atproto/common' -import { DisconnectError } from '@atproto/xrpc-server' +import { Subscription } from '@atproto/xrpc-server' +import { cborDecode, handleAllSettledErrors } from '@atproto/common' +import { ValidationError } from '@atproto/lexicon' +import { IdResolver } from '@atproto/identity' import { WriteOpAction, readCarWithRoot, @@ -10,141 +12,96 @@ import { 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 { ids, lexicons } from '../../../lexicon/lexicons' +import { OutputSchema as Message } from '../../../lexicon/types/com/atproto/sync/subscribeRepos' +import * as message from '../../../lexicon/types/com/atproto/sync/subscribeRepos' +import { subLogger as log } from '../../../logger' +import { IndexingService } from '../indexing' +import { Database } from '../db' import { ConsecutiveItem, ConsecutiveList, - LatestQueue, PartitionedQueue, - PerfectMap, ProcessableMessage, - jitter, loggableMessage, - strToInt, -} from '../subscription/util' -import IndexerContext from './context' +} from './util' +import { BackgroundQueue } from '../background' -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 +export class RepoSubscription { + ac = new AbortController() + running: Promise | undefined + cursor = 0 + seenSeq: number | null = null + repoQueue = new PartitionedQueue({ concurrency: Infinity }) + consecutive = new ConsecutiveList() + background: BackgroundQueue indexingSvc: IndexingService constructor( - public ctx: IndexerContext, - public opts: { - partitionIds: number[] - subLockId?: number - concurrency?: number - partitionBatchSize?: number + private opts: { + service: string + db: Database + idResolver: IdResolver + background: BackgroundQueue }, ) { - this.indexingSvc = ctx.services.indexing(ctx.db) + this.background = new BackgroundQueue(this.opts.db) + this.indexingSvc = new IndexingService( + this.opts.db, + this.opts.idResolver, + this.background, + ) } - 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) - }) + run() { + if (this.running) return + this.ac = new AbortController() + this.repoQueue = new PartitionedQueue({ concurrency: Infinity }) + this.consecutive = new ConsecutiveList() + this.running = this.process() + .catch((err) => { + if (err.name !== 'AbortError') { + // allow this to cause an unhandled rejection, let deployment handle the crash. + log.error({ err }, 'subscription crashed') + throw err } - } - await this.repoQueue.main.onEmpty() // backpressure - } + }) + .finally(() => (this.running = undefined)) } - 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 + private async process() { + const sub = this.getSubscription() + 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) }, + `sub ${details.info ? 'info' : 'unknown'} message`, + ) + continue } + const item = this.consecutive.push(details.seq) + this.repoQueue.add(details.repo, async () => { + await this.handleMessage(item, details) + }) + this.seenSeq = details.seq + await this.repoQueue.main.onEmpty() // backpressure } } - async requestReprocess(did: string) { - await this.repoQueue.add(did, async () => { - try { - await this.indexingSvc.indexRepo(did, undefined) - } catch (err) { - log.error({ did }, 'failed to reprocess repo') - } - }) - } - - 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 + const msg = envelope.message try { if (message.isCommit(msg)) { await this.handleCommit(msg) } else if (message.isHandle(msg)) { await this.handleUpdateHandle(msg) + } else if (message.isIdentity(msg)) { + await this.handleIdentityEvt(msg) } else if (message.isTombstone(msg)) { await this.handleTombstone(msg) } else if (message.isMigrate(msg)) { @@ -161,16 +118,9 @@ export class IndexerSubscription { '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') - }) + this.cursor = latest } } } @@ -244,9 +194,90 @@ export class IndexerSubscription { await this.indexingSvc.indexHandle(msg.did, msg.time, true) } + private async handleIdentityEvt(msg: message.Identity) { + await this.indexingSvc.indexHandle(msg.did, msg.time, true) + } + private async handleTombstone(msg: message.Tombstone) { await this.indexingSvc.tombstoneActor(msg.did) } + + private getSubscription() { + return new Subscription({ + service: this.opts.service, + method: ids.ComAtprotoSyncSubscribeRepos, + signal: this.ac.signal, + getParams: async () => { + return { cursor: this.cursor } + }, + onReconnectError: (err, reconnects, initial) => { + log.warn({ err, reconnects, initial }, 'sub reconnect') + }, + validate: (value) => { + try { + return lexicons.assertValidXrpcMessage( + ids.ComAtprotoSyncSubscribeRepos, + 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', + ) + } + }, + }) + } + + async destroy() { + this.ac.abort() + await this.running + await this.repoQueue.destroy() + await this.background.processAll() + } +} + +type Envelope = { + repo: string + message: ProcessableMessage +} + +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.isIdentity(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 getOps( @@ -290,37 +321,6 @@ async function getOps( 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 diff --git a/packages/bsky/src/subscription/util.ts b/packages/bsky/src/data-plane/server/subscription/util.ts similarity index 93% rename from packages/bsky/src/subscription/util.ts rename to packages/bsky/src/data-plane/server/subscription/util.ts index fe367bcc24c..40bf30f73c8 100644 --- a/packages/bsky/src/subscription/util.ts +++ b/packages/bsky/src/data-plane/server/subscription/util.ts @@ -1,7 +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' +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' // A queue with arbitrarily many partitions, each processing work sequentially. // Partitions are created lazily and taken out of memory when they go idle. @@ -108,6 +108,7 @@ export class PerfectMap extends Map { export type ProcessableMessage = | message.Commit | message.Handle + | message.Identity | message.Migrate | message.Tombstone @@ -127,6 +128,8 @@ export function loggableMessage(msg: RepoMessage) { } } else if (message.isHandle(msg)) { return msg + } else if (message.isIdentity(msg)) { + return msg } else if (message.isMigrate(msg)) { return msg } else if (message.isTombstone(msg)) { diff --git a/packages/bsky/src/services/feed/util.ts b/packages/bsky/src/data-plane/server/util.ts similarity index 55% rename from packages/bsky/src/services/feed/util.ts rename to packages/bsky/src/data-plane/server/util.ts index 83b5e59d705..d15b7ffa518 100644 --- a/packages/bsky/src/services/feed/util.ts +++ b/packages/bsky/src/data-plane/server/util.ts @@ -1,19 +1,77 @@ import { sql } from 'kysely' import { AtUri } from '@atproto/syntax' +import { ids } from '../../lexicon/lexicons' import { Record as PostRecord, ReplyRef, } from '../../lexicon/types/app/bsky/feed/post' -import { - Record as GateRecord, - isFollowingRule, - isListRule, - isMentionRule, -} from '../../lexicon/types/app/bsky/feed/threadgate' -import { isMention } from '../../lexicon/types/app/bsky/richtext/facet' -import { valuesList } from '../../db/util' -import DatabaseSchema from '../../db/database-schema' -import { ids } from '../../lexicon/lexicons' +import { Record as GateRecord } from '../../lexicon/types/app/bsky/feed/threadgate' +import DatabaseSchema from './db/database-schema' +import { valuesList } from './db/util' +import { parseThreadGate } from '../../views/util' + +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 +} export const invalidReplyRoot = ( reply: ReplyRef, @@ -35,46 +93,6 @@ export const invalidReplyRoot = ( // replying to a reply: ensure the parent is a reply for the same root post return parent.record.reply?.root.uri !== replyRoot } - -type ParsedThreadGate = { - canReply?: boolean - allowMentions?: boolean - allowFollowing?: boolean - allowListUris?: string[] -} - -export const parseThreadGate = ( - replierDid: string, - ownerDid: string, - rootPost: PostRecord | null, - gate: GateRecord | null, -): ParsedThreadGate => { - if (replierDid === ownerDid) { - return { canReply: true } - } - // if gate.allow is unset then *any* reply is allowed, if it is an empty array then *no* reply is allowed - if (!gate || !gate.allow) { - return { canReply: true } - } - - const allowMentions = !!gate.allow.find(isMentionRule) - const allowFollowing = !!gate.allow.find(isFollowingRule) - const allowListUris = gate.allow?.filter(isListRule).map((item) => item.list) - - // check mentions first since it's quick and synchronous - if (allowMentions) { - const isMentioned = rootPost?.facets?.some((facet) => { - return facet.features.some( - (item) => isMention(item) && item.did === replierDid, - ) - }) - if (isMentioned) { - return { canReply: true, allowMentions, allowFollowing, allowListUris } - } - } - return { allowMentions, allowFollowing, allowListUris } -} - export const violatesThreadGate = async ( db: DatabaseSchema, replierDid: string, @@ -132,9 +150,3 @@ export const postToThreadgateUri = (postUri: string) => { gateUri.collection = ids.AppBskyFeedThreadgate return gateUri.toString() } - -export const threadgateToPostUri = (gateUri: string) => { - const postUri = new AtUri(gateUri) - postUri.collection = ids.AppBskyFeedPost - return postUri.toString() -} diff --git a/packages/bsky/src/db/coordinator.ts b/packages/bsky/src/db/coordinator.ts deleted file mode 100644 index a8f4cc3016c..00000000000 --- a/packages/bsky/src/db/coordinator.ts +++ /dev/null @@ -1,107 +0,0 @@ -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?.length) { - 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 deleted file mode 100644 index cb58eb4742b..00000000000 --- a/packages/bsky/src/db/db.ts +++ /dev/null @@ -1,91 +0,0 @@ -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' -import { dbLogger } from '../logger' - -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('error', onPoolError) - pool.on('connect', (client) => { - client.on('error', onClientError) - // Used for trigram indexes, e.g. on actor search - client.query('SET pg_trgm.word_similarity_threshold TO .4;') - if (schema) { - // Shared objects such as extensions will go in the public schema - client.query(`SET search_path TO "${schema}",public;`) - } - }) - - this.pool = pool - this.db = new Kysely({ - dialect: new PostgresDialect({ pool }), - }) - } - - 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 - -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/src/db/index.ts b/packages/bsky/src/db/index.ts deleted file mode 100644 index 1c5886fb10e..00000000000 --- a/packages/bsky/src/db/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index ebd44bf98d6..00000000000 --- a/packages/bsky/src/db/leader.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { PoolClient } from 'pg' -import PrimaryDatabase from './primary' - -export class Leader { - session: Session | null = null - constructor(public id: number, public db: PrimaryDatabase) {} - - async run( - task: (ctx: { signal: AbortSignal }) => Promise, - ): Promise> { - const session = await this.lock() - if (!session) return { ran: false } - try { - const result = await task({ signal: session.abortController.signal }) - return { ran: true, result } - } finally { - this.release() - } - } - - destroy(err?: Error) { - this.session?.abortController.abort(err) - } - - private async lock(): Promise { - if (this.session) { - return null - } - - // Postgres implementation uses advisory locking, automatically released by ending connection. - - const client = await this.db.pool.connect() - try { - const lock = await client.query( - 'SELECT pg_try_advisory_lock($1) as acquired', - [this.id], - ) - if (!lock.rows[0].acquired) { - client.release() - return null - } - } catch (err) { - client.release(true) - throw err - } - - const abortController = new AbortController() - client.once('error', (err) => abortController.abort(err)) - this.session = { abortController, client } - return this.session - } - - private release() { - // The flag ensures the connection is destroyed on release, not reused. - // This is required, as that is how the pg advisory lock is released. - this.session?.client.release(true) - this.session = null - } -} - -type Session = { abortController: AbortController; client: PoolClient } - -type RunResult = { ran: false } | { ran: true; result: T } diff --git a/packages/bsky/src/db/migrations/20231205T000257238Z-remove-did-cache.ts b/packages/bsky/src/db/migrations/20231205T000257238Z-remove-did-cache.ts deleted file mode 100644 index 6b57a88bbb9..00000000000 --- a/packages/bsky/src/db/migrations/20231205T000257238Z-remove-did-cache.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema.dropTable('did_cache').execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .createTable('did_cache') - .addColumn('did', 'varchar', (col) => col.primaryKey()) - .addColumn('doc', 'jsonb', (col) => col.notNull()) - .addColumn('updatedAt', 'bigint', (col) => col.notNull()) - .execute() -} diff --git a/packages/bsky/src/db/views.ts b/packages/bsky/src/db/views.ts deleted file mode 100644 index d5aa9941436..00000000000 --- a/packages/bsky/src/db/views.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { jitter, wait } from '@atproto/common' -import { Leader } from './leader' -import { dbLogger } from '../logger' -import { PrimaryDatabase } from '.' - -export const VIEW_MAINTAINER_ID = 1010 -const VIEWS = ['algo_whats_hot_view'] - -export class ViewMaintainer { - leader = new Leader(VIEW_MAINTAINER_ID, this.db) - destroyed = false - - // @NOTE the db must be authed as the owner of the materialized view, per postgres. - constructor(public db: PrimaryDatabase, public intervalSec = 60) {} - - async run() { - while (!this.destroyed) { - try { - const { ran } = await this.leader.run(async ({ signal }) => { - await this.db.maintainMaterializedViews({ - signal, - views: VIEWS, - intervalSec: this.intervalSec, - }) - }) - if (ran && !this.destroyed) { - throw new Error('View maintainer completed, but should be persistent') - } - } catch (err) { - dbLogger.error( - { - err, - views: VIEWS, - intervalSec: this.intervalSec, - lockId: VIEW_MAINTAINER_ID, - }, - 'view maintainer errored', - ) - } - if (!this.destroyed) { - await wait(10000 + jitter(2000)) - } - } - } - - destroy() { - this.destroyed = true - this.leader.destroy() - } -} diff --git a/packages/bsky/src/did-cache.ts b/packages/bsky/src/did-cache.ts deleted file mode 100644 index 9e45d0d8b30..00000000000 --- a/packages/bsky/src/did-cache.ts +++ /dev/null @@ -1,87 +0,0 @@ -import PQueue from 'p-queue' -import { CacheResult, DidCache, DidDocument } from '@atproto/identity' -import { cacheLogger as log } from './logger' -import { Redis } from './redis' - -type CacheOptions = { - staleTTL: number - maxTTL: number -} - -export class DidRedisCache implements DidCache { - public pQueue: PQueue | null // null during teardown - - constructor(public redis: Redis, public opts: CacheOptions) { - this.pQueue = new PQueue() - } - - async cacheDid(did: string, doc: DidDocument): Promise { - const item = JSON.stringify({ - doc, - updatedAt: Date.now(), - }) - await this.redis.set(did, item, this.opts.maxTTL) - } - - async refreshCache( - did: string, - getDoc: () => Promise, - ): Promise { - this.pQueue?.add(async () => { - try { - const doc = await getDoc() - if (doc) { - await this.cacheDid(did, doc) - } else { - await this.clearEntry(did) - } - } catch (err) { - log.error({ did, err }, 'refreshing did cache failed') - } - }) - } - - async checkCache(did: string): Promise { - let got: string | null - try { - got = await this.redis.get(did) - } catch (err) { - got = null - log.error({ did, err }, 'error fetching did from cache') - } - if (!got) return null - const { doc, updatedAt } = JSON.parse(got) as CacheResult - const now = Date.now() - const expired = now > updatedAt + this.opts.maxTTL - const stale = now > updatedAt + this.opts.staleTTL - return { - doc, - updatedAt, - did, - stale, - expired, - } - } - - async clearEntry(did: string): Promise { - await this.redis.del(did) - } - - async clear(): Promise { - throw new Error('Not implemented for redis cache') - } - - async processAll() { - await this.pQueue?.onIdle() - } - - async destroy() { - const pQueue = this.pQueue - this.pQueue = null - pQueue?.pause() - pQueue?.clear() - await pQueue?.onIdle() - } -} - -export default DidRedisCache diff --git a/packages/bsky/src/hydration/actor.ts b/packages/bsky/src/hydration/actor.ts new file mode 100644 index 00000000000..3ed7f1b036e --- /dev/null +++ b/packages/bsky/src/hydration/actor.ts @@ -0,0 +1,149 @@ +import { DataPlaneClient } from '../data-plane/client' +import { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile' +import { + HydrationMap, + parseRecordBytes, + parseString, + safeTakedownRef, +} from './util' + +export type Actor = { + did: string + handle?: string + profile?: ProfileRecord + profileCid?: string + profileTakedownRef?: string + sortedAt?: Date + takedownRef?: string +} + +export type Actors = HydrationMap + +export type ProfileViewerState = { + muted?: boolean + mutedByList?: string + blockedBy?: string + blocking?: string + blockedByList?: string + blockingByList?: string + following?: string + followedBy?: string +} + +export type ProfileViewerStates = HydrationMap + +export type ProfileAgg = { + followers: number + follows: number + posts: number +} + +export type ProfileAggs = HydrationMap + +export class ActorHydrator { + constructor(public dataplane: DataPlaneClient) {} + + async getRepoRevSafe(did: string | null): Promise { + if (!did) return null + try { + const res = await this.dataplane.getLatestRev({ actorDid: did }) + return parseString(res.rev) ?? null + } catch { + return null + } + } + + async getDids(handleOrDids: string[]): Promise<(string | undefined)[]> { + const handles = handleOrDids.filter((actor) => !actor.startsWith('did:')) + const res = handles.length + ? await this.dataplane.getDidsByHandles({ handles }) + : { dids: [] } + const didByHandle = handles.reduce((acc, cur, i) => { + const did = res.dids[i] + if (did && did.length > 0) { + return acc.set(cur, did) + } + return acc + }, new Map() as Map) + return handleOrDids.map((id) => + id.startsWith('did:') ? id : didByHandle.get(id), + ) + } + + async getDidsDefined(handleOrDids: string[]): Promise { + const res = await this.getDids(handleOrDids) + // @ts-ignore + return res.filter((did) => did !== undefined) + } + + async getActors(dids: string[], includeTakedowns = false): Promise { + if (!dids.length) return new HydrationMap() + const res = await this.dataplane.getActors({ dids }) + return dids.reduce((acc, did, i) => { + const actor = res.actors[i] + if ( + !actor.exists || + (actor.takenDown && !includeTakedowns) || + !!actor.tombstonedAt + ) { + return acc.set(did, null) + } + const profile = + includeTakedowns || !actor.profile?.takenDown + ? actor.profile + : undefined + return acc.set(did, { + did, + handle: parseString(actor.handle), + profile: parseRecordBytes(profile?.record), + profileCid: profile?.cid, + profileTakedownRef: safeTakedownRef(profile), + sortedAt: profile?.sortedAt?.toDate(), + takedownRef: safeTakedownRef(actor), + }) + }, new HydrationMap()) + } + + // "naive" because this method does not verify the existence of the list itself + // a later check in the main hydrator will remove list uris that have been deleted or + // repurposed to "curate lists" + async getProfileViewerStatesNaive( + dids: string[], + viewer: string, + ): Promise { + if (!dids.length) return new HydrationMap() + const res = await this.dataplane.getRelationships({ + actorDid: viewer, + targetDids: dids, + }) + return dids.reduce((acc, did, i) => { + const rels = res.relationships[i] + if (viewer === did) { + // ignore self-follows, self-mutes, self-blocks + return acc.set(did, {}) + } + return acc.set(did, { + muted: rels.muted ?? false, + mutedByList: parseString(rels.mutedByList), + blockedBy: parseString(rels.blockedBy), + blocking: parseString(rels.blocking), + blockedByList: parseString(rels.blockedByList), + blockingByList: parseString(rels.blockingByList), + following: parseString(rels.following), + followedBy: parseString(rels.followedBy), + }) + }, new HydrationMap()) + } + + async getProfileAggregates(dids: string[]): Promise { + if (!dids.length) return new HydrationMap() + const counts = await this.dataplane.getCountsForUsers({ dids }) + return dids.reduce((acc, did, i) => { + return acc.set(did, { + followers: counts.followers[i] ?? 0, + follows: counts.following[i] ?? 0, + posts: counts.posts[i] ?? 0, + }) + }, new HydrationMap()) + } +} diff --git a/packages/bsky/src/hydration/feed.ts b/packages/bsky/src/hydration/feed.ts new file mode 100644 index 00000000000..edd29b851e8 --- /dev/null +++ b/packages/bsky/src/hydration/feed.ts @@ -0,0 +1,204 @@ +import { DataPlaneClient } from '../data-plane/client' +import { Record as PostRecord } from '../lexicon/types/app/bsky/feed/post' +import { Record as LikeRecord } from '../lexicon/types/app/bsky/feed/like' +import { Record as RepostRecord } from '../lexicon/types/app/bsky/feed/repost' +import { Record as FeedGenRecord } from '../lexicon/types/app/bsky/feed/generator' +import { Record as ThreadgateRecord } from '../lexicon/types/app/bsky/feed/threadgate' +import { + HydrationMap, + RecordInfo, + parseRecord, + parseString, + split, +} from './util' +import { AtUri } from '@atproto/syntax' +import { ids } from '../lexicon/lexicons' + +export type Post = RecordInfo & { violatesThreadGate: boolean } +export type Posts = HydrationMap + +export type PostViewerState = { + like?: string + repost?: string +} + +export type PostViewerStates = HydrationMap + +export type PostAgg = { + likes: number + replies: number + reposts: number +} + +export type PostAggs = HydrationMap + +export type Like = RecordInfo +export type Likes = HydrationMap + +export type Repost = RecordInfo +export type Reposts = HydrationMap + +export type FeedGenAgg = { + likes: number +} + +export type FeedGenAggs = HydrationMap + +export type FeedGen = RecordInfo +export type FeedGens = HydrationMap + +export type FeedGenViewerState = { + like?: string +} + +export type FeedGenViewerStates = HydrationMap + +export type Threadgate = RecordInfo +export type Threadgates = HydrationMap + +export type ItemRef = { uri: string; cid?: string } + +// @NOTE the feed item types in the protos for author feeds and timelines +// technically have additional fields, not supported by the mock dataplane. +export type FeedItem = { post: ItemRef; repost?: ItemRef } + +export class FeedHydrator { + constructor(public dataplane: DataPlaneClient) {} + + async getPosts( + uris: string[], + includeTakedowns = false, + given = new HydrationMap(), + ): Promise { + const [have, need] = split(uris, (uri) => given.has(uri)) + const base = have.reduce( + (acc, uri) => acc.set(uri, given.get(uri) ?? null), + new HydrationMap(), + ) + if (!need.length) return base + const res = await this.dataplane.getPostRecords({ uris: need }) + return need.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + const violatesThreadGate = res.meta[i].violatesThreadGate + return acc.set(uri, record ? { ...record, violatesThreadGate } : null) + }, base) + } + + async getPostViewerStates( + refs: ItemRef[], + viewer: string, + ): Promise { + if (!refs.length) return new HydrationMap() + const [likes, reposts] = await Promise.all([ + this.dataplane.getLikesByActorAndSubjects({ + actorDid: viewer, + refs, + }), + this.dataplane.getRepostsByActorAndSubjects({ + actorDid: viewer, + refs, + }), + ]) + return refs.reduce((acc, { uri }, i) => { + return acc.set(uri, { + like: parseString(likes.uris[i]), + repost: parseString(reposts.uris[i]), + }) + }, new HydrationMap()) + } + + async getPostAggregates(refs: ItemRef[]): Promise { + if (!refs.length) return new HydrationMap() + const counts = await this.dataplane.getInteractionCounts({ refs }) + return refs.reduce((acc, { uri }, i) => { + return acc.set(uri, { + likes: counts.likes[i] ?? 0, + reposts: counts.reposts[i] ?? 0, + replies: counts.replies[i] ?? 0, + }) + }, new HydrationMap()) + } + + async getFeedGens( + uris: string[], + includeTakedowns = false, + ): Promise { + if (!uris.length) return new HydrationMap() + const res = await this.dataplane.getFeedGeneratorRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord( + res.records[i], + includeTakedowns, + ) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + async getFeedGenViewerStates( + uris: string[], + viewer: string, + ): Promise { + if (!uris.length) return new HydrationMap() + const likes = await this.dataplane.getLikesByActorAndSubjects({ + actorDid: viewer, + refs: uris.map((uri) => ({ uri })), + }) + return uris.reduce((acc, uri, i) => { + return acc.set(uri, { + like: parseString(likes.uris[i]), + }) + }, new HydrationMap()) + } + + async getFeedGenAggregates(refs: ItemRef[]): Promise { + if (!refs.length) return new HydrationMap() + const counts = await this.dataplane.getInteractionCounts({ refs }) + return refs.reduce((acc, { uri }, i) => { + return acc.set(uri, { + likes: counts.likes[i] ?? 0, + }) + }, new HydrationMap()) + } + + async getThreadgatesForPosts( + postUris: string[], + includeTakedowns = false, + ): Promise { + if (!postUris.length) return new HydrationMap() + const uris = postUris.map((uri) => { + const parsed = new AtUri(uri) + return AtUri.make( + parsed.hostname, + ids.AppBskyFeedThreadgate, + parsed.rkey, + ).toString() + }) + const res = await this.dataplane.getThreadGateRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord( + res.records[i], + includeTakedowns, + ) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + // @TODO may not be supported yet by data plane + async getLikes(uris: string[], includeTakedowns = false): Promise { + if (!uris.length) return new HydrationMap() + const res = await this.dataplane.getLikeRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + async getReposts(uris: string[], includeTakedowns = false): Promise { + if (!uris.length) return new HydrationMap() + const res = await this.dataplane.getRepostRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } +} diff --git a/packages/bsky/src/hydration/graph.ts b/packages/bsky/src/hydration/graph.ts new file mode 100644 index 00000000000..efcd2fb9948 --- /dev/null +++ b/packages/bsky/src/hydration/graph.ts @@ -0,0 +1,195 @@ +import { Record as FollowRecord } from '../lexicon/types/app/bsky/graph/follow' +import { Record as BlockRecord } from '../lexicon/types/app/bsky/graph/block' +import { Record as ListRecord } from '../lexicon/types/app/bsky/graph/list' +import { Record as ListItemRecord } from '../lexicon/types/app/bsky/graph/listitem' +import { DataPlaneClient } from '../data-plane/client' +import { HydrationMap, RecordInfo, parseRecord } from './util' +import { FollowInfo } from '../proto/bsky_pb' + +export type List = RecordInfo +export type Lists = HydrationMap + +export type ListItem = RecordInfo +export type ListItems = HydrationMap + +export type ListViewerState = { + viewerMuted?: string + viewerListBlockUri?: string + viewerInList?: string +} + +export type ListViewerStates = HydrationMap + +export type Follow = RecordInfo +export type Follows = HydrationMap + +export type Block = RecordInfo + +export type RelationshipPair = [didA: string, didB: string] + +const dedupePairs = (pairs: RelationshipPair[]): RelationshipPair[] => { + const mapped = pairs.reduce((acc, cur) => { + const sorted = ([...cur] as RelationshipPair).sort() + acc[sorted.join('-')] = sorted + return acc + }, {} as Record) + return Object.values(mapped) +} + +export class Blocks { + _blocks: Map = new Map() + constructor() {} + + static key(didA: string, didB: string): string { + return [didA, didB].sort().join(',') + } + + set(didA: string, didB: string, exists: boolean): Blocks { + const key = Blocks.key(didA, didB) + this._blocks.set(key, exists) + return this + } + + has(didA: string, didB: string): boolean { + const key = Blocks.key(didA, didB) + return this._blocks.has(key) + } + + isBlocked(didA: string, didB: string): boolean { + if (didA === didB) return false // ignore self-blocks + const key = Blocks.key(didA, didB) + return this._blocks.get(key) ?? false + } + + merge(blocks: Blocks): Blocks { + blocks._blocks.forEach((exists, key) => { + this._blocks.set(key, exists) + }) + return this + } +} + +export class GraphHydrator { + constructor(public dataplane: DataPlaneClient) {} + + async getLists(uris: string[], includeTakedowns = false): Promise { + if (!uris.length) return new HydrationMap() + const res = await this.dataplane.getListRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + // @TODO may not be supported yet by data plane + async getListItems( + uris: string[], + includeTakedowns = false, + ): Promise { + if (!uris.length) return new HydrationMap() + const res = await this.dataplane.getListItemRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord( + res.records[i], + includeTakedowns, + ) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + async getListViewerStates( + uris: string[], + viewer: string, + ): Promise { + if (!uris.length) return new HydrationMap() + const mutesAndBlocks = await Promise.all( + uris.map((uri) => this.getMutesAndBlocks(uri, viewer)), + ) + const listMemberships = await this.dataplane.getListMembership({ + actorDid: viewer, + listUris: uris, + }) + return uris.reduce((acc, uri, i) => { + return acc.set(uri, { + viewerMuted: mutesAndBlocks[i].muted ? uri : undefined, + viewerListBlockUri: mutesAndBlocks[i].listBlockUri || undefined, + viewerInList: listMemberships.listitemUris[i], + }) + }, new HydrationMap()) + } + + private async getMutesAndBlocks(uri: string, viewer: string) { + const [muted, listBlockUri] = await Promise.all([ + this.dataplane.getMutelistSubscription({ + actorDid: viewer, + listUri: uri, + }), + this.dataplane.getBlocklistSubscription({ + actorDid: viewer, + listUri: uri, + }), + ]) + return { + muted: muted.subscribed, + listBlockUri: listBlockUri.listblockUri, + } + } + + async getBidirectionalBlocks(pairs: RelationshipPair[]): Promise { + if (!pairs.length) return new Blocks() + const deduped = dedupePairs(pairs).map(([a, b]) => ({ a, b })) + const res = await this.dataplane.getBlockExistence({ pairs: deduped }) + const blocks = new Blocks() + for (let i = 0; i < deduped.length; i++) { + const pair = deduped[i] + blocks.set(pair.a, pair.b, res.exists[i] ?? false) + } + return blocks + } + + async getFollows(uris: string[], includeTakedowns = false): Promise { + if (!uris.length) return new HydrationMap() + const res = await this.dataplane.getFollowRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + async getBlocks(uris: string[], includeTakedowns = false): Promise { + if (!uris.length) return new HydrationMap() + const res = await this.dataplane.getBlockRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + async getActorFollows(input: { + did: string + cursor?: string + limit?: number + }): Promise<{ follows: FollowInfo[]; cursor: string }> { + const { did, cursor, limit } = input + const res = await this.dataplane.getFollows({ + actorDid: did, + cursor, + limit, + }) + return { follows: res.follows, cursor: res.cursor } + } + + async getActorFollowers(input: { + did: string + cursor?: string + limit?: number + }): Promise<{ followers: FollowInfo[]; cursor: string }> { + const { did, cursor, limit } = input + const res = await this.dataplane.getFollowers({ + actorDid: did, + cursor, + limit, + }) + return { followers: res.followers, cursor: res.cursor } + } +} diff --git a/packages/bsky/src/hydration/hydrator.ts b/packages/bsky/src/hydration/hydrator.ts new file mode 100644 index 00000000000..c79df963a7b --- /dev/null +++ b/packages/bsky/src/hydration/hydrator.ts @@ -0,0 +1,726 @@ +import assert from 'assert' +import { mapDefined } from '@atproto/common' +import { AtUri } from '@atproto/syntax' +import { DataPlaneClient } from '../data-plane/client' +import { Notification } from '../proto/bsky_pb' +import { ids } from '../lexicon/lexicons' +import { isMain as isEmbedRecord } from '../lexicon/types/app/bsky/embed/record' +import { isMain as isEmbedRecordWithMedia } from '../lexicon/types/app/bsky/embed/recordWithMedia' +import { isListRule } from '../lexicon/types/app/bsky/feed/threadgate' +import { + ActorHydrator, + ProfileAggs, + Actors, + ProfileViewerStates, + ProfileViewerState, +} from './actor' +import { + Follows, + GraphHydrator, + ListItems, + ListViewerStates, + Lists, + RelationshipPair, +} from './graph' +import { LabelHydrator, Labels } from './label' +import { HydrationMap, RecordInfo, didFromUri, urisByCollection } from './util' +import { + FeedGenAggs, + FeedGens, + FeedGenViewerStates, + FeedHydrator, + Likes, + Post, + Posts, + Reposts, + PostAggs, + PostViewerStates, + Threadgates, + FeedItem, + ItemRef, +} from './feed' + +export type HydrationState = { + viewer?: string | null + actors?: Actors + profileViewers?: ProfileViewerStates + profileAggs?: ProfileAggs + posts?: Posts + postAggs?: PostAggs + postViewers?: PostViewerStates + postBlocks?: PostBlocks + reposts?: Reposts + follows?: Follows + followBlocks?: FollowBlocks + threadgates?: Threadgates + lists?: Lists + listViewers?: ListViewerStates + listItems?: ListItems + likes?: Likes + labels?: Labels + feedgens?: FeedGens + feedgenViewers?: FeedGenViewerStates + feedgenAggs?: FeedGenAggs +} + +export type PostBlock = { embed: boolean; reply: boolean } +export type PostBlocks = HydrationMap +type PostBlockPairs = { embed?: RelationshipPair; reply?: RelationshipPair } + +export type FollowBlock = boolean +export type FollowBlocks = HydrationMap + +export class Hydrator { + actor: ActorHydrator + feed: FeedHydrator + graph: GraphHydrator + label: LabelHydrator + + constructor( + public dataplane: DataPlaneClient, + public opts?: { labelsFromIssuerDids?: string[] }, + ) { + this.actor = new ActorHydrator(dataplane) + this.feed = new FeedHydrator(dataplane) + this.graph = new GraphHydrator(dataplane) + this.label = new LabelHydrator(dataplane, opts) + } + + // app.bsky.actor.defs#profileView + // - profile viewer + // - list basic + // Note: builds on the naive profile viewer hydrator and removes references to lists that have been deleted + async hydrateProfileViewers( + dids: string[], + viewer: string, + ): Promise { + const profileViewers = await this.actor.getProfileViewerStatesNaive( + dids, + viewer, + ) + const listUris: string[] = [] + profileViewers?.forEach((item) => { + listUris.push(...listUrisFromProfileViewer(item)) + }) + const listState = await this.hydrateListsBasic(listUris, viewer) + // if a list no longer exists or is not a mod list, then remove from viewer state + profileViewers?.forEach((item) => { + removeNonModListsFromProfileViewer(item, listState) + }) + return mergeStates(listState, { + profileViewers, + viewer, + }) + } + + // app.bsky.actor.defs#profileView + // - profile + // - list basic + async hydrateProfiles( + dids: string[], + viewer: string | null, + includeTakedowns = false, + ): Promise { + const [actors, labels, profileViewersState] = await Promise.all([ + this.actor.getActors(dids, includeTakedowns), + this.label.getLabelsForSubjects(labelSubjectsForDid(dids)), + viewer ? this.hydrateProfileViewers(dids, viewer) : undefined, + ]) + return mergeStates(profileViewersState ?? {}, { + actors, + labels, + viewer, + }) + } + + // app.bsky.actor.defs#profileViewBasic + // - profile basic + // - profile + // - list basic + async hydrateProfilesBasic( + dids: string[], + viewer: string | null, + includeTakedowns = false, + ): Promise { + return this.hydrateProfiles(dids, viewer, includeTakedowns) + } + + // app.bsky.actor.defs#profileViewDetailed + // - profile detailed + // - profile + // - list basic + async hydrateProfilesDetailed( + dids: string[], + viewer: string | null, + includeTakedowns = false, + ): Promise { + const [state, profileAggs] = await Promise.all([ + this.hydrateProfiles(dids, viewer, includeTakedowns), + this.actor.getProfileAggregates(dids), + ]) + return { + ...state, + profileAggs, + } + } + + // app.bsky.graph.defs#listView + // - list + // - profile basic + async hydrateLists( + uris: string[], + viewer: string | null, + ): Promise { + const [listsState, profilesState] = await Promise.all([ + await this.hydrateListsBasic(uris, viewer), + await this.hydrateProfilesBasic(uris.map(didFromUri), viewer), + ]) + return mergeStates(listsState, profilesState) + } + + // app.bsky.graph.defs#listViewBasic + // - list basic + async hydrateListsBasic( + uris: string[], + viewer: string | null, + ): Promise { + const [lists, listViewers] = await Promise.all([ + this.graph.getLists(uris), + viewer ? this.graph.getListViewerStates(uris, viewer) : undefined, + ]) + return { lists, listViewers, viewer } + } + + // app.bsky.graph.defs#listItemView + // - list item + // - profile + // - list basic + async hydrateListItems( + uris: string[], + viewer: string | null, + ): Promise { + const listItems = await this.graph.getListItems(uris) + const dids: string[] = [] + listItems.forEach((item) => { + if (item) { + dids.push(item.record.subject) + } + }) + const profileState = await this.hydrateProfiles(dids, viewer) + return mergeStates(profileState, { listItems, viewer }) + } + + // app.bsky.feed.defs#postView + // - post + // - profile + // - list basic + // - list + // - profile + // - list basic + // - feedgen + // - profile + // - list basic + async hydratePosts( + refs: ItemRef[], + viewer: string | null, + includeTakedowns = false, + state: HydrationState = {}, + ): Promise { + const uris = refs.map((ref) => ref.uri) + const postsLayer0 = await this.feed.getPosts( + uris, + includeTakedowns, + state.posts, + ) + // first level embeds plus thread roots we haven't fetched yet + const urisLayer1 = nestedRecordUrisFromPosts(postsLayer0) + const additionalRootUris = rootUrisFromPosts(postsLayer0) // supports computing threadgates + const urisLayer1ByCollection = urisByCollection(urisLayer1) + const postUrisLayer1 = urisLayer1ByCollection.get(ids.AppBskyFeedPost) ?? [] + const postsLayer1 = await this.feed.getPosts( + [...postUrisLayer1, ...additionalRootUris], + includeTakedowns, + ) + // second level embeds, ignoring any additional root uris we mixed-in to the previous layer + const urisLayer2 = nestedRecordUrisFromPosts(postsLayer1, postUrisLayer1) + const urisLayer2ByCollection = urisByCollection(urisLayer2) + const postUrisLayer2 = urisLayer2ByCollection.get(ids.AppBskyFeedPost) ?? [] + const threadRootUris = new Set() + for (const [uri, post] of postsLayer0) { + if (post) { + threadRootUris.add(rootUriFromPost(post) ?? uri) + } + } + const [postsLayer2, threadgates] = await Promise.all([ + this.feed.getPosts(postUrisLayer2, includeTakedowns), + this.feed.getThreadgatesForPosts([...threadRootUris.values()]), + ]) + // collect list/feedgen embeds, lists in threadgates, post record hydration + const gateListUris = getListUrisFromGates(threadgates) + const nestedListUris = [ + ...(urisLayer1ByCollection.get(ids.AppBskyGraphList) ?? []), + ...(urisLayer2ByCollection.get(ids.AppBskyGraphList) ?? []), + ] + const nestedFeedGenUris = [ + ...(urisLayer1ByCollection.get(ids.AppBskyFeedGenerator) ?? []), + ...(urisLayer2ByCollection.get(ids.AppBskyFeedGenerator) ?? []), + ] + const posts = + mergeManyMaps(postsLayer0, postsLayer1, postsLayer2) ?? postsLayer0 + const allPostUris = [...posts.keys()] + const [ + postAggs, + postViewers, + labels, + postBlocks, + profileState, + listState, + feedGenState, + ] = await Promise.all([ + this.feed.getPostAggregates(refs), + viewer ? this.feed.getPostViewerStates(refs, viewer) : undefined, + this.label.getLabelsForSubjects(allPostUris), + this.hydratePostBlocks(posts), + this.hydrateProfiles( + allPostUris.map(didFromUri), + viewer, + includeTakedowns, + ), + this.hydrateLists([...nestedListUris, ...gateListUris], viewer), + this.hydrateFeedGens(nestedFeedGenUris, viewer), + ]) + // combine all hydration state + return mergeManyStates(profileState, listState, feedGenState, { + posts, + postAggs, + postViewers, + postBlocks, + labels, + threadgates, + viewer, + }) + } + + private async hydratePostBlocks(posts: Posts): Promise { + const postBlocks = new HydrationMap() + const postBlocksPairs = new Map() + const relationships: RelationshipPair[] = [] + for (const [uri, item] of posts) { + if (!item) continue + const post = item.record + const creator = didFromUri(uri) + const postBlockPairs: PostBlockPairs = {} + postBlocksPairs.set(uri, postBlockPairs) + // 3p block for replies + const parentUri = post.reply?.parent.uri + const parentDid = parentUri && didFromUri(parentUri) + if (parentDid) { + const pair: RelationshipPair = [creator, parentDid] + relationships.push(pair) + postBlockPairs.reply = pair + } + // 3p block for record embeds + for (const embedUri of nestedRecordUris(post)) { + const pair: RelationshipPair = [creator, didFromUri(embedUri)] + relationships.push(pair) + postBlockPairs.embed = pair + } + } + // replace embed/reply pairs with block state + const blocks = await this.graph.getBidirectionalBlocks(relationships) + for (const [uri, { embed, reply }] of postBlocksPairs) { + postBlocks.set(uri, { + embed: !!embed && blocks.isBlocked(...embed), + reply: !!reply && blocks.isBlocked(...reply), + }) + } + return postBlocks + } + + // app.bsky.feed.defs#feedViewPost + // - post (+ replies) + // - profile + // - list basic + // - list + // - profile + // - list basic + // - feedgen + // - profile + // - list basic + // - repost + // - profile + // - list basic + // - post + // - ... + async hydrateFeedItems( + items: FeedItem[], + viewer: string | null, + includeTakedowns = false, + ): Promise { + const postUris = items.map((item) => item.post.uri) + const repostUris = mapDefined(items, (item) => item.repost?.uri) + const [posts, reposts, repostProfileState] = await Promise.all([ + this.feed.getPosts(postUris, includeTakedowns), + this.feed.getReposts(repostUris, includeTakedowns), + this.hydrateProfiles( + repostUris.map(didFromUri), + viewer, + includeTakedowns, + ), + ]) + const postAndReplyRefs: ItemRef[] = [] + posts.forEach((post, uri) => { + if (!post) return + postAndReplyRefs.push({ uri, cid: post.cid }) + if (post.record.reply) { + postAndReplyRefs.push(post.record.reply.root, post.record.reply.parent) + } + }) + const postState = await this.hydratePosts( + postAndReplyRefs, + viewer, + includeTakedowns, + { posts }, + ) + return mergeManyStates(postState, repostProfileState, { + reposts, + viewer, + }) + } + + // app.bsky.feed.defs#threadViewPost + // - post + // - profile + // - list basic + // - list + // - profile + // - list basic + // - feedgen + // - profile + // - list basic + async hydrateThreadPosts( + refs: ItemRef[], + viewer: string | null, + ): Promise { + return this.hydratePosts(refs, viewer) + } + + // app.bsky.feed.defs#generatorView + // - feedgen + // - profile + // - list basic + async hydrateFeedGens( + uris: string[], // @TODO any way to get refs here? + viewer: string | null, + ): Promise { + const [feedgens, feedgenAggs, feedgenViewers, profileState] = + await Promise.all([ + this.feed.getFeedGens(uris), + this.feed.getFeedGenAggregates(uris.map((uri) => ({ uri }))), + viewer ? this.feed.getFeedGenViewerStates(uris, viewer) : undefined, + this.hydrateProfiles(uris.map(didFromUri), viewer), + ]) + return mergeStates(profileState, { + feedgens, + feedgenAggs, + feedgenViewers, + viewer, + }) + } + + // app.bsky.feed.getLikes#like + // - like + // - profile + // - list basic + async hydrateLikes( + uris: string[], + viewer: string | null, + ): Promise { + const [likes, profileState] = await Promise.all([ + this.feed.getLikes(uris), + this.hydrateProfiles(uris.map(didFromUri), viewer), + ]) + return mergeStates(profileState, { likes, viewer }) + } + + // app.bsky.feed.getRepostedBy#repostedBy + // - repost + // - profile + // - list basic + async hydrateReposts(uris: string[], viewer: string | null) { + const [reposts, profileState] = await Promise.all([ + this.feed.getReposts(uris), + this.hydrateProfiles(uris.map(didFromUri), viewer), + ]) + return mergeStates(profileState, { reposts, viewer }) + } + + // app.bsky.notification.listNotifications#notification + // - notification + // - profile + // - list basic + async hydrateNotifications( + notifs: Notification[], + viewer: string | null, + ): Promise { + const uris = notifs.map((notif) => notif.uri) + const collections = urisByCollection(uris) + const postUris = collections.get(ids.AppBskyFeedPost) ?? [] + const likeUris = collections.get(ids.AppBskyFeedLike) ?? [] + const repostUris = collections.get(ids.AppBskyFeedRepost) ?? [] + const followUris = collections.get(ids.AppBskyGraphFollow) ?? [] + const [posts, likes, reposts, follows, labels, profileState] = + await Promise.all([ + this.feed.getPosts(postUris), // reason: mention, reply, quote + this.feed.getLikes(likeUris), // reason: like + this.feed.getReposts(repostUris), // reason: repost + this.graph.getFollows(followUris), // reason: follow + this.label.getLabelsForSubjects(uris), + this.hydrateProfiles(uris.map(didFromUri), viewer), + ]) + return mergeStates(profileState, { + posts, + likes, + reposts, + follows, + labels, + viewer, + }) + } + + // provides partial hydration state withing getFollows / getFollowers, mainly for applying rules + async hydrateFollows(uris: string[]): Promise { + const follows = await this.graph.getFollows(uris) + const pairs: RelationshipPair[] = [] + for (const [uri, follow] of follows) { + if (follow) { + pairs.push([didFromUri(uri), follow.record.subject]) + } + } + const blocks = await this.graph.getBidirectionalBlocks(pairs) + const followBlocks = new HydrationMap() + for (const [uri, follow] of follows) { + if (follow) { + followBlocks.set( + uri, + blocks.isBlocked(didFromUri(uri), follow.record.subject), + ) + } else { + followBlocks.set(uri, null) + } + } + return { follows, followBlocks } + } + + // ad-hoc record hydration + // in com.atproto.repo.getRecord + async getRecord( + uri: string, + includeTakedowns = false, + ): Promise> | undefined> { + const parsed = new AtUri(uri) + const collection = parsed.collection + if (collection === ids.AppBskyFeedPost) { + return ( + (await this.feed.getPosts([uri], includeTakedowns)).get(uri) ?? + undefined + ) + } else if (collection === ids.AppBskyFeedRepost) { + return ( + (await this.feed.getReposts([uri], includeTakedowns)).get(uri) ?? + undefined + ) + } else if (collection === ids.AppBskyFeedLike) { + return ( + (await this.feed.getLikes([uri], includeTakedowns)).get(uri) ?? + undefined + ) + } else if (collection === ids.AppBskyGraphFollow) { + return ( + (await this.graph.getFollows([uri], includeTakedowns)).get(uri) ?? + undefined + ) + } else if (collection === ids.AppBskyGraphList) { + return ( + (await this.graph.getLists([uri], includeTakedowns)).get(uri) ?? + undefined + ) + } else if (collection === ids.AppBskyGraphListitem) { + return ( + (await this.graph.getListItems([uri], includeTakedowns)).get(uri) ?? + undefined + ) + } else if (collection === ids.AppBskyGraphBlock) { + return ( + (await this.graph.getBlocks([uri], includeTakedowns)).get(uri) ?? + undefined + ) + } else if (collection === ids.AppBskyFeedGenerator) { + return ( + (await this.feed.getFeedGens([uri], includeTakedowns)).get(uri) ?? + undefined + ) + } else if (collection === ids.AppBskyActorProfile) { + const did = parsed.hostname + const actor = (await this.actor.getActors([did], includeTakedowns)).get( + did, + ) + if (!actor?.profile || !actor?.profileCid) return undefined + return { + record: actor.profile, + cid: actor.profileCid, + sortedAt: actor.sortedAt ?? new Date(0), // @NOTE will be present since profile record is present + takedownRef: actor.profileTakedownRef, + } + } + } +} + +const listUrisFromProfileViewer = (item: ProfileViewerState | null) => { + const listUris: string[] = [] + if (item?.mutedByList) { + listUris.push(item.mutedByList) + } + if (item?.blockingByList) { + listUris.push(item.blockingByList) + } + // blocked-by list does not appear in views, but will be used to evaluate the existence of a block between users. + if (item?.blockedByList) { + listUris.push(item.blockedByList) + } + return listUris +} + +const removeNonModListsFromProfileViewer = ( + item: ProfileViewerState | null, + state: HydrationState, +) => { + if (!isModList(item?.mutedByList, state)) { + delete item?.mutedByList + } + if (!isModList(item?.blockingByList, state)) { + delete item?.blockingByList + } + if (!isModList(item?.blockedByList, state)) { + delete item?.blockedByList + } +} + +const isModList = ( + listUri: string | undefined, + state: HydrationState, +): boolean => { + if (!listUri) return false + const list = state.lists?.get(listUri) + return list?.record.purpose === 'app.bsky.graph.defs#modlist' +} + +const labelSubjectsForDid = (dids: string[]) => { + return [ + ...dids, + ...dids.map((did) => + AtUri.make(did, ids.AppBskyActorProfile, 'self').toString(), + ), + ] +} + +const rootUrisFromPosts = (posts: Posts): string[] => { + const uris: string[] = [] + for (const item of posts.values()) { + const rootUri = item && rootUriFromPost(item) + if (rootUri) { + uris.push(rootUri) + } + } + return uris +} + +const rootUriFromPost = (post: Post): string | undefined => { + return post.record.reply?.root.uri +} + +const nestedRecordUrisFromPosts = ( + posts: Posts, + fromUris?: string[], +): string[] => { + const uris: string[] = [] + const postUris = fromUris ?? posts.keys() + for (const uri of postUris) { + const item = posts.get(uri) + if (item) { + uris.push(...nestedRecordUris(item.record)) + } + } + return uris +} + +const nestedRecordUris = (post: Post['record']): string[] => { + const uris: string[] = [] + if (!post?.embed) return uris + if (isEmbedRecord(post.embed)) { + uris.push(post.embed.record.uri) + } else if (isEmbedRecordWithMedia(post.embed)) { + uris.push(post.embed.record.record.uri) + } + return uris +} + +const getListUrisFromGates = (gates: Threadgates) => { + const uris: string[] = [] + for (const gate of gates.values()) { + const listRules = gate?.record.allow?.filter(isListRule) ?? [] + for (const rule of listRules) { + uris.push(rule.list) + } + } + return uris +} + +export const mergeStates = ( + stateA: HydrationState, + stateB: HydrationState, +): HydrationState => { + assert( + !stateA.viewer || !stateB.viewer || stateA.viewer === stateB.viewer, + 'incompatible viewers', + ) + return { + viewer: stateA.viewer ?? stateB.viewer, + actors: mergeMaps(stateA.actors, stateB.actors), + profileAggs: mergeMaps(stateA.profileAggs, stateB.profileAggs), + profileViewers: mergeMaps(stateA.profileViewers, stateB.profileViewers), + posts: mergeMaps(stateA.posts, stateB.posts), + postAggs: mergeMaps(stateA.postAggs, stateB.postAggs), + postViewers: mergeMaps(stateA.postViewers, stateB.postViewers), + postBlocks: mergeMaps(stateA.postBlocks, stateB.postBlocks), + reposts: mergeMaps(stateA.reposts, stateB.reposts), + follows: mergeMaps(stateA.follows, stateB.follows), + followBlocks: mergeMaps(stateA.followBlocks, stateB.followBlocks), + threadgates: mergeMaps(stateA.threadgates, stateB.threadgates), + lists: mergeMaps(stateA.lists, stateB.lists), + listViewers: mergeMaps(stateA.listViewers, stateB.listViewers), + listItems: mergeMaps(stateA.listItems, stateB.listItems), + likes: mergeMaps(stateA.likes, stateB.likes), + labels: mergeMaps(stateA.labels, stateB.labels), + feedgens: mergeMaps(stateA.feedgens, stateB.feedgens), + feedgenAggs: mergeMaps(stateA.feedgenAggs, stateB.feedgenAggs), + feedgenViewers: mergeMaps(stateA.feedgenViewers, stateB.feedgenViewers), + } +} + +const mergeMaps = ( + mapA?: HydrationMap, + mapB?: HydrationMap, +): HydrationMap | undefined => { + if (!mapA) return mapB + if (!mapB) return mapA + return mapA.merge(mapB) +} + +const mergeManyStates = (...states: HydrationState[]) => { + return states.reduce(mergeStates, {} as HydrationState) +} + +const mergeManyMaps = (...maps: HydrationMap[]) => { + return maps.reduce(mergeMaps, undefined as HydrationMap | undefined) +} diff --git a/packages/bsky/src/hydration/label.ts b/packages/bsky/src/hydration/label.ts new file mode 100644 index 00000000000..352c9ed4059 --- /dev/null +++ b/packages/bsky/src/hydration/label.ts @@ -0,0 +1,36 @@ +import { DataPlaneClient } from '../data-plane/client' +import { Label } from '../lexicon/types/com/atproto/label/defs' +import { HydrationMap, parseJsonBytes } from './util' + +export type { Label } from '../lexicon/types/com/atproto/label/defs' + +export type Labels = HydrationMap + +export class LabelHydrator { + constructor( + public dataplane: DataPlaneClient, + public opts?: { labelsFromIssuerDids?: string[] }, + ) {} + + async getLabelsForSubjects( + subjects: string[], + issuers?: string[], + ): Promise { + issuers = ([] as string[]) + .concat(issuers ?? []) + .concat(this.opts?.labelsFromIssuerDids ?? []) + if (!subjects.length || !issuers.length) return new HydrationMap() + const res = await this.dataplane.getLabels({ subjects, issuers }) + return res.labels.reduce((acc, cur) => { + const label = parseJsonBytes(cur) as Label | undefined + if (!label || label.neg) return acc + const entry = acc.get(label.uri) + if (entry) { + entry.push(label) + } else { + acc.set(label.uri, [label]) + } + return acc + }, new HydrationMap()) + } +} diff --git a/packages/bsky/src/hydration/util.ts b/packages/bsky/src/hydration/util.ts new file mode 100644 index 00000000000..675c0e9f3d0 --- /dev/null +++ b/packages/bsky/src/hydration/util.ts @@ -0,0 +1,125 @@ +import { AtUri } from '@atproto/syntax' +import { jsonToLex } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import * as ui8 from 'uint8arrays' +import { lexicons } from '../lexicon/lexicons' +import { Record } from '../proto/bsky_pb' + +export class HydrationMap extends Map { + merge(map: HydrationMap): HydrationMap { + map.forEach((val, key) => { + this.set(key, val) + }) + return this + } +} + +export type RecordInfo = { + record: T + cid: string + sortedAt: Date + takedownRef: string | undefined +} + +export const parseRecord = ( + entry: Record, + includeTakedowns: boolean, +): RecordInfo | undefined => { + if (!includeTakedowns && entry.takenDown) { + return undefined + } + const record = parseRecordBytes(entry.record) + const cid = entry.cid + const sortedAt = entry.sortedAt?.toDate() ?? new Date(0) + if (!record || !cid) return + if (!isValidRecord(record)) { + return + } + return { + record, + cid, + sortedAt, + takedownRef: safeTakedownRef(entry), + } +} + +const isValidRecord = (json: unknown) => { + const lexRecord = jsonToLex(json) + if (typeof lexRecord?.['$type'] !== 'string') { + return false + } + try { + lexicons.assertValidRecord(lexRecord['$type'], lexRecord) + return true + } catch { + return false + } +} + +// @NOTE not parsed into lex format, so will not match lexicon record types on CID and blob values. +export const parseRecordBytes = ( + bytes: Uint8Array | undefined, +): T | undefined => { + return parseJsonBytes(bytes) as T +} + +export const parseJsonBytes = ( + bytes: Uint8Array | undefined, +): JSON | undefined => { + if (!bytes || bytes.byteLength === 0) return + const parsed = JSON.parse(ui8.toString(bytes, 'utf8')) + return parsed ?? undefined +} + +export const parseString = (str: string | undefined): string | undefined => { + return str && str.length > 0 ? str : undefined +} + +export const parseCid = (cidStr: string | undefined): CID | undefined => { + if (!cidStr || cidStr.length === 0) return + try { + return CID.parse(cidStr) + } catch { + return + } +} + +export const didFromUri = (uri: string) => { + return new AtUri(uri).hostname +} + +export const urisByCollection = (uris: string[]): Map => { + const result = new Map() + for (const uri of uris) { + const collection = new AtUri(uri).collection + const items = result.get(collection) ?? [] + items.push(uri) + result.set(collection, items) + } + return result +} + +export const split = ( + items: T[], + predicate: (item: T) => boolean, +): [T[], T[]] => { + const yes: T[] = [] + const no: T[] = [] + for (const item of items) { + if (predicate(item)) { + yes.push(item) + } else { + no.push(item) + } + } + return [yes, no] +} + +export const safeTakedownRef = (obj?: { + takenDown: boolean + takedownRef: string +}): string | undefined => { + if (!obj) return + if (obj.takedownRef) return obj.takedownRef + if (obj.takenDown) return 'BSKY-TAKEDOWN-UNKNOWN' +} diff --git a/packages/bsky/src/image/uri.ts b/packages/bsky/src/image/uri.ts index 5e288e29d10..3de769178ed 100644 --- a/packages/bsky/src/image/uri.ts +++ b/packages/bsky/src/image/uri.ts @@ -1,4 +1,3 @@ -import { CID } from 'multiformats/cid' import { Options } from './util' // @NOTE if there are any additions here, ensure to include them on ImageUriBuilder.presets @@ -20,7 +19,7 @@ export class ImageUriBuilder { 'feed_fullsize', ] - getPresetUri(id: ImagePreset, did: string, cid: string | CID): string { + getPresetUri(id: ImagePreset, did: string, cid: string): string { const options = presets[id] if (!options) { throw new Error(`Unrecognized requested common uri type: ${id}`) @@ -30,14 +29,14 @@ export class ImageUriBuilder { ImageUriBuilder.getPath({ preset: id, did, - cid: typeof cid === 'string' ? CID.parse(cid) : cid, + cid, }) ) } static getPath(opts: { preset: ImagePreset } & BlobLocation) { const { format } = presets[opts.preset] - return `/${opts.preset}/plain/${opts.did}/${opts.cid.toString()}@${format}` + return `/${opts.preset}/plain/${opts.did}/${opts.cid}@${format}` } static getOptions( @@ -59,14 +58,14 @@ export class ImageUriBuilder { return { ...presets[preset], did, - cid: CID.parse(cid), + cid, preset, format, } } } -type BlobLocation = { cid: CID; did: string } +type BlobLocation = { cid: string; did: string } export class BadPathError extends Error {} diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 8a6bb6592e6..a968d85b584 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -5,53 +5,36 @@ import events from 'events' import { createHttpTerminator, HttpTerminator } from 'http-terminator' import cors from 'cors' import compression from 'compression' +import AtpAgent from '@atproto/api' import { IdResolver } from '@atproto/identity' -import { - RateLimiter, - RateLimiterOpts, - Options as XrpcServerOptions, -} from '@atproto/xrpc-server' -import { MINUTE } from '@atproto/common' import API, { health, wellKnown, blobResolver } from './api' -import { DatabaseCoordinator } from './db' import * as error from './error' -import { dbLogger, loggerMiddleware } from './logger' +import { 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 DidRedisCache from './did-cache' -import { - ImageInvalidator, - ImageProcessingServerInvalidator, -} from './image/invalidator' -import { BackgroundQueue } from './background' -import { AtpAgent } from '@atproto/api' import { Keypair } from '@atproto/crypto' -import { Redis } from './redis' +import { createDataPlaneClient } from './data-plane/client' +import { Hydrator } from './hydration/hydrator' +import { Views } from './views' import { AuthVerifier } from './auth-verifier' import { authWithApiKey as bsyncAuth, createBsyncClient } from './bsync' import { authWithApiKey as courierAuth, createCourierClient } from './courier' +export * from './data-plane' export type { ServerConfigValues } from './config' export { ServerConfig } from './config' -export { Database, PrimaryDatabase, DatabaseCoordinator } from './db' +export { Database } from './data-plane/server/db' export { Redis } from './redis' -export { ViewMaintainer } from './db/views' export { AppContext } from './context' -export type { ImageInvalidator } from './image/invalidator' -export * from './daemon' -export * from './indexer' -export * from './ingester' export class BskyAppView { public ctx: AppContext public app: express.Application public server?: http.Server private terminator?: HttpTerminator - private dbStatsInterval: NodeJS.Timer constructor(opts: { ctx: AppContext; app: express.Application }) { this.ctx = opts.ctx @@ -59,151 +42,89 @@ export class BskyAppView { } static create(opts: { - db: DatabaseCoordinator - redis: Redis config: ServerConfig signingKey: Keypair - imgInvalidator?: ImageInvalidator }): BskyAppView { - const { db, redis, config, signingKey } = opts - let maybeImgInvalidator = opts.imgInvalidator + const { config, signingKey } = opts const app = express() - app.set('trust proxy', true) app.use(cors()) app.use(loggerMiddleware) app.use(compression()) - const didCache = new DidRedisCache(redis.withNamespace('did-doc'), { - staleTTL: config.didCacheStaleTTL, - maxTTL: config.didCacheMaxTTL, - }) - + // used solely for handle resolution: identity lookups occur on dataplane const idResolver = new IdResolver({ plcUrl: config.didPlcUrl, - didCache, backupNameservers: config.handleResolveNameservers, }) const imgUriBuilder = new ImageUriBuilder( - config.imgUriEndpoint || `${config.publicUrl}/img`, + config.cdnUrl || `${config.publicUrl}/img`, ) let imgProcessingServer: ImageProcessingServer | undefined - if (!config.imgUriEndpoint) { + if (!config.cdnUrl) { const imgProcessingCache = new BlobDiskCache(config.blobCacheLocation) imgProcessingServer = new ImageProcessingServer( config, imgProcessingCache, ) - maybeImgInvalidator ??= new ImageProcessingServerInvalidator( - imgProcessingCache, - ) - } - - let imgInvalidator: ImageInvalidator - if (maybeImgInvalidator) { - imgInvalidator = maybeImgInvalidator - } else { - throw new Error('Missing appview image invalidator') } - const backgroundQueue = new BackgroundQueue(db.getPrimary()) - - const searchAgent = config.searchEndpoint - ? new AtpAgent({ service: config.searchEndpoint }) + const searchAgent = config.searchUrl + ? new AtpAgent({ service: config.searchUrl }) : undefined + const dataplane = createDataPlaneClient(config.dataplaneUrls, { + httpVersion: config.dataplaneHttpVersion, + rejectUnauthorized: !config.dataplaneIgnoreBadTls, + }) + const hydrator = new Hydrator(dataplane, { + labelsFromIssuerDids: config.labelsFromIssuerDids, + }) + const views = new Views(imgUriBuilder) - const services = createServices({ - imgUriBuilder, - imgInvalidator, - labelCacheOpts: { - redis: redis.withNamespace('label'), - staleTTL: config.labelCacheStaleTTL, - maxTTL: config.labelCacheMaxTTL, - }, + const bsyncClient = createBsyncClient({ + baseUrl: config.bsyncUrl, + httpVersion: config.bsyncHttpVersion ?? '2', + nodeOptions: { rejectUnauthorized: !config.bsyncIgnoreBadTls }, + interceptors: config.bsyncApiKey ? [bsyncAuth(config.bsyncApiKey)] : [], + }) + + const courierClient = createCourierClient({ + baseUrl: config.courierUrl, + httpVersion: config.courierHttpVersion ?? '2', + nodeOptions: { rejectUnauthorized: !config.courierIgnoreBadTls }, + interceptors: config.courierApiKey + ? [courierAuth(config.courierApiKey)] + : [], }) - const authVerifier = new AuthVerifier(idResolver, { + const authVerifier = new AuthVerifier(dataplane, { ownDid: config.serverDid, adminDid: config.modServiceDid, - adminPass: config.adminPassword, - moderatorPass: config.moderatorPassword, - triagePass: config.triagePassword, + adminPasses: config.adminPasswords, }) - const bsyncClient = config.bsyncUrl - ? createBsyncClient({ - baseUrl: config.bsyncUrl, - httpVersion: config.bsyncHttpVersion ?? '2', - nodeOptions: { rejectUnauthorized: !config.bsyncIgnoreBadTls }, - interceptors: config.bsyncApiKey - ? [bsyncAuth(config.bsyncApiKey)] - : [], - }) - : undefined - - const courierClient = config.courierUrl - ? createCourierClient({ - baseUrl: config.courierUrl, - httpVersion: config.courierHttpVersion ?? '2', - nodeOptions: { rejectUnauthorized: !config.courierIgnoreBadTls }, - interceptors: config.courierApiKey - ? [courierAuth(config.courierApiKey)] - : [], - }) - : undefined - const ctx = new AppContext({ - db, cfg: config, - services, - imgUriBuilder, + dataplane, + searchAgent, + hydrator, + views, signingKey, idResolver, - didCache, - redis, - backgroundQueue, - searchAgent, bsyncClient, courierClient, authVerifier, }) - const xrpcOpts: XrpcServerOptions = { + let server = createServer({ validateResponse: config.debugMode, payload: { jsonLimit: 100 * 1024, // 100kb textLimit: 100 * 1024, // 100kb blobLimit: 5 * 1024 * 1024, // 5mb }, - } - if (config.rateLimitsEnabled) { - const rlCreator = (opts: RateLimiterOpts) => - RateLimiter.redis(redis.driver, { - bypassSecret: config.rateLimitBypassKey, - bypassIps: config.rateLimitBypassIps, - ...opts, - }) - xrpcOpts['rateLimits'] = { - creator: rlCreator, - global: [ - { - name: 'global-unauthed-ip', - durationMs: 5 * MINUTE, - points: 3000, - calcKey: (ctx) => (ctx.auth ? null : ctx.req.ip), - }, - { - name: 'global-authed-did', - durationMs: 5 * MINUTE, - points: 3000, - calcKey: (ctx) => ctx.auth?.credentials?.did ?? null, - }, - ], - } - } - - let server = createServer(xrpcOpts) + }) server = API(server, ctx) @@ -220,38 +141,6 @@ export class BskyAppView { } async start(): Promise { - const { db, backgroundQueue } = this.ctx - const primary = db.getPrimary() - const replicas = db.getReplicas() - this.dbStatsInterval = setInterval(() => { - dbLogger.info( - { - 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', - ) - dbLogger.info( - { - runningCount: backgroundQueue.queue.pending, - waitingCount: backgroundQueue.queue.size, - }, - 'background queue stats', - ) - }, 10000) const server = this.app.listen(this.ctx.cfg.port) this.server = server server.keepAliveTimeout = 90000 @@ -262,13 +151,8 @@ export class BskyAppView { return server } - async destroy(opts?: { skipDb: boolean; skipRedis: boolean }): Promise { - await this.ctx.didCache.destroy() + async destroy(): Promise { await this.terminator?.terminate() - await this.ctx.backgroundQueue.destroy() - if (!opts?.skipRedis) await this.ctx.redis.destroy() - 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 deleted file mode 100644 index 1a4c14ff85e..00000000000 --- a/packages/bsky/src/indexer/config.ts +++ /dev/null @@ -1,271 +0,0 @@ -import assert from 'assert' -import { DAY, HOUR, parseIntWithFallback } from '@atproto/common' - -export interface IndexerConfigValues { - version: string - serverDid: string - dbPostgresUrl: string - dbPostgresSchema?: string - redisHost?: string // either set redis host, or both sentinel name and hosts - redisSentinelName?: string - redisSentinelHosts?: string[] - redisPassword?: string - didPlcUrl: string - didCacheStaleTTL: number - didCacheMaxTTL: number - handleResolveNameservers?: string[] - hiveApiKey?: string - imgUriEndpoint?: string - labelerKeywords: Record - moderationPushUrl: string - courierUrl?: string - courierApiKey?: string - courierHttpVersion?: '1.1' | '2' - courierIgnoreBadTls?: boolean - indexerConcurrency?: number - indexerPartitionIds: number[] - indexerPartitionBatchSize?: number - indexerSubLockId?: number - indexerPort?: number - ingesterPartitionCount: number - indexerNamespace?: string - pushNotificationEndpoint?: string -} - -export class IndexerConfig { - constructor(private cfg: IndexerConfigValues) {} - - static readEnv(overrides?: Partial) { - const version = process.env.BSKY_VERSION || '0.0.0' - const serverDid = process.env.SERVER_DID || 'did:example:test' - const dbPostgresUrl = - overrides?.dbPostgresUrl || process.env.DB_PRIMARY_POSTGRES_URL - const dbPostgresSchema = - 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 handleResolveNameservers = process.env.HANDLE_RESOLVE_NAMESERVERS - ? process.env.HANDLE_RESOLVE_NAMESERVERS.split(',') - : [] - const moderationPushUrl = - overrides?.moderationPushUrl || - process.env.MODERATION_PUSH_URL || - undefined - assert(moderationPushUrl) - const courierUrl = - overrides?.courierUrl || process.env.BSKY_COURIER_URL || undefined - const courierApiKey = - overrides?.courierApiKey || process.env.BSKY_COURIER_API_KEY || undefined - const courierHttpVersion = - overrides?.courierHttpVersion || - process.env.BSKY_COURIER_HTTP_VERSION || - '2' - const courierIgnoreBadTls = - overrides?.courierIgnoreBadTls || - process.env.BSKY_COURIER_IGNORE_BAD_TLS === 'true' - assert(courierHttpVersion === '1.1' || courierHttpVersion === '2') - const hiveApiKey = process.env.HIVE_API_KEY || undefined - const imgUriEndpoint = process.env.IMG_URI_ENDPOINT - 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 indexerPort = maybeParseInt(process.env.INDEXER_PORT) - 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) - return new IndexerConfig({ - version, - serverDid, - dbPostgresUrl, - dbPostgresSchema, - redisHost, - redisSentinelName, - redisSentinelHosts, - redisPassword, - didPlcUrl, - didCacheStaleTTL, - didCacheMaxTTL, - handleResolveNameservers, - moderationPushUrl, - courierUrl, - courierApiKey, - courierHttpVersion, - courierIgnoreBadTls, - hiveApiKey, - imgUriEndpoint, - indexerPartitionIds, - indexerConcurrency, - indexerPartitionBatchSize, - indexerNamespace, - indexerSubLockId, - indexerPort, - ingesterPartitionCount, - labelerKeywords, - pushNotificationEndpoint, - ...stripUndefineds(overrides ?? {}), - }) - } - - get version() { - return this.cfg.version - } - - get serverDid() { - return this.cfg.serverDid - } - - 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 handleResolveNameservers() { - return this.cfg.handleResolveNameservers - } - - get moderationPushUrl() { - return this.cfg.moderationPushUrl - } - - get courierUrl() { - return this.cfg.courierUrl - } - - get courierApiKey() { - return this.cfg.courierApiKey - } - - get courierHttpVersion() { - return this.cfg.courierHttpVersion - } - - get courierIgnoreBadTls() { - return this.cfg.courierIgnoreBadTls - } - - get hiveApiKey() { - return this.cfg.hiveApiKey - } - - get imgUriEndpoint() { - return this.cfg.imgUriEndpoint - } - - 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 indexerPort() { - return this.cfg.indexerPort - } - - get ingesterPartitionCount() { - return this.cfg.ingesterPartitionCount - } - - get labelerKeywords() { - return this.cfg.labelerKeywords - } - - get pushNotificationEndpoint() { - return this.cfg.pushNotificationEndpoint - } -} - -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 deleted file mode 100644 index a4c1f1f2ea0..00000000000 --- a/packages/bsky/src/indexer/context.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { IdResolver } from '@atproto/identity' -import { PrimaryDatabase } from '../db' -import { IndexerConfig } from './config' -import { Services } from './services' -import { BackgroundQueue } from '../background' -import DidSqlCache from '../did-cache' -import { Redis } from '../redis' -import { AutoModerator } from '../auto-moderator' -import { NotificationServer } from '../notifications' - -export class IndexerContext { - constructor( - private opts: { - db: PrimaryDatabase - redis: Redis - redisCache: Redis - cfg: IndexerConfig - services: Services - idResolver: IdResolver - didCache: DidSqlCache - backgroundQueue: BackgroundQueue - autoMod: AutoModerator - notifServer?: NotificationServer - }, - ) {} - - get db(): PrimaryDatabase { - return this.opts.db - } - - get redis(): Redis { - return this.opts.redis - } - - get redisCache(): Redis { - return this.opts.redisCache - } - - 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 - } - - get autoMod(): AutoModerator { - return this.opts.autoMod - } - - get notifServer(): NotificationServer | undefined { - return this.opts.notifServer - } -} - -export default IndexerContext diff --git a/packages/bsky/src/indexer/index.ts b/packages/bsky/src/indexer/index.ts deleted file mode 100644 index 7d012304573..00000000000 --- a/packages/bsky/src/indexer/index.ts +++ /dev/null @@ -1,166 +0,0 @@ -import express from 'express' -import { IdResolver } from '@atproto/identity' -import { BackgroundQueue } from '../background' -import { PrimaryDatabase } from '../db' -import DidRedisCache 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 { AutoModerator } from '../auto-moderator' -import { Redis } from '../redis' -import { - CourierNotificationServer, - GorushNotificationServer, - NotificationServer, -} from '../notifications' -import { CloseFn, createServer, startServer } from './server' -import { authWithApiKey as courierAuth, createCourierClient } from '../courier' - -export { IndexerConfig } from './config' -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 - app: express.Application - }) { - this.ctx = opts.ctx - this.sub = opts.sub - this.app = opts.app - } - - static create(opts: { - db: PrimaryDatabase - redis: Redis - redisCache: Redis - cfg: IndexerConfig - }): BskyIndexer { - const { db, redis, redisCache, cfg } = opts - const didCache = new DidRedisCache(redisCache.withNamespace('did-doc'), { - staleTTL: cfg.didCacheStaleTTL, - maxTTL: cfg.didCacheMaxTTL, - }) - const idResolver = new IdResolver({ - plcUrl: cfg.didPlcUrl, - didCache, - backupNameservers: cfg.handleResolveNameservers, - }) - const backgroundQueue = new BackgroundQueue(db) - - const autoMod = new AutoModerator({ - db, - idResolver, - cfg, - backgroundQueue, - }) - - const courierClient = cfg.courierUrl - ? createCourierClient({ - baseUrl: cfg.courierUrl, - httpVersion: cfg.courierHttpVersion ?? '2', - nodeOptions: { rejectUnauthorized: !cfg.courierIgnoreBadTls }, - interceptors: cfg.courierApiKey - ? [courierAuth(cfg.courierApiKey)] - : [], - }) - : undefined - - let notifServer: NotificationServer | undefined - if (courierClient) { - notifServer = new CourierNotificationServer(db, courierClient) - } else if (cfg.pushNotificationEndpoint) { - notifServer = new GorushNotificationServer( - db, - cfg.pushNotificationEndpoint, - ) - } - - const services = createServices({ - idResolver, - autoMod, - backgroundQueue, - notifServer, - }) - const ctx = new IndexerContext({ - db, - redis, - redisCache, - cfg, - services, - idResolver, - didCache, - backgroundQueue, - autoMod, - notifServer, - }) - const sub = new IndexerSubscription(ctx, { - partitionIds: cfg.indexerPartitionIds, - partitionBatchSize: cfg.indexerPartitionBatchSize, - concurrency: cfg.indexerConcurrency, - subLockId: cfg.indexerSubLockId, - }) - - const app = createServer(sub, cfg) - - return new BskyIndexer({ ctx, sub, app }) - } - - async start() { - const { db, backgroundQueue } = this.ctx - const pool = db.pool - this.dbStatsInterval = setInterval(() => { - dbLogger.info( - { - idleCount: pool.idleCount, - totalCount: pool.totalCount, - waitingCount: pool.waitingCount, - }, - 'db pool stats', - ) - dbLogger.info( - { - runningCount: backgroundQueue.queue.pending, - waitingCount: backgroundQueue.queue.size, - }, - 'background queue 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() - 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) - await this.ctx.didCache.destroy() - if (!opts?.skipRedis) await this.ctx.redis.destroy() - if (!opts?.skipRedis) await this.ctx.redisCache.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 deleted file mode 100644 index 45752727f99..00000000000 --- a/packages/bsky/src/indexer/logger.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { subsystemLogger } from '@atproto/common' - -const logger: ReturnType = - subsystemLogger('bsky:indexer') - -export default logger diff --git a/packages/bsky/src/indexer/server.ts b/packages/bsky/src/indexer/server.ts deleted file mode 100644 index dfafb741eb4..00000000000 --- a/packages/bsky/src/indexer/server.ts +++ /dev/null @@ -1,46 +0,0 @@ -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') - } - await 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/services.ts b/packages/bsky/src/indexer/services.ts deleted file mode 100644 index df173352046..00000000000 --- a/packages/bsky/src/indexer/services.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { IdResolver } from '@atproto/identity' -import { PrimaryDatabase } from '../db' -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 - autoMod: AutoModerator - backgroundQueue: BackgroundQueue - notifServer?: NotificationServer -}): Services { - const { idResolver, autoMod, backgroundQueue, notifServer } = resources - return { - indexing: IndexingService.creator( - idResolver, - autoMod, - backgroundQueue, - notifServer, - ), - label: LabelService.creator(null), - } -} - -export type Services = { - indexing: FromDbPrimary - label: FromDbPrimary -} - -type FromDbPrimary = (db: PrimaryDatabase) => T diff --git a/packages/bsky/src/ingester/config.ts b/packages/bsky/src/ingester/config.ts deleted file mode 100644 index 0a3d9e79e5a..00000000000 --- a/packages/bsky/src/ingester/config.ts +++ /dev/null @@ -1,182 +0,0 @@ -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 - labelProvider?: string - bsyncUrl?: string - bsyncApiKey?: string - bsyncHttpVersion?: '1.1' | '2' - bsyncIgnoreBadTls?: boolean - 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_PRIMARY_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 labelProvider = overrides?.labelProvider || process.env.LABEL_PROVIDER - const bsyncUrl = - overrides?.bsyncUrl || process.env.BSKY_BSYNC_URL || undefined - const bsyncApiKey = - overrides?.bsyncApiKey || process.env.BSKY_BSYNC_API_KEY || undefined - const bsyncHttpVersion = - overrides?.bsyncHttpVersion || process.env.BSKY_BSYNC_HTTP_VERSION || '2' - const bsyncIgnoreBadTls = - overrides?.bsyncIgnoreBadTls || - process.env.BSKY_BSYNC_IGNORE_BAD_TLS === 'true' - assert(bsyncHttpVersion === '1.1' || bsyncHttpVersion === '2') - 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, - labelProvider, - bsyncUrl, - bsyncApiKey, - bsyncHttpVersion, - bsyncIgnoreBadTls, - 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 labelProvider() { - return this.cfg.labelProvider - } - - get bsyncUrl() { - return this.cfg.bsyncUrl - } - - get bsyncApiKey() { - return this.cfg.bsyncApiKey - } - - get bsyncHttpVersion() { - return this.cfg.bsyncHttpVersion - } - - get bsyncIgnoreBadTls() { - return this.cfg.bsyncIgnoreBadTls - } - - 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 deleted file mode 100644 index debf9843ea6..00000000000 --- a/packages/bsky/src/ingester/context.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { PrimaryDatabase } from '../db' -import { Redis } from '../redis' -import { IngesterConfig } from './config' -import { LabelSubscription } from './label-subscription' -import { MuteSubscription } from './mute-subscription' - -export class IngesterContext { - constructor( - private opts: { - db: PrimaryDatabase - redis: Redis - cfg: IngesterConfig - labelSubscription?: LabelSubscription - muteSubscription?: MuteSubscription - }, - ) {} - - get db(): PrimaryDatabase { - return this.opts.db - } - - get redis(): Redis { - return this.opts.redis - } - - get cfg(): IngesterConfig { - return this.opts.cfg - } - - get labelSubscription(): LabelSubscription | undefined { - return this.opts.labelSubscription - } - - get muteSubscription(): MuteSubscription | undefined { - return this.opts.muteSubscription - } -} - -export default IngesterContext diff --git a/packages/bsky/src/ingester/index.ts b/packages/bsky/src/ingester/index.ts deleted file mode 100644 index 76225f13d38..00000000000 --- a/packages/bsky/src/ingester/index.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { PrimaryDatabase } 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' -import { authWithApiKey, createBsyncClient } from '../bsync' -import { LabelSubscription } from './label-subscription' -import { MuteSubscription } from './mute-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: PrimaryDatabase - redis: Redis - cfg: IngesterConfig - }): BskyIngester { - const { db, redis, cfg } = opts - const bsyncClient = cfg.bsyncUrl - ? createBsyncClient({ - baseUrl: cfg.bsyncUrl, - httpVersion: cfg.bsyncHttpVersion ?? '2', - nodeOptions: { rejectUnauthorized: !cfg.bsyncIgnoreBadTls }, - interceptors: cfg.bsyncApiKey - ? [authWithApiKey(cfg.bsyncApiKey)] - : [], - }) - : undefined - const labelSubscription = cfg.labelProvider - ? new LabelSubscription(db, cfg.labelProvider) - : undefined - const muteSubscription = bsyncClient - ? new MuteSubscription(db, redis, bsyncClient) - : undefined - const ctx = new IngesterContext({ - db, - redis, - cfg, - labelSubscription, - muteSubscription, - }) - 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.pool - 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) - await this.ctx.labelSubscription?.start() - await this.ctx.muteSubscription?.start() - this.sub.run() - return this - } - - async destroy(opts?: { skipDb: boolean }): Promise { - await this.ctx.muteSubscription?.destroy() - await this.ctx.labelSubscription?.destroy() - 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/label-subscription.ts b/packages/bsky/src/ingester/label-subscription.ts deleted file mode 100644 index d486473cf98..00000000000 --- a/packages/bsky/src/ingester/label-subscription.ts +++ /dev/null @@ -1,76 +0,0 @@ -import AtpAgent from '@atproto/api' -import { PrimaryDatabase } from '../db' -import { sql } from 'kysely' -import { dbLogger } from '../logger' -import { SECOND } from '@atproto/common' - -export class LabelSubscription { - destroyed = false - promise: Promise = Promise.resolve() - timer: NodeJS.Timer | undefined - lastLabel: number | undefined - labelAgent: AtpAgent - - constructor(public db: PrimaryDatabase, public labelProvider: string) { - this.labelAgent = new AtpAgent({ service: labelProvider }) - } - - async start() { - const res = await this.db.db - .selectFrom('label') - .select('cts') - .orderBy('cts', 'desc') - .limit(1) - .executeTakeFirst() - this.lastLabel = res ? new Date(res.cts).getTime() : undefined - this.poll() - } - - poll() { - if (this.destroyed) return - this.promise = this.fetchLabels() - .catch((err) => - dbLogger.error({ err }, 'failed to fetch and store labels'), - ) - .finally(() => { - this.timer = setTimeout(() => this.poll(), SECOND) - }) - } - - async fetchLabels() { - const res = await this.labelAgent.api.com.atproto.temp.fetchLabels({ - since: this.lastLabel, - }) - const last = res.data.labels.at(-1) - if (!last) { - return - } - const dbVals = res.data.labels.map((l) => ({ - ...l, - cid: l.cid ?? '', - neg: l.neg ?? false, - })) - const { ref } = this.db.db.dynamic - const excluded = (col: string) => ref(`excluded.${col}`) - await this.db - .asPrimary() - .db.insertInto('label') - .values(dbVals) - .onConflict((oc) => - oc.columns(['src', 'uri', 'cid', 'val']).doUpdateSet({ - neg: sql`${excluded('neg')}`, - cts: sql`${excluded('cts')}`, - }), - ) - .execute() - this.lastLabel = new Date(last.cts).getTime() - } - - async destroy() { - this.destroyed = true - if (this.timer) { - clearTimeout(this.timer) - } - await this.promise - } -} diff --git a/packages/bsky/src/ingester/logger.ts b/packages/bsky/src/ingester/logger.ts deleted file mode 100644 index 49855166481..00000000000 --- a/packages/bsky/src/ingester/logger.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { subsystemLogger } from '@atproto/common' - -const logger: ReturnType = - subsystemLogger('bsky:ingester') - -export default logger diff --git a/packages/bsky/src/ingester/mute-subscription.ts b/packages/bsky/src/ingester/mute-subscription.ts deleted file mode 100644 index 9adb685c160..00000000000 --- a/packages/bsky/src/ingester/mute-subscription.ts +++ /dev/null @@ -1,213 +0,0 @@ -import assert from 'node:assert' -import { PrimaryDatabase } from '../db' -import { Redis } from '../redis' -import { BsyncClient, Code, isBsyncError } from '../bsync' -import { MuteOperation, MuteOperation_Type } from '../proto/bsync_pb' -import logger from './logger' -import { wait } from '@atproto/common' -import { - AtUri, - InvalidDidError, - ensureValidAtUri, - ensureValidDid, -} from '@atproto/syntax' -import { ids } from '../lexicon/lexicons' - -const CURSOR_KEY = 'ingester:mute:cursor' - -export class MuteSubscription { - ac = new AbortController() - running: Promise | undefined - cursor: string | null = null - - constructor( - public db: PrimaryDatabase, - public redis: Redis, - public bsyncClient: BsyncClient, - ) {} - - async start() { - if (this.running) return - this.ac = new AbortController() - this.running = this.run() - .catch((err) => { - // allow this to cause an unhandled rejection, let deployment handle the crash. - logger.error({ err }, 'mute subscription crashed') - throw err - }) - .finally(() => (this.running = undefined)) - } - - private async run() { - this.cursor = await this.getCursor() - while (!this.ac.signal.aborted) { - try { - // get page of mute ops, long-polling - const page = await this.bsyncClient.scanMuteOperations( - { - limit: 100, - cursor: this.cursor ?? undefined, - }, - { signal: this.ac.signal }, - ) - if (!page.cursor) { - throw new BadResponseError('cursor is missing') - } - // process - const now = new Date() - for (const op of page.operations) { - if (this.ac.signal.aborted) return - if (op.type === MuteOperation_Type.ADD) { - await this.handleAddOp(op, now) - } else if (op.type === MuteOperation_Type.REMOVE) { - await this.handleRemoveOp(op) - } else if (op.type === MuteOperation_Type.CLEAR) { - await this.handleClearOp(op) - } else { - logger.warn( - { id: op.id, type: op.type }, - 'unknown mute subscription op type', - ) - } - } - // update cursor - await this.setCursor(page.cursor) - this.cursor = page.cursor - } catch (err) { - if (isBsyncError(err, Code.Canceled)) { - return // canceled, probably from destroy() - } - if (err instanceof BadResponseError) { - logger.warn({ err }, 'bad response from bsync') - } else { - logger.error({ err }, 'unexpected error processing mute subscription') - } - await wait(1000) // wait a second before trying again - } - } - } - - async handleAddOp(op: MuteOperation, createdAt: Date) { - assert(op.type === MuteOperation_Type.ADD) - if (!isValidDid(op.actorDid)) { - logger.warn({ id: op.id, type: op.type }, 'bad actor in mute op') - return - } - if (isValidDid(op.subject)) { - await this.db.db - .insertInto('mute') - .values({ - subjectDid: op.subject, - mutedByDid: op.actorDid, - createdAt: createdAt.toISOString(), - }) - .onConflict((oc) => oc.doNothing()) - .execute() - } else { - const listUri = isValidAtUri(op.subject) - ? new AtUri(op.subject) - : undefined - if (listUri?.collection !== ids.AppBskyGraphList) { - logger.warn({ id: op.id, type: op.type }, 'bad subject in mute op') - return - } - await this.db.db - .insertInto('list_mute') - .values({ - listUri: op.subject, - mutedByDid: op.actorDid, - createdAt: createdAt.toISOString(), - }) - .onConflict((oc) => oc.doNothing()) - .execute() - } - } - - async handleRemoveOp(op: MuteOperation) { - assert(op.type === MuteOperation_Type.REMOVE) - if (!isValidDid(op.actorDid)) { - logger.warn({ id: op.id, type: op.type }, 'bad actor in mute op') - return - } - if (isValidDid(op.subject)) { - await this.db.db - .deleteFrom('mute') - .where('subjectDid', '=', op.subject) - .where('mutedByDid', '=', op.actorDid) - .execute() - } else { - const listUri = isValidAtUri(op.subject) - ? new AtUri(op.subject) - : undefined - if (listUri?.collection !== ids.AppBskyGraphList) { - logger.warn({ id: op.id, type: op.type }, 'bad subject in mute op') - return - } - await this.db.db - .deleteFrom('list_mute') - .where('listUri', '=', op.subject) - .where('mutedByDid', '=', op.actorDid) - .execute() - } - } - - async handleClearOp(op: MuteOperation) { - assert(op.type === MuteOperation_Type.CLEAR) - if (!isValidDid(op.actorDid)) { - logger.warn({ id: op.id, type: op.type }, 'bad actor in mute op') - return - } - if (op.subject) { - logger.warn({ id: op.id, type: op.type }, 'bad subject in mute op') - return - } - await this.db.db - .deleteFrom('mute') - .where('mutedByDid', '=', op.actorDid) - .execute() - await this.db.db - .deleteFrom('list_mute') - .where('mutedByDid', '=', op.actorDid) - .execute() - } - - async getCursor(): Promise { - return await this.redis.get(CURSOR_KEY) - } - - async setCursor(cursor: string): Promise { - await this.redis.set(CURSOR_KEY, cursor) - } - - async destroy() { - this.ac.abort() - await this.running - } - - get destroyed() { - return this.ac.signal.aborted - } -} - -class BadResponseError extends Error {} - -const isValidDid = (did: string) => { - try { - ensureValidDid(did) - return true - } catch (err) { - if (err instanceof InvalidDidError) { - return false - } - throw err - } -} - -const isValidAtUri = (uri: string) => { - try { - ensureValidAtUri(uri) - return true - } catch { - return false - } -} diff --git a/packages/bsky/src/ingester/subscription.ts b/packages/bsky/src/ingester/subscription.ts deleted file mode 100644 index 14f301e07f9..00000000000 --- a/packages/bsky/src/ingester/subscription.ts +++ /dev/null @@ -1,288 +0,0 @@ -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/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index ab207718006..cf2c613e686 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -30,9 +30,14 @@ import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRep import * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' +import * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword' import * as ComAtprotoAdminUpdateCommunicationTemplate from './types/com/atproto/admin/updateCommunicationTemplate' import * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus' +import * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials' +import * as ComAtprotoIdentityRequestPlcOperationSignature from './types/com/atproto/identity/requestPlcOperationSignature' import * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle' +import * as ComAtprotoIdentitySignPlcOperation from './types/com/atproto/identity/signPlcOperation' +import * as ComAtprotoIdentitySubmitPlcOperation from './types/com/atproto/identity/submitPlcOperation' import * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle' import * as ComAtprotoLabelQueryLabels from './types/com/atproto/label/queryLabels' import * as ComAtprotoLabelSubscribeLabels from './types/com/atproto/label/subscribeLabels' @@ -42,19 +47,25 @@ import * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createReco import * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord' import * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRepo' import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord' +import * as ComAtprotoRepoImportRepo from './types/com/atproto/repo/importRepo' +import * as ComAtprotoRepoListMissingBlobs from './types/com/atproto/repo/listMissingBlobs' import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords' import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' +import * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount' +import * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus' import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword' import * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode' import * as ComAtprotoServerCreateInviteCodes from './types/com/atproto/server/createInviteCodes' import * as ComAtprotoServerCreateSession from './types/com/atproto/server/createSession' +import * as ComAtprotoServerDeactivateAccount from './types/com/atproto/server/deactivateAccount' import * as ComAtprotoServerDeleteAccount from './types/com/atproto/server/deleteAccount' import * as ComAtprotoServerDeleteSession from './types/com/atproto/server/deleteSession' import * as ComAtprotoServerDescribeServer from './types/com/atproto/server/describeServer' import * as ComAtprotoServerGetAccountInviteCodes from './types/com/atproto/server/getAccountInviteCodes' +import * as ComAtprotoServerGetServiceAuth from './types/com/atproto/server/getServiceAuth' import * as ComAtprotoServerGetSession from './types/com/atproto/server/getSession' import * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords' import * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession' @@ -80,10 +91,7 @@ import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCra import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' import * as ComAtprotoTempCheckSignupQueue from './types/com/atproto/temp/checkSignupQueue' import * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels' -import * as ComAtprotoTempImportRepo from './types/com/atproto/temp/importRepo' -import * as ComAtprotoTempPushBlob from './types/com/atproto/temp/pushBlob' import * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification' -import * as ComAtprotoTempTransferAccount from './types/com/atproto/temp/transferAccount' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' import * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles' @@ -437,6 +445,17 @@ export class ComAtprotoAdminNS { return this._server.xrpc.method(nsid, cfg) } + updateAccountPassword( + cfg: ConfigOf< + AV, + ComAtprotoAdminUpdateAccountPassword.Handler>, + ComAtprotoAdminUpdateAccountPassword.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.updateAccountPassword' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + updateCommunicationTemplate( cfg: ConfigOf< AV, @@ -467,6 +486,32 @@ export class ComAtprotoIdentityNS { this._server = server } + getRecommendedDidCredentials( + cfg: ConfigOf< + AV, + ComAtprotoIdentityGetRecommendedDidCredentials.Handler>, + ComAtprotoIdentityGetRecommendedDidCredentials.HandlerReqCtx< + ExtractAuth + > + >, + ) { + const nsid = 'com.atproto.identity.getRecommendedDidCredentials' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + requestPlcOperationSignature( + cfg: ConfigOf< + AV, + ComAtprotoIdentityRequestPlcOperationSignature.Handler>, + ComAtprotoIdentityRequestPlcOperationSignature.HandlerReqCtx< + ExtractAuth + > + >, + ) { + const nsid = 'com.atproto.identity.requestPlcOperationSignature' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + resolveHandle( cfg: ConfigOf< AV, @@ -478,6 +523,28 @@ export class ComAtprotoIdentityNS { return this._server.xrpc.method(nsid, cfg) } + signPlcOperation( + cfg: ConfigOf< + AV, + ComAtprotoIdentitySignPlcOperation.Handler>, + ComAtprotoIdentitySignPlcOperation.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.identity.signPlcOperation' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + submitPlcOperation( + cfg: ConfigOf< + AV, + ComAtprotoIdentitySubmitPlcOperation.Handler>, + ComAtprotoIdentitySubmitPlcOperation.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.identity.submitPlcOperation' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + updateHandle( cfg: ConfigOf< AV, @@ -601,6 +668,28 @@ export class ComAtprotoRepoNS { return this._server.xrpc.method(nsid, cfg) } + importRepo( + cfg: ConfigOf< + AV, + ComAtprotoRepoImportRepo.Handler>, + ComAtprotoRepoImportRepo.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.importRepo' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + listMissingBlobs( + cfg: ConfigOf< + AV, + ComAtprotoRepoListMissingBlobs.Handler>, + ComAtprotoRepoListMissingBlobs.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.listMissingBlobs' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + listRecords( cfg: ConfigOf< AV, @@ -642,6 +731,28 @@ export class ComAtprotoServerNS { this._server = server } + activateAccount( + cfg: ConfigOf< + AV, + ComAtprotoServerActivateAccount.Handler>, + ComAtprotoServerActivateAccount.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.activateAccount' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + checkAccountStatus( + cfg: ConfigOf< + AV, + ComAtprotoServerCheckAccountStatus.Handler>, + ComAtprotoServerCheckAccountStatus.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.checkAccountStatus' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + confirmEmail( cfg: ConfigOf< AV, @@ -708,6 +819,17 @@ export class ComAtprotoServerNS { return this._server.xrpc.method(nsid, cfg) } + deactivateAccount( + cfg: ConfigOf< + AV, + ComAtprotoServerDeactivateAccount.Handler>, + ComAtprotoServerDeactivateAccount.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.deactivateAccount' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + deleteAccount( cfg: ConfigOf< AV, @@ -752,6 +874,17 @@ export class ComAtprotoServerNS { return this._server.xrpc.method(nsid, cfg) } + getServiceAuth( + cfg: ConfigOf< + AV, + ComAtprotoServerGetServiceAuth.Handler>, + ComAtprotoServerGetServiceAuth.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.getServiceAuth' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getSession( cfg: ConfigOf< AV, @@ -1043,28 +1176,6 @@ export class ComAtprotoTempNS { return this._server.xrpc.method(nsid, cfg) } - importRepo( - cfg: ConfigOf< - AV, - ComAtprotoTempImportRepo.Handler>, - ComAtprotoTempImportRepo.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.temp.importRepo' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - - pushBlob( - cfg: ConfigOf< - AV, - ComAtprotoTempPushBlob.Handler>, - ComAtprotoTempPushBlob.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.temp.pushBlob' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - requestPhoneVerification( cfg: ConfigOf< AV, @@ -1075,17 +1186,6 @@ export class ComAtprotoTempNS { const nsid = 'com.atproto.temp.requestPhoneVerification' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } - - transferAccount( - cfg: ConfigOf< - AV, - ComAtprotoTempTransferAccount.Handler>, - ComAtprotoTempTransferAccount.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.temp.transferAccount' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } } export class AppNS { diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 08a70f8ca1d..14e4c1cb81e 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -91,6 +91,7 @@ export const schemaDict = { 'lex:com.atproto.admin.defs#modEventEscalate', 'lex:com.atproto.admin.defs#modEventMute', 'lex:com.atproto.admin.defs#modEventEmail', + 'lex:com.atproto.admin.defs#modEventResolveAppeal', ], }, subject: { @@ -147,6 +148,7 @@ export const schemaDict = { 'lex:com.atproto.admin.defs#modEventAcknowledge', 'lex:com.atproto.admin.defs#modEventEscalate', 'lex:com.atproto.admin.defs#modEventMute', + 'lex:com.atproto.admin.defs#modEventEmail', 'lex:com.atproto.admin.defs#modEventResolveAppeal', ], }, @@ -301,6 +303,12 @@ export const schemaDict = { type: 'string', format: 'datetime', }, + tags: { + type: 'array', + items: { + type: 'string', + }, + }, }, }, reportViewDetail: { @@ -895,6 +903,33 @@ export const schemaDict = { }, }, }, + modEventTag: { + type: 'object', + description: 'Add/Remove a tag on a subject', + required: ['add', 'remove'], + properties: { + add: { + type: 'array', + items: { + type: 'string', + }, + description: + "Tags to be added to the subject. If already exists, won't be duplicated.", + }, + remove: { + type: 'array', + items: { + type: 'string', + }, + description: + "Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated.", + }, + comment: { + type: 'string', + description: 'Additional comment about added/removed tags.', + }, + }, + }, communicationTemplateView: { type: 'object', required: [ @@ -1073,6 +1108,7 @@ export const schemaDict = { 'lex:com.atproto.admin.defs#modEventReverseTakedown', 'lex:com.atproto.admin.defs#modEventUnmute', 'lex:com.atproto.admin.defs#modEventEmail', + 'lex:com.atproto.admin.defs#modEventTag', ], }, subject: { @@ -1450,6 +1486,16 @@ export const schemaDict = { description: 'Sort direction for the events. Defaults to descending order of created at timestamp.', }, + createdAfter: { + type: 'string', + format: 'datetime', + description: 'Retrieve events created after a given timestamp', + }, + createdBefore: { + type: 'string', + format: 'datetime', + description: 'Retrieve events created before a given timestamp', + }, subject: { type: 'string', format: 'uri', @@ -1466,6 +1512,53 @@ export const schemaDict = { maximum: 100, default: 50, }, + hasComment: { + type: 'boolean', + description: 'If true, only events with comments are returned', + }, + comment: { + type: 'string', + description: + 'If specified, only events with comments containing the keyword are returned', + }, + addedLabels: { + type: 'array', + items: { + type: 'string', + }, + description: + 'If specified, only events where all of these labels were added are returned', + }, + removedLabels: { + type: 'array', + items: { + type: 'string', + }, + description: + 'If specified, only events where all of these labels were removed are returned', + }, + addedTags: { + type: 'array', + items: { + type: 'string', + }, + description: + 'If specified, only events where all of these tags were added are returned', + }, + removedTags: { + type: 'array', + items: { + type: 'string', + }, + description: + 'If specified, only events where all of these tags were removed are returned', + }, + reportTypes: { + type: 'array', + items: { + type: 'string', + }, + }, cursor: { type: 'string', }, @@ -1577,6 +1670,18 @@ export const schemaDict = { maximum: 100, default: 50, }, + tags: { + type: 'array', + items: { + type: 'string', + }, + }, + excludeTags: { + type: 'array', + items: { + type: 'string', + }, + }, cursor: { type: 'string', }, @@ -1758,6 +1863,33 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminUpdateAccountPassword: { + lexicon: 1, + id: 'com.atproto.admin.updateAccountPassword', + defs: { + main: { + type: 'procedure', + description: + 'Update the password for a user account as an administrator.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'password'], + properties: { + did: { + type: 'string', + format: 'did', + }, + password: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminUpdateCommunicationTemplate: { lexicon: 1, id: 'com.atproto.admin.updateCommunicationTemplate', @@ -1863,13 +1995,63 @@ export const schemaDict = { }, }, }, + ComAtprotoIdentityGetRecommendedDidCredentials: { + lexicon: 1, + id: 'com.atproto.identity.getRecommendedDidCredentials', + defs: { + main: { + type: 'query', + description: + 'Describe the credentials that should be included in the DID doc of an account that is migrating to this service.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + rotationKeys: { + description: + 'Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.', + type: 'array', + items: { + type: 'string', + }, + }, + alsoKnownAs: { + type: 'array', + items: { + type: 'string', + }, + }, + verificationMethods: { + type: 'unknown', + }, + services: { + type: 'unknown', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoIdentityRequestPlcOperationSignature: { + lexicon: 1, + id: 'com.atproto.identity.requestPlcOperationSignature', + defs: { + main: { + type: 'procedure', + description: + 'Request an email with a code to in order to request a signed PLC operation. Requires Auth.', + }, + }, + }, ComAtprotoIdentityResolveHandle: { lexicon: 1, id: 'com.atproto.identity.resolveHandle', defs: { main: { type: 'query', - description: 'Provides the DID of a repo.', + description: 'Resolves a handle (domain name) to a DID.', parameters: { type: 'params', required: ['handle'], @@ -1897,13 +2079,92 @@ export const schemaDict = { }, }, }, + ComAtprotoIdentitySignPlcOperation: { + lexicon: 1, + id: 'com.atproto.identity.signPlcOperation', + defs: { + main: { + type: 'procedure', + description: + "Signs a PLC operation to update some value(s) in the requesting DID's document.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + token: { + description: + 'A token received through com.atproto.identity.requestPlcOperationSignature', + type: 'string', + }, + rotationKeys: { + type: 'array', + items: { + type: 'string', + }, + }, + alsoKnownAs: { + type: 'array', + items: { + type: 'string', + }, + }, + verificationMethods: { + type: 'unknown', + }, + services: { + type: 'unknown', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['operation'], + properties: { + operation: { + type: 'unknown', + description: 'A signed DID PLC operation.', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoIdentitySubmitPlcOperation: { + lexicon: 1, + id: 'com.atproto.identity.submitPlcOperation', + defs: { + main: { + type: 'procedure', + description: + "Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['operation'], + properties: { + operation: { + type: 'unknown', + }, + }, + }, + }, + }, + }, + }, ComAtprotoIdentityUpdateHandle: { lexicon: 1, id: 'com.atproto.identity.updateHandle', defs: { main: { type: 'procedure', - description: 'Updates the handle of the account.', + description: + "Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.", input: { encoding: 'application/json', schema: { @@ -1913,6 +2174,7 @@ export const schemaDict = { handle: { type: 'string', format: 'handle', + description: 'The new handle.', }, }, }, @@ -2003,7 +2265,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Find labels relevant to the provided URI patterns.', + description: + 'Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.', parameters: { type: 'params', required: ['uriPatterns'], @@ -2064,13 +2327,14 @@ export const schemaDict = { defs: { main: { type: 'subscription', - description: 'Subscribe to label updates.', + description: + 'Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.', parameters: { type: 'params', properties: { cursor: { type: 'integer', - description: 'The last known event to backfill from.', + description: 'The last known event seq number to backfill from.', }, }, }, @@ -2126,7 +2390,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Report a repo or a record.', + description: + 'Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.', input: { encoding: 'application/json', schema: { @@ -2135,10 +2400,14 @@ export const schemaDict = { properties: { reasonType: { type: 'ref', + description: + 'Indicates the broad category of violation the report is for.', ref: 'lex:com.atproto.moderation.defs#reasonType', }, reason: { type: 'string', + description: + 'Additional context about the content and violation.', }, subject: { type: 'union', @@ -2249,7 +2518,7 @@ export const schemaDict = { main: { type: 'procedure', description: - 'Apply a batch transaction of creates, updates, and deletes.', + 'Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.', input: { encoding: 'application/json', schema: { @@ -2259,12 +2528,14 @@ export const schemaDict = { repo: { type: 'string', format: 'at-identifier', - description: 'The handle or DID of the repo.', + description: + 'The handle or DID of the repo (aka, current account).', }, validate: { type: 'boolean', default: true, - description: 'Flag for validating the records.', + description: + "Can be set to 'false' to skip Lexicon schema validation of record data, for all operations.", }, writes: { type: 'array', @@ -2280,6 +2551,8 @@ export const schemaDict = { }, swapCommit: { type: 'string', + description: + 'If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.', format: 'cid', }, }, @@ -2288,12 +2561,14 @@ export const schemaDict = { errors: [ { name: 'InvalidSwap', + description: + "Indicates that the 'swapCommit' parameter did not match current commit.", }, ], }, create: { type: 'object', - description: 'Create a new record.', + description: 'Operation which creates a new record.', required: ['collection', 'value'], properties: { collection: { @@ -2311,7 +2586,7 @@ export const schemaDict = { }, update: { type: 'object', - description: 'Update an existing record.', + description: 'Operation which updates an existing record.', required: ['collection', 'rkey', 'value'], properties: { collection: { @@ -2328,7 +2603,7 @@ export const schemaDict = { }, delete: { type: 'object', - description: 'Delete an existing record.', + description: 'Operation which deletes an existing record.', required: ['collection', 'rkey'], properties: { collection: { @@ -2348,7 +2623,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Create a new record.', + description: + 'Create a single new repository record. Requires auth, implemented by PDS.', input: { encoding: 'application/json', schema: { @@ -2358,7 +2634,8 @@ export const schemaDict = { repo: { type: 'string', format: 'at-identifier', - description: 'The handle or DID of the repo.', + description: + 'The handle or DID of the repo (aka, current account).', }, collection: { type: 'string', @@ -2367,17 +2644,18 @@ export const schemaDict = { }, rkey: { type: 'string', - description: 'The key of the record.', + description: 'The Record Key.', maxLength: 15, }, validate: { type: 'boolean', default: true, - description: 'Flag for validating the record.', + description: + "Can be set to 'false' to skip Lexicon schema validation of record data.", }, record: { type: 'unknown', - description: 'The record to create.', + description: 'The record itself. Must contain a $type field.', }, swapCommit: { type: 'string', @@ -2408,6 +2686,8 @@ export const schemaDict = { errors: [ { name: 'InvalidSwap', + description: + "Indicates that 'swapCommit' didn't match current repo commit.", }, ], }, @@ -2419,7 +2699,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: "Delete a record, or ensure it doesn't exist.", + description: + "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.", input: { encoding: 'application/json', schema: { @@ -2429,7 +2710,8 @@ export const schemaDict = { repo: { type: 'string', format: 'at-identifier', - description: 'The handle or DID of the repo.', + description: + 'The handle or DID of the repo (aka, current account).', }, collection: { type: 'string', @@ -2438,7 +2720,7 @@ export const schemaDict = { }, rkey: { type: 'string', - description: 'The key of the record.', + description: 'The Record Key.', }, swapRecord: { type: 'string', @@ -2470,7 +2752,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Get information about the repo, including the list of collections.', + 'Get information about an account and repository, including the list of collections. Does not require auth.', parameters: { type: 'params', required: ['repo'], @@ -2504,9 +2786,12 @@ export const schemaDict = { }, didDoc: { type: 'unknown', + description: 'The complete DID document for this account.', }, collections: { type: 'array', + description: + 'List of all the collections (NSIDs) for which this repo contains at least one record.', items: { type: 'string', format: 'nsid', @@ -2514,6 +2799,8 @@ export const schemaDict = { }, handleIsCorrect: { type: 'boolean', + description: + 'Indicates if handle is currently valid (resolves bi-directionally)', }, }, }, @@ -2527,7 +2814,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a record.', + description: + 'Get a single record from a repository. Does not require auth.', parameters: { type: 'params', required: ['repo', 'collection', 'rkey'], @@ -2544,7 +2832,7 @@ export const schemaDict = { }, rkey: { type: 'string', - description: 'The key of the record.', + description: 'The Record Key.', }, cid: { type: 'string', @@ -2577,13 +2865,86 @@ export const schemaDict = { }, }, }, + ComAtprotoRepoImportRepo: { + lexicon: 1, + id: 'com.atproto.repo.importRepo', + defs: { + main: { + type: 'procedure', + description: + 'Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.', + input: { + encoding: 'application/vnd.ipld.car', + }, + }, + }, + }, + ComAtprotoRepoListMissingBlobs: { + lexicon: 1, + id: 'com.atproto.repo.listMissingBlobs', + defs: { + main: { + type: 'query', + description: + 'Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 1000, + default: 500, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['blobs'], + properties: { + cursor: { + type: 'string', + }, + blobs: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.repo.listMissingBlobs#recordBlob', + }, + }, + }, + }, + }, + }, + recordBlob: { + type: 'object', + required: ['cid', 'recordUri'], + properties: { + cid: { + type: 'string', + format: 'cid', + }, + recordUri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + }, ComAtprotoRepoListRecords: { lexicon: 1, id: 'com.atproto.repo.listRecords', defs: { main: { type: 'query', - description: 'List a range of records in a collection.', + description: + 'List a range of records in a repository, matching a specific collection. Does not require auth.', parameters: { type: 'params', required: ['repo', 'collection'], @@ -2669,7 +3030,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Write a record, creating or updating it as needed.', + description: + 'Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.', input: { encoding: 'application/json', schema: { @@ -2680,7 +3042,8 @@ export const schemaDict = { repo: { type: 'string', format: 'at-identifier', - description: 'The handle or DID of the repo.', + description: + 'The handle or DID of the repo (aka, current account).', }, collection: { type: 'string', @@ -2689,13 +3052,14 @@ export const schemaDict = { }, rkey: { type: 'string', - description: 'The key of the record.', + description: 'The Record Key.', maxLength: 15, }, validate: { type: 'boolean', default: true, - description: 'Flag for validating the record.', + description: + "Can be set to 'false' to skip Lexicon schema validation of record data.", }, record: { type: 'unknown', @@ -2705,7 +3069,7 @@ export const schemaDict = { type: 'string', format: 'cid', description: - 'Compare and swap with the previous record by CID.', + 'Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation', }, swapCommit: { type: 'string', @@ -2769,7 +3133,7 @@ export const schemaDict = { main: { type: 'procedure', description: - 'Upload a new blob to be added to repo in a later request.', + 'Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.', input: { encoding: '*/*', }, @@ -2788,6 +3152,75 @@ export const schemaDict = { }, }, }, + ComAtprotoServerActivateAccount: { + lexicon: 1, + id: 'com.atproto.server.activateAccount', + defs: { + main: { + type: 'procedure', + description: + "Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup.", + }, + }, + }, + ComAtprotoServerCheckAccountStatus: { + lexicon: 1, + id: 'com.atproto.server.checkAccountStatus', + defs: { + main: { + type: 'query', + description: + 'Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: [ + 'activated', + 'validDid', + 'repoCommit', + 'repoRev', + 'repoBlocks', + 'indexedRecords', + 'privateStateValues', + 'expectedBlobs', + 'importedBlobs', + ], + properties: { + activated: { + type: 'boolean', + }, + validDid: { + type: 'boolean', + }, + repoCommit: { + type: 'string', + format: 'cid', + }, + repoRev: { + type: 'string', + }, + repoBlocks: { + type: 'integer', + }, + indexedRecords: { + type: 'integer', + }, + privateStateValues: { + type: 'integer', + }, + expectedBlobs: { + type: 'integer', + }, + importedBlobs: { + type: 'integer', + }, + }, + }, + }, + }, + }, + }, ComAtprotoServerConfirmEmail: { lexicon: 1, id: 'com.atproto.server.confirmEmail', @@ -2834,7 +3267,7 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Create an account.', + description: 'Create an account. Implemented by PDS.', input: { encoding: 'application/json', schema: { @@ -2847,10 +3280,13 @@ export const schemaDict = { handle: { type: 'string', format: 'handle', + description: 'Requested handle for the account.', }, did: { type: 'string', format: 'did', + description: + 'Pre-existing atproto DID, being imported to a new account.', }, inviteCode: { type: 'string', @@ -2863,12 +3299,18 @@ export const schemaDict = { }, password: { type: 'string', + description: + 'Initial account password. May need to meet instance-specific password strength requirements.', }, recoveryKey: { type: 'string', + description: + 'DID PLC rotation key (aka, recovery key) to be included in PLC creation operation.', }, plcOp: { type: 'unknown', + description: + 'A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented.', }, }, }, @@ -2877,6 +3319,8 @@ export const schemaDict = { encoding: 'application/json', schema: { type: 'object', + description: + 'Account login session returned on successful account creation.', required: ['accessJwt', 'refreshJwt', 'handle', 'did'], properties: { accessJwt: { @@ -2892,9 +3336,11 @@ export const schemaDict = { did: { type: 'string', format: 'did', + description: 'The DID of the new account.', }, didDoc: { type: 'unknown', + description: 'Complete DID document.', }, }, }, @@ -2940,6 +3386,8 @@ export const schemaDict = { properties: { name: { type: 'string', + description: + 'A short name for the App Password, to help distinguish them.', }, }, }, @@ -3141,6 +3589,31 @@ export const schemaDict = { }, }, }, + ComAtprotoServerDeactivateAccount: { + lexicon: 1, + id: 'com.atproto.server.deactivateAccount', + defs: { + main: { + type: 'procedure', + description: + 'Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + deleteAfter: { + type: 'string', + format: 'datetime', + description: + 'A recommendation to server as to how long they should hold onto the deactivated account before deleting.', + }, + }, + }, + }, + }, + }, + }, ComAtprotoServerDefs: { lexicon: 1, id: 'com.atproto.server.defs', @@ -3207,7 +3680,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: "Delete an actor's account with a token and password.", + description: + "Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.", input: { encoding: 'application/json', schema: { @@ -3244,7 +3718,7 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Delete the current session.', + description: 'Delete the current session. Requires auth.', }, }, }, @@ -3255,29 +3729,40 @@ export const schemaDict = { main: { type: 'query', description: - "Get a document describing the service's accounts configuration.", + "Describes the server's account creation requirements and capabilities. Implemented by PDS.", output: { encoding: 'application/json', schema: { type: 'object', - required: ['availableUserDomains'], + required: ['did', 'availableUserDomains'], properties: { inviteCodeRequired: { type: 'boolean', + description: + 'If true, an invite code must be supplied to create an account on this instance.', }, phoneVerificationRequired: { type: 'boolean', + description: + 'If true, a phone verification token must be supplied to create an account on this instance.', }, availableUserDomains: { type: 'array', + description: + 'List of domain suffixes that can be used in account handles.', items: { type: 'string', }, }, links: { type: 'ref', + description: 'URLs of service policy documents.', ref: 'lex:com.atproto.server.describeServer#links', }, + did: { + type: 'string', + format: 'did', + }, }, }, }, @@ -3301,7 +3786,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get all invite codes for a given account.', + description: + 'Get all invite codes for the current account. Requires auth.', parameters: { type: 'params', properties: { @@ -3312,6 +3798,8 @@ export const schemaDict = { createAvailable: { type: 'boolean', default: true, + description: + "Controls whether any new 'earned' but not 'created' invites should be created.", }, }, }, @@ -3339,13 +3827,49 @@ export const schemaDict = { }, }, }, + ComAtprotoServerGetServiceAuth: { + lexicon: 1, + id: 'com.atproto.server.getServiceAuth', + defs: { + main: { + type: 'query', + description: + 'Get a signed token on behalf of the requesting DID for the requested service.', + parameters: { + type: 'params', + required: ['aud'], + properties: { + aud: { + type: 'string', + format: 'did', + description: + 'The DID of the service that the token will be used to authenticate with', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['token'], + properties: { + token: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoServerGetSession: { lexicon: 1, id: 'com.atproto.server.getSession', defs: { main: { type: 'query', - description: 'Get information about the current session.', + description: + 'Get information about the current auth session. Requires auth.', output: { encoding: 'application/json', schema: { @@ -3425,7 +3949,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Refresh an authentication session.', + description: + "Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').", output: { encoding: 'application/json', schema: { @@ -3531,7 +4056,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Reserve a repo signing key for account creation.', + description: + 'Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented.', input: { encoding: 'application/json', schema: { @@ -3539,7 +4065,8 @@ export const schemaDict = { properties: { did: { type: 'string', - description: 'The did to reserve a new did:key for', + format: 'did', + description: 'The DID to reserve a key for.', }, }, }, @@ -3552,7 +4079,8 @@ export const schemaDict = { properties: { signingKey: { type: 'string', - description: 'Public signing key in the form of a did:key.', + description: + 'The public key for the reserved signing key, in did:key serialization.', }, }, }, @@ -3659,7 +4187,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a blob associated with a given repo.', + description: + 'Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.', parameters: { type: 'params', required: ['did', 'cid'], @@ -3667,7 +4196,7 @@ export const schemaDict = { did: { type: 'string', format: 'did', - description: 'The DID of the repo.', + description: 'The DID of the account.', }, cid: { type: 'string', @@ -3688,7 +4217,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get blocks from a given repo.', + description: + 'Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.', parameters: { type: 'params', required: ['did', 'cids'], @@ -3783,7 +4313,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get the current commit CID & revision of the repo.', + description: + 'Get the current commit CID & revision of the specified repo. Does not require auth.', parameters: { type: 'params', required: ['did'], @@ -3826,7 +4357,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Get blocks needed for existence or non-existence of record.', + 'Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.', parameters: { type: 'params', required: ['did', 'collection', 'rkey'], @@ -3842,6 +4373,7 @@ export const schemaDict = { }, rkey: { type: 'string', + description: 'Record Key', }, commit: { type: 'string', @@ -3863,7 +4395,7 @@ export const schemaDict = { main: { type: 'query', description: - "Gets the DID's repo, optionally catching up from a specific revision.", + "Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.", parameters: { type: 'params', required: ['did'], @@ -3875,7 +4407,8 @@ export const schemaDict = { }, since: { type: 'string', - description: 'The revision of the repo to catch up from.', + description: + "The revision ('rev') of the repo to create a diff from.", }, }, }, @@ -3891,7 +4424,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'List blob CIDs since some revision.', + description: + 'List blob CIDso for an account, since some repo revision. Does not require auth; implemented by PDS.', parameters: { type: 'params', required: ['did'], @@ -3944,7 +4478,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'List DIDs and root CIDs of hosted repos.', + description: + 'Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.', parameters: { type: 'params', properties: { @@ -3990,6 +4525,7 @@ export const schemaDict = { head: { type: 'string', format: 'cid', + description: 'Current repo commit CID', }, rev: { type: 'string', @@ -4005,7 +4541,7 @@ export const schemaDict = { main: { type: 'procedure', description: - 'Notify a crawling service of a recent update; often when a long break between updates causes the connection with the crawling service to break.', + 'Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay.', input: { encoding: 'application/json', schema: { @@ -4015,7 +4551,7 @@ export const schemaDict = { hostname: { type: 'string', description: - 'Hostname of the service that is notifying of update.', + 'Hostname of the current service (usually a PDS) that is notifying of update.', }, }, }, @@ -4029,7 +4565,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Request a service to persistently crawl hosted repos.', + description: + 'Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.', input: { encoding: 'application/json', schema: { @@ -4039,7 +4576,7 @@ export const schemaDict = { hostname: { type: 'string', description: - 'Hostname of the service that is requesting to be crawled.', + 'Hostname of the current service (eg, PDS) that is requesting to be crawled.', }, }, }, @@ -4053,13 +4590,14 @@ export const schemaDict = { defs: { main: { type: 'subscription', - description: 'Subscribe to repo updates.', + description: + 'Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.', parameters: { type: 'params', properties: { cursor: { type: 'integer', - description: 'The last known event to backfill from.', + description: 'The last known event seq number to backfill from.', }, }, }, @@ -4068,6 +4606,7 @@ export const schemaDict = { type: 'union', refs: [ 'lex:com.atproto.sync.subscribeRepos#commit', + 'lex:com.atproto.sync.subscribeRepos#identity', 'lex:com.atproto.sync.subscribeRepos#handle', 'lex:com.atproto.sync.subscribeRepos#migrate', 'lex:com.atproto.sync.subscribeRepos#tombstone', @@ -4081,11 +4620,15 @@ export const schemaDict = { }, { name: 'ConsumerTooSlow', + description: + 'If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection.', }, ], }, commit: { type: 'object', + description: + 'Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.', required: [ 'seq', 'rebase', @@ -4103,34 +4646,45 @@ export const schemaDict = { properties: { seq: { type: 'integer', + description: 'The stream sequence number of this message.', }, rebase: { type: 'boolean', + description: 'DEPRECATED -- unused', }, tooBig: { type: 'boolean', + description: + 'Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data.', }, repo: { type: 'string', format: 'did', + description: 'The repo this event comes from.', }, commit: { type: 'cid-link', + description: 'Repo commit object CID.', }, prev: { type: 'cid-link', + description: + 'DEPRECATED -- unused. WARNING -- nullable and optional; stick with optional to ensure golang interoperability.', }, rev: { type: 'string', - description: 'The rev of the emitted commit.', + description: + 'The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event.', }, since: { type: 'string', - description: 'The rev of the last emitted commit from this repo.', + description: + 'The rev of the last emitted commit from this repo (if any).', }, blocks: { type: 'bytes', - description: 'CAR file containing relevant blocks.', + description: + 'CAR file containing relevant blocks, as a diff since the previous repo state.', maxLength: 1000000, }, ops: { @@ -4138,6 +4692,8 @@ export const schemaDict = { items: { type: 'ref', ref: 'lex:com.atproto.sync.subscribeRepos#repoOp', + description: + 'List of repo mutation operations in this commit (eg, records created, updated, or deleted).', }, maxLength: 200, }, @@ -4145,8 +4701,31 @@ export const schemaDict = { type: 'array', items: { type: 'cid-link', + description: + 'List of new blobs (by CID) referenced by records in this commit.', }, }, + time: { + type: 'string', + format: 'datetime', + description: + 'Timestamp of when this message was originally broadcast.', + }, + }, + }, + identity: { + type: 'object', + description: + "Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.", + required: ['seq', 'did', 'time'], + properties: { + seq: { + type: 'integer', + }, + did: { + type: 'string', + format: 'did', + }, time: { type: 'string', format: 'datetime', @@ -4155,6 +4734,8 @@ export const schemaDict = { }, handle: { type: 'object', + description: + "Represents an update of the account's handle, or transition to/from invalid state. NOTE: Will be deprecated in favor of #identity.", required: ['seq', 'did', 'handle', 'time'], properties: { seq: { @@ -4176,6 +4757,8 @@ export const schemaDict = { }, migrate: { type: 'object', + description: + 'Represents an account moving from one PDS instance to another. NOTE: not implemented; account migration uses #identity instead', required: ['seq', 'did', 'migrateTo', 'time'], nullable: ['migrateTo'], properties: { @@ -4197,6 +4780,8 @@ export const schemaDict = { }, tombstone: { type: 'object', + description: + 'Indicates that an account has been deleted. NOTE: may be deprecated in favor of #identity or a future #account event', required: ['seq', 'did', 'time'], properties: { seq: { @@ -4227,8 +4812,7 @@ 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.", + description: 'A repo operation, ie a mutation of a single record.', required: ['action', 'path', 'cid'], nullable: ['cid'], properties: { @@ -4241,6 +4825,8 @@ export const schemaDict = { }, cid: { type: 'cid-link', + description: + 'For creates and updates, the new record CID. For deletions, null.', }, }, }, @@ -4281,7 +4867,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Fetch all labels from a labeler created after a certain date.', + 'DEPRECATED: use queryLabels or subscribeLabels instead -- Fetch all labels from a labeler created after a certain date.', parameters: { type: 'params', properties: { @@ -4315,59 +4901,6 @@ export const schemaDict = { }, }, }, - ComAtprotoTempImportRepo: { - lexicon: 1, - id: 'com.atproto.temp.importRepo', - defs: { - main: { - type: 'procedure', - description: - "Gets the did's repo, optionally catching up from a specific revision.", - parameters: { - type: 'params', - required: ['did'], - properties: { - did: { - type: 'string', - format: 'did', - description: 'The DID of the repo.', - }, - }, - }, - input: { - encoding: 'application/vnd.ipld.car', - }, - output: { - encoding: 'text/plain', - }, - }, - }, - }, - ComAtprotoTempPushBlob: { - lexicon: 1, - id: 'com.atproto.temp.pushBlob', - defs: { - main: { - type: 'procedure', - description: - "Gets the did's repo, optionally catching up from a specific revision.", - parameters: { - type: 'params', - required: ['did'], - properties: { - did: { - type: 'string', - format: 'did', - description: 'The DID of the repo.', - }, - }, - }, - input: { - encoding: '*/*', - }, - }, - }, - }, ComAtprotoTempRequestPhoneVerification: { lexicon: 1, id: 'com.atproto.temp.requestPhoneVerification', @@ -4391,86 +4924,9 @@ export const schemaDict = { }, }, }, - ComAtprotoTempTransferAccount: { - lexicon: 1, - id: 'com.atproto.temp.transferAccount', - defs: { - main: { - type: 'procedure', - description: 'Transfer an account.', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['handle', 'did', 'plcOp'], - properties: { - handle: { - type: 'string', - format: 'handle', - }, - did: { - type: 'string', - format: 'did', - }, - plcOp: { - type: 'unknown', - }, - }, - }, - }, - output: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['accessJwt', 'refreshJwt', 'handle', 'did'], - properties: { - accessJwt: { - type: 'string', - }, - refreshJwt: { - type: 'string', - }, - handle: { - type: 'string', - format: 'handle', - }, - did: { - type: 'string', - format: 'did', - }, - }, - }, - }, - errors: [ - { - name: 'InvalidHandle', - }, - { - name: 'InvalidPassword', - }, - { - name: 'InvalidInviteCode', - }, - { - name: 'HandleNotAvailable', - }, - { - name: 'UnsupportedDomain', - }, - { - name: 'UnresolvableDid', - }, - { - name: 'IncompatibleDidDoc', - }, - ], - }, - }, - }, AppBskyActorDefs: { lexicon: 1, id: 'app.bsky.actor.defs', - description: 'A reference to an actor in the network.', defs: { profileViewBasic: { type: 'object', @@ -4603,6 +5059,8 @@ export const schemaDict = { }, viewerState: { type: 'object', + description: + "Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.", properties: { muted: { type: 'boolean', @@ -4644,6 +5102,8 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#feedViewPref', 'lex:app.bsky.actor.defs#threadViewPref', 'lex:app.bsky.actor.defs#interestsPref', + 'lex:app.bsky.actor.defs#mutedWordsPref', + 'lex:app.bsky.actor.defs#hiddenPostsPref', ], }, }, @@ -4688,6 +5148,9 @@ export const schemaDict = { format: 'at-uri', }, }, + timelineIndex: { + type: 'integer', + }, }, }, personalDetailsPref: { @@ -4764,6 +5227,62 @@ export const schemaDict = { }, }, }, + mutedWordTarget: { + type: 'string', + knownValues: ['content', 'tag'], + maxLength: 640, + maxGraphemes: 64, + }, + mutedWord: { + type: 'object', + description: 'A word that the account owner has muted.', + required: ['value', 'targets'], + properties: { + value: { + type: 'string', + description: 'The muted word itself.', + maxLength: 10000, + maxGraphemes: 1000, + }, + targets: { + type: 'array', + description: 'The intended targets of the muted word.', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWordTarget', + }, + }, + }, + }, + mutedWordsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#mutedWord', + }, + description: 'A list of words the account owner has muted.', + }, + }, + }, + hiddenPostsPref: { + type: 'object', + required: ['items'], + properties: { + items: { + type: 'array', + items: { + type: 'string', + format: 'at-uri', + }, + description: + 'A list of URIs of posts the account owner has hidden.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { @@ -4772,7 +5291,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get private preferences attached to the account.', + description: + 'Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth.', parameters: { type: 'params', properties: {}, @@ -4799,7 +5319,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get detailed profile view of an actor.', + description: + 'Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.', parameters: { type: 'params', required: ['actor'], @@ -4807,6 +5328,7 @@ export const schemaDict = { actor: { type: 'string', format: 'at-identifier', + description: 'Handle or DID of account to fetch profile of.', }, }, }, @@ -4866,7 +5388,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of suggested actors, used for discovery.', + description: + 'Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding.', parameters: { type: 'params', properties: { @@ -4909,7 +5432,7 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a profile.', + description: 'A declaration of a Bluesky account profile.', key: 'literal:self', record: { type: 'object', @@ -4921,21 +5444,28 @@ export const schemaDict = { }, description: { type: 'string', + description: 'Free-form profile description text.', maxGraphemes: 256, maxLength: 2560, }, avatar: { type: 'blob', + description: + "Small image to be displayed next to posts from account. AKA, 'profile picture'", accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, banner: { type: 'blob', + description: + 'Larger horizontal image to display behind profile view.', accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, labels: { type: 'union', + description: + 'Self-label values, specific to the Bluesky application, on the overall account.', refs: ['lex:com.atproto.label.defs#selfLabels'], }, }, @@ -4972,7 +5502,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Find actors (profiles) matching search criteria.', + description: + 'Find actors (profiles) matching search criteria. Does not require auth.', parameters: { type: 'params', properties: { @@ -5024,7 +5555,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Find actor suggestions for a prefix search term.', + description: + 'Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth.', parameters: { type: 'params', properties: { @@ -5066,11 +5598,11 @@ export const schemaDict = { AppBskyEmbedExternal: { lexicon: 1, id: 'app.bsky.embed.external', - description: - 'A representation of some externally linked content, embedded in another form of content.', defs: { main: { type: 'object', + description: + "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).", required: ['external'], properties: { external: { @@ -5134,7 +5666,7 @@ export const schemaDict = { AppBskyEmbedImages: { lexicon: 1, id: 'app.bsky.embed.images', - description: 'A set of images embedded in some other form of content.', + description: 'A set of images embedded in a Bluesky record (eg, a post).', defs: { main: { type: 'object', @@ -5161,6 +5693,8 @@ export const schemaDict = { }, alt: { type: 'string', + description: + 'Alt text description of the image, for accessibility.', }, aspectRatio: { type: 'ref', @@ -5204,12 +5738,18 @@ export const schemaDict = { properties: { thumb: { type: 'string', + description: + 'Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View.', }, fullsize: { type: 'string', + description: + 'Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View.', }, alt: { type: 'string', + description: + 'Alt text description of the image, for accessibility.', }, aspectRatio: { type: 'ref', @@ -5223,7 +5763,7 @@ export const schemaDict = { lexicon: 1, id: 'app.bsky.embed.record', description: - 'A representation of a record embedded in another form of content.', + 'A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.', defs: { main: { type: 'object', @@ -5269,6 +5809,7 @@ export const schemaDict = { }, value: { type: 'unknown', + description: 'The record data itself.', }, labels: { type: 'array', @@ -5333,7 +5874,7 @@ export const schemaDict = { lexicon: 1, id: 'app.bsky.embed.recordWithMedia', description: - 'A representation of a record embedded in another form of content, alongside other compatible embeds.', + 'A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.', defs: { main: { type: 'object', @@ -5432,6 +5973,8 @@ export const schemaDict = { }, viewerState: { type: 'object', + description: + "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.", properties: { repost: { type: 'string', @@ -5692,7 +6235,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Get information about a feed generator, including policies and offered feed URIs.', + 'Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View).', output: { encoding: 'application/json', schema: { @@ -5747,7 +6290,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of the existence of a feed generator.', + description: + 'Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository.', key: 'any', record: { type: 'object', @@ -5781,6 +6325,7 @@ export const schemaDict = { }, labels: { type: 'union', + description: 'Self-label values', refs: ['lex:com.atproto.label.defs#selfLabels'], }, createdAt: { @@ -5798,7 +6343,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of feeds created by the actor.', + description: + "Get a list of feeds (feed generator records) created by the actor (in the actor's repo).", parameters: { type: 'params', required: ['actor'], @@ -5846,7 +6392,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of posts liked by an actor.', + description: + 'Get a list of posts liked by an actor. Does not require auth.', parameters: { type: 'params', required: ['actor'], @@ -5902,7 +6449,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: "Get a view of an actor's feed.", + description: + "Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.", parameters: { type: 'params', required: ['actor'], @@ -5922,6 +6470,8 @@ export const schemaDict = { }, filter: { type: 'string', + description: + 'Combinations of post/repost types to include in response.', knownValues: [ 'posts_with_replies', 'posts_no_replies', @@ -5969,7 +6519,7 @@ export const schemaDict = { main: { type: 'query', description: - "Get a hydrated feed from an actor's selected feed generator.", + "Get a hydrated feed from an actor's selected feed generator. Implemented by App View.", parameters: { type: 'params', required: ['feed'], @@ -6022,7 +6572,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get information about a feed generator.', + description: + 'Get information about a feed generator. Implemented by AppView.', parameters: { type: 'params', required: ['feed'], @@ -6030,6 +6581,7 @@ export const schemaDict = { feed: { type: 'string', format: 'at-uri', + description: 'AT-URI of the feed generator record.', }, }, }, @@ -6045,9 +6597,13 @@ export const schemaDict = { }, isOnline: { type: 'boolean', + description: + 'Indicates whether the feed generator service has been online recently, or else seems to be inactive.', }, isValid: { type: 'boolean', + description: + 'Indicates whether the feed generator service is compatible with the record declaration.', }, }, }, @@ -6100,7 +6656,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a skeleton of a feed provided by a feed generator.', + description: + 'Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service.', parameters: { type: 'params', required: ['feed'], @@ -6108,6 +6665,8 @@ export const schemaDict = { feed: { type: 'string', format: 'at-uri', + description: + 'Reference to feed generator record describing the specific feed being requested.', }, limit: { type: 'integer', @@ -6153,7 +6712,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get the list of likes.', + description: + 'Get like records which reference a subject (by AT-URI and CID).', parameters: { type: 'params', required: ['uri'], @@ -6161,10 +6721,13 @@ export const schemaDict = { uri: { type: 'string', format: 'at-uri', + description: 'AT-URI of the subject (eg, a post record).', }, cid: { type: 'string', format: 'cid', + description: + 'CID of the subject record (aka, specific version of record), to filter likes.', }, limit: { type: 'integer', @@ -6231,7 +6794,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a view of a recent posts from actors in a list.', + description: + 'Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth.', parameters: { type: 'params', required: ['list'], @@ -6239,6 +6803,7 @@ export const schemaDict = { list: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) to the list record.', }, limit: { type: 'integer', @@ -6284,7 +6849,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get posts in a thread.', + description: + 'Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests.', parameters: { type: 'params', required: ['uri'], @@ -6292,15 +6858,20 @@ export const schemaDict = { uri: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) to post record.', }, depth: { type: 'integer', + description: + 'How many levels of reply depth should be included in response.', default: 6, minimum: 0, maximum: 1000, }, parentHeight: { type: 'integer', + description: + 'How many levels of parent (and grandparent, etc) post to include.', default: 80, minimum: 0, maximum: 1000, @@ -6338,13 +6909,15 @@ export const schemaDict = { defs: { main: { type: 'query', - description: "Get a view of an actor's feed.", + description: + "Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'.", parameters: { type: 'params', required: ['uris'], properties: { uris: { type: 'array', + description: 'List of post AT-URIs to return hydrated views for.', items: { type: 'string', format: 'at-uri', @@ -6378,7 +6951,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of reposts.', + description: 'Get a list of reposts for a given post.', parameters: { type: 'params', required: ['uri'], @@ -6386,10 +6959,13 @@ export const schemaDict = { uri: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) of post record', }, cid: { type: 'string', format: 'cid', + description: + 'If supplied, filters to reposts of specific version (by CID) of the post record.', }, limit: { type: 'integer', @@ -6438,7 +7014,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of suggested feeds for the viewer.', + description: + 'Get a list of suggested feeds (feed generators) for the requesting account.', parameters: { type: 'params', properties: { @@ -6481,12 +7058,15 @@ export const schemaDict = { defs: { main: { type: 'query', - description: "Get a view of the actor's home timeline.", + description: + "Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed.", parameters: { type: 'params', properties: { algorithm: { type: 'string', + description: + "Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism.", }, limit: { type: 'integer', @@ -6527,7 +7107,7 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a like.', + description: "Record declaring a 'like' of a piece of subject content.", key: 'tid', record: { type: 'object', @@ -6552,7 +7132,7 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a post.', + description: 'Record containing a Bluesky post.', key: 'tid', record: { type: 'object', @@ -6562,10 +7142,12 @@ export const schemaDict = { type: 'string', maxLength: 3000, maxGraphemes: 300, + description: + 'The primary post content. May be an empty string, if there are embeds.', }, entities: { type: 'array', - description: 'Deprecated: replaced by app.bsky.richtext.facet.', + description: 'DEPRECATED: replaced by app.bsky.richtext.facet.', items: { type: 'ref', ref: 'lex:app.bsky.feed.post#entity', @@ -6573,6 +7155,8 @@ export const schemaDict = { }, facets: { type: 'array', + description: + 'Annotations of text (mentions, URLs, hashtags, etc)', items: { type: 'ref', ref: 'lex:app.bsky.richtext.facet', @@ -6593,6 +7177,8 @@ export const schemaDict = { }, langs: { type: 'array', + description: + 'Indicates human language of post primary text content.', maxLength: 3, items: { type: 'string', @@ -6601,21 +7187,26 @@ export const schemaDict = { }, labels: { type: 'union', + description: + 'Self-label values for this post. Effectively content warnings.', refs: ['lex:com.atproto.label.defs#selfLabels'], }, tags: { type: 'array', + description: + 'Additional hashtags, in addition to any included in post text and facets.', maxLength: 8, items: { type: 'string', maxLength: 640, maxGraphemes: 64, }, - description: 'Additional non-inline tags describing this post.', }, createdAt: { type: 'string', format: 'datetime', + description: + 'Client-declared timestamp when this post was originally created.', }, }, }, @@ -6675,7 +7266,8 @@ export const schemaDict = { id: 'app.bsky.feed.repost', defs: { main: { - description: 'A declaration of a repost.', + description: + "Record representing a 'repost' of an existing Bluesky post.", type: 'record', key: 'tid', record: { @@ -6701,7 +7293,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Find posts matching search criteria.', + description: + 'Find posts matching search criteria, returning views of those posts.', parameters: { type: 'params', required: ['q'], @@ -6764,7 +7357,7 @@ export const schemaDict = { type: 'record', key: 'tid', description: - "Defines interaction gating rules for a thread. The rkey of the threadgate record should match the rkey of the thread's root post.", + "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository..", record: { type: 'object', required: ['post', 'createdAt'], @@ -6772,6 +7365,7 @@ export const schemaDict = { post: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) to the post record.', }, allow: { type: 'array', @@ -6821,7 +7415,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a block.', + description: + "Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details.", key: 'tid', record: { type: 'object', @@ -6830,6 +7425,7 @@ export const schemaDict = { subject: { type: 'string', format: 'did', + description: 'DID of the account to be blocked.', }, createdAt: { type: 'string', @@ -7018,7 +7614,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a social follow.', + description: + "Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.", key: 'tid', record: { type: 'object', @@ -7043,7 +7640,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of who the actor is blocking.', + description: + 'Enumerates which accounts the requesting account is currently blocking. Requires auth.', parameters: { type: 'params', properties: { @@ -7086,7 +7684,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: "Get a list of an actor's followers.", + description: + 'Enumerates accounts which follow a specified account (actor).', parameters: { type: 'params', required: ['actor'], @@ -7138,7 +7737,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of who the actor follows.', + description: + 'Enumerates accounts which a specified account (actor) follows.', parameters: { type: 'params', required: ['actor'], @@ -7190,7 +7790,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of actors.', + description: + "Gets a 'view' (with additional context) of a specified list.", parameters: { type: 'params', required: ['list'], @@ -7198,6 +7799,7 @@ export const schemaDict = { list: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) of the list record to hydrate.', }, limit: { type: 'integer', @@ -7242,7 +7844,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get lists that the actor is blocking.', + description: + 'Get mod lists that the requesting account (actor) is blocking. Requires auth.', parameters: { type: 'params', properties: { @@ -7285,7 +7888,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get lists that the actor is muting.', + description: + 'Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth.', parameters: { type: 'params', properties: { @@ -7328,7 +7932,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of lists that belong to an actor.', + description: + 'Enumerates the lists created by a specified account (actor).', parameters: { type: 'params', required: ['actor'], @@ -7336,6 +7941,7 @@ export const schemaDict = { actor: { type: 'string', format: 'at-identifier', + description: 'The account (actor) to enumerate lists from.', }, limit: { type: 'integer', @@ -7376,7 +7982,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of who the actor mutes.', + description: + 'Enumerates accounts that the requesting account (actor) currently has muted. Requires auth.', parameters: { type: 'params', properties: { @@ -7420,7 +8027,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Enumerates public relationships between one account, and a list of other accounts', + 'Enumerates public relationships between one account, and a list of other accounts. Does not require auth.', parameters: { type: 'params', required: ['actor'], @@ -7428,9 +8035,12 @@ export const schemaDict = { actor: { type: 'string', format: 'at-identifier', + description: 'Primary account requesting relationships for.', }, others: { type: 'array', + description: + "List of 'other' accounts to be related back to the primary.", maxLength: 30, items: { type: 'string', @@ -7478,7 +8088,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get suggested follows related to a given actor.', + description: + 'Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account.', parameters: { type: 'params', required: ['actor'], @@ -7514,7 +8125,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A declaration of a list of actors.', + description: + 'Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists.', key: 'tid', record: { type: 'object', @@ -7522,12 +8134,15 @@ export const schemaDict = { properties: { purpose: { type: 'ref', + description: + 'Defines the purpose of the list (aka, moderation-oriented or curration-oriented)', ref: 'lex:app.bsky.graph.defs#listPurpose', }, name: { type: 'string', maxLength: 64, minLength: 1, + description: 'Display name for list; can not be empty.', }, description: { type: 'string', @@ -7565,7 +8180,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'A block of an entire list of actors.', + description: + 'Record representing a block relationship against an entire an entire list of accounts (actors).', key: 'tid', record: { type: 'object', @@ -7574,6 +8190,7 @@ export const schemaDict = { subject: { type: 'string', format: 'at-uri', + description: 'Reference (AT-URI) to the mod list record.', }, createdAt: { type: 'string', @@ -7590,7 +8207,8 @@ export const schemaDict = { defs: { main: { type: 'record', - description: 'An item under a declared list of actors.', + description: + "Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records.", key: 'tid', record: { type: 'object', @@ -7599,10 +8217,13 @@ export const schemaDict = { subject: { type: 'string', format: 'did', + description: 'The account which is included on the list.', }, list: { type: 'string', format: 'at-uri', + description: + 'Reference (AT-URI) to the list record (app.bsky.graph.list).', }, createdAt: { type: 'string', @@ -7619,7 +8240,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Mute an actor by DID or handle.', + description: + 'Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7642,7 +8264,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Mute a list of actors.', + description: + 'Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7665,7 +8288,7 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Unmute an actor by DID or handle.', + description: 'Unmutes the specified account. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7688,7 +8311,7 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Unmute a list of actors.', + description: 'Unmutes the specified list of accounts. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7711,7 +8334,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get the count of unread notifications.', + description: + 'Count the number of unread notifications for the requesting account. Requires auth.', parameters: { type: 'params', properties: { @@ -7742,7 +8366,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Get a list of notifications.', + description: + 'Enumerate notifications for the requesting account. Requires auth.', parameters: { type: 'params', properties: { @@ -7853,7 +8478,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Register for push notifications with a service.', + description: + 'Register to receive push notifications, via a specified service, for the requesting account. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7886,7 +8512,8 @@ export const schemaDict = { defs: { main: { type: 'procedure', - description: 'Notify server that the user has seen notifications.', + description: + 'Notify server that the requesting account has seen notifications. Requires auth.', input: { encoding: 'application/json', schema: { @@ -7909,6 +8536,7 @@ export const schemaDict = { defs: { main: { type: 'object', + description: 'Annotation of a sub-string within rich text.', required: ['index', 'features'], properties: { index: { @@ -7930,7 +8558,8 @@ export const schemaDict = { }, mention: { type: 'object', - description: 'A facet feature for actor mentions.', + description: + "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.", required: ['did'], properties: { did: { @@ -7941,7 +8570,8 @@ export const schemaDict = { }, link: { type: 'object', - description: 'A facet feature for links.', + description: + 'Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.', required: ['uri'], properties: { uri: { @@ -7952,7 +8582,8 @@ export const schemaDict = { }, tag: { type: 'object', - description: 'A hashtag.', + description: + "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').", required: ['tag'], properties: { tag: { @@ -7965,7 +8596,7 @@ export const schemaDict = { byteSlice: { type: 'object', description: - 'A text segment. Start is inclusive, end is exclusive. Indices are for utf8-encoded strings.', + 'Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.', required: ['byteStart', 'byteEnd'], properties: { byteStart: { @@ -8258,10 +8889,19 @@ export const ids = { ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail', ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', + ComAtprotoAdminUpdateAccountPassword: + 'com.atproto.admin.updateAccountPassword', ComAtprotoAdminUpdateCommunicationTemplate: 'com.atproto.admin.updateCommunicationTemplate', ComAtprotoAdminUpdateSubjectStatus: 'com.atproto.admin.updateSubjectStatus', + ComAtprotoIdentityGetRecommendedDidCredentials: + 'com.atproto.identity.getRecommendedDidCredentials', + ComAtprotoIdentityRequestPlcOperationSignature: + 'com.atproto.identity.requestPlcOperationSignature', ComAtprotoIdentityResolveHandle: 'com.atproto.identity.resolveHandle', + ComAtprotoIdentitySignPlcOperation: 'com.atproto.identity.signPlcOperation', + ComAtprotoIdentitySubmitPlcOperation: + 'com.atproto.identity.submitPlcOperation', ComAtprotoIdentityUpdateHandle: 'com.atproto.identity.updateHandle', ComAtprotoLabelDefs: 'com.atproto.label.defs', ComAtprotoLabelQueryLabels: 'com.atproto.label.queryLabels', @@ -8273,22 +8913,28 @@ export const ids = { ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord', ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo', ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord', + ComAtprotoRepoImportRepo: 'com.atproto.repo.importRepo', + ComAtprotoRepoListMissingBlobs: 'com.atproto.repo.listMissingBlobs', ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords', ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', + ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount', + ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus', ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword', ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode', ComAtprotoServerCreateInviteCodes: 'com.atproto.server.createInviteCodes', ComAtprotoServerCreateSession: 'com.atproto.server.createSession', + ComAtprotoServerDeactivateAccount: 'com.atproto.server.deactivateAccount', ComAtprotoServerDefs: 'com.atproto.server.defs', ComAtprotoServerDeleteAccount: 'com.atproto.server.deleteAccount', ComAtprotoServerDeleteSession: 'com.atproto.server.deleteSession', ComAtprotoServerDescribeServer: 'com.atproto.server.describeServer', ComAtprotoServerGetAccountInviteCodes: 'com.atproto.server.getAccountInviteCodes', + ComAtprotoServerGetServiceAuth: 'com.atproto.server.getServiceAuth', ComAtprotoServerGetSession: 'com.atproto.server.getSession', ComAtprotoServerListAppPasswords: 'com.atproto.server.listAppPasswords', ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession', @@ -8317,11 +8963,8 @@ export const ids = { ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', ComAtprotoTempCheckSignupQueue: 'com.atproto.temp.checkSignupQueue', ComAtprotoTempFetchLabels: 'com.atproto.temp.fetchLabels', - ComAtprotoTempImportRepo: 'com.atproto.temp.importRepo', - ComAtprotoTempPushBlob: 'com.atproto.temp.pushBlob', ComAtprotoTempRequestPhoneVerification: 'com.atproto.temp.requestPhoneVerification', - ComAtprotoTempTransferAccount: 'com.atproto.temp.transferAccount', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', AppBskyActorGetProfile: 'app.bsky.actor.getProfile', 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 8cdcafcb72f..6836fa7e516 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts @@ -82,6 +82,7 @@ export function validateProfileViewDetailed(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#profileViewDetailed', v) } +/** Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests. */ export interface ViewerState { muted?: boolean mutedByList?: AppBskyGraphDefs.ListViewBasic @@ -113,6 +114,8 @@ export type Preferences = ( | FeedViewPref | ThreadViewPref | InterestsPref + | MutedWordsPref + | HiddenPostsPref | { $type: string; [k: string]: unknown } )[] @@ -154,6 +157,7 @@ export function validateContentLabelPref(v: unknown): ValidationResult { export interface SavedFeedsPref { pinned: string[] saved: string[] + timelineIndex?: number [k: string]: unknown } @@ -252,3 +256,62 @@ export function isInterestsPref(v: unknown): v is InterestsPref { export function validateInterestsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#interestsPref', v) } + +export type MutedWordTarget = 'content' | 'tag' | (string & {}) + +/** A word that the account owner has muted. */ +export interface MutedWord { + /** The muted word itself. */ + value: string + /** The intended targets of the muted word. */ + targets: MutedWordTarget[] + [k: string]: unknown +} + +export function isMutedWord(v: unknown): v is MutedWord { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWord' + ) +} + +export function validateMutedWord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWord', v) +} + +export interface MutedWordsPref { + /** A list of words the account owner has muted. */ + items: MutedWord[] + [k: string]: unknown +} + +export function isMutedWordsPref(v: unknown): v is MutedWordsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#mutedWordsPref' + ) +} + +export function validateMutedWordsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#mutedWordsPref', v) +} + +export interface HiddenPostsPref { + /** A list of URIs of posts the account owner has hidden. */ + items: string[] + [k: string]: unknown +} + +export function isHiddenPostsPref(v: unknown): v is HiddenPostsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#hiddenPostsPref' + ) +} + +export function validateHiddenPostsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#hiddenPostsPref', v) +} 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 88d78a57cba..305e80484be 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/getPreferences.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/getPreferences.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from './defs' export interface QueryParams {} @@ -31,7 +31,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 802afda5361..5a7b1f25bfc 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/getProfile.ts @@ -6,10 +6,11 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from './defs' export interface QueryParams { + /** Handle or DID of account to fetch profile of. */ actor: string } @@ -28,7 +29,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 2549b264e33..16438505654 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/getProfiles.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/getProfiles.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from './defs' export interface QueryParams { @@ -33,7 +33,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 a6d4d6102af..33b89a18bfa 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/getSuggestions.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/getSuggestions.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from './defs' export interface QueryParams { @@ -35,7 +35,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 7dbc4c1ccec..8810ce7bed9 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/profile.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/profile.ts @@ -9,8 +9,11 @@ import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' export interface Record { displayName?: string + /** Free-form profile description text. */ description?: string + /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ avatar?: BlobRef + /** Larger horizontal image to display behind profile view. */ banner?: BlobRef labels?: | ComAtprotoLabelDefs.SelfLabels 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 1e5ee2d834e..670e752fea3 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/putPreferences.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/putPreferences.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from './defs' export interface QueryParams {} 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 f072b8a4d04..dcda0c41854 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/searchActors.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from './defs' export interface QueryParams { @@ -39,7 +39,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 0cf56753db2..0198b23d790 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from './defs' export interface QueryParams { @@ -37,7 +37,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/embed/external.ts b/packages/bsky/src/lexicon/types/app/bsky/embed/external.ts index f42a6cfd95c..b137ee4b6f5 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/embed/external.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/embed/external.ts @@ -6,6 +6,7 @@ import { lexicons } from '../../../../lexicons' import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' +/** A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post). */ export interface Main { external: External [k: string]: unknown 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 4864fad3dea..96399867a1a 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/embed/images.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/embed/images.ts @@ -26,6 +26,7 @@ export function validateMain(v: unknown): ValidationResult { export interface Image { image: BlobRef + /** Alt text description of the image, for accessibility. */ alt: string aspectRatio?: AspectRatio [k: string]: unknown @@ -76,8 +77,11 @@ export function validateView(v: unknown): ValidationResult { } export interface ViewImage { + /** Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. */ thumb: string + /** Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. */ fullsize: string + /** Alt text description of the image, for accessibility. */ alt: string aspectRatio?: AspectRatio [k: string]: unknown 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 cea5742a45e..dbe7f13152b 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/embed/record.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/embed/record.ts @@ -57,6 +57,7 @@ export interface ViewRecord { uri: string cid: string author: AppBskyActorDefs.ProfileViewBasic + /** The record data itself. */ value: {} labels?: ComAtprotoLabelDefs.Label[] embeds?: ( 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 382d3f58ecf..261d8a622ec 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts @@ -45,6 +45,7 @@ export function validatePostView(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#postView', v) } +/** Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests. */ export interface ViewerState { repost?: string like?: string 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 d329bf20a5a..5bf8699a3ca 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -32,7 +32,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 3e930cbe201..0b8afff4ec8 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getActorFeeds.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getActorFeeds.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { @@ -36,7 +36,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 df2f291e1a7..da315ae33c7 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getActorLikes.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { @@ -37,7 +37,7 @@ export interface HandlerError { error?: 'BlockedActor' | 'BlockedByActor' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 25f51f6fe5f..017c7a6a2d4 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -6,13 +6,14 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { actor: string limit: number cursor?: string + /** Combinations of post/repost types to include in response. */ filter: | 'posts_with_replies' | 'posts_no_replies' @@ -43,7 +44,7 @@ export interface HandlerError { error?: 'BlockedActor' | 'BlockedByActor' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 e72b1010aea..e03913a6fb3 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeed.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { @@ -37,7 +37,7 @@ export interface HandlerError { error?: 'UnknownFeed' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 fab3b30c316..7ab89057a8c 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts @@ -6,10 +6,11 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** AT-URI of the feed generator record. */ feed: string } @@ -17,7 +18,9 @@ export type InputSchema = undefined export interface OutputSchema { view: AppBskyFeedDefs.GeneratorView + /** Indicates whether the feed generator service has been online recently, or else seems to be inactive. */ isOnline: boolean + /** Indicates whether the feed generator service is compatible with the record declaration. */ isValid: boolean [k: string]: unknown } @@ -35,7 +38,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 d7e082f2362..21963a91e2e 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { @@ -33,7 +33,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 1c8f349b42b..ca1cef20f08 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts @@ -6,10 +6,11 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** Reference to feed generator record describing the specific feed being requested. */ feed: string limit: number cursor?: string @@ -37,7 +38,7 @@ export interface HandlerError { error?: 'UnknownFeed' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 d581f5bfa9c..275d99bba3d 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getLikes.ts @@ -6,11 +6,13 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from '../actor/defs' export interface QueryParams { + /** AT-URI of the subject (eg, a post record). */ uri: string + /** CID of the subject record (aka, specific version of record), to filter likes. */ cid?: string limit: number cursor?: string @@ -39,7 +41,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts index e24c3f8ed22..84e12deaa92 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts @@ -6,10 +6,11 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** Reference (AT-URI) to the list record. */ list: string limit: number cursor?: string @@ -37,7 +38,7 @@ export interface HandlerError { error?: 'UnknownList' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 61de94b729d..ae232fd91a2 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getPostThread.ts @@ -6,12 +6,15 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** Reference (AT-URI) to post record. */ uri: string + /** How many levels of reply depth should be included in response. */ depth: number + /** How many levels of parent (and grandparent, etc) post to include. */ parentHeight: number } @@ -40,7 +43,7 @@ export interface HandlerError { error?: 'NotFound' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 4282f5d349f..85000c74787 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getPosts.ts @@ -6,10 +6,11 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** List of post AT-URIs to return hydrated views for. */ uris: string[] } @@ -33,7 +34,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 0b9c1a6f68b..40e008815d9 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getRepostedBy.ts @@ -6,11 +6,13 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from '../actor/defs' export interface QueryParams { + /** Reference (AT-URI) of post record */ uri: string + /** If supplied, filters to reposts of specific version (by CID) of the post record. */ cid?: string limit: number cursor?: string @@ -39,7 +41,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts index 9b271335466..d1ec590f33d 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { @@ -35,7 +35,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 832caf5c6f7..5202c9eb6e3 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getTimeline.ts @@ -6,10 +6,11 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { + /** Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism. */ algorithm?: string limit: number cursor?: string @@ -36,7 +37,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts index 93870b4452d..881e3d199aa 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts @@ -14,9 +14,11 @@ import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' export interface Record { + /** The primary post content. May be an empty string, if there are embeds. */ text: string - /** Deprecated: replaced by app.bsky.richtext.facet. */ + /** DEPRECATED: replaced by app.bsky.richtext.facet. */ entities?: Entity[] + /** Annotations of text (mentions, URLs, hashtags, etc) */ facets?: AppBskyRichtextFacet.Main[] reply?: ReplyRef embed?: @@ -25,12 +27,14 @@ export interface Record { | AppBskyEmbedRecord.Main | AppBskyEmbedRecordWithMedia.Main | { $type: string; [k: string]: unknown } + /** Indicates human language of post primary text content. */ langs?: string[] labels?: | ComAtprotoLabelDefs.SelfLabels | { $type: string; [k: string]: unknown } - /** Additional non-inline tags describing this post. */ + /** Additional hashtags, in addition to any included in post text and facets. */ tags?: string[] + /** Client-declared timestamp when this post was originally created. */ createdAt: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/searchPosts.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/searchPosts.ts index 36ac7cbb67d..9dae079c226 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/searchPosts.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/searchPosts.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from './defs' export interface QueryParams { @@ -41,7 +41,7 @@ export interface HandlerError { error?: 'BadQueryString' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts index 6a190d6e98a..e14140d5609 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts @@ -7,6 +7,7 @@ import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' export interface Record { + /** Reference (AT-URI) to the post record. */ post: string allow?: ( | MentionRule diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/block.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/block.ts index 947463af422..b7f19f126b7 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/block.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/block.ts @@ -7,6 +7,7 @@ import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' export interface Record { + /** DID of the account to be blocked. */ subject: string createdAt: string [k: string]: unknown 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 d380a14880a..1fc9cd8ce37 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getBlocks.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getBlocks.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from '../actor/defs' export interface QueryParams { @@ -35,7 +35,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 b337be52c1b..f5645eaef29 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getFollowers.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from '../actor/defs' export interface QueryParams { @@ -37,7 +37,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 71e9ca0270c..b9bd249da45 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getFollows.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from '../actor/defs' export interface QueryParams { @@ -37,7 +37,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 fc45dd20985..864a81b3833 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getList.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getList.ts @@ -6,10 +6,11 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyGraphDefs from './defs' export interface QueryParams { + /** Reference (AT-URI) of the list record to hydrate. */ list: string limit: number cursor?: string @@ -37,7 +38,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getListBlocks.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getListBlocks.ts index 04cca70b44d..7399a14fadc 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getListBlocks.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getListBlocks.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyGraphDefs from './defs' export interface QueryParams { @@ -35,7 +35,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 04cca70b44d..7399a14fadc 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getListMutes.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getListMutes.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyGraphDefs from './defs' export interface QueryParams { @@ -35,7 +35,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 8acf9362c00..dc0c4f18bea 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getLists.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getLists.ts @@ -6,10 +6,11 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyGraphDefs from './defs' export interface QueryParams { + /** The account (actor) to enumerate lists from. */ actor: string limit: number cursor?: string @@ -36,7 +37,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 0034095b975..f450393522d 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getMutes.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getMutes.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from '../actor/defs' export interface QueryParams { @@ -35,7 +35,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getRelationships.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getRelationships.ts index 32a27434782..bd6b6e765ed 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getRelationships.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getRelationships.ts @@ -6,11 +6,13 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyGraphDefs from './defs' export interface QueryParams { + /** Primary account requesting relationships for. */ actor: string + /** List of 'other' accounts to be related back to the primary. */ others?: string[] } @@ -40,7 +42,7 @@ export interface HandlerError { error?: 'ActorNotFound' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts index a2245846fd2..8f310334d0a 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from '../actor/defs' export interface QueryParams { @@ -33,7 +33,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 36a7fb17a3f..91c8ccee5bb 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/list.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/list.ts @@ -11,6 +11,7 @@ import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' export interface Record { purpose: AppBskyGraphDefs.ListPurpose + /** Display name for list; can not be empty. */ name: string description?: string descriptionFacets?: AppBskyRichtextFacet.Main[] diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/listblock.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/listblock.ts index 59f2e057eb5..592778c7a51 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/listblock.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/listblock.ts @@ -7,6 +7,7 @@ import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' export interface Record { + /** Reference (AT-URI) to the mod list record. */ subject: string createdAt: string [k: string]: unknown diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/listitem.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/listitem.ts index 69eff329ed4..5e93b34a111 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/listitem.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/listitem.ts @@ -7,7 +7,9 @@ import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' export interface Record { + /** The account which is included on the list. */ subject: string + /** Reference (AT-URI) to the list record (app.bsky.graph.list). */ list: string createdAt: string [k: string]: unknown 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 52d1b864989..baa9844046a 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/muteActor.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/muteActor.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 bf803f388af..6a68f680a1c 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/muteActorList.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/muteActorList.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 52d1b864989..baa9844046a 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActor.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActor.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 bf803f388af..6a68f680a1c 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActorList.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActorList.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 6cf3c84beb5..eae30df7c1b 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/notification/getUnreadCount.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/notification/getUnreadCount.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { seenAt?: string @@ -32,7 +32,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 b50d6e8282e..d494494e569 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/notification/listNotifications.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyActorDefs from '../actor/defs' import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' @@ -38,7 +38,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/notification/registerPush.ts b/packages/bsky/src/lexicon/types/app/bsky/notification/registerPush.ts index 9923aeb058e..cce8f95839d 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/notification/registerPush.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/notification/registerPush.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 136191edc40..93db017a152 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/notification/updateSeen.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/notification/updateSeen.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} diff --git a/packages/bsky/src/lexicon/types/app/bsky/richtext/facet.ts b/packages/bsky/src/lexicon/types/app/bsky/richtext/facet.ts index 2c5b2d723a9..139b5382caf 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/richtext/facet.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/richtext/facet.ts @@ -6,6 +6,7 @@ import { lexicons } from '../../../../lexicons' import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' +/** Annotation of a sub-string within rich text. */ export interface Main { index: ByteSlice features: (Mention | Link | Tag | { $type: string; [k: string]: unknown })[] @@ -25,7 +26,7 @@ export function validateMain(v: unknown): ValidationResult { return lexicons.validate('app.bsky.richtext.facet#main', v) } -/** A facet feature for actor mentions. */ +/** Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. */ export interface Mention { did: string [k: string]: unknown @@ -43,7 +44,7 @@ export function validateMention(v: unknown): ValidationResult { return lexicons.validate('app.bsky.richtext.facet#mention', v) } -/** A facet feature for links. */ +/** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. */ export interface Link { uri: string [k: string]: unknown @@ -61,7 +62,7 @@ export function validateLink(v: unknown): ValidationResult { return lexicons.validate('app.bsky.richtext.facet#link', v) } -/** A hashtag. */ +/** Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). */ export interface Tag { tag: string [k: string]: unknown @@ -77,7 +78,7 @@ export function validateTag(v: unknown): ValidationResult { return lexicons.validate('app.bsky.richtext.facet#tag', v) } -/** A text segment. Start is inclusive, end is exclusive. Indices are for utf8-encoded strings. */ +/** Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. */ export interface ByteSlice { byteStart: number byteEnd: number 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 97937e926c2..02f19f3cc6a 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from '../feed/defs' export interface QueryParams { @@ -36,7 +36,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts index e6319c54b4e..a03f442140d 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -30,7 +30,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts index 5c45b9fb622..4634407b890 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyUnspeccedDefs from './defs' export interface QueryParams { @@ -43,7 +43,7 @@ export interface HandlerError { error?: 'BadQueryString' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts index 15532087b82..860d4e8407c 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as AppBskyUnspeccedDefs from './defs' export interface QueryParams { @@ -41,7 +41,7 @@ export interface HandlerError { error?: 'BadQueryString' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/createCommunicationTemplate.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/createCommunicationTemplate.ts index d42a8f2ef1d..b910b7987b4 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/createCommunicationTemplate.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/createCommunicationTemplate.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams {} @@ -41,7 +41,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 41be2ad96e7..a713a635635 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts @@ -40,6 +40,7 @@ export interface ModEventView { | ModEventEscalate | ModEventMute | ModEventEmail + | ModEventResolveAppeal | { $type: string; [k: string]: unknown } subject: | RepoRef @@ -76,6 +77,7 @@ export interface ModEventViewDetail { | ModEventAcknowledge | ModEventEscalate | ModEventMute + | ModEventEmail | ModEventResolveAppeal | { $type: string; [k: string]: unknown } subject: @@ -154,6 +156,7 @@ export interface SubjectStatusView { /** True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators. */ appealed?: boolean suspendUntil?: string + tags?: string[] [k: string]: unknown } @@ -718,6 +721,29 @@ export function validateModEventEmail(v: unknown): ValidationResult { return lexicons.validate('com.atproto.admin.defs#modEventEmail', v) } +/** Add/Remove a tag on a subject */ +export interface ModEventTag { + /** Tags to be added to the subject. If already exists, won't be duplicated. */ + add: string[] + /** Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated. */ + remove: string[] + /** Additional comment about added/removed tags. */ + comment?: string + [k: string]: unknown +} + +export function isModEventTag(v: unknown): v is ModEventTag { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventTag' + ) +} + +export function validateModEventTag(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventTag', v) +} + export interface CommunicationTemplateView { id: string /** Name of the template. */ diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/deleteAccount.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/deleteAccount.ts index 13e68eb5c7d..003c1b5ebcd 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/deleteAccount.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/deleteAccount.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/deleteCommunicationTemplate.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/deleteCommunicationTemplate.ts index 4bc6ec86fe4..c5ae5cd469f 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/deleteCommunicationTemplate.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/deleteCommunicationTemplate.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 62864923dfd..68c6503d95e 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 2b64371f1ed..2bf8de35583 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/emitModerationEvent.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/emitModerationEvent.ts index df44702b51c..99d08c7f1b7 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/emitModerationEvent.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/emitModerationEvent.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' import * as ComAtprotoRepoStrongRef from '../repo/strongRef' @@ -24,6 +24,7 @@ export interface InputSchema { | ComAtprotoAdminDefs.ModEventReverseTakedown | ComAtprotoAdminDefs.ModEventUnmute | ComAtprotoAdminDefs.ModEventEmail + | ComAtprotoAdminDefs.ModEventTag | { $type: string; [k: string]: unknown } subject: | ComAtprotoAdminDefs.RepoRef @@ -53,7 +54,7 @@ export interface HandlerError { error?: 'SubjectHasAction' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 fb3aa8b8375..3f2836e7142 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfo.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfo.ts index 88a2b17a4b8..c7b840a153d 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfo.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { @@ -28,7 +28,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfos.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfos.ts index 46d917293a8..99ef44a99f5 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfos.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfos.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { @@ -33,7 +33,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 1eb099aae66..d68b97d775a 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getInviteCodes.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getInviteCodes.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoServerDefs from '../server/defs' export interface QueryParams { @@ -36,7 +36,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationEvent.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationEvent.ts index 7de567a73db..99c8bbe20ef 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationEvent.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationEvent.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { @@ -28,7 +28,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 48222d9d819..557945e2fbd 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getRecord.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { @@ -30,7 +30,7 @@ export interface HandlerError { error?: 'RecordNotFound' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 19911baa90a..ede9fcf3ce8 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getRepo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getRepo.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { @@ -29,7 +29,7 @@ export interface HandlerError { error?: 'RepoNotFound' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts index 7315e51e8c2..d5976db70b1 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' import * as ComAtprotoRepoStrongRef from '../repo/strongRef' @@ -41,7 +41,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/listCommunicationTemplates.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/listCommunicationTemplates.ts index cb479533d39..843c228e6f9 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/listCommunicationTemplates.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/listCommunicationTemplates.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams {} @@ -31,7 +31,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/queryModerationEvents.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/queryModerationEvents.ts index f3c4f1fbb95..9f4738578aa 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/queryModerationEvents.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/queryModerationEvents.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { @@ -15,10 +15,27 @@ export interface QueryParams { createdBy?: string /** Sort direction for the events. Defaults to descending order of created at timestamp. */ sortDirection: 'asc' | 'desc' + /** Retrieve events created after a given timestamp */ + createdAfter?: string + /** Retrieve events created before a given timestamp */ + createdBefore?: string subject?: string /** If true, events on all record types (posts, lists, profile etc.) owned by the did are returned */ includeAllUserRecords: boolean limit: number + /** If true, only events with comments are returned */ + hasComment?: boolean + /** If specified, only events with comments containing the keyword are returned */ + comment?: string + /** If specified, only events where all of these labels were added are returned */ + addedLabels?: string[] + /** If specified, only events where all of these labels were removed are returned */ + removedLabels?: string[] + /** If specified, only events where all of these tags were added are returned */ + addedTags?: string[] + /** If specified, only events where all of these tags were removed are returned */ + removedTags?: string[] + reportTypes?: string[] cursor?: string } @@ -43,7 +60,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/queryModerationStatuses.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/queryModerationStatuses.ts index 6e1aea1f679..f5031d25117 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/queryModerationStatuses.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/queryModerationStatuses.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { @@ -35,6 +35,8 @@ export interface QueryParams { /** Get subjects in unresolved appealed status */ appealed?: boolean limit: number + tags?: string[] + excludeTags?: string[] cursor?: string } @@ -59,7 +61,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 1e7e1a36bb6..d1529956c17 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/searchRepos.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/searchRepos.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { @@ -38,7 +38,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 f94cfb3a083..836fba39f79 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/sendEmail.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/sendEmail.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -41,7 +41,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 9e6140256ef..ebabffbccdb 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 c378f421926..d6dc4a2dc25 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts new file mode 100644 index 00000000000..948568f0d3d --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts @@ -0,0 +1,39 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + did: string + password: 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/admin/updateCommunicationTemplate.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/updateCommunicationTemplate.ts index 5dc5cecda4a..73e079cfe58 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/updateCommunicationTemplate.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/updateCommunicationTemplate.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams {} @@ -44,7 +44,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts index 559ee948380..94df3041cf3 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' import * as ComAtprotoRepoStrongRef from '../repo/strongRef' @@ -48,7 +48,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/identity/getRecommendedDidCredentials.ts b/packages/bsky/src/lexicon/types/com/atproto/identity/getRecommendedDidCredentials.ts new file mode 100644 index 00000000000..5fa374de737 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/identity/getRecommendedDidCredentials.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, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + /** Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs. */ + rotationKeys?: string[] + alsoKnownAs?: string[] + verificationMethods?: {} + services?: {} + [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 | HandlerPipeThrough +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/identity/requestPlcOperationSignature.ts b/packages/bsky/src/lexicon/types/com/atproto/identity/requestPlcOperationSignature.ts new file mode 100644 index 00000000000..82672f1d1c7 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/identity/requestPlcOperationSignature.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined +export type HandlerInput = undefined + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/identity/resolveHandle.ts b/packages/bsky/src/lexicon/types/com/atproto/identity/resolveHandle.ts index ef90e99bb30..05019df6166 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/identity/resolveHandle.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/identity/resolveHandle.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { /** The handle to resolve. */ @@ -33,7 +33,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/temp/transferAccount.ts b/packages/bsky/src/lexicon/types/com/atproto/identity/signPlcOperation.ts similarity index 70% rename from packages/bsky/src/lexicon/types/com/atproto/temp/transferAccount.ts rename to packages/bsky/src/lexicon/types/com/atproto/identity/signPlcOperation.ts index 86c1d750e07..3c908c049f2 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/temp/transferAccount.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/identity/signPlcOperation.ts @@ -6,22 +6,23 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { - handle: string - did: string - plcOp: {} + /** A token received through com.atproto.identity.requestPlcOperationSignature */ + token?: string + rotationKeys?: string[] + alsoKnownAs?: string[] + verificationMethods?: {} + services?: {} [k: string]: unknown } export interface OutputSchema { - accessJwt: string - refreshJwt: string - handle: string - did: string + /** A signed DID PLC operation. */ + operation: {} [k: string]: unknown } @@ -39,17 +40,9 @@ export interface HandlerSuccess { export interface HandlerError { status: number message?: string - error?: - | 'InvalidHandle' - | 'InvalidPassword' - | 'InvalidInviteCode' - | 'HandleNotAvailable' - | 'UnsupportedDomain' - | 'UnresolvableDid' - | 'IncompatibleDidDoc' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/identity/submitPlcOperation.ts b/packages/bsky/src/lexicon/types/com/atproto/identity/submitPlcOperation.ts new file mode 100644 index 00000000000..5290b55d023 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/identity/submitPlcOperation.ts @@ -0,0 +1,38 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + operation: {} + [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/identity/updateHandle.ts b/packages/bsky/src/lexicon/types/com/atproto/identity/updateHandle.ts index 1f639c344e9..f451d1f57c7 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/identity/updateHandle.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/identity/updateHandle.ts @@ -6,11 +6,12 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { + /** The new handle. */ handle: string [k: string]: unknown } 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 1d7f8a4def5..0c9d55a6961 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/label/queryLabels.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/label/queryLabels.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoLabelDefs from './defs' export interface QueryParams { @@ -39,7 +39,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 9d4b4441ae0..6034b35d895 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/label/subscribeLabels.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/label/subscribeLabels.ts @@ -10,7 +10,7 @@ import { IncomingMessage } from 'http' import * as ComAtprotoLabelDefs from './defs' export interface QueryParams { - /** The last known event to backfill from. */ + /** The last known event seq number to backfill from. */ cursor?: 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 96aaf4a9c29..aa3f810a91c 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/moderation/createReport.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/moderation/createReport.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoModerationDefs from './defs' import * as ComAtprotoAdminDefs from '../admin/defs' import * as ComAtprotoRepoStrongRef from '../repo/strongRef' @@ -15,6 +15,7 @@ export interface QueryParams {} export interface InputSchema { reasonType: ComAtprotoModerationDefs.ReasonType + /** Additional context about the content and violation. */ reason?: string subject: | ComAtprotoAdminDefs.RepoRef @@ -52,7 +53,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 61d1e7c28e4..3956d7c3048 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/applyWrites.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/applyWrites.ts @@ -6,16 +6,17 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { - /** The handle or DID of the repo. */ + /** The handle or DID of the repo (aka, current account). */ repo: string - /** Flag for validating the records. */ + /** Can be set to 'false' to skip Lexicon schema validation of record data, for all operations. */ validate: boolean writes: (Create | Update | Delete)[] + /** If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations. */ swapCommit?: string [k: string]: unknown } @@ -43,7 +44,7 @@ export type Handler = ( ctx: HandlerReqCtx, ) => Promise | HandlerOutput -/** Create a new record. */ +/** Operation which creates a new record. */ export interface Create { collection: string rkey?: string @@ -63,7 +64,7 @@ export function validateCreate(v: unknown): ValidationResult { return lexicons.validate('com.atproto.repo.applyWrites#create', v) } -/** Update an existing record. */ +/** Operation which updates an existing record. */ export interface Update { collection: string rkey: string @@ -83,7 +84,7 @@ export function validateUpdate(v: unknown): ValidationResult { return lexicons.validate('com.atproto.repo.applyWrites#update', v) } -/** Delete an existing record. */ +/** Operation which deletes an existing record. */ export interface Delete { collection: string rkey: string 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 df8c5d9e600..55cc95d0ad7 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/createRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/createRecord.ts @@ -6,20 +6,20 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { - /** The handle or DID of the repo. */ + /** The handle or DID of the repo (aka, current account). */ repo: string /** The NSID of the record collection. */ collection: string - /** The key of the record. */ + /** The Record Key. */ rkey?: string - /** Flag for validating the record. */ + /** Can be set to 'false' to skip Lexicon schema validation of record data. */ validate: boolean - /** The record to create. */ + /** The record itself. Must contain a $type field. */ record: {} /** Compare and swap with the previous commit by CID. */ swapCommit?: string @@ -49,7 +49,7 @@ export interface HandlerError { error?: 'InvalidSwap' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 f45118a3769..3bb97be0aad 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/deleteRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/deleteRecord.ts @@ -6,16 +6,16 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { - /** The handle or DID of the repo. */ + /** The handle or DID of the repo (aka, current account). */ repo: string /** The NSID of the record collection. */ collection: string - /** The key of the record. */ + /** The Record Key. */ rkey: string /** Compare and swap with the previous record by CID. */ swapRecord?: string 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 7b8a2b995eb..749bedcfeb7 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/describeRepo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/describeRepo.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { /** The handle or DID of the repo. */ @@ -18,8 +18,11 @@ export type InputSchema = undefined export interface OutputSchema { handle: string did: string + /** The complete DID document for this account. */ didDoc: {} + /** List of all the collections (NSIDs) for which this repo contains at least one record. */ collections: string[] + /** Indicates if handle is currently valid (resolves bi-directionally) */ handleIsCorrect: boolean [k: string]: unknown } @@ -37,7 +40,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 35c9b4b7166..1a737a848be 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/getRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/getRecord.ts @@ -6,14 +6,14 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { /** The handle or DID of the repo. */ repo: string /** The NSID of the record collection. */ collection: string - /** The key of the record. */ + /** The Record Key. */ rkey: string /** The CID of the version of the record. If not specified, then return the most recent version. */ cid?: string @@ -41,7 +41,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/temp/pushBlob.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/importRepo.ts similarity index 84% rename from packages/bsky/src/lexicon/types/com/atproto/temp/pushBlob.ts rename to packages/bsky/src/lexicon/types/com/atproto/repo/importRepo.ts index 97e890dbb14..921798c0ded 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/temp/pushBlob.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/importRepo.ts @@ -7,17 +7,14 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' -export interface QueryParams { - /** The DID of the repo. */ - did: string -} +export interface QueryParams {} export type InputSchema = string | Uint8Array export interface HandlerInput { - encoding: '*/*' + encoding: 'application/vnd.ipld.car' body: stream.Readable } diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/listMissingBlobs.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/listMissingBlobs.ts new file mode 100644 index 00000000000..40f4d385e47 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/listMissingBlobs.ts @@ -0,0 +1,65 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams { + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + blobs: RecordBlob[] + [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 | HandlerPipeThrough +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput + +export interface RecordBlob { + cid: string + recordUri: string + [k: string]: unknown +} + +export function isRecordBlob(v: unknown): v is RecordBlob { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.listMissingBlobs#recordBlob' + ) +} + +export function validateRecordBlob(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.listMissingBlobs#recordBlob', v) +} 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 a6cf6abd1f3..f46f6eb0f7f 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/listRecords.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/listRecords.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { /** The handle or DID of the repo. */ @@ -45,7 +45,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 f10f773c1c4..193841a2294 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/putRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/putRecord.ts @@ -6,22 +6,22 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { - /** The handle or DID of the repo. */ + /** The handle or DID of the repo (aka, current account). */ repo: string /** The NSID of the record collection. */ collection: string - /** The key of the record. */ + /** The Record Key. */ rkey: string - /** Flag for validating the record. */ + /** Can be set to 'false' to skip Lexicon schema validation of record data. */ validate: boolean /** The record to write. */ record: {} - /** Compare and swap with the previous record by CID. */ + /** Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation */ swapRecord?: string | null /** Compare and swap with the previous commit by CID. */ swapCommit?: string @@ -51,7 +51,7 @@ export interface HandlerError { error?: 'InvalidSwap' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 ad6002df925..febbbff9d16 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/uploadBlob.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/uploadBlob.ts @@ -7,7 +7,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -34,7 +34,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/activateAccount.ts b/packages/bsky/src/lexicon/types/com/atproto/server/activateAccount.ts new file mode 100644 index 00000000000..82672f1d1c7 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/server/activateAccount.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined +export type HandlerInput = undefined + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/checkAccountStatus.ts b/packages/bsky/src/lexicon/types/com/atproto/server/checkAccountStatus.ts new file mode 100644 index 00000000000..f17182a8dce --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/server/checkAccountStatus.ts @@ -0,0 +1,51 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + activated: boolean + validDid: boolean + repoCommit: string + repoRev: string + repoBlocks: number + indexedRecords: number + privateStateValues: number + expectedBlobs: number + importedBlobs: number + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough +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/server/confirmEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts index ffaeeb8fe75..b667a04b996 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 bbf2c009bf5..6e9b2f9f3c2 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts @@ -6,28 +6,36 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { email?: string + /** Requested handle for the account. */ handle: string + /** Pre-existing atproto DID, being imported to a new account. */ did?: string inviteCode?: string verificationCode?: string verificationPhone?: string + /** Initial account password. May need to meet instance-specific password strength requirements. */ password?: string + /** DID PLC rotation key (aka, recovery key) to be included in PLC creation operation. */ recoveryKey?: string + /** A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented. */ plcOp?: {} [k: string]: unknown } +/** Account login session returned on successful account creation. */ export interface OutputSchema { accessJwt: string refreshJwt: string handle: string + /** The DID of the new account. */ did: string + /** Complete DID document. */ didDoc?: {} [k: string]: unknown } @@ -56,7 +64,7 @@ export interface HandlerError { | 'IncompatibleDidDoc' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 8e4a0a519e0..dcc5178ecfa 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createAppPassword.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createAppPassword.ts @@ -6,11 +6,12 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { + /** A short name for the App Password, to help distinguish them. */ name: string [k: string]: unknown } @@ -34,7 +35,7 @@ export interface HandlerError { error?: 'AccountTakedown' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 acfac56ba76..9cfeacc7e28 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCode.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCode.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -37,7 +37,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 5887d77fada..eb6cd2bb1b1 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCodes.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCodes.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -38,7 +38,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 2cd448703a6..3952959fe5e 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -45,7 +45,7 @@ export interface HandlerError { error?: 'AccountTakedown' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/deactivateAccount.ts b/packages/bsky/src/lexicon/types/com/atproto/server/deactivateAccount.ts new file mode 100644 index 00000000000..b3793d6b2e0 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/server/deactivateAccount.ts @@ -0,0 +1,39 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** A recommendation to server as to how long they should hold onto the deactivated account before deleting. */ + deleteAfter?: 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/server/deleteAccount.ts b/packages/bsky/src/lexicon/types/com/atproto/server/deleteAccount.ts index 37ddbba13e0..4fcec360a11 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/deleteAccount.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/deleteAccount.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 e4244870425..82672f1d1c7 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/deleteSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/deleteSession.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 bb574dba9ff..c2625347f20 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/describeServer.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/describeServer.ts @@ -6,17 +6,21 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export type InputSchema = undefined export interface OutputSchema { + /** If true, an invite code must be supplied to create an account on this instance. */ inviteCodeRequired?: boolean + /** If true, a phone verification token must be supplied to create an account on this instance. */ phoneVerificationRequired?: boolean + /** List of domain suffixes that can be used in account handles. */ availableUserDomains: string[] links?: Links + did: string [k: string]: unknown } @@ -33,7 +37,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 e387a5e38e4..82c3ffa8c31 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts @@ -6,11 +6,12 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoServerDefs from './defs' export interface QueryParams { includeUsed: boolean + /** Controls whether any new 'earned' but not 'created' invites should be created. */ createAvailable: boolean } @@ -35,7 +36,7 @@ export interface HandlerError { error?: 'DuplicateCreate' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/temp/importRepo.ts b/packages/bsky/src/lexicon/types/com/atproto/server/getServiceAuth.ts similarity index 62% rename from packages/bsky/src/lexicon/types/com/atproto/temp/importRepo.ts rename to packages/bsky/src/lexicon/types/com/atproto/server/getServiceAuth.ts index d88361d9856..73efe2313a9 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/temp/importRepo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/getServiceAuth.ts @@ -2,28 +2,29 @@ * GENERATED CODE - DO NOT MODIFY */ import express from 'express' -import stream from 'stream' import { ValidationResult, BlobRef } from '@atproto/lexicon' import { lexicons } from '../../../../lexicons' import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' -import { HandlerAuth } from '@atproto/xrpc-server' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { - /** The DID of the repo. */ - did: string + /** The DID of the service that the token will be used to authenticate with */ + aud: string } -export type InputSchema = string | Uint8Array +export type InputSchema = undefined -export interface HandlerInput { - encoding: 'application/vnd.ipld.car' - body: stream.Readable +export interface OutputSchema { + token: string + [k: string]: unknown } +export type HandlerInput = undefined + export interface HandlerSuccess { - encoding: 'text/plain' - body: Uint8Array | stream.Readable + encoding: 'application/json' + body: OutputSchema headers?: { [key: string]: string } } @@ -32,7 +33,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 4f95acf523d..5a8c40b947e 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -34,7 +34,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 ebd74da9d39..241418d932d 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/listAppPasswords.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/listAppPasswords.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -31,7 +31,7 @@ export interface HandlerError { error?: 'AccountTakedown' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 35874f78a69..3adeb7fc20e 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/refreshSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/refreshSession.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -35,7 +35,7 @@ export interface HandlerError { error?: 'AccountTakedown' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 e4244870425..82672f1d1c7 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/requestAccountDelete.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestAccountDelete.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts index e4244870425..82672f1d1c7 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts index 6876d44ca46..24dce3e12af 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -30,7 +30,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 47fb4bb62f3..d0f3f2ad769 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/requestPasswordReset.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestPasswordReset.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/reserveSigningKey.ts b/packages/bsky/src/lexicon/types/com/atproto/server/reserveSigningKey.ts index ad5a5a8758c..0ec1e80c77c 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/reserveSigningKey.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/reserveSigningKey.ts @@ -6,18 +6,18 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { - /** The did to reserve a new did:key for */ + /** The DID to reserve a key for. */ did?: string [k: string]: unknown } export interface OutputSchema { - /** Public signing key in the form of a did:key. */ + /** The public key for the reserved signing key, in did:key serialization. */ signingKey: string [k: string]: unknown } @@ -38,7 +38,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 9e6ece3e4c4..38f63382cf0 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/resetPassword.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/resetPassword.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 4627f68eaa2..769ad6aa521 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/revokeAppPassword.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/revokeAppPassword.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts index c88bd3021b2..5473d7571e9 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} 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 60750902472..93e50403f20 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getBlob.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getBlob.ts @@ -7,10 +7,10 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { - /** The DID of the repo. */ + /** The DID of the account. */ did: string /** The CID of the blob to fetch */ cid: string @@ -30,7 +30,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 e73410efb41..f1b8ebe5db1 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getBlocks.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getBlocks.ts @@ -7,7 +7,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ @@ -29,7 +29,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 63a657e56b9..51856b9088d 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getCheckout.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getCheckout.ts @@ -7,7 +7,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ @@ -28,7 +28,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 586ae1a4189..adedd4cf211 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getHead.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getHead.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ @@ -34,7 +34,7 @@ export interface HandlerError { error?: 'HeadNotFound' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/getLatestCommit.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/getLatestCommit.ts index 9b91e878724..bbae68bbe76 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getLatestCommit.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getLatestCommit.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ @@ -35,7 +35,7 @@ export interface HandlerError { error?: 'RepoNotFound' } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 297f0ac7794..c78ff8c2089 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getRecord.ts @@ -7,12 +7,13 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ did: string collection: string + /** Record Key */ rkey: string /** An optional past commit CID. */ commit?: string @@ -32,7 +33,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 495d31a1a22..0d426557c5f 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getRepo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getRepo.ts @@ -7,12 +7,12 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ did: string - /** The revision of the repo to catch up from. */ + /** The revision ('rev') of the repo to create a diff from. */ since?: string } @@ -30,7 +30,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 b397bb3b3df..67a66577809 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/listBlobs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/listBlobs.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ @@ -38,7 +38,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams 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 783a8e314c2..e5a8e2ca9d6 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/listRepos.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/listRepos.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams { limit: number @@ -34,7 +34,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams @@ -48,6 +48,7 @@ export type Handler = ( export interface Repo { did: string + /** Current repo commit CID */ head: string rev: string [k: string]: unknown 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 3d310c1139a..8a0af577c7c 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts @@ -6,12 +6,12 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { - /** Hostname of the service that is notifying of update. */ + /** Hostname of the current service (usually a PDS) that is notifying of update. */ hostname: string [k: string]: unknown } 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 87ef20d7297..31180aabf58 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/requestCrawl.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/requestCrawl.ts @@ -6,12 +6,12 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { - /** Hostname of the service that is requesting to be crawled. */ + /** Hostname of the current service (eg, PDS) that is requesting to be crawled. */ hostname: 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 fb334778bf6..19874b06083 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts @@ -9,12 +9,13 @@ import { HandlerAuth, ErrorFrame } from '@atproto/xrpc-server' import { IncomingMessage } from 'http' export interface QueryParams { - /** The last known event to backfill from. */ + /** The last known event seq number to backfill from. */ cursor?: number } export type OutputSchema = | Commit + | Identity | Handle | Migrate | Tombstone @@ -32,21 +33,29 @@ export type Handler = ( ctx: HandlerReqCtx, ) => AsyncIterable +/** Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature. */ export interface Commit { + /** The stream sequence number of this message. */ seq: number + /** DEPRECATED -- unused */ rebase: boolean + /** Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data. */ tooBig: boolean + /** The repo this event comes from. */ repo: string + /** Repo commit object CID. */ commit: CID + /** DEPRECATED -- unused. WARNING -- nullable and optional; stick with optional to ensure golang interoperability. */ prev?: CID | null - /** The rev of the emitted commit. */ + /** The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event. */ rev: string - /** The rev of the last emitted commit from this repo. */ + /** The rev of the last emitted commit from this repo (if any). */ since: string | null - /** CAR file containing relevant blocks. */ + /** CAR file containing relevant blocks, as a diff since the previous repo state. */ blocks: Uint8Array ops: RepoOp[] blobs: CID[] + /** Timestamp of when this message was originally broadcast. */ time: string [k: string]: unknown } @@ -63,6 +72,27 @@ export function validateCommit(v: unknown): ValidationResult { return lexicons.validate('com.atproto.sync.subscribeRepos#commit', v) } +/** Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache. */ +export interface Identity { + seq: number + did: string + time: string + [k: string]: unknown +} + +export function isIdentity(v: unknown): v is Identity { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.sync.subscribeRepos#identity' + ) +} + +export function validateIdentity(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.sync.subscribeRepos#identity', v) +} + +/** Represents an update of the account's handle, or transition to/from invalid state. NOTE: Will be deprecated in favor of #identity. */ export interface Handle { seq: number did: string @@ -83,6 +113,7 @@ export function validateHandle(v: unknown): ValidationResult { return lexicons.validate('com.atproto.sync.subscribeRepos#handle', v) } +/** Represents an account moving from one PDS instance to another. NOTE: not implemented; account migration uses #identity instead */ export interface Migrate { seq: number did: string @@ -103,6 +134,7 @@ export function validateMigrate(v: unknown): ValidationResult { return lexicons.validate('com.atproto.sync.subscribeRepos#migrate', v) } +/** Indicates that an account has been deleted. NOTE: may be deprecated in favor of #identity or a future #account event */ export interface Tombstone { seq: number did: string @@ -140,10 +172,11 @@ 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. */ +/** A repo operation, ie a mutation of a single record. */ export interface RepoOp { action: 'create' | 'update' | 'delete' | (string & {}) path: string + /** For creates and updates, the new record CID. For deletions, null. */ cid: CID | null [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/com/atproto/temp/checkSignupQueue.ts b/packages/bsky/src/lexicon/types/com/atproto/temp/checkSignupQueue.ts index d2a431430a8..9486bce2b2b 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/temp/checkSignupQueue.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/temp/checkSignupQueue.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} @@ -32,7 +32,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/temp/fetchLabels.ts b/packages/bsky/src/lexicon/types/com/atproto/temp/fetchLabels.ts index 39341fd3a0e..0fbdeed1196 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/temp/fetchLabels.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/temp/fetchLabels.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' import * as ComAtprotoLabelDefs from '../label/defs' export interface QueryParams { @@ -34,7 +34,7 @@ export interface HandlerError { message?: string } -export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts b/packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts index 5a295f701eb..c977500fc33 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts @@ -6,7 +6,7 @@ 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 { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' export interface QueryParams {} diff --git a/packages/bsky/src/notifications.ts b/packages/bsky/src/notifications.ts deleted file mode 100644 index d29913ec7d4..00000000000 --- a/packages/bsky/src/notifications.ts +++ /dev/null @@ -1,398 +0,0 @@ -import axios from 'axios' -import { Insertable, sql } from 'kysely' -import TTLCache from '@isaacs/ttlcache' -import { Struct, Timestamp } from '@bufbuild/protobuf' -import murmur from 'murmurhash' -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, valuesList } from './db/util' -import { ids } from './lexicon/lexicons' -import { retryConnect, retryHttp } from './util/retry' -import { Notification as CourierNotification } from './proto/courier_pb' -import { CourierClient } from './courier' - -export type Platform = 'ios' | 'android' | 'web' - -type GorushNotification = { - tokens: string[] - platform: 1 | 2 // 1 = ios, 2 = android - title: string - message: string - topic: string - data?: { - [key: string]: string - } - collapse_id?: string - collapse_key?: string -} - -type NotifRow = Insertable - -type NotifView = { - key: string - rateLimit: boolean - title: string - body: string - notif: NotifRow -} - -export abstract class NotificationServer { - constructor(public db: Database) {} - - abstract prepareNotifications(notifs: NotifRow[]): Promise - - abstract processNotifications(prepared: N[]): Promise - - async getNotificationViews(notifs: NotifRow[]): 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, blocksAndMutes] = 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(), - this.findBlocksAndMutes(notifs), - ]) - - 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: NotifView[] = [] - - for (const notif of notifs) { - const { - author: authorDid, - reason, - 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 - - const author = - authorsByDid[authorDid]?.displayName || authorsByDid[authorDid]?.handle - const postRecord = postsByUri[recordUri] - const postSubject = subjectUri ? postsByUri[subjectUri] : null - - // 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 - - // 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 - 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({ key, title, body, notif, rateLimit }) - continue - } 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 || '' - rateLimit = false // always deliver - results.push({ key, title, body, notif, rateLimit }) - continue - } - - // 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 || '' - rateLimit = true // always deliver - } 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({ key, title, body, notif, rateLimit }) - } - - return results - } - - private async findBlocksAndMutes(notifs: NotifRow[]) { - 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 }[] - } -} - -export class GorushNotificationServer extends NotificationServer { - private rateLimiter = new RateLimiter(1, 30 * MINUTE) - - constructor(public db: Database, public pushEndpoint: string) { - super(db) - } - - async prepareNotifications( - notifs: NotifRow[], - ): Promise { - const now = Date.now() - const notifsToSend: GorushNotification[] = [] - const tokensByDid = await this.getTokensByDid( - unique(notifs.map((n) => n.did)), - ) - // views for all notifications that have tokens - const notificationViews = await this.getNotificationViews( - notifs.filter((n) => tokensByDid[n.did]), - ) - - 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) { - const { appId, platform, token } = t - if (notifView.rateLimit && !this.rateLimiter.check(token, now)) { - continue - } - 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, - }, - collapse_id: notifView.key, - collapse_key: notifView.key, - }) - } else { - // @TODO: Handle web notifs - logger.warn({ did: userDid }, 'cannot send web notification to user') - } - } - } - return notifsToSend - } - - 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 processNotifications(prepared: GorushNotification[]): Promise { - for (const batch of chunkArray(prepared, 20)) { - try { - await this.sendToGorush(batch) - } catch (err) { - logger.error({ err, batch }, 'notification push batch failed') - } - } - } - - private async sendToGorush(prepared: GorushNotification[]) { - // if no notifications, skip and return early - if (prepared.length === 0) { - return - } - const pushEndpoint = this.pushEndpoint - await retryHttp(() => - axios.post( - pushEndpoint, - { notifications: prepared }, - { - headers: { - 'content-type': 'application/json', - accept: 'application/json', - }, - }, - ), - ) - } -} - -export class CourierNotificationServer extends NotificationServer { - constructor(public db: Database, public courierClient: CourierClient) { - super(db) - } - - async prepareNotifications( - notifs: NotifRow[], - ): Promise { - const notificationViews = await this.getNotificationViews(notifs) - const notifsToSend = notificationViews.map((n) => { - return new CourierNotification({ - id: getCourierId(n), - recipientDid: n.notif.did, - title: n.title, - message: n.body, - collapseKey: n.key, - alwaysDeliver: !n.rateLimit, - timestamp: Timestamp.fromDate(new Date(n.notif.sortAt)), - additional: Struct.fromJson({ - uri: n.notif.recordUri, - reason: n.notif.reason, - subject: n.notif.reasonSubject || '', - }), - }) - }) - return notifsToSend - } - - async processNotifications(prepared: CourierNotification[]): Promise { - try { - await retryConnect(() => - this.courierClient.pushNotifications({ notifications: prepared }), - ) - } catch (err) { - logger.error({ err }, 'notification push to courier failed') - } - } -} - -const getCourierId = (notif: NotifView) => { - const key = [ - notif.notif.recordUri, - notif.notif.did, - notif.notif.reason, - notif.notif.reasonSubject || '', - ].join('::') - return murmur.v3(key).toString(16) -} - -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 { - 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/pipeline.ts b/packages/bsky/src/pipeline.ts index 7798519bfa2..50f1abfd566 100644 --- a/packages/bsky/src/pipeline.ts +++ b/packages/bsky/src/pipeline.ts @@ -1,22 +1,48 @@ -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, +import { HydrationState } from './hydration/hydrator' + +export function createPipeline( + skeletonFn: (input: SkeletonFnInput) => Promise, + hydrationFn: ( + input: HydrationFnInput, + ) => Promise, + rulesFn: (input: RulesFnInput) => Skeleton, + presentationFn: ( + input: PresentationFnInput, + ) => 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) + const skeleton = await skeletonFn({ ctx, params }) + const hydration = await hydrationFn({ ctx, params, skeleton }) + const appliedRules = rulesFn({ ctx, params, skeleton, hydration }) + return presentationFn({ ctx, params, skeleton: appliedRules, hydration }) } } -export function noRules(state: T) { - return state +export type SkeletonFnInput = { + ctx: Context + params: Params +} + +export type HydrationFnInput = { + ctx: Context + params: Params + skeleton: Skeleton +} + +export type RulesFnInput = { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +} + +export type PresentationFnInput = { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +} + +export function noRules(input: { skeleton: S }) { + return input.skeleton } diff --git a/packages/bsky/src/proto/bsky_connect.ts b/packages/bsky/src/proto/bsky_connect.ts new file mode 100644 index 00000000000..00e2e5b9204 --- /dev/null +++ b/packages/bsky/src/proto/bsky_connect.ts @@ -0,0 +1,920 @@ +// @generated by protoc-gen-connect-es v1.3.0 with parameter "target=ts,import_extension=.ts" +// @generated from file bsky.proto (package bsky, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { + ClearActorMutelistSubscriptionsRequest, + ClearActorMutelistSubscriptionsResponse, + ClearActorMutesRequest, + ClearActorMutesResponse, + CreateActorMutelistSubscriptionRequest, + CreateActorMutelistSubscriptionResponse, + CreateActorMuteRequest, + CreateActorMuteResponse, + DeleteActorMutelistSubscriptionRequest, + DeleteActorMutelistSubscriptionResponse, + DeleteActorMuteRequest, + DeleteActorMuteResponse, + GetActorFeedsRequest, + GetActorFeedsResponse, + GetActorFollowsActorsRequest, + GetActorFollowsActorsResponse, + GetActorLikesRequest, + GetActorLikesResponse, + GetActorListsRequest, + GetActorListsResponse, + GetActorMutesActorRequest, + GetActorMutesActorResponse, + GetActorMutesActorViaListRequest, + GetActorMutesActorViaListResponse, + GetActorRepostsRequest, + GetActorRepostsResponse, + GetActorsRequest, + GetActorsResponse, + GetActorTakedownRequest, + GetActorTakedownResponse, + GetAuthorFeedRequest, + GetAuthorFeedResponse, + GetBidirectionalBlockRequest, + GetBidirectionalBlockResponse, + GetBidirectionalBlockViaListRequest, + GetBidirectionalBlockViaListResponse, + GetBlobTakedownRequest, + GetBlobTakedownResponse, + GetBlockExistenceRequest, + GetBlockExistenceResponse, + GetBlocklistSubscriptionRequest, + GetBlocklistSubscriptionResponse, + GetBlocklistSubscriptionsRequest, + GetBlocklistSubscriptionsResponse, + GetBlockRecordsRequest, + GetBlockRecordsResponse, + GetBlocksRequest, + GetBlocksResponse, + GetCountsForUsersRequest, + GetCountsForUsersResponse, + GetDidsByHandlesRequest, + GetDidsByHandlesResponse, + GetFeedGeneratorRecordsRequest, + GetFeedGeneratorRecordsResponse, + GetFeedGeneratorStatusRequest, + GetFeedGeneratorStatusResponse, + GetFollowersRequest, + GetFollowersResponse, + GetFollowRecordsRequest, + GetFollowRecordsResponse, + GetFollowsRequest, + GetFollowsResponse, + GetFollowSuggestionsRequest, + GetFollowSuggestionsResponse, + GetIdentityByDidRequest, + GetIdentityByDidResponse, + GetIdentityByHandleRequest, + GetIdentityByHandleResponse, + GetInteractionCountsRequest, + GetInteractionCountsResponse, + GetLabelsRequest, + GetLabelsResponse, + GetLatestRevRequest, + GetLatestRevResponse, + GetLikeRecordsRequest, + GetLikeRecordsResponse, + GetLikesByActorAndSubjectsRequest, + GetLikesByActorAndSubjectsResponse, + GetLikesBySubjectRequest, + GetLikesBySubjectResponse, + GetListBlockRecordsRequest, + GetListBlockRecordsResponse, + GetListCountRequest, + GetListCountResponse, + GetListFeedRequest, + GetListFeedResponse, + GetListItemRecordsRequest, + GetListItemRecordsResponse, + GetListMembershipRequest, + GetListMembershipResponse, + GetListMembersRequest, + GetListMembersResponse, + GetListRecordsRequest, + GetListRecordsResponse, + GetMutelistSubscriptionRequest, + GetMutelistSubscriptionResponse, + GetMutelistSubscriptionsRequest, + GetMutelistSubscriptionsResponse, + GetMutesRequest, + GetMutesResponse, + GetNotificationSeenRequest, + GetNotificationSeenResponse, + GetNotificationsRequest, + GetNotificationsResponse, + GetPostRecordsRequest, + GetPostRecordsResponse, + GetPostReplyCountsRequest, + GetPostReplyCountsResponse, + GetProfileRecordsRequest, + GetProfileRecordsResponse, + GetRecordTakedownRequest, + GetRecordTakedownResponse, + GetRelationshipsRequest, + GetRelationshipsResponse, + GetRepostRecordsRequest, + GetRepostRecordsResponse, + GetRepostsByActorAndSubjectsRequest, + GetRepostsByActorAndSubjectsResponse, + GetRepostsBySubjectRequest, + GetRepostsBySubjectResponse, + GetSuggestedEntitiesRequest, + GetSuggestedEntitiesResponse, + GetSuggestedFeedsRequest, + GetSuggestedFeedsResponse, + GetThreadGateRecordsRequest, + GetThreadGateRecordsResponse, + GetThreadRequest, + GetThreadResponse, + GetTimelineRequest, + GetTimelineResponse, + GetUnreadNotificationCountRequest, + GetUnreadNotificationCountResponse, + PingRequest, + PingResponse, + SearchActorsRequest, + SearchActorsResponse, + SearchFeedGeneratorsRequest, + SearchFeedGeneratorsResponse, + SearchPostsRequest, + SearchPostsResponse, + TakedownActorRequest, + TakedownActorResponse, + TakedownBlobRequest, + TakedownBlobResponse, + TakedownRecordRequest, + TakedownRecordResponse, + UntakedownActorRequest, + UntakedownActorResponse, + UntakedownBlobRequest, + UntakedownBlobResponse, + UntakedownRecordRequest, + UntakedownRecordResponse, + UpdateNotificationSeenRequest, + UpdateNotificationSeenResponse, +} from './bsky_pb.ts' +import { MethodKind } from '@bufbuild/protobuf' + +/** + * + * Read Path + * + * + * @generated from service bsky.Service + */ +export const Service = { + typeName: 'bsky.Service', + methods: { + /** + * Records + * + * @generated from rpc bsky.Service.GetBlockRecords + */ + getBlockRecords: { + name: 'GetBlockRecords', + I: GetBlockRecordsRequest, + O: GetBlockRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetFeedGeneratorRecords + */ + getFeedGeneratorRecords: { + name: 'GetFeedGeneratorRecords', + I: GetFeedGeneratorRecordsRequest, + O: GetFeedGeneratorRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetFollowRecords + */ + getFollowRecords: { + name: 'GetFollowRecords', + I: GetFollowRecordsRequest, + O: GetFollowRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetLikeRecords + */ + getLikeRecords: { + name: 'GetLikeRecords', + I: GetLikeRecordsRequest, + O: GetLikeRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetListBlockRecords + */ + getListBlockRecords: { + name: 'GetListBlockRecords', + I: GetListBlockRecordsRequest, + O: GetListBlockRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetListItemRecords + */ + getListItemRecords: { + name: 'GetListItemRecords', + I: GetListItemRecordsRequest, + O: GetListItemRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetListRecords + */ + getListRecords: { + name: 'GetListRecords', + I: GetListRecordsRequest, + O: GetListRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetPostRecords + */ + getPostRecords: { + name: 'GetPostRecords', + I: GetPostRecordsRequest, + O: GetPostRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetProfileRecords + */ + getProfileRecords: { + name: 'GetProfileRecords', + I: GetProfileRecordsRequest, + O: GetProfileRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetRepostRecords + */ + getRepostRecords: { + name: 'GetRepostRecords', + I: GetRepostRecordsRequest, + O: GetRepostRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetThreadGateRecords + */ + getThreadGateRecords: { + name: 'GetThreadGateRecords', + I: GetThreadGateRecordsRequest, + O: GetThreadGateRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * Follows + * + * @generated from rpc bsky.Service.GetActorFollowsActors + */ + getActorFollowsActors: { + name: 'GetActorFollowsActors', + I: GetActorFollowsActorsRequest, + O: GetActorFollowsActorsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetFollowers + */ + getFollowers: { + name: 'GetFollowers', + I: GetFollowersRequest, + O: GetFollowersResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetFollows + */ + getFollows: { + name: 'GetFollows', + I: GetFollowsRequest, + O: GetFollowsResponse, + kind: MethodKind.Unary, + }, + /** + * Likes + * + * @generated from rpc bsky.Service.GetLikesBySubject + */ + getLikesBySubject: { + name: 'GetLikesBySubject', + I: GetLikesBySubjectRequest, + O: GetLikesBySubjectResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetLikesByActorAndSubjects + */ + getLikesByActorAndSubjects: { + name: 'GetLikesByActorAndSubjects', + I: GetLikesByActorAndSubjectsRequest, + O: GetLikesByActorAndSubjectsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetActorLikes + */ + getActorLikes: { + name: 'GetActorLikes', + I: GetActorLikesRequest, + O: GetActorLikesResponse, + kind: MethodKind.Unary, + }, + /** + * Reposts + * + * @generated from rpc bsky.Service.GetRepostsBySubject + */ + getRepostsBySubject: { + name: 'GetRepostsBySubject', + I: GetRepostsBySubjectRequest, + O: GetRepostsBySubjectResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetRepostsByActorAndSubjects + */ + getRepostsByActorAndSubjects: { + name: 'GetRepostsByActorAndSubjects', + I: GetRepostsByActorAndSubjectsRequest, + O: GetRepostsByActorAndSubjectsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetActorReposts + */ + getActorReposts: { + name: 'GetActorReposts', + I: GetActorRepostsRequest, + O: GetActorRepostsResponse, + kind: MethodKind.Unary, + }, + /** + * Interaction Counts + * + * @generated from rpc bsky.Service.GetInteractionCounts + */ + getInteractionCounts: { + name: 'GetInteractionCounts', + I: GetInteractionCountsRequest, + O: GetInteractionCountsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetCountsForUsers + */ + getCountsForUsers: { + name: 'GetCountsForUsers', + I: GetCountsForUsersRequest, + O: GetCountsForUsersResponse, + kind: MethodKind.Unary, + }, + /** + * Profile + * + * @generated from rpc bsky.Service.GetActors + */ + getActors: { + name: 'GetActors', + I: GetActorsRequest, + O: GetActorsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetDidsByHandles + */ + getDidsByHandles: { + name: 'GetDidsByHandles', + I: GetDidsByHandlesRequest, + O: GetDidsByHandlesResponse, + kind: MethodKind.Unary, + }, + /** + * Relationships + * + * @generated from rpc bsky.Service.GetRelationships + */ + getRelationships: { + name: 'GetRelationships', + I: GetRelationshipsRequest, + O: GetRelationshipsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetBlockExistence + */ + getBlockExistence: { + name: 'GetBlockExistence', + I: GetBlockExistenceRequest, + O: GetBlockExistenceResponse, + kind: MethodKind.Unary, + }, + /** + * Lists + * + * @generated from rpc bsky.Service.GetActorLists + */ + getActorLists: { + name: 'GetActorLists', + I: GetActorListsRequest, + O: GetActorListsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetListMembers + */ + getListMembers: { + name: 'GetListMembers', + I: GetListMembersRequest, + O: GetListMembersResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetListMembership + */ + getListMembership: { + name: 'GetListMembership', + I: GetListMembershipRequest, + O: GetListMembershipResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetListCount + */ + getListCount: { + name: 'GetListCount', + I: GetListCountRequest, + O: GetListCountResponse, + kind: MethodKind.Unary, + }, + /** + * Mutes + * + * @generated from rpc bsky.Service.GetActorMutesActor + */ + getActorMutesActor: { + name: 'GetActorMutesActor', + I: GetActorMutesActorRequest, + O: GetActorMutesActorResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetMutes + */ + getMutes: { + name: 'GetMutes', + I: GetMutesRequest, + O: GetMutesResponse, + kind: MethodKind.Unary, + }, + /** + * Mutelists + * + * @generated from rpc bsky.Service.GetActorMutesActorViaList + */ + getActorMutesActorViaList: { + name: 'GetActorMutesActorViaList', + I: GetActorMutesActorViaListRequest, + O: GetActorMutesActorViaListResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetMutelistSubscription + */ + getMutelistSubscription: { + name: 'GetMutelistSubscription', + I: GetMutelistSubscriptionRequest, + O: GetMutelistSubscriptionResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetMutelistSubscriptions + */ + getMutelistSubscriptions: { + name: 'GetMutelistSubscriptions', + I: GetMutelistSubscriptionsRequest, + O: GetMutelistSubscriptionsResponse, + kind: MethodKind.Unary, + }, + /** + * Blocks + * + * @generated from rpc bsky.Service.GetBidirectionalBlock + */ + getBidirectionalBlock: { + name: 'GetBidirectionalBlock', + I: GetBidirectionalBlockRequest, + O: GetBidirectionalBlockResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetBlocks + */ + getBlocks: { + name: 'GetBlocks', + I: GetBlocksRequest, + O: GetBlocksResponse, + kind: MethodKind.Unary, + }, + /** + * Blocklists + * + * @generated from rpc bsky.Service.GetBidirectionalBlockViaList + */ + getBidirectionalBlockViaList: { + name: 'GetBidirectionalBlockViaList', + I: GetBidirectionalBlockViaListRequest, + O: GetBidirectionalBlockViaListResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetBlocklistSubscription + */ + getBlocklistSubscription: { + name: 'GetBlocklistSubscription', + I: GetBlocklistSubscriptionRequest, + O: GetBlocklistSubscriptionResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetBlocklistSubscriptions + */ + getBlocklistSubscriptions: { + name: 'GetBlocklistSubscriptions', + I: GetBlocklistSubscriptionsRequest, + O: GetBlocklistSubscriptionsResponse, + kind: MethodKind.Unary, + }, + /** + * Notifications + * + * @generated from rpc bsky.Service.GetNotifications + */ + getNotifications: { + name: 'GetNotifications', + I: GetNotificationsRequest, + O: GetNotificationsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetNotificationSeen + */ + getNotificationSeen: { + name: 'GetNotificationSeen', + I: GetNotificationSeenRequest, + O: GetNotificationSeenResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetUnreadNotificationCount + */ + getUnreadNotificationCount: { + name: 'GetUnreadNotificationCount', + I: GetUnreadNotificationCountRequest, + O: GetUnreadNotificationCountResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.UpdateNotificationSeen + */ + updateNotificationSeen: { + name: 'UpdateNotificationSeen', + I: UpdateNotificationSeenRequest, + O: UpdateNotificationSeenResponse, + kind: MethodKind.Unary, + }, + /** + * FeedGenerators + * + * @generated from rpc bsky.Service.GetActorFeeds + */ + getActorFeeds: { + name: 'GetActorFeeds', + I: GetActorFeedsRequest, + O: GetActorFeedsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetSuggestedFeeds + */ + getSuggestedFeeds: { + name: 'GetSuggestedFeeds', + I: GetSuggestedFeedsRequest, + O: GetSuggestedFeedsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetFeedGeneratorStatus + */ + getFeedGeneratorStatus: { + name: 'GetFeedGeneratorStatus', + I: GetFeedGeneratorStatusRequest, + O: GetFeedGeneratorStatusResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.SearchFeedGenerators + */ + searchFeedGenerators: { + name: 'SearchFeedGenerators', + I: SearchFeedGeneratorsRequest, + O: SearchFeedGeneratorsResponse, + kind: MethodKind.Unary, + }, + /** + * Feeds + * + * @generated from rpc bsky.Service.GetAuthorFeed + */ + getAuthorFeed: { + name: 'GetAuthorFeed', + I: GetAuthorFeedRequest, + O: GetAuthorFeedResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetTimeline + */ + getTimeline: { + name: 'GetTimeline', + I: GetTimelineRequest, + O: GetTimelineResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetListFeed + */ + getListFeed: { + name: 'GetListFeed', + I: GetListFeedRequest, + O: GetListFeedResponse, + kind: MethodKind.Unary, + }, + /** + * Threads + * + * @generated from rpc bsky.Service.GetThread + */ + getThread: { + name: 'GetThread', + I: GetThreadRequest, + O: GetThreadResponse, + kind: MethodKind.Unary, + }, + /** + * Search + * + * @generated from rpc bsky.Service.SearchActors + */ + searchActors: { + name: 'SearchActors', + I: SearchActorsRequest, + O: SearchActorsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.SearchPosts + */ + searchPosts: { + name: 'SearchPosts', + I: SearchPostsRequest, + O: SearchPostsResponse, + kind: MethodKind.Unary, + }, + /** + * Suggestions + * + * @generated from rpc bsky.Service.GetFollowSuggestions + */ + getFollowSuggestions: { + name: 'GetFollowSuggestions', + I: GetFollowSuggestionsRequest, + O: GetFollowSuggestionsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetSuggestedEntities + */ + getSuggestedEntities: { + name: 'GetSuggestedEntities', + I: GetSuggestedEntitiesRequest, + O: GetSuggestedEntitiesResponse, + kind: MethodKind.Unary, + }, + /** + * Posts + * + * @generated from rpc bsky.Service.GetPostReplyCounts + */ + getPostReplyCounts: { + name: 'GetPostReplyCounts', + I: GetPostReplyCountsRequest, + O: GetPostReplyCountsResponse, + kind: MethodKind.Unary, + }, + /** + * Labels + * + * @generated from rpc bsky.Service.GetLabels + */ + getLabels: { + name: 'GetLabels', + I: GetLabelsRequest, + O: GetLabelsResponse, + kind: MethodKind.Unary, + }, + /** + * Sync + * + * @generated from rpc bsky.Service.GetLatestRev + */ + getLatestRev: { + name: 'GetLatestRev', + I: GetLatestRevRequest, + O: GetLatestRevResponse, + kind: MethodKind.Unary, + }, + /** + * Moderation + * + * @generated from rpc bsky.Service.GetBlobTakedown + */ + getBlobTakedown: { + name: 'GetBlobTakedown', + I: GetBlobTakedownRequest, + O: GetBlobTakedownResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetRecordTakedown + */ + getRecordTakedown: { + name: 'GetRecordTakedown', + I: GetRecordTakedownRequest, + O: GetRecordTakedownResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetActorTakedown + */ + getActorTakedown: { + name: 'GetActorTakedown', + I: GetActorTakedownRequest, + O: GetActorTakedownResponse, + kind: MethodKind.Unary, + }, + /** + * Identity + * + * @generated from rpc bsky.Service.GetIdentityByDid + */ + getIdentityByDid: { + name: 'GetIdentityByDid', + I: GetIdentityByDidRequest, + O: GetIdentityByDidResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetIdentityByHandle + */ + getIdentityByHandle: { + name: 'GetIdentityByHandle', + I: GetIdentityByHandleRequest, + O: GetIdentityByHandleResponse, + kind: MethodKind.Unary, + }, + /** + * Ping + * + * @generated from rpc bsky.Service.Ping + */ + ping: { + name: 'Ping', + I: PingRequest, + O: PingResponse, + kind: MethodKind.Unary, + }, + /** + * Moderation + * + * @generated from rpc bsky.Service.TakedownBlob + */ + takedownBlob: { + name: 'TakedownBlob', + I: TakedownBlobRequest, + O: TakedownBlobResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.TakedownRecord + */ + takedownRecord: { + name: 'TakedownRecord', + I: TakedownRecordRequest, + O: TakedownRecordResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.TakedownActor + */ + takedownActor: { + name: 'TakedownActor', + I: TakedownActorRequest, + O: TakedownActorResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.UntakedownBlob + */ + untakedownBlob: { + name: 'UntakedownBlob', + I: UntakedownBlobRequest, + O: UntakedownBlobResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.UntakedownRecord + */ + untakedownRecord: { + name: 'UntakedownRecord', + I: UntakedownRecordRequest, + O: UntakedownRecordResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.UntakedownActor + */ + untakedownActor: { + name: 'UntakedownActor', + I: UntakedownActorRequest, + O: UntakedownActorResponse, + kind: MethodKind.Unary, + }, + /** + * Ingestion + * + * @generated from rpc bsky.Service.CreateActorMute + */ + createActorMute: { + name: 'CreateActorMute', + I: CreateActorMuteRequest, + O: CreateActorMuteResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.DeleteActorMute + */ + deleteActorMute: { + name: 'DeleteActorMute', + I: DeleteActorMuteRequest, + O: DeleteActorMuteResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.ClearActorMutes + */ + clearActorMutes: { + name: 'ClearActorMutes', + I: ClearActorMutesRequest, + O: ClearActorMutesResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.CreateActorMutelistSubscription + */ + createActorMutelistSubscription: { + name: 'CreateActorMutelistSubscription', + I: CreateActorMutelistSubscriptionRequest, + O: CreateActorMutelistSubscriptionResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.DeleteActorMutelistSubscription + */ + deleteActorMutelistSubscription: { + name: 'DeleteActorMutelistSubscription', + I: DeleteActorMutelistSubscriptionRequest, + O: DeleteActorMutelistSubscriptionResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.ClearActorMutelistSubscriptions + */ + clearActorMutelistSubscriptions: { + name: 'ClearActorMutelistSubscriptions', + I: ClearActorMutelistSubscriptionsRequest, + O: ClearActorMutelistSubscriptionsResponse, + kind: MethodKind.Unary, + }, + }, +} as const diff --git a/packages/bsky/src/proto/bsky_pb.ts b/packages/bsky/src/proto/bsky_pb.ts new file mode 100644 index 00000000000..7c5ddcf1865 --- /dev/null +++ b/packages/bsky/src/proto/bsky_pb.ts @@ -0,0 +1,10619 @@ +// @generated by protoc-gen-es v1.6.0 with parameter "target=ts,import_extension=.ts" +// @generated from file bsky.proto (package bsky, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import type { + BinaryReadOptions, + FieldList, + JsonReadOptions, + JsonValue, + PartialMessage, + PlainMessage, +} from '@bufbuild/protobuf' +import { Message, proto3, protoInt64, Timestamp } from '@bufbuild/protobuf' + +/** + * @generated from enum bsky.FeedType + */ +export enum FeedType { + /** + * @generated from enum value: FEED_TYPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: FEED_TYPE_POSTS_AND_AUTHOR_THREADS = 1; + */ + POSTS_AND_AUTHOR_THREADS = 1, + + /** + * @generated from enum value: FEED_TYPE_POSTS_NO_REPLIES = 2; + */ + POSTS_NO_REPLIES = 2, + + /** + * @generated from enum value: FEED_TYPE_POSTS_WITH_MEDIA = 3; + */ + POSTS_WITH_MEDIA = 3, +} +// Retrieve enum metadata with: proto3.getEnumType(FeedType) +proto3.util.setEnumType(FeedType, 'bsky.FeedType', [ + { no: 0, name: 'FEED_TYPE_UNSPECIFIED' }, + { no: 1, name: 'FEED_TYPE_POSTS_AND_AUTHOR_THREADS' }, + { no: 2, name: 'FEED_TYPE_POSTS_NO_REPLIES' }, + { no: 3, name: 'FEED_TYPE_POSTS_WITH_MEDIA' }, +]) + +/** + * @generated from message bsky.Record + */ +export class Record extends Message { + /** + * @generated from field: bytes record = 1; + */ + record = new Uint8Array(0) + + /** + * @generated from field: string cid = 2; + */ + cid = '' + + /** + * @generated from field: google.protobuf.Timestamp indexed_at = 4; + */ + indexedAt?: Timestamp + + /** + * @generated from field: bool taken_down = 5; + */ + takenDown = false + + /** + * @generated from field: google.protobuf.Timestamp created_at = 6; + */ + createdAt?: Timestamp + + /** + * @generated from field: google.protobuf.Timestamp sorted_at = 7; + */ + sortedAt?: Timestamp + + /** + * @generated from field: string takedown_ref = 8; + */ + takedownRef = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.Record' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'record', kind: 'scalar', T: 12 /* ScalarType.BYTES */ }, + { no: 2, name: 'cid', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 4, name: 'indexed_at', kind: 'message', T: Timestamp }, + { no: 5, name: 'taken_down', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { no: 6, name: 'created_at', kind: 'message', T: Timestamp }, + { no: 7, name: 'sorted_at', kind: 'message', T: Timestamp }, + { + no: 8, + name: 'takedown_ref', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): Record { + return new Record().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): Record { + return new Record().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): Record { + return new Record().fromJsonString(jsonString, options) + } + + static equals( + a: Record | PlainMessage | undefined, + b: Record | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(Record, a, b) + } +} + +/** + * @generated from message bsky.GetBlockRecordsRequest + */ +export class GetBlockRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlockRecordsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlockRecordsRequest { + return new GetBlockRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlockRecordsRequest { + return new GetBlockRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlockRecordsRequest { + return new GetBlockRecordsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetBlockRecordsRequest + | PlainMessage + | undefined, + b: + | GetBlockRecordsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBlockRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetBlockRecordsResponse + */ +export class GetBlockRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlockRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlockRecordsResponse { + return new GetBlockRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlockRecordsResponse { + return new GetBlockRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlockRecordsResponse { + return new GetBlockRecordsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetBlockRecordsResponse + | PlainMessage + | undefined, + b: + | GetBlockRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBlockRecordsResponse, a, b) + } +} + +/** + * @generated from message bsky.GetFeedGeneratorRecordsRequest + */ +export class GetFeedGeneratorRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFeedGeneratorRecordsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFeedGeneratorRecordsRequest { + return new GetFeedGeneratorRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFeedGeneratorRecordsRequest { + return new GetFeedGeneratorRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFeedGeneratorRecordsRequest { + return new GetFeedGeneratorRecordsRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetFeedGeneratorRecordsRequest + | PlainMessage + | undefined, + b: + | GetFeedGeneratorRecordsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetFeedGeneratorRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetFeedGeneratorRecordsResponse + */ +export class GetFeedGeneratorRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFeedGeneratorRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFeedGeneratorRecordsResponse { + return new GetFeedGeneratorRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFeedGeneratorRecordsResponse { + return new GetFeedGeneratorRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFeedGeneratorRecordsResponse { + return new GetFeedGeneratorRecordsResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetFeedGeneratorRecordsResponse + | PlainMessage + | undefined, + b: + | GetFeedGeneratorRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetFeedGeneratorRecordsResponse, a, b) + } +} + +/** + * @generated from message bsky.GetFollowRecordsRequest + */ +export class GetFollowRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFollowRecordsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFollowRecordsRequest { + return new GetFollowRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFollowRecordsRequest { + return new GetFollowRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFollowRecordsRequest { + return new GetFollowRecordsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetFollowRecordsRequest + | PlainMessage + | undefined, + b: + | GetFollowRecordsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetFollowRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetFollowRecordsResponse + */ +export class GetFollowRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFollowRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFollowRecordsResponse { + return new GetFollowRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFollowRecordsResponse { + return new GetFollowRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFollowRecordsResponse { + return new GetFollowRecordsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetFollowRecordsResponse + | PlainMessage + | undefined, + b: + | GetFollowRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetFollowRecordsResponse, a, b) + } +} + +/** + * @generated from message bsky.GetLikeRecordsRequest + */ +export class GetLikeRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetLikeRecordsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetLikeRecordsRequest { + return new GetLikeRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetLikeRecordsRequest { + return new GetLikeRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetLikeRecordsRequest { + return new GetLikeRecordsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetLikeRecordsRequest | PlainMessage | undefined, + b: GetLikeRecordsRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetLikeRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetLikeRecordsResponse + */ +export class GetLikeRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetLikeRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetLikeRecordsResponse { + return new GetLikeRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetLikeRecordsResponse { + return new GetLikeRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetLikeRecordsResponse { + return new GetLikeRecordsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetLikeRecordsResponse + | PlainMessage + | undefined, + b: + | GetLikeRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetLikeRecordsResponse, a, b) + } +} + +/** + * @generated from message bsky.GetListBlockRecordsRequest + */ +export class GetListBlockRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListBlockRecordsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListBlockRecordsRequest { + return new GetListBlockRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListBlockRecordsRequest { + return new GetListBlockRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListBlockRecordsRequest { + return new GetListBlockRecordsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetListBlockRecordsRequest + | PlainMessage + | undefined, + b: + | GetListBlockRecordsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetListBlockRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetListBlockRecordsResponse + */ +export class GetListBlockRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListBlockRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListBlockRecordsResponse { + return new GetListBlockRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListBlockRecordsResponse { + return new GetListBlockRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListBlockRecordsResponse { + return new GetListBlockRecordsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetListBlockRecordsResponse + | PlainMessage + | undefined, + b: + | GetListBlockRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetListBlockRecordsResponse, a, b) + } +} + +/** + * @generated from message bsky.GetListItemRecordsRequest + */ +export class GetListItemRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListItemRecordsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListItemRecordsRequest { + return new GetListItemRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListItemRecordsRequest { + return new GetListItemRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListItemRecordsRequest { + return new GetListItemRecordsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetListItemRecordsRequest + | PlainMessage + | undefined, + b: + | GetListItemRecordsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetListItemRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetListItemRecordsResponse + */ +export class GetListItemRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListItemRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListItemRecordsResponse { + return new GetListItemRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListItemRecordsResponse { + return new GetListItemRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListItemRecordsResponse { + return new GetListItemRecordsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetListItemRecordsResponse + | PlainMessage + | undefined, + b: + | GetListItemRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetListItemRecordsResponse, a, b) + } +} + +/** + * @generated from message bsky.GetListRecordsRequest + */ +export class GetListRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListRecordsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListRecordsRequest { + return new GetListRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListRecordsRequest { + return new GetListRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListRecordsRequest { + return new GetListRecordsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetListRecordsRequest | PlainMessage | undefined, + b: GetListRecordsRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetListRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetListRecordsResponse + */ +export class GetListRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListRecordsResponse { + return new GetListRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListRecordsResponse { + return new GetListRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListRecordsResponse { + return new GetListRecordsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetListRecordsResponse + | PlainMessage + | undefined, + b: + | GetListRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetListRecordsResponse, a, b) + } +} + +/** + * @generated from message bsky.PostRecordMeta + */ +export class PostRecordMeta extends Message { + /** + * @generated from field: bool violates_thread_gate = 1; + */ + violatesThreadGate = false + + /** + * @generated from field: bool has_media = 2; + */ + hasMedia = false + + /** + * @generated from field: bool is_reply = 3; + */ + isReply = false + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.PostRecordMeta' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'violates_thread_gate', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + { no: 2, name: 'has_media', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { no: 3, name: 'is_reply', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): PostRecordMeta { + return new PostRecordMeta().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): PostRecordMeta { + return new PostRecordMeta().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): PostRecordMeta { + return new PostRecordMeta().fromJsonString(jsonString, options) + } + + static equals( + a: PostRecordMeta | PlainMessage | undefined, + b: PostRecordMeta | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(PostRecordMeta, a, b) + } +} + +/** + * @generated from message bsky.GetPostRecordsRequest + */ +export class GetPostRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetPostRecordsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetPostRecordsRequest { + return new GetPostRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetPostRecordsRequest { + return new GetPostRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetPostRecordsRequest { + return new GetPostRecordsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetPostRecordsRequest | PlainMessage | undefined, + b: GetPostRecordsRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetPostRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetPostRecordsResponse + */ +export class GetPostRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = [] + + /** + * @generated from field: repeated bsky.PostRecordMeta meta = 2; + */ + meta: PostRecordMeta[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetPostRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + { no: 2, name: 'meta', kind: 'message', T: PostRecordMeta, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetPostRecordsResponse { + return new GetPostRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetPostRecordsResponse { + return new GetPostRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetPostRecordsResponse { + return new GetPostRecordsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetPostRecordsResponse + | PlainMessage + | undefined, + b: + | GetPostRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetPostRecordsResponse, a, b) + } +} + +/** + * @generated from message bsky.GetProfileRecordsRequest + */ +export class GetProfileRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetProfileRecordsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetProfileRecordsRequest { + return new GetProfileRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetProfileRecordsRequest { + return new GetProfileRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetProfileRecordsRequest { + return new GetProfileRecordsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetProfileRecordsRequest + | PlainMessage + | undefined, + b: + | GetProfileRecordsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetProfileRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetProfileRecordsResponse + */ +export class GetProfileRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetProfileRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetProfileRecordsResponse { + return new GetProfileRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetProfileRecordsResponse { + return new GetProfileRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetProfileRecordsResponse { + return new GetProfileRecordsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetProfileRecordsResponse + | PlainMessage + | undefined, + b: + | GetProfileRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetProfileRecordsResponse, a, b) + } +} + +/** + * @generated from message bsky.GetRepostRecordsRequest + */ +export class GetRepostRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetRepostRecordsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetRepostRecordsRequest { + return new GetRepostRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetRepostRecordsRequest { + return new GetRepostRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetRepostRecordsRequest { + return new GetRepostRecordsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetRepostRecordsRequest + | PlainMessage + | undefined, + b: + | GetRepostRecordsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetRepostRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetRepostRecordsResponse + */ +export class GetRepostRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetRepostRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetRepostRecordsResponse { + return new GetRepostRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetRepostRecordsResponse { + return new GetRepostRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetRepostRecordsResponse { + return new GetRepostRecordsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetRepostRecordsResponse + | PlainMessage + | undefined, + b: + | GetRepostRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetRepostRecordsResponse, a, b) + } +} + +/** + * @generated from message bsky.GetThreadGateRecordsRequest + */ +export class GetThreadGateRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetThreadGateRecordsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetThreadGateRecordsRequest { + return new GetThreadGateRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetThreadGateRecordsRequest { + return new GetThreadGateRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetThreadGateRecordsRequest { + return new GetThreadGateRecordsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetThreadGateRecordsRequest + | PlainMessage + | undefined, + b: + | GetThreadGateRecordsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetThreadGateRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetThreadGateRecordsResponse + */ +export class GetThreadGateRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetThreadGateRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetThreadGateRecordsResponse { + return new GetThreadGateRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetThreadGateRecordsResponse { + return new GetThreadGateRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetThreadGateRecordsResponse { + return new GetThreadGateRecordsResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetThreadGateRecordsResponse + | PlainMessage + | undefined, + b: + | GetThreadGateRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetThreadGateRecordsResponse, a, b) + } +} + +/** + * - Return follow uris where user A follows users B, C, D, … + * - E.g. for viewer state on `getProfiles` + * + * @generated from message bsky.GetActorFollowsActorsRequest + */ +export class GetActorFollowsActorsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: repeated string target_dids = 2; + */ + targetDids: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorFollowsActorsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 2, + name: 'target_dids', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorFollowsActorsRequest { + return new GetActorFollowsActorsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorFollowsActorsRequest { + return new GetActorFollowsActorsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorFollowsActorsRequest { + return new GetActorFollowsActorsRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetActorFollowsActorsRequest + | PlainMessage + | undefined, + b: + | GetActorFollowsActorsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetActorFollowsActorsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetActorFollowsActorsResponse + */ +export class GetActorFollowsActorsResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorFollowsActorsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorFollowsActorsResponse { + return new GetActorFollowsActorsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorFollowsActorsResponse { + return new GetActorFollowsActorsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorFollowsActorsResponse { + return new GetActorFollowsActorsResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetActorFollowsActorsResponse + | PlainMessage + | undefined, + b: + | GetActorFollowsActorsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetActorFollowsActorsResponse, a, b) + } +} + +/** + * - Return follow uris of users who follows user A + * - For `getFollowers` list + * + * @generated from message bsky.GetFollowersRequest + */ +export class GetFollowersRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFollowersRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFollowersRequest { + return new GetFollowersRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFollowersRequest { + return new GetFollowersRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFollowersRequest { + return new GetFollowersRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetFollowersRequest | PlainMessage | undefined, + b: GetFollowersRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetFollowersRequest, a, b) + } +} + +/** + * @generated from message bsky.FollowInfo + */ +export class FollowInfo extends Message { + /** + * @generated from field: string uri = 1; + */ + uri = '' + + /** + * @generated from field: string actor_did = 2; + */ + actorDid = '' + + /** + * @generated from field: string subject_did = 3; + */ + subjectDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.FollowInfo' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 3, + name: 'subject_did', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): FollowInfo { + return new FollowInfo().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): FollowInfo { + return new FollowInfo().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): FollowInfo { + return new FollowInfo().fromJsonString(jsonString, options) + } + + static equals( + a: FollowInfo | PlainMessage | undefined, + b: FollowInfo | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(FollowInfo, a, b) + } +} + +/** + * @generated from message bsky.GetFollowersResponse + */ +export class GetFollowersResponse extends Message { + /** + * @generated from field: repeated bsky.FollowInfo followers = 1; + */ + followers: FollowInfo[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFollowersResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'followers', + kind: 'message', + T: FollowInfo, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFollowersResponse { + return new GetFollowersResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFollowersResponse { + return new GetFollowersResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFollowersResponse { + return new GetFollowersResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetFollowersResponse | PlainMessage | undefined, + b: GetFollowersResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetFollowersResponse, a, b) + } +} + +/** + * - Return follow uris of users A follows + * - For `getFollows` list + * + * @generated from message bsky.GetFollowsRequest + */ +export class GetFollowsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFollowsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFollowsRequest { + return new GetFollowsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFollowsRequest { + return new GetFollowsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFollowsRequest { + return new GetFollowsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetFollowsRequest | PlainMessage | undefined, + b: GetFollowsRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetFollowsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetFollowsResponse + */ +export class GetFollowsResponse extends Message { + /** + * @generated from field: repeated bsky.FollowInfo follows = 1; + */ + follows: FollowInfo[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFollowsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'follows', kind: 'message', T: FollowInfo, repeated: true }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFollowsResponse { + return new GetFollowsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFollowsResponse { + return new GetFollowsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFollowsResponse { + return new GetFollowsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetFollowsResponse | PlainMessage | undefined, + b: GetFollowsResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetFollowsResponse, a, b) + } +} + +/** + * - return like uris where subject uri is subject A + * - `getLikes` list for a post + * + * @generated from message bsky.GetLikesBySubjectRequest + */ +export class GetLikesBySubjectRequest extends Message { + /** + * @generated from field: bsky.RecordRef subject = 1; + */ + subject?: RecordRef + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetLikesBySubjectRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'subject', kind: 'message', T: RecordRef }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetLikesBySubjectRequest { + return new GetLikesBySubjectRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetLikesBySubjectRequest { + return new GetLikesBySubjectRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetLikesBySubjectRequest { + return new GetLikesBySubjectRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetLikesBySubjectRequest + | PlainMessage + | undefined, + b: + | GetLikesBySubjectRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetLikesBySubjectRequest, a, b) + } +} + +/** + * @generated from message bsky.GetLikesBySubjectResponse + */ +export class GetLikesBySubjectResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetLikesBySubjectResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetLikesBySubjectResponse { + return new GetLikesBySubjectResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetLikesBySubjectResponse { + return new GetLikesBySubjectResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetLikesBySubjectResponse { + return new GetLikesBySubjectResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetLikesBySubjectResponse + | PlainMessage + | undefined, + b: + | GetLikesBySubjectResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetLikesBySubjectResponse, a, b) + } +} + +/** + * - return like uris for user A on subject B, C, D... + * - viewer state on posts + * + * @generated from message bsky.GetLikesByActorAndSubjectsRequest + */ +export class GetLikesByActorAndSubjectsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: repeated bsky.RecordRef refs = 2; + */ + refs: RecordRef[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetLikesByActorAndSubjectsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'refs', kind: 'message', T: RecordRef, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetLikesByActorAndSubjectsRequest { + return new GetLikesByActorAndSubjectsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetLikesByActorAndSubjectsRequest { + return new GetLikesByActorAndSubjectsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetLikesByActorAndSubjectsRequest { + return new GetLikesByActorAndSubjectsRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetLikesByActorAndSubjectsRequest + | PlainMessage + | undefined, + b: + | GetLikesByActorAndSubjectsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetLikesByActorAndSubjectsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetLikesByActorAndSubjectsResponse + */ +export class GetLikesByActorAndSubjectsResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetLikesByActorAndSubjectsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetLikesByActorAndSubjectsResponse { + return new GetLikesByActorAndSubjectsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetLikesByActorAndSubjectsResponse { + return new GetLikesByActorAndSubjectsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetLikesByActorAndSubjectsResponse { + return new GetLikesByActorAndSubjectsResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetLikesByActorAndSubjectsResponse + | PlainMessage + | undefined, + b: + | GetLikesByActorAndSubjectsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetLikesByActorAndSubjectsResponse, a, b) + } +} + +/** + * - return recent like uris for user A + * - `getActorLikes` list for a user + * + * @generated from message bsky.GetActorLikesRequest + */ +export class GetActorLikesRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorLikesRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorLikesRequest { + return new GetActorLikesRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorLikesRequest { + return new GetActorLikesRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorLikesRequest { + return new GetActorLikesRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetActorLikesRequest | PlainMessage | undefined, + b: GetActorLikesRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetActorLikesRequest, a, b) + } +} + +/** + * @generated from message bsky.LikeInfo + */ +export class LikeInfo extends Message { + /** + * @generated from field: string uri = 1; + */ + uri = '' + + /** + * @generated from field: string subject = 2; + */ + subject = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.LikeInfo' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'subject', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): LikeInfo { + return new LikeInfo().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): LikeInfo { + return new LikeInfo().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): LikeInfo { + return new LikeInfo().fromJsonString(jsonString, options) + } + + static equals( + a: LikeInfo | PlainMessage | undefined, + b: LikeInfo | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(LikeInfo, a, b) + } +} + +/** + * @generated from message bsky.GetActorLikesResponse + */ +export class GetActorLikesResponse extends Message { + /** + * @generated from field: repeated bsky.LikeInfo likes = 1; + */ + likes: LikeInfo[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorLikesResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'likes', kind: 'message', T: LikeInfo, repeated: true }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorLikesResponse { + return new GetActorLikesResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorLikesResponse { + return new GetActorLikesResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorLikesResponse { + return new GetActorLikesResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetActorLikesResponse | PlainMessage | undefined, + b: GetActorLikesResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetActorLikesResponse, a, b) + } +} + +/** + * + * Interactions + * + * + * @generated from message bsky.GetInteractionCountsRequest + */ +export class GetInteractionCountsRequest extends Message { + /** + * @generated from field: repeated bsky.RecordRef refs = 1; + */ + refs: RecordRef[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetInteractionCountsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'refs', kind: 'message', T: RecordRef, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetInteractionCountsRequest { + return new GetInteractionCountsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetInteractionCountsRequest { + return new GetInteractionCountsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetInteractionCountsRequest { + return new GetInteractionCountsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetInteractionCountsRequest + | PlainMessage + | undefined, + b: + | GetInteractionCountsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetInteractionCountsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetInteractionCountsResponse + */ +export class GetInteractionCountsResponse extends Message { + /** + * @generated from field: repeated int32 likes = 1; + */ + likes: number[] = [] + + /** + * @generated from field: repeated int32 reposts = 2; + */ + reposts: number[] = [] + + /** + * @generated from field: repeated int32 replies = 3; + */ + replies: number[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetInteractionCountsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'likes', + kind: 'scalar', + T: 5 /* ScalarType.INT32 */, + repeated: true, + }, + { + no: 2, + name: 'reposts', + kind: 'scalar', + T: 5 /* ScalarType.INT32 */, + repeated: true, + }, + { + no: 3, + name: 'replies', + kind: 'scalar', + T: 5 /* ScalarType.INT32 */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetInteractionCountsResponse { + return new GetInteractionCountsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetInteractionCountsResponse { + return new GetInteractionCountsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetInteractionCountsResponse { + return new GetInteractionCountsResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetInteractionCountsResponse + | PlainMessage + | undefined, + b: + | GetInteractionCountsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetInteractionCountsResponse, a, b) + } +} + +/** + * @generated from message bsky.GetCountsForUsersRequest + */ +export class GetCountsForUsersRequest extends Message { + /** + * @generated from field: repeated string dids = 1; + */ + dids: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetCountsForUsersRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'dids', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetCountsForUsersRequest { + return new GetCountsForUsersRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetCountsForUsersRequest { + return new GetCountsForUsersRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetCountsForUsersRequest { + return new GetCountsForUsersRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetCountsForUsersRequest + | PlainMessage + | undefined, + b: + | GetCountsForUsersRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetCountsForUsersRequest, a, b) + } +} + +/** + * @generated from message bsky.GetCountsForUsersResponse + */ +export class GetCountsForUsersResponse extends Message { + /** + * @generated from field: repeated int32 posts = 1; + */ + posts: number[] = [] + + /** + * @generated from field: repeated int32 reposts = 2; + */ + reposts: number[] = [] + + /** + * @generated from field: repeated int32 following = 3; + */ + following: number[] = [] + + /** + * @generated from field: repeated int32 followers = 4; + */ + followers: number[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetCountsForUsersResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'posts', + kind: 'scalar', + T: 5 /* ScalarType.INT32 */, + repeated: true, + }, + { + no: 2, + name: 'reposts', + kind: 'scalar', + T: 5 /* ScalarType.INT32 */, + repeated: true, + }, + { + no: 3, + name: 'following', + kind: 'scalar', + T: 5 /* ScalarType.INT32 */, + repeated: true, + }, + { + no: 4, + name: 'followers', + kind: 'scalar', + T: 5 /* ScalarType.INT32 */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetCountsForUsersResponse { + return new GetCountsForUsersResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetCountsForUsersResponse { + return new GetCountsForUsersResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetCountsForUsersResponse { + return new GetCountsForUsersResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetCountsForUsersResponse + | PlainMessage + | undefined, + b: + | GetCountsForUsersResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetCountsForUsersResponse, a, b) + } +} + +/** + * - return repost uris where subject uri is subject A + * - `getReposts` list for a post + * + * @generated from message bsky.GetRepostsBySubjectRequest + */ +export class GetRepostsBySubjectRequest extends Message { + /** + * @generated from field: bsky.RecordRef subject = 1; + */ + subject?: RecordRef + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetRepostsBySubjectRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'subject', kind: 'message', T: RecordRef }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetRepostsBySubjectRequest { + return new GetRepostsBySubjectRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetRepostsBySubjectRequest { + return new GetRepostsBySubjectRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetRepostsBySubjectRequest { + return new GetRepostsBySubjectRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetRepostsBySubjectRequest + | PlainMessage + | undefined, + b: + | GetRepostsBySubjectRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetRepostsBySubjectRequest, a, b) + } +} + +/** + * @generated from message bsky.GetRepostsBySubjectResponse + */ +export class GetRepostsBySubjectResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetRepostsBySubjectResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetRepostsBySubjectResponse { + return new GetRepostsBySubjectResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetRepostsBySubjectResponse { + return new GetRepostsBySubjectResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetRepostsBySubjectResponse { + return new GetRepostsBySubjectResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetRepostsBySubjectResponse + | PlainMessage + | undefined, + b: + | GetRepostsBySubjectResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetRepostsBySubjectResponse, a, b) + } +} + +/** + * - return repost uris for user A on subject B, C, D... + * - viewer state on posts + * + * @generated from message bsky.GetRepostsByActorAndSubjectsRequest + */ +export class GetRepostsByActorAndSubjectsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: repeated bsky.RecordRef refs = 2; + */ + refs: RecordRef[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetRepostsByActorAndSubjectsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'refs', kind: 'message', T: RecordRef, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetRepostsByActorAndSubjectsRequest { + return new GetRepostsByActorAndSubjectsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetRepostsByActorAndSubjectsRequest { + return new GetRepostsByActorAndSubjectsRequest().fromJson( + jsonValue, + options, + ) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetRepostsByActorAndSubjectsRequest { + return new GetRepostsByActorAndSubjectsRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetRepostsByActorAndSubjectsRequest + | PlainMessage + | undefined, + b: + | GetRepostsByActorAndSubjectsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetRepostsByActorAndSubjectsRequest, a, b) + } +} + +/** + * @generated from message bsky.RecordRef + */ +export class RecordRef extends Message { + /** + * @generated from field: string uri = 1; + */ + uri = '' + + /** + * @generated from field: string cid = 2; + */ + cid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.RecordRef' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'cid', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): RecordRef { + return new RecordRef().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): RecordRef { + return new RecordRef().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): RecordRef { + return new RecordRef().fromJsonString(jsonString, options) + } + + static equals( + a: RecordRef | PlainMessage | undefined, + b: RecordRef | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(RecordRef, a, b) + } +} + +/** + * @generated from message bsky.GetRepostsByActorAndSubjectsResponse + */ +export class GetRepostsByActorAndSubjectsResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetRepostsByActorAndSubjectsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetRepostsByActorAndSubjectsResponse { + return new GetRepostsByActorAndSubjectsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetRepostsByActorAndSubjectsResponse { + return new GetRepostsByActorAndSubjectsResponse().fromJson( + jsonValue, + options, + ) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetRepostsByActorAndSubjectsResponse { + return new GetRepostsByActorAndSubjectsResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetRepostsByActorAndSubjectsResponse + | PlainMessage + | undefined, + b: + | GetRepostsByActorAndSubjectsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetRepostsByActorAndSubjectsResponse, a, b) + } +} + +/** + * - return recent repost uris for user A + * - `getActorReposts` list for a user + * + * @generated from message bsky.GetActorRepostsRequest + */ +export class GetActorRepostsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorRepostsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorRepostsRequest { + return new GetActorRepostsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorRepostsRequest { + return new GetActorRepostsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorRepostsRequest { + return new GetActorRepostsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetActorRepostsRequest + | PlainMessage + | undefined, + b: + | GetActorRepostsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetActorRepostsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetActorRepostsResponse + */ +export class GetActorRepostsResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorRepostsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorRepostsResponse { + return new GetActorRepostsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorRepostsResponse { + return new GetActorRepostsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorRepostsResponse { + return new GetActorRepostsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetActorRepostsResponse + | PlainMessage + | undefined, + b: + | GetActorRepostsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetActorRepostsResponse, a, b) + } +} + +/** + * - return actor information for dids A, B, C… + * - profile hydration + * - should this include handles? apply repo takedown? + * + * @generated from message bsky.GetActorsRequest + */ +export class GetActorsRequest extends Message { + /** + * @generated from field: repeated string dids = 1; + */ + dids: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'dids', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorsRequest { + return new GetActorsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorsRequest { + return new GetActorsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorsRequest { + return new GetActorsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetActorsRequest | PlainMessage | undefined, + b: GetActorsRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetActorsRequest, a, b) + } +} + +/** + * @generated from message bsky.ActorInfo + */ +export class ActorInfo extends Message { + /** + * @generated from field: bool exists = 1; + */ + exists = false + + /** + * @generated from field: string handle = 2; + */ + handle = '' + + /** + * @generated from field: bsky.Record profile = 3; + */ + profile?: Record + + /** + * @generated from field: bool taken_down = 4; + */ + takenDown = false + + /** + * @generated from field: string takedown_ref = 5; + */ + takedownRef = '' + + /** + * @generated from field: google.protobuf.Timestamp tombstoned_at = 6; + */ + tombstonedAt?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.ActorInfo' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'exists', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { no: 2, name: 'handle', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'profile', kind: 'message', T: Record }, + { no: 4, name: 'taken_down', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { + no: 5, + name: 'takedown_ref', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + { no: 6, name: 'tombstoned_at', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): ActorInfo { + return new ActorInfo().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): ActorInfo { + return new ActorInfo().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): ActorInfo { + return new ActorInfo().fromJsonString(jsonString, options) + } + + static equals( + a: ActorInfo | PlainMessage | undefined, + b: ActorInfo | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(ActorInfo, a, b) + } +} + +/** + * @generated from message bsky.GetActorsResponse + */ +export class GetActorsResponse extends Message { + /** + * @generated from field: repeated bsky.ActorInfo actors = 1; + */ + actors: ActorInfo[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actors', kind: 'message', T: ActorInfo, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorsResponse { + return new GetActorsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorsResponse { + return new GetActorsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorsResponse { + return new GetActorsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetActorsResponse | PlainMessage | undefined, + b: GetActorsResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetActorsResponse, a, b) + } +} + +/** + * - return did for handle A + * - `resolveHandle` + * - answering queries where the query param is a handle + * + * @generated from message bsky.GetDidsByHandlesRequest + */ +export class GetDidsByHandlesRequest extends Message { + /** + * @generated from field: repeated string handles = 1; + */ + handles: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetDidsByHandlesRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'handles', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetDidsByHandlesRequest { + return new GetDidsByHandlesRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetDidsByHandlesRequest { + return new GetDidsByHandlesRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetDidsByHandlesRequest { + return new GetDidsByHandlesRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetDidsByHandlesRequest + | PlainMessage + | undefined, + b: + | GetDidsByHandlesRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetDidsByHandlesRequest, a, b) + } +} + +/** + * @generated from message bsky.GetDidsByHandlesResponse + */ +export class GetDidsByHandlesResponse extends Message { + /** + * @generated from field: repeated string dids = 1; + */ + dids: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetDidsByHandlesResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'dids', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetDidsByHandlesResponse { + return new GetDidsByHandlesResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetDidsByHandlesResponse { + return new GetDidsByHandlesResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetDidsByHandlesResponse { + return new GetDidsByHandlesResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetDidsByHandlesResponse + | PlainMessage + | undefined, + b: + | GetDidsByHandlesResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetDidsByHandlesResponse, a, b) + } +} + +/** + * - return relationships between user A and users B, C, D... + * - profile hydration + * - block application + * + * @generated from message bsky.GetRelationshipsRequest + */ +export class GetRelationshipsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: repeated string target_dids = 2; + */ + targetDids: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetRelationshipsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 2, + name: 'target_dids', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetRelationshipsRequest { + return new GetRelationshipsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetRelationshipsRequest { + return new GetRelationshipsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetRelationshipsRequest { + return new GetRelationshipsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetRelationshipsRequest + | PlainMessage + | undefined, + b: + | GetRelationshipsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetRelationshipsRequest, a, b) + } +} + +/** + * @generated from message bsky.Relationships + */ +export class Relationships extends Message { + /** + * @generated from field: bool muted = 1; + */ + muted = false + + /** + * @generated from field: string muted_by_list = 2; + */ + mutedByList = '' + + /** + * @generated from field: string blocked_by = 3; + */ + blockedBy = '' + + /** + * @generated from field: string blocking = 4; + */ + blocking = '' + + /** + * @generated from field: string blocked_by_list = 5; + */ + blockedByList = '' + + /** + * @generated from field: string blocking_by_list = 6; + */ + blockingByList = '' + + /** + * @generated from field: string following = 7; + */ + following = '' + + /** + * @generated from field: string followed_by = 8; + */ + followedBy = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.Relationships' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'muted', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { + no: 2, + name: 'muted_by_list', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + { no: 3, name: 'blocked_by', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 4, name: 'blocking', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 5, + name: 'blocked_by_list', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + { + no: 6, + name: 'blocking_by_list', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + { no: 7, name: 'following', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 8, + name: 'followed_by', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): Relationships { + return new Relationships().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): Relationships { + return new Relationships().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): Relationships { + return new Relationships().fromJsonString(jsonString, options) + } + + static equals( + a: Relationships | PlainMessage | undefined, + b: Relationships | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(Relationships, a, b) + } +} + +/** + * @generated from message bsky.GetRelationshipsResponse + */ +export class GetRelationshipsResponse extends Message { + /** + * @generated from field: repeated bsky.Relationships relationships = 1; + */ + relationships: Relationships[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetRelationshipsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'relationships', + kind: 'message', + T: Relationships, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetRelationshipsResponse { + return new GetRelationshipsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetRelationshipsResponse { + return new GetRelationshipsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetRelationshipsResponse { + return new GetRelationshipsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetRelationshipsResponse + | PlainMessage + | undefined, + b: + | GetRelationshipsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetRelationshipsResponse, a, b) + } +} + +/** + * - return whether a block (bidrectionally and either direct or through a list) exists between two dids + * - enforcing 3rd party block violations + * + * @generated from message bsky.RelationshipPair + */ +export class RelationshipPair extends Message { + /** + * @generated from field: string a = 1; + */ + a = '' + + /** + * @generated from field: string b = 2; + */ + b = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.RelationshipPair' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'a', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'b', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): RelationshipPair { + return new RelationshipPair().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): RelationshipPair { + return new RelationshipPair().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): RelationshipPair { + return new RelationshipPair().fromJsonString(jsonString, options) + } + + static equals( + a: RelationshipPair | PlainMessage | undefined, + b: RelationshipPair | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(RelationshipPair, a, b) + } +} + +/** + * @generated from message bsky.GetBlockExistenceRequest + */ +export class GetBlockExistenceRequest extends Message { + /** + * @generated from field: repeated bsky.RelationshipPair pairs = 1; + */ + pairs: RelationshipPair[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlockExistenceRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'pairs', + kind: 'message', + T: RelationshipPair, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlockExistenceRequest { + return new GetBlockExistenceRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlockExistenceRequest { + return new GetBlockExistenceRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlockExistenceRequest { + return new GetBlockExistenceRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetBlockExistenceRequest + | PlainMessage + | undefined, + b: + | GetBlockExistenceRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBlockExistenceRequest, a, b) + } +} + +/** + * @generated from message bsky.GetBlockExistenceResponse + */ +export class GetBlockExistenceResponse extends Message { + /** + * @generated from field: repeated bool exists = 1; + */ + exists: boolean[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlockExistenceResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'exists', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlockExistenceResponse { + return new GetBlockExistenceResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlockExistenceResponse { + return new GetBlockExistenceResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlockExistenceResponse { + return new GetBlockExistenceResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetBlockExistenceResponse + | PlainMessage + | undefined, + b: + | GetBlockExistenceResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBlockExistenceResponse, a, b) + } +} + +/** + * @generated from message bsky.ListItemInfo + */ +export class ListItemInfo extends Message { + /** + * @generated from field: string uri = 1; + */ + uri = '' + + /** + * @generated from field: string did = 2; + */ + did = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.ListItemInfo' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): ListItemInfo { + return new ListItemInfo().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): ListItemInfo { + return new ListItemInfo().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): ListItemInfo { + return new ListItemInfo().fromJsonString(jsonString, options) + } + + static equals( + a: ListItemInfo | PlainMessage | undefined, + b: ListItemInfo | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(ListItemInfo, a, b) + } +} + +/** + * - Return dids of users in list A + * - E.g. to view items in one of your mute lists + * + * @generated from message bsky.GetListMembersRequest + */ +export class GetListMembersRequest extends Message { + /** + * @generated from field: string list_uri = 1; + */ + listUri = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListMembersRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'list_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListMembersRequest { + return new GetListMembersRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListMembersRequest { + return new GetListMembersRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListMembersRequest { + return new GetListMembersRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetListMembersRequest | PlainMessage | undefined, + b: GetListMembersRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetListMembersRequest, a, b) + } +} + +/** + * @generated from message bsky.GetListMembersResponse + */ +export class GetListMembersResponse extends Message { + /** + * @generated from field: repeated bsky.ListItemInfo listitems = 1; + */ + listitems: ListItemInfo[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListMembersResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'listitems', + kind: 'message', + T: ListItemInfo, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListMembersResponse { + return new GetListMembersResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListMembersResponse { + return new GetListMembersResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListMembersResponse { + return new GetListMembersResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetListMembersResponse + | PlainMessage + | undefined, + b: + | GetListMembersResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetListMembersResponse, a, b) + } +} + +/** + * - Return list uris where user A in list B, C, D… + * - Used in thread reply gates + * + * @generated from message bsky.GetListMembershipRequest + */ +export class GetListMembershipRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: repeated string list_uris = 2; + */ + listUris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListMembershipRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 2, + name: 'list_uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListMembershipRequest { + return new GetListMembershipRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListMembershipRequest { + return new GetListMembershipRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListMembershipRequest { + return new GetListMembershipRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetListMembershipRequest + | PlainMessage + | undefined, + b: + | GetListMembershipRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetListMembershipRequest, a, b) + } +} + +/** + * @generated from message bsky.GetListMembershipResponse + */ +export class GetListMembershipResponse extends Message { + /** + * @generated from field: repeated string listitem_uris = 1; + */ + listitemUris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListMembershipResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'listitem_uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListMembershipResponse { + return new GetListMembershipResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListMembershipResponse { + return new GetListMembershipResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListMembershipResponse { + return new GetListMembershipResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetListMembershipResponse + | PlainMessage + | undefined, + b: + | GetListMembershipResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetListMembershipResponse, a, b) + } +} + +/** + * - Return number of items in list A + * - For aggregate + * + * @generated from message bsky.GetListCountRequest + */ +export class GetListCountRequest extends Message { + /** + * @generated from field: string list_uri = 1; + */ + listUri = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListCountRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'list_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListCountRequest { + return new GetListCountRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListCountRequest { + return new GetListCountRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListCountRequest { + return new GetListCountRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetListCountRequest | PlainMessage | undefined, + b: GetListCountRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetListCountRequest, a, b) + } +} + +/** + * @generated from message bsky.GetListCountResponse + */ +export class GetListCountResponse extends Message { + /** + * @generated from field: int32 count = 1; + */ + count = 0 + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListCountResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'count', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListCountResponse { + return new GetListCountResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListCountResponse { + return new GetListCountResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListCountResponse { + return new GetListCountResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetListCountResponse | PlainMessage | undefined, + b: GetListCountResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetListCountResponse, a, b) + } +} + +/** + * - return list of uris of lists created by A + * - `getLists` + * + * @generated from message bsky.GetActorListsRequest + */ +export class GetActorListsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorListsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorListsRequest { + return new GetActorListsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorListsRequest { + return new GetActorListsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorListsRequest { + return new GetActorListsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetActorListsRequest | PlainMessage | undefined, + b: GetActorListsRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetActorListsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetActorListsResponse + */ +export class GetActorListsResponse extends Message { + /** + * @generated from field: repeated string list_uris = 1; + */ + listUris: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorListsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'list_uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorListsResponse { + return new GetActorListsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorListsResponse { + return new GetActorListsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorListsResponse { + return new GetActorListsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetActorListsResponse | PlainMessage | undefined, + b: GetActorListsResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetActorListsResponse, a, b) + } +} + +/** + * - return boolean if user A has muted user B + * - hydrating mute state onto profiles + * + * @generated from message bsky.GetActorMutesActorRequest + */ +export class GetActorMutesActorRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: string target_did = 2; + */ + targetDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorMutesActorRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'target_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorMutesActorRequest { + return new GetActorMutesActorRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorMutesActorRequest { + return new GetActorMutesActorRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorMutesActorRequest { + return new GetActorMutesActorRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetActorMutesActorRequest + | PlainMessage + | undefined, + b: + | GetActorMutesActorRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetActorMutesActorRequest, a, b) + } +} + +/** + * @generated from message bsky.GetActorMutesActorResponse + */ +export class GetActorMutesActorResponse extends Message { + /** + * @generated from field: bool muted = 1; + */ + muted = false + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorMutesActorResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'muted', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorMutesActorResponse { + return new GetActorMutesActorResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorMutesActorResponse { + return new GetActorMutesActorResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorMutesActorResponse { + return new GetActorMutesActorResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetActorMutesActorResponse + | PlainMessage + | undefined, + b: + | GetActorMutesActorResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetActorMutesActorResponse, a, b) + } +} + +/** + * - return list of user dids of users who A mutes + * - `getMutes` + * + * @generated from message bsky.GetMutesRequest + */ +export class GetMutesRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetMutesRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetMutesRequest { + return new GetMutesRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetMutesRequest { + return new GetMutesRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetMutesRequest { + return new GetMutesRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetMutesRequest | PlainMessage | undefined, + b: GetMutesRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetMutesRequest, a, b) + } +} + +/** + * @generated from message bsky.GetMutesResponse + */ +export class GetMutesResponse extends Message { + /** + * @generated from field: repeated string dids = 1; + */ + dids: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetMutesResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'dids', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetMutesResponse { + return new GetMutesResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetMutesResponse { + return new GetMutesResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetMutesResponse { + return new GetMutesResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetMutesResponse | PlainMessage | undefined, + b: GetMutesResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetMutesResponse, a, b) + } +} + +/** + * - return list uri of *any* list through which user A has muted user B + * - hydrating mute state onto profiles + * - note: we only need *one* uri even if a user is muted by multiple lists + * + * @generated from message bsky.GetActorMutesActorViaListRequest + */ +export class GetActorMutesActorViaListRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: string target_did = 2; + */ + targetDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorMutesActorViaListRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'target_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorMutesActorViaListRequest { + return new GetActorMutesActorViaListRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorMutesActorViaListRequest { + return new GetActorMutesActorViaListRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorMutesActorViaListRequest { + return new GetActorMutesActorViaListRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetActorMutesActorViaListRequest + | PlainMessage + | undefined, + b: + | GetActorMutesActorViaListRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetActorMutesActorViaListRequest, a, b) + } +} + +/** + * @generated from message bsky.GetActorMutesActorViaListResponse + */ +export class GetActorMutesActorViaListResponse extends Message { + /** + * @generated from field: string list_uri = 1; + */ + listUri = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorMutesActorViaListResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'list_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorMutesActorViaListResponse { + return new GetActorMutesActorViaListResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorMutesActorViaListResponse { + return new GetActorMutesActorViaListResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorMutesActorViaListResponse { + return new GetActorMutesActorViaListResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetActorMutesActorViaListResponse + | PlainMessage + | undefined, + b: + | GetActorMutesActorViaListResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetActorMutesActorViaListResponse, a, b) + } +} + +/** + * - return boolean if actor A has subscribed to mutelist B + * - list view hydration + * + * @generated from message bsky.GetMutelistSubscriptionRequest + */ +export class GetMutelistSubscriptionRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: string list_uri = 2; + */ + listUri = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetMutelistSubscriptionRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'list_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetMutelistSubscriptionRequest { + return new GetMutelistSubscriptionRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetMutelistSubscriptionRequest { + return new GetMutelistSubscriptionRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetMutelistSubscriptionRequest { + return new GetMutelistSubscriptionRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetMutelistSubscriptionRequest + | PlainMessage + | undefined, + b: + | GetMutelistSubscriptionRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetMutelistSubscriptionRequest, a, b) + } +} + +/** + * @generated from message bsky.GetMutelistSubscriptionResponse + */ +export class GetMutelistSubscriptionResponse extends Message { + /** + * @generated from field: bool subscribed = 1; + */ + subscribed = false + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetMutelistSubscriptionResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'subscribed', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetMutelistSubscriptionResponse { + return new GetMutelistSubscriptionResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetMutelistSubscriptionResponse { + return new GetMutelistSubscriptionResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetMutelistSubscriptionResponse { + return new GetMutelistSubscriptionResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetMutelistSubscriptionResponse + | PlainMessage + | undefined, + b: + | GetMutelistSubscriptionResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetMutelistSubscriptionResponse, a, b) + } +} + +/** + * - return list of list uris of mutelists that A subscribes to + * - `getListMutes` + * + * @generated from message bsky.GetMutelistSubscriptionsRequest + */ +export class GetMutelistSubscriptionsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetMutelistSubscriptionsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetMutelistSubscriptionsRequest { + return new GetMutelistSubscriptionsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetMutelistSubscriptionsRequest { + return new GetMutelistSubscriptionsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetMutelistSubscriptionsRequest { + return new GetMutelistSubscriptionsRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetMutelistSubscriptionsRequest + | PlainMessage + | undefined, + b: + | GetMutelistSubscriptionsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetMutelistSubscriptionsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetMutelistSubscriptionsResponse + */ +export class GetMutelistSubscriptionsResponse extends Message { + /** + * @generated from field: repeated string list_uris = 1; + */ + listUris: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetMutelistSubscriptionsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'list_uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetMutelistSubscriptionsResponse { + return new GetMutelistSubscriptionsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetMutelistSubscriptionsResponse { + return new GetMutelistSubscriptionsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetMutelistSubscriptionsResponse { + return new GetMutelistSubscriptionsResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetMutelistSubscriptionsResponse + | PlainMessage + | undefined, + b: + | GetMutelistSubscriptionsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetMutelistSubscriptionsResponse, a, b) + } +} + +/** + * - Return block uri if there is a block between users A & B (bidirectional) + * - hydrating (& actioning) block state on profiles + * - handling 3rd party blocks + * + * @generated from message bsky.GetBidirectionalBlockRequest + */ +export class GetBidirectionalBlockRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: string target_did = 2; + */ + targetDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBidirectionalBlockRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'target_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBidirectionalBlockRequest { + return new GetBidirectionalBlockRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBidirectionalBlockRequest { + return new GetBidirectionalBlockRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBidirectionalBlockRequest { + return new GetBidirectionalBlockRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetBidirectionalBlockRequest + | PlainMessage + | undefined, + b: + | GetBidirectionalBlockRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBidirectionalBlockRequest, a, b) + } +} + +/** + * @generated from message bsky.GetBidirectionalBlockResponse + */ +export class GetBidirectionalBlockResponse extends Message { + /** + * @generated from field: string block_uri = 1; + */ + blockUri = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBidirectionalBlockResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'block_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBidirectionalBlockResponse { + return new GetBidirectionalBlockResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBidirectionalBlockResponse { + return new GetBidirectionalBlockResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBidirectionalBlockResponse { + return new GetBidirectionalBlockResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetBidirectionalBlockResponse + | PlainMessage + | undefined, + b: + | GetBidirectionalBlockResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBidirectionalBlockResponse, a, b) + } +} + +/** + * - Return list of block uris and user dids of users who A blocks + * - `getBlocks` + * + * @generated from message bsky.GetBlocksRequest + */ +export class GetBlocksRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlocksRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlocksRequest { + return new GetBlocksRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlocksRequest { + return new GetBlocksRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlocksRequest { + return new GetBlocksRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetBlocksRequest | PlainMessage | undefined, + b: GetBlocksRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetBlocksRequest, a, b) + } +} + +/** + * @generated from message bsky.GetBlocksResponse + */ +export class GetBlocksResponse extends Message { + /** + * @generated from field: repeated string block_uris = 1; + */ + blockUris: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlocksResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'block_uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlocksResponse { + return new GetBlocksResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlocksResponse { + return new GetBlocksResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlocksResponse { + return new GetBlocksResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetBlocksResponse | PlainMessage | undefined, + b: GetBlocksResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetBlocksResponse, a, b) + } +} + +/** + * - Return list uri of ***any*** list through which users A & B have a block (bidirectional) + * - hydrating (& actioning) block state on profiles + * - handling 3rd party blocks + * + * @generated from message bsky.GetBidirectionalBlockViaListRequest + */ +export class GetBidirectionalBlockViaListRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: string target_did = 2; + */ + targetDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBidirectionalBlockViaListRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'target_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBidirectionalBlockViaListRequest { + return new GetBidirectionalBlockViaListRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBidirectionalBlockViaListRequest { + return new GetBidirectionalBlockViaListRequest().fromJson( + jsonValue, + options, + ) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBidirectionalBlockViaListRequest { + return new GetBidirectionalBlockViaListRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetBidirectionalBlockViaListRequest + | PlainMessage + | undefined, + b: + | GetBidirectionalBlockViaListRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBidirectionalBlockViaListRequest, a, b) + } +} + +/** + * @generated from message bsky.GetBidirectionalBlockViaListResponse + */ +export class GetBidirectionalBlockViaListResponse extends Message { + /** + * @generated from field: string list_uri = 1; + */ + listUri = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBidirectionalBlockViaListResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'list_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBidirectionalBlockViaListResponse { + return new GetBidirectionalBlockViaListResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBidirectionalBlockViaListResponse { + return new GetBidirectionalBlockViaListResponse().fromJson( + jsonValue, + options, + ) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBidirectionalBlockViaListResponse { + return new GetBidirectionalBlockViaListResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetBidirectionalBlockViaListResponse + | PlainMessage + | undefined, + b: + | GetBidirectionalBlockViaListResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBidirectionalBlockViaListResponse, a, b) + } +} + +/** + * - return boolean if user A has subscribed to blocklist B + * - list view hydration + * + * @generated from message bsky.GetBlocklistSubscriptionRequest + */ +export class GetBlocklistSubscriptionRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: string list_uri = 2; + */ + listUri = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlocklistSubscriptionRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'list_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlocklistSubscriptionRequest { + return new GetBlocklistSubscriptionRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlocklistSubscriptionRequest { + return new GetBlocklistSubscriptionRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlocklistSubscriptionRequest { + return new GetBlocklistSubscriptionRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetBlocklistSubscriptionRequest + | PlainMessage + | undefined, + b: + | GetBlocklistSubscriptionRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBlocklistSubscriptionRequest, a, b) + } +} + +/** + * @generated from message bsky.GetBlocklistSubscriptionResponse + */ +export class GetBlocklistSubscriptionResponse extends Message { + /** + * @generated from field: string listblock_uri = 1; + */ + listblockUri = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlocklistSubscriptionResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'listblock_uri', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlocklistSubscriptionResponse { + return new GetBlocklistSubscriptionResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlocklistSubscriptionResponse { + return new GetBlocklistSubscriptionResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlocklistSubscriptionResponse { + return new GetBlocklistSubscriptionResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetBlocklistSubscriptionResponse + | PlainMessage + | undefined, + b: + | GetBlocklistSubscriptionResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBlocklistSubscriptionResponse, a, b) + } +} + +/** + * - return list of list uris of Blockslists that A subscribes to + * - `getListBlocks` + * + * @generated from message bsky.GetBlocklistSubscriptionsRequest + */ +export class GetBlocklistSubscriptionsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlocklistSubscriptionsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlocklistSubscriptionsRequest { + return new GetBlocklistSubscriptionsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlocklistSubscriptionsRequest { + return new GetBlocklistSubscriptionsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlocklistSubscriptionsRequest { + return new GetBlocklistSubscriptionsRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetBlocklistSubscriptionsRequest + | PlainMessage + | undefined, + b: + | GetBlocklistSubscriptionsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBlocklistSubscriptionsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetBlocklistSubscriptionsResponse + */ +export class GetBlocklistSubscriptionsResponse extends Message { + /** + * @generated from field: repeated string list_uris = 1; + */ + listUris: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlocklistSubscriptionsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'list_uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlocklistSubscriptionsResponse { + return new GetBlocklistSubscriptionsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlocklistSubscriptionsResponse { + return new GetBlocklistSubscriptionsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlocklistSubscriptionsResponse { + return new GetBlocklistSubscriptionsResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetBlocklistSubscriptionsResponse + | PlainMessage + | undefined, + b: + | GetBlocklistSubscriptionsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBlocklistSubscriptionsResponse, a, b) + } +} + +/** + * - list recent notifications for a user + * - notifications should include a uri for the record that caused the notif & a “reason” for the notification (reply, like, quotepost, etc) + * - this should include both read & unread notifs + * + * @generated from message bsky.GetNotificationsRequest + */ +export class GetNotificationsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetNotificationsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetNotificationsRequest { + return new GetNotificationsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetNotificationsRequest { + return new GetNotificationsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetNotificationsRequest { + return new GetNotificationsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetNotificationsRequest + | PlainMessage + | undefined, + b: + | GetNotificationsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetNotificationsRequest, a, b) + } +} + +/** + * @generated from message bsky.Notification + */ +export class Notification extends Message { + /** + * @generated from field: string recipient_did = 1; + */ + recipientDid = '' + + /** + * @generated from field: string uri = 2; + */ + uri = '' + + /** + * @generated from field: string reason = 3; + */ + reason = '' + + /** + * @generated from field: string reason_subject = 4; + */ + reasonSubject = '' + + /** + * @generated from field: google.protobuf.Timestamp timestamp = 5; + */ + timestamp?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.Notification' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'recipient_did', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + { no: 2, name: 'uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'reason', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 4, + name: 'reason_subject', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + { no: 5, name: 'timestamp', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): Notification { + return new Notification().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): Notification { + return new Notification().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): Notification { + return new Notification().fromJsonString(jsonString, options) + } + + static equals( + a: Notification | PlainMessage | undefined, + b: Notification | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(Notification, a, b) + } +} + +/** + * @generated from message bsky.GetNotificationsResponse + */ +export class GetNotificationsResponse extends Message { + /** + * @generated from field: repeated bsky.Notification notifications = 1; + */ + notifications: Notification[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetNotificationsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'notifications', + kind: 'message', + T: Notification, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetNotificationsResponse { + return new GetNotificationsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetNotificationsResponse { + return new GetNotificationsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetNotificationsResponse { + return new GetNotificationsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetNotificationsResponse + | PlainMessage + | undefined, + b: + | GetNotificationsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetNotificationsResponse, a, b) + } +} + +/** + * - update a user’s “last seen time” + * - `updateSeen` + * + * @generated from message bsky.UpdateNotificationSeenRequest + */ +export class UpdateNotificationSeenRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: google.protobuf.Timestamp timestamp = 2; + */ + timestamp?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.UpdateNotificationSeenRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'timestamp', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): UpdateNotificationSeenRequest { + return new UpdateNotificationSeenRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): UpdateNotificationSeenRequest { + return new UpdateNotificationSeenRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): UpdateNotificationSeenRequest { + return new UpdateNotificationSeenRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | UpdateNotificationSeenRequest + | PlainMessage + | undefined, + b: + | UpdateNotificationSeenRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(UpdateNotificationSeenRequest, a, b) + } +} + +/** + * @generated from message bsky.UpdateNotificationSeenResponse + */ +export class UpdateNotificationSeenResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.UpdateNotificationSeenResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): UpdateNotificationSeenResponse { + return new UpdateNotificationSeenResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): UpdateNotificationSeenResponse { + return new UpdateNotificationSeenResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): UpdateNotificationSeenResponse { + return new UpdateNotificationSeenResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | UpdateNotificationSeenResponse + | PlainMessage + | undefined, + b: + | UpdateNotificationSeenResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(UpdateNotificationSeenResponse, a, b) + } +} + +/** + * - get a user’s “last seen time” + * - hydrating read state onto notifications + * + * @generated from message bsky.GetNotificationSeenRequest + */ +export class GetNotificationSeenRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetNotificationSeenRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetNotificationSeenRequest { + return new GetNotificationSeenRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetNotificationSeenRequest { + return new GetNotificationSeenRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetNotificationSeenRequest { + return new GetNotificationSeenRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetNotificationSeenRequest + | PlainMessage + | undefined, + b: + | GetNotificationSeenRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetNotificationSeenRequest, a, b) + } +} + +/** + * @generated from message bsky.GetNotificationSeenResponse + */ +export class GetNotificationSeenResponse extends Message { + /** + * @generated from field: google.protobuf.Timestamp timestamp = 1; + */ + timestamp?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetNotificationSeenResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'timestamp', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetNotificationSeenResponse { + return new GetNotificationSeenResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetNotificationSeenResponse { + return new GetNotificationSeenResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetNotificationSeenResponse { + return new GetNotificationSeenResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetNotificationSeenResponse + | PlainMessage + | undefined, + b: + | GetNotificationSeenResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetNotificationSeenResponse, a, b) + } +} + +/** + * - get a count of all unread notifications (notifications after `updateSeen`) + * - `getUnreadCount` + * + * @generated from message bsky.GetUnreadNotificationCountRequest + */ +export class GetUnreadNotificationCountRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetUnreadNotificationCountRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetUnreadNotificationCountRequest { + return new GetUnreadNotificationCountRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetUnreadNotificationCountRequest { + return new GetUnreadNotificationCountRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetUnreadNotificationCountRequest { + return new GetUnreadNotificationCountRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetUnreadNotificationCountRequest + | PlainMessage + | undefined, + b: + | GetUnreadNotificationCountRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetUnreadNotificationCountRequest, a, b) + } +} + +/** + * @generated from message bsky.GetUnreadNotificationCountResponse + */ +export class GetUnreadNotificationCountResponse extends Message { + /** + * @generated from field: int32 count = 1; + */ + count = 0 + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetUnreadNotificationCountResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'count', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetUnreadNotificationCountResponse { + return new GetUnreadNotificationCountResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetUnreadNotificationCountResponse { + return new GetUnreadNotificationCountResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetUnreadNotificationCountResponse { + return new GetUnreadNotificationCountResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetUnreadNotificationCountResponse + | PlainMessage + | undefined, + b: + | GetUnreadNotificationCountResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetUnreadNotificationCountResponse, a, b) + } +} + +/** + * - Return uris of feed generator records created by user A + * - `getActorFeeds` + * + * @generated from message bsky.GetActorFeedsRequest + */ +export class GetActorFeedsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorFeedsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorFeedsRequest { + return new GetActorFeedsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorFeedsRequest { + return new GetActorFeedsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorFeedsRequest { + return new GetActorFeedsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetActorFeedsRequest | PlainMessage | undefined, + b: GetActorFeedsRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetActorFeedsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetActorFeedsResponse + */ +export class GetActorFeedsResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorFeedsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorFeedsResponse { + return new GetActorFeedsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorFeedsResponse { + return new GetActorFeedsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorFeedsResponse { + return new GetActorFeedsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetActorFeedsResponse | PlainMessage | undefined, + b: GetActorFeedsResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetActorFeedsResponse, a, b) + } +} + +/** + * - Returns a list of suggested feed generator uris for an actor, paginated + * - `getSuggestedFeeds` + * - This is currently just hardcoded in the Appview DB + * + * @generated from message bsky.GetSuggestedFeedsRequest + */ +export class GetSuggestedFeedsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetSuggestedFeedsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetSuggestedFeedsRequest { + return new GetSuggestedFeedsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetSuggestedFeedsRequest { + return new GetSuggestedFeedsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetSuggestedFeedsRequest { + return new GetSuggestedFeedsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetSuggestedFeedsRequest + | PlainMessage + | undefined, + b: + | GetSuggestedFeedsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetSuggestedFeedsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetSuggestedFeedsResponse + */ +export class GetSuggestedFeedsResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetSuggestedFeedsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetSuggestedFeedsResponse { + return new GetSuggestedFeedsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetSuggestedFeedsResponse { + return new GetSuggestedFeedsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetSuggestedFeedsResponse { + return new GetSuggestedFeedsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetSuggestedFeedsResponse + | PlainMessage + | undefined, + b: + | GetSuggestedFeedsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetSuggestedFeedsResponse, a, b) + } +} + +/** + * @generated from message bsky.SearchFeedGeneratorsRequest + */ +export class SearchFeedGeneratorsRequest extends Message { + /** + * @generated from field: string query = 1; + */ + query = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.SearchFeedGeneratorsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'query', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): SearchFeedGeneratorsRequest { + return new SearchFeedGeneratorsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): SearchFeedGeneratorsRequest { + return new SearchFeedGeneratorsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): SearchFeedGeneratorsRequest { + return new SearchFeedGeneratorsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | SearchFeedGeneratorsRequest + | PlainMessage + | undefined, + b: + | SearchFeedGeneratorsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(SearchFeedGeneratorsRequest, a, b) + } +} + +/** + * @generated from message bsky.SearchFeedGeneratorsResponse + */ +export class SearchFeedGeneratorsResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.SearchFeedGeneratorsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): SearchFeedGeneratorsResponse { + return new SearchFeedGeneratorsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): SearchFeedGeneratorsResponse { + return new SearchFeedGeneratorsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): SearchFeedGeneratorsResponse { + return new SearchFeedGeneratorsResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | SearchFeedGeneratorsResponse + | PlainMessage + | undefined, + b: + | SearchFeedGeneratorsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(SearchFeedGeneratorsResponse, a, b) + } +} + +/** + * - Returns feed generator validity and online status with uris A, B, C… + * - Not currently being used, but could be worhthwhile. + * + * @generated from message bsky.GetFeedGeneratorStatusRequest + */ +export class GetFeedGeneratorStatusRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFeedGeneratorStatusRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFeedGeneratorStatusRequest { + return new GetFeedGeneratorStatusRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFeedGeneratorStatusRequest { + return new GetFeedGeneratorStatusRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFeedGeneratorStatusRequest { + return new GetFeedGeneratorStatusRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetFeedGeneratorStatusRequest + | PlainMessage + | undefined, + b: + | GetFeedGeneratorStatusRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetFeedGeneratorStatusRequest, a, b) + } +} + +/** + * @generated from message bsky.GetFeedGeneratorStatusResponse + */ +export class GetFeedGeneratorStatusResponse extends Message { + /** + * @generated from field: repeated string status = 1; + */ + status: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFeedGeneratorStatusResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'status', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFeedGeneratorStatusResponse { + return new GetFeedGeneratorStatusResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFeedGeneratorStatusResponse { + return new GetFeedGeneratorStatusResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFeedGeneratorStatusResponse { + return new GetFeedGeneratorStatusResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetFeedGeneratorStatusResponse + | PlainMessage + | undefined, + b: + | GetFeedGeneratorStatusResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetFeedGeneratorStatusResponse, a, b) + } +} + +/** + * - Returns recent posts authored by a given DID, paginated + * - `getAuthorFeed` + * - Optionally: filter by if a post is/isn’t a reply and if a post has a media object in it + * + * @generated from message bsky.GetAuthorFeedRequest + */ +export class GetAuthorFeedRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + /** + * @generated from field: bsky.FeedType feed_type = 4; + */ + feedType = FeedType.UNSPECIFIED + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetAuthorFeedRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 4, name: 'feed_type', kind: 'enum', T: proto3.getEnumType(FeedType) }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetAuthorFeedRequest { + return new GetAuthorFeedRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetAuthorFeedRequest { + return new GetAuthorFeedRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetAuthorFeedRequest { + return new GetAuthorFeedRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetAuthorFeedRequest | PlainMessage | undefined, + b: GetAuthorFeedRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetAuthorFeedRequest, a, b) + } +} + +/** + * @generated from message bsky.AuthorFeedItem + */ +export class AuthorFeedItem extends Message { + /** + * @generated from field: string uri = 1; + */ + uri = '' + + /** + * @generated from field: string cid = 2; + */ + cid = '' + + /** + * @generated from field: string repost = 3; + */ + repost = '' + + /** + * @generated from field: string repost_cid = 4; + */ + repostCid = '' + + /** + * @generated from field: bool posts_and_author_threads = 5; + */ + postsAndAuthorThreads = false + + /** + * @generated from field: bool posts_no_replies = 6; + */ + postsNoReplies = false + + /** + * @generated from field: bool posts_with_media = 7; + */ + postsWithMedia = false + + /** + * @generated from field: bool is_reply = 8; + */ + isReply = false + + /** + * @generated from field: bool is_repost = 9; + */ + isRepost = false + + /** + * @generated from field: bool is_quote_post = 10; + */ + isQuotePost = false + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.AuthorFeedItem' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'cid', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'repost', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 4, name: 'repost_cid', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 5, + name: 'posts_and_author_threads', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + { + no: 6, + name: 'posts_no_replies', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + { + no: 7, + name: 'posts_with_media', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + { no: 8, name: 'is_reply', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { no: 9, name: 'is_repost', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { + no: 10, + name: 'is_quote_post', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): AuthorFeedItem { + return new AuthorFeedItem().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): AuthorFeedItem { + return new AuthorFeedItem().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): AuthorFeedItem { + return new AuthorFeedItem().fromJsonString(jsonString, options) + } + + static equals( + a: AuthorFeedItem | PlainMessage | undefined, + b: AuthorFeedItem | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(AuthorFeedItem, a, b) + } +} + +/** + * @generated from message bsky.GetAuthorFeedResponse + */ +export class GetAuthorFeedResponse extends Message { + /** + * @generated from field: repeated bsky.AuthorFeedItem items = 1; + */ + items: AuthorFeedItem[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetAuthorFeedResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'items', + kind: 'message', + T: AuthorFeedItem, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetAuthorFeedResponse { + return new GetAuthorFeedResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetAuthorFeedResponse { + return new GetAuthorFeedResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetAuthorFeedResponse { + return new GetAuthorFeedResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetAuthorFeedResponse | PlainMessage | undefined, + b: GetAuthorFeedResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetAuthorFeedResponse, a, b) + } +} + +/** + * - Returns recent posts authored by users followed by a given DID, paginated + * - `getTimeline` + * + * @generated from message bsky.GetTimelineRequest + */ +export class GetTimelineRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + /** + * @generated from field: bool exclude_replies = 4; + */ + excludeReplies = false + + /** + * @generated from field: bool exclude_reposts = 5; + */ + excludeReposts = false + + /** + * @generated from field: bool exclude_quotes = 6; + */ + excludeQuotes = false + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetTimelineRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 4, + name: 'exclude_replies', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + { + no: 5, + name: 'exclude_reposts', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + { + no: 6, + name: 'exclude_quotes', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetTimelineRequest { + return new GetTimelineRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetTimelineRequest { + return new GetTimelineRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetTimelineRequest { + return new GetTimelineRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetTimelineRequest | PlainMessage | undefined, + b: GetTimelineRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetTimelineRequest, a, b) + } +} + +/** + * @generated from message bsky.GetTimelineResponse + */ +export class GetTimelineResponse extends Message { + /** + * @generated from field: repeated bsky.TimelineFeedItem items = 1; + */ + items: TimelineFeedItem[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetTimelineResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'items', + kind: 'message', + T: TimelineFeedItem, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetTimelineResponse { + return new GetTimelineResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetTimelineResponse { + return new GetTimelineResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetTimelineResponse { + return new GetTimelineResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetTimelineResponse | PlainMessage | undefined, + b: GetTimelineResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetTimelineResponse, a, b) + } +} + +/** + * @generated from message bsky.TimelineFeedItem + */ +export class TimelineFeedItem extends Message { + /** + * @generated from field: string uri = 1; + */ + uri = '' + + /** + * @generated from field: string cid = 2; + */ + cid = '' + + /** + * @generated from field: string repost = 3; + */ + repost = '' + + /** + * @generated from field: string repost_cid = 4; + */ + repostCid = '' + + /** + * @generated from field: bool is_reply = 5; + */ + isReply = false + + /** + * @generated from field: bool is_repost = 6; + */ + isRepost = false + + /** + * @generated from field: bool is_quote_post = 7; + */ + isQuotePost = false + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.TimelineFeedItem' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'cid', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'repost', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 4, name: 'repost_cid', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 5, name: 'is_reply', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { no: 6, name: 'is_repost', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { + no: 7, + name: 'is_quote_post', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): TimelineFeedItem { + return new TimelineFeedItem().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): TimelineFeedItem { + return new TimelineFeedItem().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): TimelineFeedItem { + return new TimelineFeedItem().fromJsonString(jsonString, options) + } + + static equals( + a: TimelineFeedItem | PlainMessage | undefined, + b: TimelineFeedItem | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(TimelineFeedItem, a, b) + } +} + +/** + * - Return recent post uris from users in list A + * - `getListFeed` + * - (This is essentially the same as `getTimeline` but instead of follows of a did, it is list items of a list) + * + * @generated from message bsky.GetListFeedRequest + */ +export class GetListFeedRequest extends Message { + /** + * @generated from field: string list_uri = 1; + */ + listUri = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + /** + * @generated from field: bool exclude_replies = 4; + */ + excludeReplies = false + + /** + * @generated from field: bool exclude_reposts = 5; + */ + excludeReposts = false + + /** + * @generated from field: bool exclude_quotes = 6; + */ + excludeQuotes = false + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListFeedRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'list_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 4, + name: 'exclude_replies', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + { + no: 5, + name: 'exclude_reposts', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + { + no: 6, + name: 'exclude_quotes', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListFeedRequest { + return new GetListFeedRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListFeedRequest { + return new GetListFeedRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListFeedRequest { + return new GetListFeedRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetListFeedRequest | PlainMessage | undefined, + b: GetListFeedRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetListFeedRequest, a, b) + } +} + +/** + * @generated from message bsky.GetListFeedResponse + */ +export class GetListFeedResponse extends Message { + /** + * @generated from field: repeated bsky.TimelineFeedItem items = 1; + */ + items: TimelineFeedItem[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetListFeedResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'items', + kind: 'message', + T: TimelineFeedItem, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetListFeedResponse { + return new GetListFeedResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetListFeedResponse { + return new GetListFeedResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetListFeedResponse { + return new GetListFeedResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetListFeedResponse | PlainMessage | undefined, + b: GetListFeedResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetListFeedResponse, a, b) + } +} + +/** + * Return posts uris of any replies N levels above or M levels below post A + * + * @generated from message bsky.GetThreadRequest + */ +export class GetThreadRequest extends Message { + /** + * @generated from field: string post_uri = 1; + */ + postUri = '' + + /** + * @generated from field: int32 above = 2; + */ + above = 0 + + /** + * @generated from field: int32 below = 3; + */ + below = 0 + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetThreadRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'post_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'above', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'below', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetThreadRequest { + return new GetThreadRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetThreadRequest { + return new GetThreadRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetThreadRequest { + return new GetThreadRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetThreadRequest | PlainMessage | undefined, + b: GetThreadRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetThreadRequest, a, b) + } +} + +/** + * @generated from message bsky.GetThreadResponse + */ +export class GetThreadResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetThreadResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetThreadResponse { + return new GetThreadResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetThreadResponse { + return new GetThreadResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetThreadResponse { + return new GetThreadResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetThreadResponse | PlainMessage | undefined, + b: GetThreadResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetThreadResponse, a, b) + } +} + +/** + * - Return DIDs of actors matching term, paginated + * - `searchActors` skeleton + * + * @generated from message bsky.SearchActorsRequest + */ +export class SearchActorsRequest extends Message { + /** + * @generated from field: string term = 1; + */ + term = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.SearchActorsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'term', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): SearchActorsRequest { + return new SearchActorsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): SearchActorsRequest { + return new SearchActorsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): SearchActorsRequest { + return new SearchActorsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: SearchActorsRequest | PlainMessage | undefined, + b: SearchActorsRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(SearchActorsRequest, a, b) + } +} + +/** + * @generated from message bsky.SearchActorsResponse + */ +export class SearchActorsResponse extends Message { + /** + * @generated from field: repeated string dids = 1; + */ + dids: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.SearchActorsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'dids', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): SearchActorsResponse { + return new SearchActorsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): SearchActorsResponse { + return new SearchActorsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): SearchActorsResponse { + return new SearchActorsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: SearchActorsResponse | PlainMessage | undefined, + b: SearchActorsResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(SearchActorsResponse, a, b) + } +} + +/** + * - Return uris of posts matching term, paginated + * - `searchPosts` skeleton + * + * @generated from message bsky.SearchPostsRequest + */ +export class SearchPostsRequest extends Message { + /** + * @generated from field: string term = 1; + */ + term = '' + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0 + + /** + * @generated from field: string cursor = 3; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.SearchPostsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'term', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): SearchPostsRequest { + return new SearchPostsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): SearchPostsRequest { + return new SearchPostsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): SearchPostsRequest { + return new SearchPostsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: SearchPostsRequest | PlainMessage | undefined, + b: SearchPostsRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(SearchPostsRequest, a, b) + } +} + +/** + * @generated from message bsky.SearchPostsResponse + */ +export class SearchPostsResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.SearchPostsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'uris', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): SearchPostsResponse { + return new SearchPostsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): SearchPostsResponse { + return new SearchPostsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): SearchPostsResponse { + return new SearchPostsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: SearchPostsResponse | PlainMessage | undefined, + b: SearchPostsResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(SearchPostsResponse, a, b) + } +} + +/** + * - Return DIDs of suggested follows for a user, excluding anyone they already follow + * - `getSuggestions`, `getSuggestedFollowsByActor` + * + * @generated from message bsky.GetFollowSuggestionsRequest + */ +export class GetFollowSuggestionsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: string relative_to_did = 2; + */ + relativeToDid = '' + + /** + * @generated from field: int32 limit = 3; + */ + limit = 0 + + /** + * @generated from field: string cursor = 4; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFollowSuggestionsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 2, + name: 'relative_to_did', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + { no: 3, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 4, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFollowSuggestionsRequest { + return new GetFollowSuggestionsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFollowSuggestionsRequest { + return new GetFollowSuggestionsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFollowSuggestionsRequest { + return new GetFollowSuggestionsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetFollowSuggestionsRequest + | PlainMessage + | undefined, + b: + | GetFollowSuggestionsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetFollowSuggestionsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetFollowSuggestionsResponse + */ +export class GetFollowSuggestionsResponse extends Message { + /** + * @generated from field: repeated string dids = 1; + */ + dids: string[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetFollowSuggestionsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'dids', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetFollowSuggestionsResponse { + return new GetFollowSuggestionsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetFollowSuggestionsResponse { + return new GetFollowSuggestionsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetFollowSuggestionsResponse { + return new GetFollowSuggestionsResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetFollowSuggestionsResponse + | PlainMessage + | undefined, + b: + | GetFollowSuggestionsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetFollowSuggestionsResponse, a, b) + } +} + +/** + * @generated from message bsky.SuggestedEntity + */ +export class SuggestedEntity extends Message { + /** + * @generated from field: string tag = 1; + */ + tag = '' + + /** + * @generated from field: string subject = 2; + */ + subject = '' + + /** + * @generated from field: string subject_type = 3; + */ + subjectType = '' + + /** + * @generated from field: int64 priority = 4; + */ + priority = protoInt64.zero + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.SuggestedEntity' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'tag', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'subject', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 3, + name: 'subject_type', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + { no: 4, name: 'priority', kind: 'scalar', T: 3 /* ScalarType.INT64 */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): SuggestedEntity { + return new SuggestedEntity().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): SuggestedEntity { + return new SuggestedEntity().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): SuggestedEntity { + return new SuggestedEntity().fromJsonString(jsonString, options) + } + + static equals( + a: SuggestedEntity | PlainMessage | undefined, + b: SuggestedEntity | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(SuggestedEntity, a, b) + } +} + +/** + * @generated from message bsky.GetSuggestedEntitiesRequest + */ +export class GetSuggestedEntitiesRequest extends Message { + /** + * @generated from field: int32 limit = 1; + */ + limit = 0 + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetSuggestedEntitiesRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetSuggestedEntitiesRequest { + return new GetSuggestedEntitiesRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetSuggestedEntitiesRequest { + return new GetSuggestedEntitiesRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetSuggestedEntitiesRequest { + return new GetSuggestedEntitiesRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetSuggestedEntitiesRequest + | PlainMessage + | undefined, + b: + | GetSuggestedEntitiesRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetSuggestedEntitiesRequest, a, b) + } +} + +/** + * @generated from message bsky.GetSuggestedEntitiesResponse + */ +export class GetSuggestedEntitiesResponse extends Message { + /** + * @generated from field: repeated bsky.SuggestedEntity entities = 1; + */ + entities: SuggestedEntity[] = [] + + /** + * @generated from field: string cursor = 2; + */ + cursor = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetSuggestedEntitiesResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'entities', + kind: 'message', + T: SuggestedEntity, + repeated: true, + }, + { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetSuggestedEntitiesResponse { + return new GetSuggestedEntitiesResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetSuggestedEntitiesResponse { + return new GetSuggestedEntitiesResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetSuggestedEntitiesResponse { + return new GetSuggestedEntitiesResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetSuggestedEntitiesResponse + | PlainMessage + | undefined, + b: + | GetSuggestedEntitiesResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetSuggestedEntitiesResponse, a, b) + } +} + +/** + * - Return post reply count with uris A, B, C… + * - All feed hydration + * + * @generated from message bsky.GetPostReplyCountsRequest + */ +export class GetPostReplyCountsRequest extends Message { + /** + * @generated from field: repeated bsky.RecordRef refs = 1; + */ + refs: RecordRef[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetPostReplyCountsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'refs', kind: 'message', T: RecordRef, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetPostReplyCountsRequest { + return new GetPostReplyCountsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetPostReplyCountsRequest { + return new GetPostReplyCountsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetPostReplyCountsRequest { + return new GetPostReplyCountsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetPostReplyCountsRequest + | PlainMessage + | undefined, + b: + | GetPostReplyCountsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetPostReplyCountsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetPostReplyCountsResponse + */ +export class GetPostReplyCountsResponse extends Message { + /** + * @generated from field: repeated int32 counts = 1; + */ + counts: number[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetPostReplyCountsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'counts', + kind: 'scalar', + T: 5 /* ScalarType.INT32 */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetPostReplyCountsResponse { + return new GetPostReplyCountsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetPostReplyCountsResponse { + return new GetPostReplyCountsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetPostReplyCountsResponse { + return new GetPostReplyCountsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetPostReplyCountsResponse + | PlainMessage + | undefined, + b: + | GetPostReplyCountsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetPostReplyCountsResponse, a, b) + } +} + +/** + * - Get all labels on a subjects A, B, C (uri or did) issued by dids D, E, F… + * - label hydration on nearly every view + * + * @generated from message bsky.GetLabelsRequest + */ +export class GetLabelsRequest extends Message { + /** + * @generated from field: repeated string subjects = 1; + */ + subjects: string[] = [] + + /** + * @generated from field: repeated string issuers = 2; + */ + issuers: string[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetLabelsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'subjects', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + { + no: 2, + name: 'issuers', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetLabelsRequest { + return new GetLabelsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetLabelsRequest { + return new GetLabelsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetLabelsRequest { + return new GetLabelsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetLabelsRequest | PlainMessage | undefined, + b: GetLabelsRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetLabelsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetLabelsResponse + */ +export class GetLabelsResponse extends Message { + /** + * @generated from field: repeated bytes labels = 1; + */ + labels: Uint8Array[] = [] + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetLabelsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { + no: 1, + name: 'labels', + kind: 'scalar', + T: 12 /* ScalarType.BYTES */, + repeated: true, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetLabelsResponse { + return new GetLabelsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetLabelsResponse { + return new GetLabelsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetLabelsResponse { + return new GetLabelsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetLabelsResponse | PlainMessage | undefined, + b: GetLabelsResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetLabelsResponse, a, b) + } +} + +/** + * - Latest repo rev of user w/ DID + * - Read-after-write header in`getProfile`, `getProfiles`, `getActorLikes`, `getAuthorFeed`, `getListFeed`, `getPostThread`, `getTimeline`. Could it be view dependent? + * + * @generated from message bsky.GetLatestRevRequest + */ +export class GetLatestRevRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetLatestRevRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetLatestRevRequest { + return new GetLatestRevRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetLatestRevRequest { + return new GetLatestRevRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetLatestRevRequest { + return new GetLatestRevRequest().fromJsonString(jsonString, options) + } + + static equals( + a: GetLatestRevRequest | PlainMessage | undefined, + b: GetLatestRevRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetLatestRevRequest, a, b) + } +} + +/** + * @generated from message bsky.GetLatestRevResponse + */ +export class GetLatestRevResponse extends Message { + /** + * @generated from field: string rev = 1; + */ + rev = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetLatestRevResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'rev', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetLatestRevResponse { + return new GetLatestRevResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetLatestRevResponse { + return new GetLatestRevResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetLatestRevResponse { + return new GetLatestRevResponse().fromJsonString(jsonString, options) + } + + static equals( + a: GetLatestRevResponse | PlainMessage | undefined, + b: GetLatestRevResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(GetLatestRevResponse, a, b) + } +} + +/** + * @generated from message bsky.GetIdentityByDidRequest + */ +export class GetIdentityByDidRequest extends Message { + /** + * @generated from field: string did = 1; + */ + did = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetIdentityByDidRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetIdentityByDidRequest { + return new GetIdentityByDidRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetIdentityByDidRequest { + return new GetIdentityByDidRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetIdentityByDidRequest { + return new GetIdentityByDidRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetIdentityByDidRequest + | PlainMessage + | undefined, + b: + | GetIdentityByDidRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetIdentityByDidRequest, a, b) + } +} + +/** + * @generated from message bsky.GetIdentityByDidResponse + */ +export class GetIdentityByDidResponse extends Message { + /** + * @generated from field: string did = 1; + */ + did = '' + + /** + * @generated from field: string handle = 2; + */ + handle = '' + + /** + * @generated from field: bytes keys = 3; + */ + keys = new Uint8Array(0) + + /** + * @generated from field: bytes services = 4; + */ + services = new Uint8Array(0) + + /** + * @generated from field: google.protobuf.Timestamp updated = 5; + */ + updated?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetIdentityByDidResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'handle', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'keys', kind: 'scalar', T: 12 /* ScalarType.BYTES */ }, + { no: 4, name: 'services', kind: 'scalar', T: 12 /* ScalarType.BYTES */ }, + { no: 5, name: 'updated', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetIdentityByDidResponse { + return new GetIdentityByDidResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetIdentityByDidResponse { + return new GetIdentityByDidResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetIdentityByDidResponse { + return new GetIdentityByDidResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetIdentityByDidResponse + | PlainMessage + | undefined, + b: + | GetIdentityByDidResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetIdentityByDidResponse, a, b) + } +} + +/** + * @generated from message bsky.GetIdentityByHandleRequest + */ +export class GetIdentityByHandleRequest extends Message { + /** + * @generated from field: string handle = 1; + */ + handle = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetIdentityByHandleRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'handle', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetIdentityByHandleRequest { + return new GetIdentityByHandleRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetIdentityByHandleRequest { + return new GetIdentityByHandleRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetIdentityByHandleRequest { + return new GetIdentityByHandleRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetIdentityByHandleRequest + | PlainMessage + | undefined, + b: + | GetIdentityByHandleRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetIdentityByHandleRequest, a, b) + } +} + +/** + * @generated from message bsky.GetIdentityByHandleResponse + */ +export class GetIdentityByHandleResponse extends Message { + /** + * @generated from field: string handle = 1; + */ + handle = '' + + /** + * @generated from field: string did = 2; + */ + did = '' + + /** + * @generated from field: bytes keys = 3; + */ + keys = new Uint8Array(0) + + /** + * @generated from field: bytes services = 4; + */ + services = new Uint8Array(0) + + /** + * @generated from field: google.protobuf.Timestamp updated = 5; + */ + updated?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetIdentityByHandleResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'handle', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'keys', kind: 'scalar', T: 12 /* ScalarType.BYTES */ }, + { no: 4, name: 'services', kind: 'scalar', T: 12 /* ScalarType.BYTES */ }, + { no: 5, name: 'updated', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetIdentityByHandleResponse { + return new GetIdentityByHandleResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetIdentityByHandleResponse { + return new GetIdentityByHandleResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetIdentityByHandleResponse { + return new GetIdentityByHandleResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetIdentityByHandleResponse + | PlainMessage + | undefined, + b: + | GetIdentityByHandleResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetIdentityByHandleResponse, a, b) + } +} + +/** + * @generated from message bsky.GetBlobTakedownRequest + */ +export class GetBlobTakedownRequest extends Message { + /** + * @generated from field: string did = 1; + */ + did = '' + + /** + * @generated from field: string cid = 2; + */ + cid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlobTakedownRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'cid', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlobTakedownRequest { + return new GetBlobTakedownRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlobTakedownRequest { + return new GetBlobTakedownRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlobTakedownRequest { + return new GetBlobTakedownRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetBlobTakedownRequest + | PlainMessage + | undefined, + b: + | GetBlobTakedownRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBlobTakedownRequest, a, b) + } +} + +/** + * @generated from message bsky.GetBlobTakedownResponse + */ +export class GetBlobTakedownResponse extends Message { + /** + * @generated from field: bool taken_down = 1; + */ + takenDown = false + + /** + * @generated from field: string takedown_ref = 2; + */ + takedownRef = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetBlobTakedownResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'taken_down', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { + no: 2, + name: 'takedown_ref', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetBlobTakedownResponse { + return new GetBlobTakedownResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetBlobTakedownResponse { + return new GetBlobTakedownResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetBlobTakedownResponse { + return new GetBlobTakedownResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetBlobTakedownResponse + | PlainMessage + | undefined, + b: + | GetBlobTakedownResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetBlobTakedownResponse, a, b) + } +} + +/** + * @generated from message bsky.GetActorTakedownRequest + */ +export class GetActorTakedownRequest extends Message { + /** + * @generated from field: string did = 1; + */ + did = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorTakedownRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorTakedownRequest { + return new GetActorTakedownRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorTakedownRequest { + return new GetActorTakedownRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorTakedownRequest { + return new GetActorTakedownRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetActorTakedownRequest + | PlainMessage + | undefined, + b: + | GetActorTakedownRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetActorTakedownRequest, a, b) + } +} + +/** + * @generated from message bsky.GetActorTakedownResponse + */ +export class GetActorTakedownResponse extends Message { + /** + * @generated from field: bool taken_down = 1; + */ + takenDown = false + + /** + * @generated from field: string takedown_ref = 2; + */ + takedownRef = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetActorTakedownResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'taken_down', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { + no: 2, + name: 'takedown_ref', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetActorTakedownResponse { + return new GetActorTakedownResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetActorTakedownResponse { + return new GetActorTakedownResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetActorTakedownResponse { + return new GetActorTakedownResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetActorTakedownResponse + | PlainMessage + | undefined, + b: + | GetActorTakedownResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetActorTakedownResponse, a, b) + } +} + +/** + * @generated from message bsky.GetRecordTakedownRequest + */ +export class GetRecordTakedownRequest extends Message { + /** + * @generated from field: string record_uri = 1; + */ + recordUri = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetRecordTakedownRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'record_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetRecordTakedownRequest { + return new GetRecordTakedownRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetRecordTakedownRequest { + return new GetRecordTakedownRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetRecordTakedownRequest { + return new GetRecordTakedownRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetRecordTakedownRequest + | PlainMessage + | undefined, + b: + | GetRecordTakedownRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetRecordTakedownRequest, a, b) + } +} + +/** + * @generated from message bsky.GetRecordTakedownResponse + */ +export class GetRecordTakedownResponse extends Message { + /** + * @generated from field: bool taken_down = 1; + */ + takenDown = false + + /** + * @generated from field: string takedown_ref = 2; + */ + takedownRef = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetRecordTakedownResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'taken_down', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { + no: 2, + name: 'takedown_ref', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetRecordTakedownResponse { + return new GetRecordTakedownResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetRecordTakedownResponse { + return new GetRecordTakedownResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetRecordTakedownResponse { + return new GetRecordTakedownResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetRecordTakedownResponse + | PlainMessage + | undefined, + b: + | GetRecordTakedownResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetRecordTakedownResponse, a, b) + } +} + +/** + * Ping + * + * @generated from message bsky.PingRequest + */ +export class PingRequest extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.PingRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): PingRequest { + return new PingRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): PingRequest { + return new PingRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): PingRequest { + return new PingRequest().fromJsonString(jsonString, options) + } + + static equals( + a: PingRequest | PlainMessage | undefined, + b: PingRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(PingRequest, a, b) + } +} + +/** + * @generated from message bsky.PingResponse + */ +export class PingResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.PingResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): PingResponse { + return new PingResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): PingResponse { + return new PingResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): PingResponse { + return new PingResponse().fromJsonString(jsonString, options) + } + + static equals( + a: PingResponse | PlainMessage | undefined, + b: PingResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(PingResponse, a, b) + } +} + +/** + * @generated from message bsky.TakedownActorRequest + */ +export class TakedownActorRequest extends Message { + /** + * @generated from field: string did = 1; + */ + did = '' + + /** + * @generated from field: string ref = 2; + */ + ref = '' + + /** + * @generated from field: google.protobuf.Timestamp seen = 3; + */ + seen?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.TakedownActorRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'ref', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'seen', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): TakedownActorRequest { + return new TakedownActorRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): TakedownActorRequest { + return new TakedownActorRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): TakedownActorRequest { + return new TakedownActorRequest().fromJsonString(jsonString, options) + } + + static equals( + a: TakedownActorRequest | PlainMessage | undefined, + b: TakedownActorRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(TakedownActorRequest, a, b) + } +} + +/** + * @generated from message bsky.TakedownActorResponse + */ +export class TakedownActorResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.TakedownActorResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): TakedownActorResponse { + return new TakedownActorResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): TakedownActorResponse { + return new TakedownActorResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): TakedownActorResponse { + return new TakedownActorResponse().fromJsonString(jsonString, options) + } + + static equals( + a: TakedownActorResponse | PlainMessage | undefined, + b: TakedownActorResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(TakedownActorResponse, a, b) + } +} + +/** + * @generated from message bsky.UntakedownActorRequest + */ +export class UntakedownActorRequest extends Message { + /** + * @generated from field: string did = 1; + */ + did = '' + + /** + * @generated from field: google.protobuf.Timestamp seen = 2; + */ + seen?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.UntakedownActorRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'seen', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): UntakedownActorRequest { + return new UntakedownActorRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): UntakedownActorRequest { + return new UntakedownActorRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): UntakedownActorRequest { + return new UntakedownActorRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | UntakedownActorRequest + | PlainMessage + | undefined, + b: + | UntakedownActorRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(UntakedownActorRequest, a, b) + } +} + +/** + * @generated from message bsky.UntakedownActorResponse + */ +export class UntakedownActorResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.UntakedownActorResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): UntakedownActorResponse { + return new UntakedownActorResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): UntakedownActorResponse { + return new UntakedownActorResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): UntakedownActorResponse { + return new UntakedownActorResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | UntakedownActorResponse + | PlainMessage + | undefined, + b: + | UntakedownActorResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(UntakedownActorResponse, a, b) + } +} + +/** + * @generated from message bsky.TakedownBlobRequest + */ +export class TakedownBlobRequest extends Message { + /** + * @generated from field: string did = 1; + */ + did = '' + + /** + * @generated from field: string cid = 2; + */ + cid = '' + + /** + * @generated from field: string ref = 3; + */ + ref = '' + + /** + * @generated from field: google.protobuf.Timestamp seen = 4; + */ + seen?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.TakedownBlobRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'cid', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'ref', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 4, name: 'seen', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): TakedownBlobRequest { + return new TakedownBlobRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): TakedownBlobRequest { + return new TakedownBlobRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): TakedownBlobRequest { + return new TakedownBlobRequest().fromJsonString(jsonString, options) + } + + static equals( + a: TakedownBlobRequest | PlainMessage | undefined, + b: TakedownBlobRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(TakedownBlobRequest, a, b) + } +} + +/** + * @generated from message bsky.TakedownBlobResponse + */ +export class TakedownBlobResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.TakedownBlobResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): TakedownBlobResponse { + return new TakedownBlobResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): TakedownBlobResponse { + return new TakedownBlobResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): TakedownBlobResponse { + return new TakedownBlobResponse().fromJsonString(jsonString, options) + } + + static equals( + a: TakedownBlobResponse | PlainMessage | undefined, + b: TakedownBlobResponse | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(TakedownBlobResponse, a, b) + } +} + +/** + * @generated from message bsky.UntakedownBlobRequest + */ +export class UntakedownBlobRequest extends Message { + /** + * @generated from field: string did = 1; + */ + did = '' + + /** + * @generated from field: string cid = 2; + */ + cid = '' + + /** + * @generated from field: google.protobuf.Timestamp seen = 3; + */ + seen?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.UntakedownBlobRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'cid', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'seen', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): UntakedownBlobRequest { + return new UntakedownBlobRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): UntakedownBlobRequest { + return new UntakedownBlobRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): UntakedownBlobRequest { + return new UntakedownBlobRequest().fromJsonString(jsonString, options) + } + + static equals( + a: UntakedownBlobRequest | PlainMessage | undefined, + b: UntakedownBlobRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(UntakedownBlobRequest, a, b) + } +} + +/** + * @generated from message bsky.UntakedownBlobResponse + */ +export class UntakedownBlobResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.UntakedownBlobResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): UntakedownBlobResponse { + return new UntakedownBlobResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): UntakedownBlobResponse { + return new UntakedownBlobResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): UntakedownBlobResponse { + return new UntakedownBlobResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | UntakedownBlobResponse + | PlainMessage + | undefined, + b: + | UntakedownBlobResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(UntakedownBlobResponse, a, b) + } +} + +/** + * @generated from message bsky.TakedownRecordRequest + */ +export class TakedownRecordRequest extends Message { + /** + * @generated from field: string record_uri = 1; + */ + recordUri = '' + + /** + * @generated from field: string ref = 2; + */ + ref = '' + + /** + * @generated from field: google.protobuf.Timestamp seen = 3; + */ + seen?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.TakedownRecordRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'record_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'ref', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 3, name: 'seen', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): TakedownRecordRequest { + return new TakedownRecordRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): TakedownRecordRequest { + return new TakedownRecordRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): TakedownRecordRequest { + return new TakedownRecordRequest().fromJsonString(jsonString, options) + } + + static equals( + a: TakedownRecordRequest | PlainMessage | undefined, + b: TakedownRecordRequest | PlainMessage | undefined, + ): boolean { + return proto3.util.equals(TakedownRecordRequest, a, b) + } +} + +/** + * @generated from message bsky.TakedownRecordResponse + */ +export class TakedownRecordResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.TakedownRecordResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): TakedownRecordResponse { + return new TakedownRecordResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): TakedownRecordResponse { + return new TakedownRecordResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): TakedownRecordResponse { + return new TakedownRecordResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | TakedownRecordResponse + | PlainMessage + | undefined, + b: + | TakedownRecordResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(TakedownRecordResponse, a, b) + } +} + +/** + * @generated from message bsky.UntakedownRecordRequest + */ +export class UntakedownRecordRequest extends Message { + /** + * @generated from field: string record_uri = 1; + */ + recordUri = '' + + /** + * @generated from field: google.protobuf.Timestamp seen = 2; + */ + seen?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.UntakedownRecordRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'record_uri', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { no: 2, name: 'seen', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): UntakedownRecordRequest { + return new UntakedownRecordRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): UntakedownRecordRequest { + return new UntakedownRecordRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): UntakedownRecordRequest { + return new UntakedownRecordRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | UntakedownRecordRequest + | PlainMessage + | undefined, + b: + | UntakedownRecordRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(UntakedownRecordRequest, a, b) + } +} + +/** + * @generated from message bsky.UntakedownRecordResponse + */ +export class UntakedownRecordResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.UntakedownRecordResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): UntakedownRecordResponse { + return new UntakedownRecordResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): UntakedownRecordResponse { + return new UntakedownRecordResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): UntakedownRecordResponse { + return new UntakedownRecordResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | UntakedownRecordResponse + | PlainMessage + | undefined, + b: + | UntakedownRecordResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(UntakedownRecordResponse, a, b) + } +} + +/** + * @generated from message bsky.CreateActorMuteRequest + */ +export class CreateActorMuteRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: string subject_did = 2; + */ + subjectDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.CreateActorMuteRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 2, + name: 'subject_did', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): CreateActorMuteRequest { + return new CreateActorMuteRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): CreateActorMuteRequest { + return new CreateActorMuteRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): CreateActorMuteRequest { + return new CreateActorMuteRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | CreateActorMuteRequest + | PlainMessage + | undefined, + b: + | CreateActorMuteRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(CreateActorMuteRequest, a, b) + } +} + +/** + * @generated from message bsky.CreateActorMuteResponse + */ +export class CreateActorMuteResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.CreateActorMuteResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): CreateActorMuteResponse { + return new CreateActorMuteResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): CreateActorMuteResponse { + return new CreateActorMuteResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): CreateActorMuteResponse { + return new CreateActorMuteResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | CreateActorMuteResponse + | PlainMessage + | undefined, + b: + | CreateActorMuteResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(CreateActorMuteResponse, a, b) + } +} + +/** + * @generated from message bsky.DeleteActorMuteRequest + */ +export class DeleteActorMuteRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: string subject_did = 2; + */ + subjectDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.DeleteActorMuteRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 2, + name: 'subject_did', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): DeleteActorMuteRequest { + return new DeleteActorMuteRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): DeleteActorMuteRequest { + return new DeleteActorMuteRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): DeleteActorMuteRequest { + return new DeleteActorMuteRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | DeleteActorMuteRequest + | PlainMessage + | undefined, + b: + | DeleteActorMuteRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(DeleteActorMuteRequest, a, b) + } +} + +/** + * @generated from message bsky.DeleteActorMuteResponse + */ +export class DeleteActorMuteResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.DeleteActorMuteResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): DeleteActorMuteResponse { + return new DeleteActorMuteResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): DeleteActorMuteResponse { + return new DeleteActorMuteResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): DeleteActorMuteResponse { + return new DeleteActorMuteResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | DeleteActorMuteResponse + | PlainMessage + | undefined, + b: + | DeleteActorMuteResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(DeleteActorMuteResponse, a, b) + } +} + +/** + * @generated from message bsky.ClearActorMutesRequest + */ +export class ClearActorMutesRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.ClearActorMutesRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): ClearActorMutesRequest { + return new ClearActorMutesRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): ClearActorMutesRequest { + return new ClearActorMutesRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): ClearActorMutesRequest { + return new ClearActorMutesRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | ClearActorMutesRequest + | PlainMessage + | undefined, + b: + | ClearActorMutesRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(ClearActorMutesRequest, a, b) + } +} + +/** + * @generated from message bsky.ClearActorMutesResponse + */ +export class ClearActorMutesResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.ClearActorMutesResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): ClearActorMutesResponse { + return new ClearActorMutesResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): ClearActorMutesResponse { + return new ClearActorMutesResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): ClearActorMutesResponse { + return new ClearActorMutesResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | ClearActorMutesResponse + | PlainMessage + | undefined, + b: + | ClearActorMutesResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(ClearActorMutesResponse, a, b) + } +} + +/** + * @generated from message bsky.CreateActorMutelistSubscriptionRequest + */ +export class CreateActorMutelistSubscriptionRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: string subject_uri = 2; + */ + subjectUri = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.CreateActorMutelistSubscriptionRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 2, + name: 'subject_uri', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): CreateActorMutelistSubscriptionRequest { + return new CreateActorMutelistSubscriptionRequest().fromBinary( + bytes, + options, + ) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): CreateActorMutelistSubscriptionRequest { + return new CreateActorMutelistSubscriptionRequest().fromJson( + jsonValue, + options, + ) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): CreateActorMutelistSubscriptionRequest { + return new CreateActorMutelistSubscriptionRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | CreateActorMutelistSubscriptionRequest + | PlainMessage + | undefined, + b: + | CreateActorMutelistSubscriptionRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(CreateActorMutelistSubscriptionRequest, a, b) + } +} + +/** + * @generated from message bsky.CreateActorMutelistSubscriptionResponse + */ +export class CreateActorMutelistSubscriptionResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.CreateActorMutelistSubscriptionResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): CreateActorMutelistSubscriptionResponse { + return new CreateActorMutelistSubscriptionResponse().fromBinary( + bytes, + options, + ) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): CreateActorMutelistSubscriptionResponse { + return new CreateActorMutelistSubscriptionResponse().fromJson( + jsonValue, + options, + ) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): CreateActorMutelistSubscriptionResponse { + return new CreateActorMutelistSubscriptionResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | CreateActorMutelistSubscriptionResponse + | PlainMessage + | undefined, + b: + | CreateActorMutelistSubscriptionResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(CreateActorMutelistSubscriptionResponse, a, b) + } +} + +/** + * @generated from message bsky.DeleteActorMutelistSubscriptionRequest + */ +export class DeleteActorMutelistSubscriptionRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + /** + * @generated from field: string subject_uri = 2; + */ + subjectUri = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.DeleteActorMutelistSubscriptionRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + { + no: 2, + name: 'subject_uri', + kind: 'scalar', + T: 9 /* ScalarType.STRING */, + }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): DeleteActorMutelistSubscriptionRequest { + return new DeleteActorMutelistSubscriptionRequest().fromBinary( + bytes, + options, + ) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): DeleteActorMutelistSubscriptionRequest { + return new DeleteActorMutelistSubscriptionRequest().fromJson( + jsonValue, + options, + ) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): DeleteActorMutelistSubscriptionRequest { + return new DeleteActorMutelistSubscriptionRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | DeleteActorMutelistSubscriptionRequest + | PlainMessage + | undefined, + b: + | DeleteActorMutelistSubscriptionRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(DeleteActorMutelistSubscriptionRequest, a, b) + } +} + +/** + * @generated from message bsky.DeleteActorMutelistSubscriptionResponse + */ +export class DeleteActorMutelistSubscriptionResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.DeleteActorMutelistSubscriptionResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): DeleteActorMutelistSubscriptionResponse { + return new DeleteActorMutelistSubscriptionResponse().fromBinary( + bytes, + options, + ) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): DeleteActorMutelistSubscriptionResponse { + return new DeleteActorMutelistSubscriptionResponse().fromJson( + jsonValue, + options, + ) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): DeleteActorMutelistSubscriptionResponse { + return new DeleteActorMutelistSubscriptionResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | DeleteActorMutelistSubscriptionResponse + | PlainMessage + | undefined, + b: + | DeleteActorMutelistSubscriptionResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(DeleteActorMutelistSubscriptionResponse, a, b) + } +} + +/** + * @generated from message bsky.ClearActorMutelistSubscriptionsRequest + */ +export class ClearActorMutelistSubscriptionsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = '' + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.ClearActorMutelistSubscriptionsRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): ClearActorMutelistSubscriptionsRequest { + return new ClearActorMutelistSubscriptionsRequest().fromBinary( + bytes, + options, + ) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): ClearActorMutelistSubscriptionsRequest { + return new ClearActorMutelistSubscriptionsRequest().fromJson( + jsonValue, + options, + ) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): ClearActorMutelistSubscriptionsRequest { + return new ClearActorMutelistSubscriptionsRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | ClearActorMutelistSubscriptionsRequest + | PlainMessage + | undefined, + b: + | ClearActorMutelistSubscriptionsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(ClearActorMutelistSubscriptionsRequest, a, b) + } +} + +/** + * @generated from message bsky.ClearActorMutelistSubscriptionsResponse + */ +export class ClearActorMutelistSubscriptionsResponse extends Message { + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.ClearActorMutelistSubscriptionsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => []) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): ClearActorMutelistSubscriptionsResponse { + return new ClearActorMutelistSubscriptionsResponse().fromBinary( + bytes, + options, + ) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): ClearActorMutelistSubscriptionsResponse { + return new ClearActorMutelistSubscriptionsResponse().fromJson( + jsonValue, + options, + ) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): ClearActorMutelistSubscriptionsResponse { + return new ClearActorMutelistSubscriptionsResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | ClearActorMutelistSubscriptionsResponse + | PlainMessage + | undefined, + b: + | ClearActorMutelistSubscriptionsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(ClearActorMutelistSubscriptionsResponse, a, b) + } +} diff --git a/packages/bsky/src/services/actor/index.ts b/packages/bsky/src/services/actor/index.ts deleted file mode 100644 index 096bf18be9b..00000000000 --- a/packages/bsky/src/services/actor/index.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { sql } from 'kysely' -import { wait } from '@atproto/common' -import { Database } from '../../db' -import { notSoftDeletedClause } from '../../db/util' -import { ActorViews } from './views' -import { ImageUriBuilder } from '../../image/uri' -import { Actor } from '../../db/tables/actor' -import { TimeCidKeyset, paginate } from '../../db/pagination' -import { SearchKeyset, getUserSearchQuery } from '../util/search' -import { FromDb } from '../types' -import { GraphService } from '../graph' -import { LabelService } from '../label' -import { AtUri } from '@atproto/syntax' -import { ids } from '../../lexicon/lexicons' -import { Platform } from '../../notifications' - -export * from './types' - -export class ActorService { - views: ActorViews - - constructor( - public db: Database, - public imgUriBuilder: ImageUriBuilder, - graph: FromDb, - label: FromDb, - ) { - this.views = new ActorViews(this.db, this.imgUriBuilder, graph, label) - } - - static creator( - imgUriBuilder: ImageUriBuilder, - graph: FromDb, - label: FromDb, - ) { - return (db: Database) => new ActorService(db, imgUriBuilder, graph, label) - } - - async getActorDid(handleOrDid: string): Promise { - if (handleOrDid.startsWith('did:')) { - return handleOrDid - } - const subject = await this.getActor(handleOrDid, true) - return subject?.did ?? null - } - - async getActor( - handleOrDid: string, - includeSoftDeleted = false, - ): Promise { - const actors = await this.getActors([handleOrDid], includeSoftDeleted) - return actors[0] || null - } - - async getActors( - handleOrDids: string[], - includeSoftDeleted = false, - ): Promise { - const { ref } = this.db.db.dynamic - const dids: string[] = [] - const handles: string[] = [] - const order: Record = {} - handleOrDids.forEach((item, i) => { - if (item.startsWith('did:')) { - order[item] = i - dids.push(item) - } else { - order[item.toLowerCase()] = i - handles.push(item.toLowerCase()) - } - }) - const results = await this.db.db - .selectFrom('actor') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('actor'))), - ) - .where((qb) => { - if (dids.length) { - qb = qb.orWhere('actor.did', 'in', dids) - } - if (handles.length) { - qb = qb.orWhere( - 'actor.handle', - 'in', - handles.length === 1 - ? [handles[0], handles[0]] // a silly (but worthwhile) optimization to avoid usage of actor_handle_tgrm_idx - : handles, - ) - } - return qb - }) - .selectAll() - .execute() - - return results.sort((a, b) => { - const orderA = order[a.did] ?? order[a.handle?.toLowerCase() ?? ''] - const orderB = order[b.did] ?? order[b.handle?.toLowerCase() ?? ''] - return orderA - orderB - }) - } - - async getProfileRecords(dids: string[], includeSoftDeleted = false) { - if (dids.length === 0) return new Map() - const profileUris = dids.map((did) => - AtUri.make(did, ids.AppBskyActorProfile, 'self').toString(), - ) - const { ref } = this.db.db.dynamic - const res = await this.db.db - .selectFrom('record') - .innerJoin('actor', 'actor.did', 'record.did') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('actor'))), - ) - .where('uri', 'in', profileUris) - .select(['record.did', 'record.json']) - .execute() - return res.reduce((acc, cur) => { - return acc.set(cur.did, JSON.parse(cur.json)) - }, new Map()) - } - - async getSearchResults({ - cursor, - limit = 25, - query = '', - includeSoftDeleted, - }: { - cursor?: string - limit?: number - query?: string - includeSoftDeleted?: boolean - }) { - const searchField = query.startsWith('did:') ? 'did' : 'handle' - let paginatedBuilder - const { ref } = this.db.db.dynamic - const paginationOptions = { - limit, - cursor, - direction: 'asc' as const, - } - let keyset - - if (query && searchField === 'handle') { - keyset = new SearchKeyset(sql``, sql``) - paginatedBuilder = getUserSearchQuery(this.db, { - query, - 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 query will always be a valid string because - // searchField is set to 'did' after checking that the query is a valid did - if (query && searchField === 'did') { - paginatedBuilder = paginatedBuilder.where('actor.did', '=', query) - } - paginatedBuilder = paginate(paginatedBuilder, { - keyset, - ...paginationOptions, - }) - } - - const results: Actor[] = await paginatedBuilder.selectAll('actor').execute() - return { results, cursor: keyset.packFromResult(results) } - } - - 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 - } - - async *all( - opts: { - batchSize?: number - forever?: boolean - cooldownMs?: number - startFromDid?: string - } = {}, - ) { - const { - cooldownMs = 1000, - batchSize = 1000, - forever = false, - startFromDid, - } = opts - const baseQuery = this.db.db - .selectFrom('actor') - .selectAll() - .orderBy('did') - .limit(batchSize) - while (true) { - let cursor = startFromDid - do { - const actors = cursor - ? await baseQuery.where('did', '>', cursor).execute() - : await baseQuery.execute() - for (const actor of actors) { - yield actor - } - cursor = actors.at(-1)?.did - } while (cursor) - if (forever) { - await wait(cooldownMs) - } else { - return - } - } - } - - async registerPushDeviceToken( - did: string, - token: string, - platform: Platform, - appId: string, - ) { - await this.db - .asPrimary() - .db.insertInto('notification_push_token') - .values({ did, token, platform, appId }) - .onConflict((oc) => oc.doNothing()) - .execute() - } -} - -type ActorResult = Actor -export class ListKeyset extends TimeCidKeyset<{ - indexedAt: string - did: string // handles are treated identically to cids in TimeCidKeyset -}> { - labelResult(result: { indexedAt: string; did: string }) { - return { primary: result.indexedAt, secondary: result.did } - } -} diff --git a/packages/bsky/src/services/actor/types.ts b/packages/bsky/src/services/actor/types.ts deleted file mode 100644 index d622e641099..00000000000 --- a/packages/bsky/src/services/actor/types.ts +++ /dev/null @@ -1,75 +0,0 @@ -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 - blockingByList?: ListViewBasic - 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 deleted file mode 100644 index 32e267a8868..00000000000 --- a/packages/bsky/src/services/actor/views.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { mapDefined } from '@atproto/common' -import { INVALID_HANDLE } from '@atproto/syntax' -import { jsonStringToLex } from '@atproto/lexicon' -import { - ProfileViewDetailed, - ProfileView, -} 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, Labels, getSelfLabels } from '../label' -import { BlockAndMuteState, GraphService } from '../graph' -import { - ActorInfoMap, - ProfileDetailHydrationState, - ProfileHydrationState, - ProfileInfoMap, - ProfileViewMap, - toMapByDid, -} from './types' -import { ListInfoMap } from '../graph/types' -import { FromDb } from '../types' - -export class ActorViews { - services: { - label: LabelService - graph: GraphService - } - - constructor( - private db: Database, - private imgUriBuilder: ImageUriBuilder, - private graph: FromDb, - private label: FromDb, - ) { - this.services = { - label: label(db), - graph: graph(db), - } - } - - async profiles( - results: (ActorResult | string)[], // @TODO simplify down to just string[] - viewer: string | null, - 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) - } - - async profilesBasic( - results: (ActorResult | string)[], - viewer: string | null, - 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, - includeSoftDeleted: opts?.includeSoftDeleted, - }) - return this.profileBasicPresentation(dids, hydrated, viewer) - } - - 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.length ? 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'))), - ) - .select([ - '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.bannerCid as bannerCid', - 'profile.indexedAt as indexedAt', - '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)) - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('actor.did')) - .select('uri') - .as('viewerFollowing'), - this.db.db - .selectFrom('follow') - .if(!viewer, (q) => q.where(noMatch)) - .whereRef('creator', '=', ref('actor.did')) - .where('subjectDid', '=', viewer ?? '') - .select('uri') - .as('viewerFollowedBy'), - ]) - const [profiles, labels, bam] = await Promise.all([ - profileInfosQb.execute(), - 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 muteList = viewer && bam.muteList([viewer, did]) - const blockList = viewer && bam.blockList([viewer, did]) - const lists: string[] = [] - if (muteList) lists.push(muteList) - if (blockList) lists.push(blockList) - return lists - }).flat() - const lists = await this.services.graph.getListViews(listUris, viewer) - return { profilesDetailed: toMapByDid(profiles), labels, bam, lists } - } - - 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 = prof?.bannerCid - ? this.imgUriBuilder.getPresetUri('banner', prof.did, prof.bannerCid) - : undefined - const mutedByListUri = viewer && bam.muteList([viewer, did]) - const mutedByList = - mutedByListUri && lists[mutedByListUri] - ? this.services.graph.formatListViewBasic(lists[mutedByListUri]) - : undefined - const blockingByListUri = viewer && bam.blockList([viewer, did]) - const blockingByList = - blockingByListUri && lists[blockingByListUri] - ? this.services.graph.formatListViewBasic(lists[blockingByListUri]) - : undefined - const actorLabels = labels[did] ?? [] - const selfLabels = getSelfLabels({ - uri: prof.profileUri, - cid: prof.profileCid, - record: - prof.profileJson !== null - ? (jsonStringToLex(prof.profileJson) as Record) - : null, - }) - acc[did] = { - did: prof.did, - handle: prof.handle ?? INVALID_HANDLE, - displayName: prof?.displayName || undefined, - description: prof?.description || undefined, - avatar, - banner, - followsCount: prof?.followsCount ?? 0, - followersCount: prof?.followersCount ?? 0, - postsCount: prof?.postsCount ?? 0, - indexedAt: prof?.indexedAt || undefined, - viewer: viewer - ? { - muted: bam.mute([viewer, did]), - mutedByList, - blockedBy: !!bam.blockedBy([viewer, did]), - blocking: bam.blocking([viewer, did]) ?? undefined, - blockingByList, - following: - prof?.viewerFollowing && !bam.block([viewer, did]) - ? prof.viewerFollowing - : undefined, - followedBy: - prof?.viewerFollowedBy && !bam.block([viewer, did]) - ? prof.viewerFollowedBy - : undefined, - } - : undefined, - labels: [...actorLabels, ...selfLabels], - } - return acc - }, {} as Record) - } - - 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 profileInfosQb = this.db.db - .selectFrom('actor') - .where('actor.did', 'in', dids.length ? dids : ['']) - .leftJoin('profile', 'profile.creator', 'actor.did') - .leftJoin('record', 'record.uri', 'profile.uri') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('actor'))), - ) - .select([ - '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)) - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('actor.did')) - .select('uri') - .as('viewerFollowing'), - this.db.db - .selectFrom('follow') - .if(!viewer, (q) => q.where(noMatch)) - .whereRef('creator', '=', ref('actor.did')) - .where('subjectDid', '=', viewer ?? '') - .select('uri') - .as('viewerFollowedBy'), - ]) - const [profiles, labels, bam] = await Promise.all([ - profileInfosQb.execute(), - 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 muteList = viewer && bam.muteList([viewer, did]) - const blockList = viewer && bam.blockList([viewer, did]) - const lists: string[] = [] - if (muteList) lists.push(muteList) - if (blockList) lists.push(blockList) - return lists - }).flat() - const lists = await this.services.graph.getListViews(listUris, viewer) - return { profiles: toMapByDid(profiles), labels, bam, lists } - } - - profilePresentation( - dids: string[], - state: { - profiles: ProfileInfoMap - lists: ListInfoMap - labels: Labels - bam: BlockAndMuteState - }, - viewer: string | null, - ): ProfileViewMap { - 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 = - mutedByListUri && lists[mutedByListUri] - ? this.services.graph.formatListViewBasic(lists[mutedByListUri]) - : undefined - const blockingByListUri = viewer && bam.blockList([viewer, did]) - const blockingByList = - blockingByListUri && lists[blockingByListUri] - ? this.services.graph.formatListViewBasic(lists[blockingByListUri]) - : undefined - const actorLabels = labels[did] ?? [] - const selfLabels = getSelfLabels({ - uri: prof.profileUri, - cid: prof.profileCid, - record: - prof.profileJson !== null - ? (jsonStringToLex(prof.profileJson) as Record) - : null, - }) - acc[did] = { - did: prof.did, - handle: prof.handle ?? INVALID_HANDLE, - displayName: prof?.displayName || undefined, - description: prof?.description || undefined, - avatar, - indexedAt: prof?.indexedAt || undefined, - viewer: viewer - ? { - muted: bam.mute([viewer, did]), - mutedByList, - blockedBy: !!bam.blockedBy([viewer, did]), - blocking: bam.blocking([viewer, did]) ?? undefined, - blockingByList, - following: - prof?.viewerFollowing && !bam.block([viewer, did]) - ? prof.viewerFollowing - : undefined, - followedBy: - prof?.viewerFollowedBy && !bam.block([viewer, did]) - ? prof.viewerFollowedBy - : undefined, - } - : undefined, - labels: [...actorLabels, ...selfLabels], - } - return acc - }, {} as ProfileViewMap) - } - - profileBasicPresentation( - dids: string[], - state: ProfileHydrationState, - viewer: string | null, - ): ProfileViewMap { - const result = this.profilePresentation(dids, state, viewer) - 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: prof.labels, - } - acc[prof.did] = profileBasic - return acc - }, {} as ProfileViewMap) - } -} - -type ActorResult = Actor diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts deleted file mode 100644 index a8768518d70..00000000000 --- a/packages/bsky/src/services/feed/index.ts +++ /dev/null @@ -1,561 +0,0 @@ -import { sql } from 'kysely' -import { AtUri } from '@atproto/syntax' -import { jsonStringToLex } from '@atproto/lexicon' -import { mapDefined } from '@atproto/common' -import { Database } from '../../db' -import { countAll, noMatch, notSoftDeletedClause } from '../../db/util' -import { ImageUriBuilder } from '../../image/uri' -import { ids } from '../../lexicon/lexicons' -import { - Record as PostRecord, - isRecord as isPostRecord, -} from '../../lexicon/types/app/bsky/feed/post' -import { - Record as ThreadgateRecord, - isListRule, -} from '../../lexicon/types/app/bsky/feed/threadgate' -import { isMain as isEmbedImages } from '../../lexicon/types/app/bsky/embed/images' -import { isMain as isEmbedExternal } from '../../lexicon/types/app/bsky/embed/external' -import { - isMain as isEmbedRecord, - isViewRecord, -} from '../../lexicon/types/app/bsky/embed/record' -import { isMain as isEmbedRecordWithMedia } from '../../lexicon/types/app/bsky/embed/recordWithMedia' -import { - PostInfoMap, - FeedItemType, - FeedRow, - FeedGenInfoMap, - PostEmbedViews, - RecordEmbedViewRecordMap, - PostInfo, - RecordEmbedViewRecord, - PostBlocksMap, - FeedHydrationState, - ThreadgateInfoMap, -} from './types' -import { LabelService } from '../label' -import { ActorService } from '../actor' -import { - BlockAndMuteState, - GraphService, - ListInfoMap, - RelationshipPair, -} from '../graph' -import { FeedViews } from './views' -import { threadgateToPostUri, postToThreadgateUri } from './util' -import { FromDb } from '../types' - -export * from './types' - -export class FeedService { - views: FeedViews - services: { - label: LabelService - actor: ActorService - graph: GraphService - } - - constructor( - public db: Database, - public imgUriBuilder: ImageUriBuilder, - private actor: FromDb, - private label: FromDb, - private graph: FromDb, - ) { - this.views = new FeedViews(this.db, this.imgUriBuilder, actor, graph) - this.services = { - label: label(this.db), - actor: actor(this.db), - graph: graph(this.db), - } - } - - static creator( - imgUriBuilder: ImageUriBuilder, - actor: FromDb, - label: FromDb, - graph: FromDb, - ) { - return (db: Database) => - new FeedService(db, imgUriBuilder, actor, label, graph) - } - - selectPostQb() { - return this.db.db - .selectFrom('post') - .select([ - sql`${'post'}`.as('type'), - 'post.uri as uri', - 'post.cid as cid', - 'post.uri as postUri', - 'post.creator as originatorDid', - 'post.creator as postAuthorDid', - 'post.replyParent as replyParent', - 'post.replyRoot as replyRoot', - 'post.sortAt as sortAt', - ]) - } - - selectFeedItemQb() { - return this.db.db - .selectFrom('feed_item') - .innerJoin('post', 'post.uri', 'feed_item.postUri') - .selectAll('feed_item') - .select([ - 'post.replyRoot', - 'post.replyParent', - 'post.creator as postAuthorDid', - ]) - } - - selectFeedGeneratorQb(viewer?: string | null) { - const { ref } = this.db.db.dynamic - return this.db.db - .selectFrom('feed_generator') - .innerJoin('actor', 'actor.did', 'feed_generator.creator') - .innerJoin('record', 'record.uri', 'feed_generator.uri') - .selectAll('feed_generator') - .where(notSoftDeletedClause(ref('actor'))) - .where(notSoftDeletedClause(ref('record'))) - .select((qb) => - qb - .selectFrom('like') - .whereRef('like.subject', '=', 'feed_generator.uri') - .select(countAll.as('count')) - .as('likeCount'), - ) - .select((qb) => - qb - .selectFrom('like') - .if(!viewer, (q) => q.where(noMatch)) - .where('like.creator', '=', viewer ?? '') - .whereRef('like.subject', '=', 'feed_generator.uri') - .select('uri') - .as('viewerLike'), - ) - } - - async getPostInfos( - postUris: string[], - viewer: string | null, - ): Promise { - if (postUris.length < 1) return {} - const db = this.db.db - const { ref } = db.dynamic - const posts = await db - .selectFrom('post') - .where('post.uri', 'in', postUris) - .innerJoin('actor', 'actor.did', 'post.creator') - .innerJoin('record', 'record.uri', 'post.uri') - .leftJoin('post_agg', 'post_agg.uri', 'post.uri') - .where(notSoftDeletedClause(ref('actor'))) // Ensures post reply parent/roots get omitted from views when taken down - .where(notSoftDeletedClause(ref('record'))) - .select([ - 'post.uri as uri', - 'post.cid as cid', - 'post.creator as creator', - 'post.sortAt as indexedAt', - 'post.invalidReplyRoot as invalidReplyRoot', - 'post.violatesThreadGate as violatesThreadGate', - 'record.json as recordJson', - 'post_agg.likeCount as likeCount', - 'post_agg.repostCount as repostCount', - 'post_agg.replyCount as replyCount', - 'post.tags as tags', - db - .selectFrom('repost') - .if(!viewer, (q) => q.where(noMatch)) - .where('creator', '=', viewer ?? '') - .whereRef('subject', '=', ref('post.uri')) - .select('uri') - .as('requesterRepost'), - db - .selectFrom('like') - .if(!viewer, (q) => q.where(noMatch)) - .where('creator', '=', viewer ?? '') - .whereRef('subject', '=', ref('post.uri')) - .select('uri') - .as('requesterLike'), - ]) - .execute() - return posts.reduce((acc, cur) => { - const { recordJson, ...post } = cur - const record = jsonStringToLex(recordJson) as PostRecord - const info: PostInfo = { - ...post, - invalidReplyRoot: post.invalidReplyRoot ?? false, - violatesThreadGate: post.violatesThreadGate ?? false, - record, - viewer, - } - return Object.assign(acc, { [post.uri]: info }) - }, {} as PostInfoMap) - } - - async getFeedGeneratorInfos(generatorUris: string[], viewer: string | null) { - if (generatorUris.length < 1) return {} - const feedGens = await this.selectFeedGeneratorQb(viewer) - .where('feed_generator.uri', 'in', generatorUris) - .execute() - return feedGens.reduce( - (acc, cur) => ({ - ...acc, - [cur.uri]: { - ...cur, - viewer: viewer ? { like: cur.viewerLike } : undefined, - }, - }), - {} as FeedGenInfoMap, - ) - } - - async getFeedItems(uris: string[]): Promise> { - if (uris.length < 1) return {} - const feedItems = await this.selectFeedItemQb() - .where('feed_item.uri', 'in', uris) - .execute() - return feedItems.reduce((acc, item) => { - return Object.assign(acc, { [item.uri]: item }) - }, {} as Record) - } - - async postUrisToFeedItems(uris: string[]): Promise { - const feedItems = await this.getFeedItems(uris) - return mapDefined(uris, (uri) => feedItems[uri]) - } - - feedItemRefs(items: FeedRow[]) { - const actorDids = new Set() - const postUris = new Set() - for (const item of items) { - postUris.add(item.postUri) - actorDids.add(item.postAuthorDid) - actorDids.add(item.originatorDid) - if (item.replyParent) { - postUris.add(item.replyParent) - actorDids.add(new AtUri(item.replyParent).hostname) - } - if (item.replyRoot) { - postUris.add(item.replyRoot) - actorDids.add(new AtUri(item.replyRoot).hostname) - } - } - return { dids: actorDids, uris: postUris } - } - - async feedHydration( - refs: { - dids: Set - uris: Set - viewer: string | null - }, - depth = 0, - ): Promise { - const { viewer, dids, uris } = refs - const [posts, threadgates, labels, bam] = await Promise.all([ - this.getPostInfos(Array.from(uris), viewer), - this.threadgatesByPostUri(Array.from(uris)), - this.services.label.getLabelsForSubjects([...uris, ...dids]), - this.services.graph.getBlockAndMuteState( - viewer ? [...dids].map((did) => [viewer, did]) : [], - ), - ]) - - // profileState for labels and bam handled above, profileHydration() shouldn't fetch additional - const [profileState, blocks, lists] = await Promise.all([ - this.services.actor.views.profileHydration( - Array.from(dids), - { viewer }, - { bam, labels }, - ), - this.blocksForPosts(posts, bam), - this.listsForThreadgates(threadgates, viewer), - ]) - const embeds = await this.embedsForPosts(posts, blocks, viewer, depth) - return { - posts, - threadgates, - blocks, - embeds, - labels, // includes info for profiles - bam, // includes info for profiles - profiles: profileState.profiles, - lists: Object.assign(lists, profileState.lists), - } - } - - // applies blocks for visibility to third-parties (i.e. based on post content) - async blocksForPosts( - posts: PostInfoMap, - bam?: BlockAndMuteState, - ): 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 blockState = await this.services.graph.getBlockState( - relationships, - bam, - ) - const result: PostBlocksMap = {} - Object.entries(byPost).forEach(([uri, block]) => { - if (block.embed && blockState.block(block.embed)) { - result[uri] ??= {} - result[uri].embed = true - } - if (block.reply && blockState.block(block.reply)) { - result[uri] ??= {} - result[uri].reply = true - } - }) - return result - } - - async embedsForPosts( - postInfos: PostInfoMap, - blocks: PostBlocksMap, - viewer: string | null, - depth: number, - ) { - 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: 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( - creator, - post.embed, - applyEmbedBlock(uri, blocks, 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 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) - nestedDids.add(parsed.hostname) - if (parsed.collection === ids.AppBskyFeedPost) { - nestedPostUris.add(uri) - } else if (parsed.collection === ids.AppBskyFeedGenerator) { - nestedFeedGenUris.add(uri) - } else if (parsed.collection === ids.AppBskyGraphList) { - nestedListUris.add(uri) - } - } - 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) { - const collection = new AtUri(uri).collection - if (collection === ids.AppBskyFeedGenerator && feedGenInfos[uri]) { - const genView = this.views.formatFeedGeneratorView( - feedGenInfos[uri], - actorInfos, - ) - if (genView) { - recordEmbedViews[uri] = { - $type: 'app.bsky.feed.defs#generatorView', - ...genView, - } - } - } else if (collection === ids.AppBskyGraphList && listViews[uri]) { - const listView = this.services.graph.formatListView( - listViews[uri], - actorInfos, - ) - if (listView) { - recordEmbedViews[uri] = { - $type: 'app.bsky.graph.defs#listView', - ...listView, - } - } - } else if (collection === ids.AppBskyFeedPost && feedState.posts[uri]) { - const formatted = this.views.formatPostView( - uri, - actorInfos, - feedState.posts, - feedState.threadgates, - feedState.embeds, - feedState.labels, - feedState.lists, - viewer, - ) - recordEmbedViews[uri] = this.views.getRecordEmbedView( - uri, - formatted, - depth > 0, - ) - } else { - recordEmbedViews[uri] = { - $type: 'app.bsky.embed.record#viewNotFound', - uri, - notFound: true, - } - } - } - return recordEmbedViews - } - - async threadgatesByPostUri(postUris: string[]): Promise { - const gates = postUris.length - ? await this.db.db - .selectFrom('record') - .where('uri', 'in', postUris.map(postToThreadgateUri)) - .select(['uri', 'cid', 'json']) - .execute() - : [] - const gatesByPostUri = gates.reduce((acc, gate) => { - const record = jsonStringToLex(gate.json) as ThreadgateRecord - const postUri = threadgateToPostUri(gate.uri) - if (record.post !== postUri) return acc // invalid, skip - acc[postUri] = { uri: gate.uri, cid: gate.cid, record } - return acc - }, {} as ThreadgateInfoMap) - return gatesByPostUri - } - - listsForThreadgates( - threadgates: ThreadgateInfoMap, - viewer: string | null, - ): Promise { - const listsUris = new Set() - Object.values(threadgates).forEach((gate) => { - gate?.record.allow?.forEach((rule) => { - if (isListRule(rule)) { - listsUris.add(rule.list) - } - }) - }) - return this.services.graph.getListViews([...listsUris], viewer) - } -} - -const postRecordsFromInfos = ( - infos: PostInfoMap, -): { [uri: string]: PostRecord } => { - const records: { [uri: string]: PostRecord } = {} - for (const [uri, info] of Object.entries(infos)) { - if (isPostRecord(info.record)) { - records[uri] = info.record - } - } - return records -} - -const nestedRecordUris = (posts: PostRecord[]): string[] => { - const uris: string[] = [] - for (const post of posts) { - if (!post.embed) continue - if (isEmbedRecord(post.embed)) { - uris.push(post.embed.record.uri) - } else if (isEmbedRecordWithMedia(post.embed)) { - uris.push(post.embed.record.record.uri) - } else { - continue - } - } - return uris -} - -type PostRelationships = { reply?: RelationshipPair; embed?: RelationshipPair } - -function applyEmbedBlock( - uri: string, - blocks: PostBlocksMap, - view: RecordEmbedViewRecord, -): RecordEmbedViewRecord { - if (isViewRecord(view) && blocks[uri]?.embed) { - return { - $type: 'app.bsky.embed.record#viewBlocked', - uri: view.uri, - blocked: true, - author: { - did: view.author.did, - viewer: view.author.viewer - ? { - blockedBy: view.author.viewer?.blockedBy, - blocking: view.author.viewer?.blocking, - } - : undefined, - }, - } - } - return view -} diff --git a/packages/bsky/src/services/feed/types.ts b/packages/bsky/src/services/feed/types.ts deleted file mode 100644 index 8d4bd67f6bb..00000000000 --- a/packages/bsky/src/services/feed/types.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Selectable } from 'kysely' -import { Record as ThreadgateRecord } from '../../lexicon/types/app/bsky/feed/threadgate' -import { View as ImagesEmbedView } from '../../lexicon/types/app/bsky/embed/images' -import { View as ExternalEmbedView } from '../../lexicon/types/app/bsky/embed/external' -import { - 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 { 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 -} - -export type PostEmbedView = - | ImagesEmbedView - | ExternalEmbedView - | RecordEmbedView - | RecordWithMediaEmbedView - -export type PostInfo = { - uri: string - cid: string - creator: string - record: Record - indexedAt: string - likeCount: number | null - repostCount: number | null - replyCount: number | null - requesterRepost: string | null - requesterLike: string | null - invalidReplyRoot: boolean - violatesThreadGate: boolean - viewer: string | null -} - -export type PostInfoMap = { [uri: string]: PostInfo } - -export type PostBlocksMap = { - [uri: string]: { reply?: boolean; embed?: boolean } -} - -export type ThreadgateInfo = { - uri: string - cid: string - record: ThreadgateRecord -} - -export type ThreadgateInfoMap = { - [postUri: string]: ThreadgateInfo -} - -export type FeedGenInfo = Selectable & { - likeCount: number - viewer?: { - 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 } - -export type FeedHydrationState = ProfileHydrationState & { - posts: PostInfoMap - threadgates: ThreadgateInfoMap - 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 deleted file mode 100644 index 7f9fd12e082..00000000000 --- a/packages/bsky/src/services/feed/views.ts +++ /dev/null @@ -1,473 +0,0 @@ -import { mapDefined } from '@atproto/common' -import { AtUri } from '@atproto/syntax' -import { Database } from '../../db' -import { - FeedViewPost, - GeneratorView, - PostView, -} from '../../lexicon/types/app/bsky/feed/defs' -import { - Main as EmbedImages, - isMain as isEmbedImages, - View as EmbedImagesView, -} from '../../lexicon/types/app/bsky/embed/images' -import { - Main as EmbedExternal, - isMain as isEmbedExternal, - View as EmbedExternalView, -} from '../../lexicon/types/app/bsky/embed/external' -import { Main as EmbedRecordWithMedia } from '../../lexicon/types/app/bsky/embed/recordWithMedia' -import { - ViewBlocked, - ViewNotFound, - ViewRecord, -} from '../../lexicon/types/app/bsky/embed/record' -import { Record as PostRecord } from '../../lexicon/types/app/bsky/feed/post' -import { isListRule } from '../../lexicon/types/app/bsky/feed/threadgate' -import { - PostEmbedViews, - FeedGenInfo, - FeedRow, - MaybePostView, - PostInfoMap, - RecordEmbedViewRecord, - PostBlocksMap, - FeedHydrationState, - ThreadgateInfoMap, - ThreadgateInfo, -} from './types' -import { Labels, getSelfLabels } from '../label' -import { ImageUriBuilder } from '../../image/uri' -import { ActorInfoMap, ActorService } from '../actor' -import { ListInfoMap, GraphService } from '../graph' -import { FromDb } from '../types' -import { parseThreadGate } from './util' - -export class FeedViews { - services: { - actor: ActorService - graph: GraphService - } - - constructor( - public db: Database, - public imgUriBuilder: ImageUriBuilder, - private actor: FromDb, - private graph: FromDb, - ) { - this.services = { - actor: actor(this.db), - graph: graph(this.db), - } - } - - static creator( - imgUriBuilder: ImageUriBuilder, - actor: FromDb, - graph: FromDb, - ) { - return (db: Database) => new FeedViews(db, imgUriBuilder, actor, graph) - } - - formatFeedGeneratorView( - info: FeedGenInfo, - profiles: ActorInfoMap, - ): GeneratorView | undefined { - const profile = profiles[info.creator] - if (!profile) { - return undefined - } - 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.getPresetUri( - 'avatar', - info.creator, - info.avatarCid, - ) - : undefined, - likeCount: info.likeCount, - viewer: info.viewer - ? { - like: info.viewer.like ?? undefined, - } - : undefined, - indexedAt: info.indexedAt, - } - } - - formatFeed( - items: FeedRow[], - state: FeedHydrationState, - viewer: string | null, - opts?: { - usePostViewUnion?: boolean - }, - ): FeedViewPost[] { - const { posts, threadgates, profiles, blocks, embeds, labels, lists } = - state - const actors = this.services.actor.views.profileBasicPresentation( - Object.keys(profiles), - state, - viewer, - ) - const feed: FeedViewPost[] = [] - for (const item of items) { - const info = posts[item.postUri] - const post = this.formatPostView( - item.postUri, - actors, - posts, - threadgates, - embeds, - labels, - lists, - viewer, - ) - // skip over not found post - if (!post) { - continue - } - const feedPost = { post } - if (item.type === 'repost') { - const originator = actors[item.originatorDid] - // skip over reposts where we don't have reposter profile - if (!originator) { - continue - } else { - feedPost['reason'] = { - $type: 'app.bsky.feed.defs#reasonRepost', - by: originator, - indexedAt: item.sortAt, - } - } - } - // posts that violate reply-gating may appear in feeds, but without any thread context - if ( - item.replyParent && - item.replyRoot && - !info?.invalidReplyRoot && - !info?.violatesThreadGate - ) { - const replyParent = this.formatMaybePostView( - item.replyParent, - item.uri, - actors, - posts, - threadgates, - embeds, - labels, - lists, - blocks, - viewer, - opts, - ) - const replyRoot = this.formatMaybePostView( - item.replyRoot, - item.uri, - actors, - posts, - threadgates, - embeds, - labels, - lists, - blocks, - viewer, - opts, - ) - if (replyRoot && replyParent) { - feedPost['reply'] = { - root: replyRoot, - parent: replyParent, - } - } - } - feed.push(feedPost) - } - return feed - } - - formatPostView( - uri: string, - actors: ActorInfoMap, - posts: PostInfoMap, - threadgates: ThreadgateInfoMap, - embeds: PostEmbedViews, - labels: Labels, - lists: ListInfoMap, - viewer: string | null, - ): PostView | undefined { - const post = posts[uri] - const gate = threadgates[uri] - const author = actors[post?.creator] - if (!post || !author) return undefined - const postLabels = labels[uri] ?? [] - const postSelfLabels = getSelfLabels({ - uri: post.uri, - cid: post.cid, - record: post.record, - }) - return { - uri: post.uri, - cid: post.cid, - author: author, - record: post.record, - 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, - replyDisabled: this.userReplyDisabled( - uri, - actors, - posts, - threadgates, - lists, - viewer, - ), - } - : undefined, - labels: [...postLabels, ...postSelfLabels], - threadgate: - !post.record.reply && gate - ? this.formatThreadgate(gate, lists) - : undefined, - } - } - - userReplyDisabled( - uri: string, - actors: ActorInfoMap, - posts: PostInfoMap, - threadgates: ThreadgateInfoMap, - lists: ListInfoMap, - viewer: string | null, - ): boolean | undefined { - if (viewer === null) { - return undefined - } else if (posts[uri]?.violatesThreadGate) { - return true - } - - const rootUriStr: string = - posts[uri]?.record?.['reply']?.['root']?.['uri'] ?? uri - const gate = threadgates[rootUriStr]?.record - if (!gate) { - return undefined - } - const rootPost = posts[rootUriStr]?.record as PostRecord | undefined - const ownerDid = new AtUri(rootUriStr).hostname - - const { - canReply, - allowFollowing, - allowListUris = [], - } = parseThreadGate(viewer, ownerDid, rootPost ?? null, gate ?? null) - - if (canReply) { - return false - } - if (allowFollowing && actors[ownerDid]?.viewer?.followedBy) { - return false - } - for (const listUri of allowListUris) { - const list = lists[listUri] - if (list?.viewerInList) { - return false - } - } - return true - } - - formatMaybePostView( - uri: string, - replyUri: string | null, - actors: ActorInfoMap, - posts: PostInfoMap, - threadgates: ThreadgateInfoMap, - embeds: PostEmbedViews, - labels: Labels, - lists: ListInfoMap, - blocks: PostBlocksMap, - viewer: string | null, - opts?: { - usePostViewUnion?: boolean - }, - ): MaybePostView | undefined { - const post = this.formatPostView( - uri, - actors, - posts, - threadgates, - embeds, - labels, - lists, - viewer, - ) - if (!post) { - if (!opts?.usePostViewUnion) return - return this.notFoundPost(uri) - } - if ( - post.author.viewer?.blockedBy || - post.author.viewer?.blocking || - (replyUri !== null && blocks[replyUri]?.reply) - ) { - if (!opts?.usePostViewUnion) return - return this.blockedPost(post) - } - return { - $type: 'app.bsky.feed.defs#postView', - ...post, - } - } - - blockedPost(post: PostView) { - return { - $type: 'app.bsky.feed.defs#blockedPost', - uri: post.uri, - blocked: true as const, - author: { - did: post.author.did, - viewer: post.author.viewer - ? { - blockedBy: post.author.viewer?.blockedBy, - blocking: post.author.viewer?.blocking, - } - : undefined, - }, - } - } - - notFoundPost(uri: string) { - return { - $type: 'app.bsky.feed.defs#notFoundPost', - uri: uri, - notFound: true as const, - } - } - - imagesEmbedView(did: string, embed: EmbedImages) { - const imgViews = embed.images.map((img) => ({ - thumb: this.imgUriBuilder.getPresetUri( - 'feed_thumbnail', - did, - img.image.ref, - ), - fullsize: this.imgUriBuilder.getPresetUri( - 'feed_fullsize', - did, - img.image.ref, - ), - alt: img.alt, - aspectRatio: img.aspectRatio, - })) - 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.getPresetUri('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, - notFound: true, - } - } - if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { - return { - $type: 'app.bsky.embed.record#viewBlocked', - uri, - blocked: true, - author: { - did: post.author.did, - viewer: post.author.viewer - ? { - blockedBy: post.author.viewer?.blockedBy, - blocking: post.author.viewer?.blocking, - } - : undefined, - }, - } - } - return { - $type: 'app.bsky.embed.record#viewRecord', - uri: post.uri, - cid: post.cid, - author: post.author, - value: post.record, - labels: post.labels, - indexedAt: post.indexedAt, - embeds: omitEmbeds ? undefined : post.embed ? [post.embed] : [], - } - } - - getRecordWithMediaEmbedView( - 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, - } - } - - formatThreadgate(gate: ThreadgateInfo, lists: ListInfoMap) { - return { - uri: gate.uri, - cid: gate.cid, - record: gate.record, - lists: mapDefined(gate.record.allow ?? [], (rule) => { - if (!isListRule(rule)) return - const list = lists[rule.list] - if (!list) return - return this.services.graph.formatListViewBasic(list) - }), - } - } -} diff --git a/packages/bsky/src/services/graph/index.ts b/packages/bsky/src/services/graph/index.ts deleted file mode 100644 index bc6d3d05677..00000000000 --- a/packages/bsky/src/services/graph/index.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { sql } from 'kysely' -import { Database } from '../../db' -import { ImageUriBuilder } from '../../image/uri' -import { DbRef, Subquery, valuesList } from '../../db/util' -import { ListInfo } from './types' -import { ActorInfoMap } from '../actor' -import { - ListView, - ListViewBasic, -} from '../../lexicon/types/app/bsky/graph/defs' - -export * from './types' - -export class GraphService { - constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} - - static creator(imgUriBuilder: ImageUriBuilder) { - return (db: Database) => new GraphService(db, imgUriBuilder) - } - - async muteActor(info: { - subjectDid: string - mutedByDid: string - createdAt?: Date - }) { - const { subjectDid, mutedByDid, createdAt = new Date() } = info - await this.db - .asPrimary() - .db.insertInto('mute') - .values({ - subjectDid, - mutedByDid, - createdAt: createdAt.toISOString(), - }) - .onConflict((oc) => oc.doNothing()) - .execute() - } - - async unmuteActor(info: { subjectDid: string; mutedByDid: string }) { - const { subjectDid, mutedByDid } = info - await this.db - .asPrimary() - .db.deleteFrom('mute') - .where('subjectDid', '=', subjectDid) - .where('mutedByDid', '=', mutedByDid) - .execute() - } - - async muteActorList(info: { - list: string - mutedByDid: string - createdAt?: Date - }) { - const { list, mutedByDid, createdAt = new Date() } = info - await this.db - .asPrimary() - .db.insertInto('list_mute') - .values({ - listUri: list, - mutedByDid, - createdAt: createdAt.toISOString(), - }) - .onConflict((oc) => oc.doNothing()) - .execute() - } - - async unmuteActorList(info: { list: string; mutedByDid: string }) { - const { list, mutedByDid } = info - await this.db - .asPrimary() - .db.deleteFrom('list_mute') - .where('listUri', '=', list) - .where('mutedByDid', '=', mutedByDid) - .execute() - } - - getListsQb(viewer: string | null) { - const { ref } = this.db.db.dynamic - return this.db.db - .selectFrom('list') - .innerJoin('actor', 'actor.did', 'list.creator') - .selectAll('list') - .selectAll('actor') - .select('list.sortAt as sortAt') - .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'), - this.db.db - .selectFrom('list_item') - .whereRef('list_item.listUri', '=', ref('list.uri')) - .where('list_item.subjectDid', '=', viewer ?? '') - .select('list_item.uri') - .as('viewerInList'), - ]) - } - - getListItemsQb() { - return this.db.db - .selectFrom('list_item') - .innerJoin('actor as subject', 'subject.did', 'list_item.subjectDid') - .selectAll('subject') - .select([ - 'list_item.uri as uri', - 'list_item.cid as cid', - 'list_item.sortAt as sortAt', - ]) - } - - 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) - .whereExists((qb) => - this.modListSubquery(qb, ref('list_item.listUri')), - ) - .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) - .whereExists((qb) => - this.modListSubquery(qb, ref('list_item.listUri')), - ) - .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) - .whereExists((qb) => - this.modListSubquery(qb, ref('list_item.listUri')), - ) - .select('list_item.listUri') - .limit(1) - .as('mutingViaList'), - ]) - .selectAll() - .execute() - items.forEach((item) => result.add(item)) - return result - } - - modListSubquery(qb: Subquery, ref: DbRef) { - return qb - .selectFrom('list') - .select('uri') - .where('list.purpose', '=', 'app.bsky.graph.defs#modlist') - .whereRef('list.uri', '=', ref) - } - - 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) - .whereExists((qb) => - this.modListSubquery(qb, ref('list_item.listUri')), - ) - .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) - .whereExists((qb) => - this.modListSubquery(qb, ref('list_item.listUri')), - ) - .select('list_item.listUri') - .limit(1) - .as('blockedByViaList'), - ]) - .selectAll() - .execute() - items.forEach((item) => result.add(item)) - return result - } - - async getListViews(listUris: string[], requester: string | null) { - if (listUris.length < 1) return {} - const lists = await this.getListsQb(requester) - .where('list.uri', 'in', listUris) - .execute() - return lists.reduce( - (acc, cur) => ({ - ...acc, - [cur.uri]: cur, - }), - {}, - ) - } - - formatListView(list: ListInfo, profiles: ActorInfoMap): ListView | undefined { - if (!profiles[list.creator]) { - return undefined - } - return { - ...this.formatListViewBasic(list), - creator: profiles[list.creator], - description: list.description ?? undefined, - descriptionFacets: list.descriptionFacets - ? JSON.parse(list.descriptionFacets) - : undefined, - indexedAt: list.sortAt, - } - } - - formatListViewBasic(list: ListInfo): ListViewBasic { - return { - uri: list.uri, - cid: list.cid, - name: list.name, - purpose: list.purpose, - avatar: list.avatarCid - ? this.imgUriBuilder.getPresetUri( - 'avatar', - list.creator, - list.avatarCid, - ) - : undefined, - indexedAt: list.sortAt, - viewer: { - muted: !!list.viewerMuted, - blocked: list.viewerListBlockUri ?? undefined, - }, - } - } -} - -export type RelationshipPair = [didA: string, didB: string] - -export class BlockAndMuteState { - hasIdx = new Map>() // did -> did - blockIdx = new Map>() // did -> did -> block uri - blockListIdx = new Map>() // did -> did -> list 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) { - if (item.source === item.target) { - return // we do not respect self-blocks or self-mutes - } - if (item.blocking) { - const map = this.blockIdx.get(item.source) ?? new Map() - map.set(item.target, item.blocking) - if (!this.blockIdx.has(item.source)) { - this.blockIdx.set(item.source, map) - } - } - if (item.blockingViaList) { - const map = this.blockListIdx.get(item.source) ?? new Map() - map.set(item.target, item.blockingViaList) - if (!this.blockListIdx.has(item.source)) { - this.blockListIdx.set(item.source, map) - } - } - if (item.blockedBy) { - const map = this.blockIdx.get(item.target) ?? new Map() - map.set(item.source, item.blockedBy) - if (!this.blockIdx.has(item.target)) { - this.blockIdx.set(item.target, map) - } - } - if (item.blockedByViaList) { - const map = this.blockListIdx.get(item.target) ?? new Map() - map.set(item.source, item.blockedByViaList) - if (!this.blockListIdx.has(item.target)) { - this.blockListIdx.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]) ?? this.blockList(pair) - } - // 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 - blockList(pair: RelationshipPair): string | null { - return this.blockListIdx.get(pair[0])?.get(pair[1]) ?? null - } - 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 deleted file mode 100644 index 5ff254dc383..00000000000 --- a/packages/bsky/src/services/graph/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Selectable } from 'kysely' -import { List } from '../../db/tables/list' - -export type ListInfo = Selectable & { - viewerMuted: string | null - viewerListBlockUri: string | null - viewerInList: string | null -} - -export type ListInfoMap = Record diff --git a/packages/bsky/src/services/index.ts b/packages/bsky/src/services/index.ts deleted file mode 100644 index 2e5b4725681..00000000000 --- a/packages/bsky/src/services/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ImageUriBuilder } from '../image/uri' -import { ActorService } from './actor' -import { FeedService } from './feed' -import { GraphService } from './graph' -import { ModerationService } from './moderation' -import { LabelCacheOpts, LabelService } from './label' -import { ImageInvalidator } from '../image/invalidator' -import { FromDb, FromDbPrimary } from './types' - -export function createServices(resources: { - imgUriBuilder: ImageUriBuilder - imgInvalidator: ImageInvalidator - labelCacheOpts: LabelCacheOpts -}): Services { - const { imgUriBuilder, imgInvalidator, labelCacheOpts } = resources - const label = LabelService.creator(labelCacheOpts) - const graph = GraphService.creator(imgUriBuilder) - const actor = ActorService.creator(imgUriBuilder, graph, label) - const moderation = ModerationService.creator(imgUriBuilder, imgInvalidator) - const feed = FeedService.creator(imgUriBuilder, actor, label, graph) - return { - actor, - feed, - moderation, - graph, - label, - } -} - -export type Services = { - actor: FromDb - feed: FromDb - graph: FromDb - moderation: FromDbPrimary - label: FromDb -} diff --git a/packages/bsky/src/services/label/index.ts b/packages/bsky/src/services/label/index.ts deleted file mode 100644 index f4c11295da7..00000000000 --- a/packages/bsky/src/services/label/index.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { sql } from 'kysely' -import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax' -import { Database } from '../../db' -import { Label, isSelfLabels } from '../../lexicon/types/com/atproto/label/defs' -import { ids } from '../../lexicon/lexicons' -import { ReadThroughCache } from '../../cache/read-through' -import { Redis } from '../../redis' - -export type Labels = Record - -export type LabelCacheOpts = { - redis: Redis - staleTTL: number - maxTTL: number -} - -export class LabelService { - public cache: ReadThroughCache | null - - constructor(public db: Database, cacheOpts: LabelCacheOpts | null) { - if (cacheOpts) { - this.cache = new ReadThroughCache(cacheOpts.redis, { - ...cacheOpts, - fetchMethod: async (subject: string) => { - const res = await fetchLabelsForSubjects(db, [subject]) - return res[subject] ?? [] - }, - fetchManyMethod: (subjects: string[]) => - fetchLabelsForSubjects(db, subjects), - }) - } - } - - static creator(cacheOpts: LabelCacheOpts | null) { - return (db: Database) => new LabelService(db, cacheOpts) - } - - async formatAndCreate( - src: string, - uri: string, - cid: string | null, - labels: { create?: string[]; negate?: string[] }, - ): Promise { - const { create = [], negate = [] } = labels - const toCreate = create.map((val) => ({ - src, - uri, - cid: cid ?? undefined, - val, - neg: false, - cts: new Date().toISOString(), - })) - const toNegate = negate.map((val) => ({ - src, - uri, - cid: cid ?? undefined, - val, - neg: true, - cts: new Date().toISOString(), - })) - const formatted = [...toCreate, ...toNegate] - await this.createLabels(formatted) - return formatted - } - - async createLabels(labels: Label[]) { - if (labels.length < 1) return - const dbVals = labels.map((l) => ({ - ...l, - cid: l.cid ?? '', - neg: !!l.neg, - })) - const { ref } = this.db.db.dynamic - const excluded = (col: string) => ref(`excluded.${col}`) - await this.db - .asPrimary() - .db.insertInto('label') - .values(dbVals) - .onConflict((oc) => - oc.columns(['src', 'uri', 'cid', 'val']).doUpdateSet({ - neg: sql`${excluded('neg')}`, - cts: sql`${excluded('cts')}`, - }), - ) - .execute() - } - - async getLabelsForUris( - subjects: string[], - opts?: { - includeNeg?: boolean - skipCache?: boolean - }, - ): Promise { - if (subjects.length < 1) return {} - const res = this.cache - ? await this.cache.getMany(subjects, { revalidate: opts?.skipCache }) - : await fetchLabelsForSubjects(this.db, subjects) - - if (opts?.includeNeg) { - return res - } - - const noNegs: Labels = {} - for (const [key, val] of Object.entries(res)) { - noNegs[key] = val.filter((label) => !label.neg) - } - return noNegs - } - - // gets labels for any record. when did is present, combine labels for both did & profile record. - async getLabelsForSubjects( - subjects: string[], - opts?: { - includeNeg?: boolean - skipCache?: boolean - }, - labels: Labels = {}, - ): Promise { - 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, - AtUri.make(subject, ids.AppBskyActorProfile, 'self').toString(), - ] - } - return subject - }) - 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 && - uri.collection === ids.AppBskyActorProfile && - uri.rkey === 'self' - ) { - // combine labels for profile + did - const did = uri.hostname - acc[did] ??= [] - acc[did].push(...labelsByUri[cur]) - } - acc[cur] ??= [] - acc[cur].push(...labelsByUri[cur]) - return acc - }, labels) - } - - async getLabels( - subject: string, - opts?: { - includeNeg?: boolean - skipCache?: boolean - }, - ): Promise { - const labels = await this.getLabelsForUris([subject], opts) - return labels[subject] ?? [] - } - - async getLabelsForProfile( - did: string, - opts?: { - includeNeg?: boolean - skipCache?: boolean - }, - ): Promise { - const labels = await this.getLabelsForSubjects([did], opts) - return labels[did] ?? [] - } -} - -export function getSelfLabels(details: { - uri: string | null - cid: string | null - record: Record | null -}): Label[] { - const { uri, cid, record } = details - if (!uri || !cid || !record) return [] - if (!isSelfLabels(record.labels)) return [] - const src = new AtUri(uri).host // record creator - const cts = - typeof record.createdAt === 'string' - ? normalizeDatetimeAlways(record.createdAt) - : new Date(0).toISOString() - return record.labels.values.map(({ val }) => { - return { src, uri, cid, val, cts, neg: false } - }) -} - -const fetchLabelsForSubjects = async ( - db: Database, - subjects: string[], -): Promise> => { - if (subjects.length === 0) { - return {} - } - const res = await db.db - .selectFrom('label') - .where('label.uri', 'in', subjects) - .selectAll() - .execute() - const labelMap = res.reduce((acc, cur) => { - acc[cur.uri] ??= [] - acc[cur.uri].push({ - ...cur, - cid: cur.cid === '' ? undefined : cur.cid, - neg: cur.neg, - }) - return acc - }, {} as Record) - // ensure we cache negatives - for (const subject of subjects) { - labelMap[subject] ??= [] - } - return labelMap -} diff --git a/packages/bsky/src/services/moderation/index.ts b/packages/bsky/src/services/moderation/index.ts deleted file mode 100644 index 71380e16884..00000000000 --- a/packages/bsky/src/services/moderation/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/syntax' -import { PrimaryDatabase } from '../../db' -import { ImageUriBuilder } from '../../image/uri' -import { ImageInvalidator } from '../../image/invalidator' -import { StatusAttr } from '../../lexicon/types/com/atproto/admin/defs' - -export class ModerationService { - constructor( - public db: PrimaryDatabase, - public imgUriBuilder: ImageUriBuilder, - public imgInvalidator: ImageInvalidator, - ) {} - - static creator( - imgUriBuilder: ImageUriBuilder, - imgInvalidator: ImageInvalidator, - ) { - return (db: PrimaryDatabase) => - new ModerationService(db, imgUriBuilder, imgInvalidator) - } - - async takedownRepo(info: { takedownRef: string; did: string }) { - const { takedownRef, did } = info - await this.db.db - .updateTable('actor') - .set({ takedownRef }) - .where('did', '=', did) - .where('takedownRef', 'is', null) - .executeTakeFirst() - } - - async reverseTakedownRepo(info: { did: string }) { - await this.db.db - .updateTable('actor') - .set({ takedownRef: null }) - .where('did', '=', info.did) - .execute() - } - - async takedownRecord(info: { takedownRef: string; uri: AtUri; cid: CID }) { - const { takedownRef, uri } = info - await this.db.db - .updateTable('record') - .set({ takedownRef }) - .where('uri', '=', uri.toString()) - .where('takedownRef', 'is', null) - .executeTakeFirst() - } - - async reverseTakedownRecord(info: { uri: AtUri }) { - await this.db.db - .updateTable('record') - .set({ takedownRef: null }) - .where('uri', '=', info.uri.toString()) - .execute() - } - - async takedownBlob(info: { takedownRef: string; did: string; cid: string }) { - const { takedownRef, did, cid } = info - await this.db.db - .insertInto('blob_takedown') - .values({ did, cid, takedownRef }) - .onConflict((oc) => oc.doNothing()) - .execute() - const paths = ImageUriBuilder.presets.map((id) => { - const imgUri = this.imgUriBuilder.getPresetUri(id, did, cid) - return imgUri.replace(this.imgUriBuilder.endpoint, '') - }) - await this.imgInvalidator.invalidate(cid.toString(), paths) - } - - async reverseTakedownBlob(info: { did: string; cid: string }) { - const { did, cid } = info - await this.db.db - .deleteFrom('blob_takedown') - .where('did', '=', did) - .where('cid', '=', cid) - .execute() - } - - async getRepoTakedownRef(did: string): Promise { - const res = await this.db.db - .selectFrom('actor') - .where('did', '=', did) - .selectAll() - .executeTakeFirst() - return res ? formatStatus(res.takedownRef) : null - } - - async getRecordTakedownRef(uri: string): Promise { - const res = await this.db.db - .selectFrom('record') - .where('uri', '=', uri) - .selectAll() - .executeTakeFirst() - return res ? formatStatus(res.takedownRef) : null - } - - async getBlobTakedownRef( - did: string, - cid: string, - ): Promise { - const res = await this.db.db - .selectFrom('blob_takedown') - .where('did', '=', did) - .where('cid', '=', cid) - .selectAll() - .executeTakeFirst() - // this table only tracks takedowns not all blobs - // so if no result is returned then the blob is not taken down (rather than not found) - return formatStatus(res?.takedownRef ?? null) - } -} - -const formatStatus = (ref: string | null): StatusAttr => { - return ref ? { applied: true, ref } : { applied: false } -} diff --git a/packages/bsky/src/services/types.ts b/packages/bsky/src/services/types.ts deleted file mode 100644 index 2039d6c07de..00000000000 --- a/packages/bsky/src/services/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Database, PrimaryDatabase } from '../db' - -export type FromDb = (db: Database) => T -export type FromDbPrimary = (db: PrimaryDatabase) => T diff --git a/packages/bsky/src/services/util/notification.ts b/packages/bsky/src/services/util/notification.ts deleted file mode 100644 index e8eb618cff6..00000000000 --- a/packages/bsky/src/services/util/notification.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { sql } from 'kysely' -import { countAll } from '../../db/util' -import { PrimaryDatabase } from '../../db' - -// i.e. 30 days before the last time the user checked their notifs -export const BEFORE_LAST_SEEN_DAYS = 30 -// i.e. 180 days before the latest unread notification -export const BEFORE_LATEST_UNREAD_DAYS = 180 -// don't consider culling unreads until they hit this threshold, and then enforce beforeLatestUnreadThresholdDays -export const UNREAD_KEPT_COUNT = 500 - -export const tidyNotifications = async (db: PrimaryDatabase, did: string) => { - const stats = await db.db - .selectFrom('notification') - .select([ - sql<0 | 1>`("sortAt" < "lastSeenNotifs")`.as('read'), - countAll.as('count'), - sql`min("sortAt")`.as('earliestAt'), - sql`max("sortAt")`.as('latestAt'), - sql`max("lastSeenNotifs")`.as('lastSeenAt'), - ]) - .leftJoin('actor_state', 'actor_state.did', 'notification.did') - .where('notification.did', '=', did) - .groupBy(sql`1`) // group by read (i.e. 1st column) - .execute() - const readStats = stats.find((stat) => stat.read) - const unreadStats = stats.find((stat) => !stat.read) - let readCutoffAt: Date | undefined - let unreadCutoffAt: Date | undefined - if (readStats) { - readCutoffAt = addDays( - new Date(readStats.lastSeenAt), - -BEFORE_LAST_SEEN_DAYS, - ) - } - if (unreadStats && unreadStats.count > UNREAD_KEPT_COUNT) { - unreadCutoffAt = addDays( - new Date(unreadStats.latestAt), - -BEFORE_LATEST_UNREAD_DAYS, - ) - } - // take most recent of read/unread cutoffs - const cutoffAt = greatest(readCutoffAt, unreadCutoffAt) - if (cutoffAt) { - // skip delete if it won't catch any notifications - const earliestAt = least(readStats?.earliestAt, unreadStats?.earliestAt) - if (earliestAt && earliestAt < cutoffAt.toISOString()) { - await db.db - .deleteFrom('notification') - .where('did', '=', did) - .where('sortAt', '<', cutoffAt.toISOString()) - .execute() - } - } -} - -const addDays = (date: Date, days: number) => { - date.setDate(date.getDate() + days) - return date -} - -const least = (a: T | undefined, b: T | undefined) => { - return a !== undefined && (b === undefined || a < b) ? a : b -} - -const greatest = (a: T | undefined, b: T | undefined) => { - return a !== undefined && (b === undefined || a > b) ? a : b -} - -type Ordered = string | number | Date diff --git a/packages/bsky/src/services/util/post.ts b/packages/bsky/src/services/util/post.ts deleted file mode 100644 index 19e7fa3ee2c..00000000000 --- a/packages/bsky/src/services/util/post.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { sql } from 'kysely' -import DatabaseSchema from '../../db/database-schema' - -export const getDescendentsQb = ( - db: DatabaseSchema, - opts: { - uri: string - depth: number // required, protects against cycles - }, -) => { - const { uri, depth } = opts - const query = db.withRecursive('descendent(uri, depth)', (cte) => { - return cte - .selectFrom('post') - .select(['post.uri as uri', sql`1`.as('depth')]) - .where(sql`1`, '<=', depth) - .where('replyParent', '=', uri) - .unionAll( - cte - .selectFrom('post') - .innerJoin('descendent', 'descendent.uri', 'post.replyParent') - .where('descendent.depth', '<', depth) - .select([ - 'post.uri as uri', - sql`descendent.depth + 1`.as('depth'), - ]), - ) - }) - return query -} - -export const getAncestorsAndSelfQb = ( - db: DatabaseSchema, - opts: { - uri: string - parentHeight: number // required, protects against cycles - }, -) => { - const { uri, parentHeight } = opts - const query = db.withRecursive( - 'ancestor(uri, ancestorUri, height)', - (cte) => { - return cte - .selectFrom('post') - .select([ - 'post.uri as uri', - 'post.replyParent as ancestorUri', - sql`0`.as('height'), - ]) - .where('uri', '=', uri) - .unionAll( - cte - .selectFrom('post') - .innerJoin('ancestor', 'ancestor.ancestorUri', 'post.uri') - .where('ancestor.height', '<', parentHeight) - .select([ - 'post.uri as uri', - 'post.replyParent as ancestorUri', - sql`ancestor.height + 1`.as('height'), - ]), - ) - }, - ) - return query -} diff --git a/packages/bsky/src/services/util/search.ts b/packages/bsky/src/services/util/search.ts deleted file mode 100644 index 994d2f43879..00000000000 --- a/packages/bsky/src/services/util/search.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { sql } from 'kysely' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Database } from '../../db' -import { notSoftDeletedClause, DbRef, AnyQb } from '../../db/util' -import { GenericKeyset, paginate } from '../../db/pagination' - -export const getUserSearchQuery = ( - db: Database, - opts: { - query: string - limit: number - cursor?: string - includeSoftDeleted?: boolean - }, -) => { - const { ref } = db.db.dynamic - const { query, limit, cursor, includeSoftDeleted } = opts - // Matching user accounts based on handle - const distanceAccount = distance(query, ref('handle')) - let accountsQb = getMatchingAccountsQb(db, { query, includeSoftDeleted }) - accountsQb = paginate(accountsQb, { - limit, - cursor, - direction: 'asc', - keyset: new SearchKeyset(distanceAccount, ref('actor.did')), - }) - // Matching profiles based on display name - const distanceProfile = distance(query, ref('displayName')) - let profilesQb = getMatchingProfilesQb(db, { query, includeSoftDeleted }) - profilesQb = paginate(profilesQb, { - limit, - cursor, - direction: 'asc', - 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('actor.did')), - }) -} - -// Takes maximal advantage of trigram index at the expense of ability to paginate. -export const getUserSearchQuerySimple = ( - db: Database, - opts: { - query: string - limit: number - }, -) => { - const { ref } = db.db.dynamic - const { query, limit } = opts - // Matching user accounts based on handle - const accountsQb = getMatchingAccountsQb(db, { query }) - .orderBy('distance', 'asc') - .limit(limit) - // Matching profiles based on display name - const profilesQb = getMatchingProfilesQb(db, { query }) - .orderBy('distance', 'asc') - .limit(limit) - // Combine and paginate result set - return paginate(combineAccountsAndProfilesQb(db, accountsQb, profilesQb), { - limit, - direction: 'asc', - keyset: new SearchKeyset(ref('distance'), ref('actor.did')), - }) -} - -// Matching user accounts based on handle -const getMatchingAccountsQb = ( - db: Database, - opts: { query: string; includeSoftDeleted?: boolean }, -) => { - const { ref } = db.db.dynamic - const { query, includeSoftDeleted } = opts - const distanceAccount = distance(query, ref('handle')) - return db.db - .selectFrom('actor') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('actor'))), - ) - .where('actor.handle', 'is not', null) - .where(similar(query, ref('handle'))) // Coarse filter engaging trigram index - .select(['actor.did as did', distanceAccount.as('distance')]) -} - -// Matching profiles based on display name -const getMatchingProfilesQb = ( - db: Database, - opts: { query: string; includeSoftDeleted?: boolean }, -) => { - const { ref } = db.db.dynamic - const { query, includeSoftDeleted } = opts - const distanceProfile = distance(query, ref('displayName')) - return db.db - .selectFrom('profile') - .innerJoin('actor', 'actor.did', 'profile.creator') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('actor'))), - ) - .where('actor.handle', 'is not', null) - .where(similar(query, ref('displayName'))) // Coarse filter engaging trigram index - .select(['profile.creator as did', distanceProfile.as('distance')]) -} - -// Combine profile and account result sets -const combineAccountsAndProfilesQb = ( - db: Database, - accountsQb: AnyQb, - profilesQb: AnyQb, -) => { - // Combine user account and profile results, taking best matches from each - const emptyQb = db.db - .selectFrom('actor') - .where(sql`1 = 0`) - .select([sql.literal('').as('did'), sql`0`.as('distance')]) - const resultsQb = db.db - .selectFrom( - emptyQb - .unionAll(sql`${accountsQb}`) // The sql`` is adding parens - .unionAll(sql`${profilesQb}`) - .as('accounts_and_profiles'), - ) - .selectAll() - .distinctOn('did') // Per did, take whichever of account and profile distance is best - .orderBy('did') - .orderBy('distance') - return db.db - .selectFrom(resultsQb.as('results')) - .innerJoin('actor', 'actor.did', 'results.did') -} - -// Remove leading @ in case a handle is input that way -export const cleanQuery = (query: string) => query.trim().replace(/^@/g, '') - -// Uses pg_trgm strict word similarity to check similarity between a search query and a stored value -const distance = (query: string, ref: DbRef) => - sql`(${query} <<-> ${ref})` - -// Can utilize trigram index to match on strict word similarity. -// The word_similarity_threshold is set to .4 (i.e. distance < .6) in db/index.ts. -const similar = (query: string, ref: DbRef) => - sql`(${query} <% ${ref})` - -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.did, - } - } - labeledResultToCursor(labeled: LabeledResult) { - return { - primary: labeled.primary.toString().replace('0.', '.'), - secondary: labeled.secondary, - } - } - cursorToLabeledResult(cursor: { primary: string; secondary: string }) { - const distance = parseFloat(cursor.primary) - if (isNaN(distance)) { - throw new InvalidRequestError('Malformed cursor') - } - return { - primary: distance, - secondary: cursor.secondary, - } - } -} diff --git a/packages/bsky/src/views/index.ts b/packages/bsky/src/views/index.ts new file mode 100644 index 00000000000..99450f0491c --- /dev/null +++ b/packages/bsky/src/views/index.ts @@ -0,0 +1,854 @@ +import { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax' +import { mapDefined } from '@atproto/common' +import { ImageUriBuilder } from '../image/uri' +import { HydrationState } from '../hydration/hydrator' +import { ids } from '../lexicon/lexicons' +import { + ProfileViewDetailed, + ProfileView, + ProfileViewBasic, + ViewerState as ProfileViewerState, +} from '../lexicon/types/app/bsky/actor/defs' +import { + BlockedPost, + FeedViewPost, + GeneratorView, + NotFoundPost, + PostView, + ReasonRepost, + ThreadViewPost, + ThreadgateView, +} from '../lexicon/types/app/bsky/feed/defs' +import { ListView, ListViewBasic } from '../lexicon/types/app/bsky/graph/defs' +import { creatorFromUri, parseThreadGate, cidFromBlobJson } from './util' +import { isListRule } from '../lexicon/types/app/bsky/feed/threadgate' +import { isSelfLabels } from '../lexicon/types/com/atproto/label/defs' +import { + Embed, + EmbedBlocked, + EmbedNotFound, + EmbedView, + ExternalEmbed, + ExternalEmbedView, + ImagesEmbed, + ImagesEmbedView, + MaybePostView, + NotificationView, + PostEmbedView, + RecordEmbed, + RecordEmbedView, + RecordEmbedViewInternal, + RecordWithMedia, + RecordWithMediaView, + isExternalEmbed, + isImagesEmbed, + isRecordEmbed, + isRecordWithMedia, +} from './types' +import { Label } from '../hydration/label' +import { FeedItem, Post, Repost } from '../hydration/feed' +import { RecordInfo } from '../hydration/util' +import { Notification } from '../proto/bsky_pb' + +export class Views { + constructor(public imgUriBuilder: ImageUriBuilder) {} + + // Actor + // ------------ + + actorIsTakendown(did: string, state: HydrationState): boolean { + return !!state.actors?.get(did)?.takedownRef + } + + viewerBlockExists(did: string, state: HydrationState): boolean { + const actor = state.profileViewers?.get(did) + if (!actor) return false + return ( + !!actor.blockedBy || + !!actor.blocking || + !!actor.blockedByList || + !!actor.blockingByList + ) + } + + viewerMuteExists(did: string, state: HydrationState): boolean { + const actor = state.profileViewers?.get(did) + if (!actor) return false + return actor.muted || !!actor.mutedByList + } + + profileDetailed( + did: string, + state: HydrationState, + ): ProfileViewDetailed | undefined { + const actor = state.actors?.get(did) + if (!actor) return + const baseView = this.profile(did, state) + if (!baseView) return + const profileAggs = state.profileAggs?.get(did) + return { + ...baseView, + banner: actor.profile?.banner + ? this.imgUriBuilder.getPresetUri( + 'banner', + did, + cidFromBlobJson(actor.profile.banner), + ) + : undefined, + followersCount: profileAggs?.followers, + followsCount: profileAggs?.follows, + postsCount: profileAggs?.posts, + } + } + + profile(did: string, state: HydrationState): ProfileView | undefined { + const actor = state.actors?.get(did) + if (!actor) return + const basicView = this.profileBasic(did, state) + if (!basicView) return + return { + ...basicView, + description: actor.profile?.description || undefined, + indexedAt: actor.sortedAt?.toISOString(), + } + } + + profileBasic( + did: string, + state: HydrationState, + ): ProfileViewBasic | undefined { + const actor = state.actors?.get(did) + if (!actor) return + const profileUri = AtUri.make( + did, + ids.AppBskyActorProfile, + 'self', + ).toString() + const labels = [ + ...(state.labels?.get(did) ?? []), + ...(state.labels?.get(profileUri) ?? []), + ...this.selfLabels({ + uri: profileUri, + cid: actor.profileCid?.toString(), + record: actor.profile, + }), + ] + return { + did, + handle: actor.handle ?? INVALID_HANDLE, + displayName: actor.profile?.displayName, + avatar: actor.profile?.avatar + ? this.imgUriBuilder.getPresetUri( + 'avatar', + did, + cidFromBlobJson(actor.profile.avatar), + ) + : undefined, + viewer: this.profileViewer(did, state), + labels, + } + } + + profileViewer( + did: string, + state: HydrationState, + ): ProfileViewerState | undefined { + const viewer = state.profileViewers?.get(did) + if (!viewer) return + const blockedByUri = viewer.blockedBy || viewer.blockedByList + const blockingUri = viewer.blocking || viewer.blockingByList + const block = !!blockedByUri || !!blockingUri + return { + muted: viewer.muted || !!viewer.mutedByList, + mutedByList: viewer.mutedByList + ? this.listBasic(viewer.mutedByList, state) + : undefined, + blockedBy: !!blockedByUri, + blocking: blockingUri, + blockingByList: viewer.blockingByList + ? this.listBasic(viewer.blockingByList, state) + : undefined, + following: viewer.following && !block ? viewer.following : undefined, + followedBy: viewer.followedBy && !block ? viewer.followedBy : undefined, + } + } + + blockedProfileViewer( + did: string, + state: HydrationState, + ): ProfileViewerState | undefined { + const viewer = state.profileViewers?.get(did) + if (!viewer) return + const blockedByUri = viewer.blockedBy || viewer.blockedByList + const blockingUri = viewer.blocking || viewer.blockingByList + return { + blockedBy: !!blockedByUri, + blocking: blockingUri, + } + } + + // Graph + // ------------ + + list(uri: string, state: HydrationState): ListView | undefined { + const creatorDid = new AtUri(uri).hostname + const list = state.lists?.get(uri) + if (!list) return + const creator = this.profile(creatorDid, state) + if (!creator) return + const basicView = this.listBasic(uri, state) + if (!basicView) return + + return { + ...basicView, + creator, + description: list.record.description, + descriptionFacets: list.record.descriptionFacets, + indexedAt: list.sortedAt.toISOString(), + } + } + + listBasic(uri: string, state: HydrationState): ListViewBasic | undefined { + const list = state.lists?.get(uri) + if (!list) { + return undefined + } + const listViewer = state.listViewers?.get(uri) + const creator = new AtUri(uri).hostname + return { + uri, + cid: list.cid, + name: list.record.name, + purpose: list.record.purpose, + avatar: list.record.avatar + ? this.imgUriBuilder.getPresetUri( + 'avatar', + creator, + cidFromBlobJson(list.record.avatar), + ) + : undefined, + indexedAt: list.sortedAt.toISOString(), + viewer: listViewer + ? { + muted: !!listViewer.viewerMuted, + blocked: listViewer.viewerListBlockUri, + } + : undefined, + } + } + + // Labels + // ------------ + + selfLabels(details: { + uri?: string + cid?: string + record?: Record + }): Label[] { + const { uri, cid, record } = details + if (!uri || !cid || !record) return [] + if (!isSelfLabels(record.labels)) return [] + const src = new AtUri(uri).host // record creator + const cts = + typeof record.createdAt === 'string' + ? normalizeDatetimeAlways(record.createdAt) + : new Date(0).toISOString() + return record.labels.values.map(({ val }) => { + return { src, uri, cid, val, cts, neg: false } + }) + } + + // Feed + // ------------ + + feedItemBlocksAndMutes( + item: FeedItem, + state: HydrationState, + ): { + originatorMuted: boolean + originatorBlocked: boolean + authorMuted: boolean + authorBlocked: boolean + } { + const authorDid = creatorFromUri(item.post.uri) + const originatorDid = item.repost + ? creatorFromUri(item.repost.uri) + : authorDid + return { + originatorMuted: this.viewerMuteExists(originatorDid, state), + originatorBlocked: this.viewerBlockExists(originatorDid, state), + authorMuted: this.viewerMuteExists(authorDid, state), + authorBlocked: this.viewerBlockExists(authorDid, state), + } + } + + feedGenerator(uri: string, state: HydrationState): GeneratorView | undefined { + const feedgen = state.feedgens?.get(uri) + if (!feedgen) return + const creatorDid = creatorFromUri(uri) + const creator = this.profile(creatorDid, state) + if (!creator) return + const viewer = state.feedgenViewers?.get(uri) + const aggs = state.feedgenAggs?.get(uri) + + return { + uri, + cid: feedgen.cid, + did: feedgen.record.did, + creator, + displayName: feedgen.record.displayName, + description: feedgen.record.description, + descriptionFacets: feedgen.record.descriptionFacets, + avatar: feedgen.record.avatar + ? this.imgUriBuilder.getPresetUri( + 'avatar', + creatorDid, + cidFromBlobJson(feedgen.record.avatar), + ) + : undefined, + likeCount: aggs?.likes, + viewer: viewer + ? { + like: viewer.like, + } + : undefined, + indexedAt: feedgen.sortedAt.toISOString(), + } + } + + threadGate(uri: string, state: HydrationState): ThreadgateView | undefined { + const gate = state.threadgates?.get(uri) + if (!gate) return + return { + uri, + cid: gate.cid, + record: gate.record, + lists: mapDefined(gate.record.allow ?? [], (rule) => { + if (!isListRule(rule)) return + return this.listBasic(rule.list, state) + }), + } + } + + post(uri: string, state: HydrationState, depth = 0): PostView | undefined { + const post = state.posts?.get(uri) + if (!post) return + const parsedUri = new AtUri(uri) + const authorDid = parsedUri.hostname + const author = this.profileBasic(authorDid, state) + if (!author) return + const aggs = state.postAggs?.get(uri) + const viewer = state.postViewers?.get(uri) + const gateUri = AtUri.make( + authorDid, + ids.AppBskyFeedThreadgate, + parsedUri.rkey, + ).toString() + const labels = [ + ...(state.labels?.get(uri) ?? []), + ...this.selfLabels({ + uri, + cid: post.cid, + record: post.record, + }), + ] + return { + uri, + cid: post.cid, + author, + record: post.record, + embed: + depth < 2 && post.record.embed + ? this.embed(uri, post.record.embed, state, depth + 1) + : undefined, + replyCount: aggs?.replies, + repostCount: aggs?.reposts, + likeCount: aggs?.likes, + indexedAt: post.sortedAt.toISOString(), + viewer: viewer + ? { + repost: viewer.repost, + like: viewer.like, + replyDisabled: this.userReplyDisabled(uri, state), + } + : undefined, + labels, + threadgate: !post.record.reply // only hydrate gate on root post + ? this.threadGate(gateUri, state) + : undefined, + } + } + + feedViewPost( + item: FeedItem, + state: HydrationState, + ): FeedViewPost | undefined { + const postInfo = state.posts?.get(item.post.uri) + let reason: ReasonRepost | undefined + if (item.repost) { + const repost = state.reposts?.get(item.repost.uri) + if (!repost) return + if (repost.record.subject.uri !== item.post.uri) return + reason = this.reasonRepost(creatorFromUri(item.repost.uri), repost, state) + if (!reason) return + } + const post = this.post(item.post.uri, state) + if (!post) return + return { + post, + reason, + reply: !postInfo?.violatesThreadGate + ? this.replyRef(item.post.uri, state) + : undefined, + } + } + + replyRef(uri: string, state: HydrationState, usePostViewUnion = false) { + // don't hydrate reply if there isn't it violates a block + if (state.postBlocks?.get(uri)?.reply) return undefined + const postRecord = state.posts?.get(uri.toString())?.record + if (!postRecord?.reply) return + const root = this.maybePost( + postRecord.reply.root.uri, + state, + usePostViewUnion, + ) + const parent = this.maybePost( + postRecord.reply.parent.uri, + state, + usePostViewUnion, + ) + return root && parent ? { root, parent } : undefined + } + + maybePost( + uri: string, + state: HydrationState, + usePostViewUnion = false, + ): MaybePostView | undefined { + const post = this.post(uri, state) + if (!post) return usePostViewUnion ? this.notFoundPost(uri) : undefined + if (this.viewerBlockExists(post.author.did, state)) { + return usePostViewUnion + ? this.blockedPost(uri, post.author.did, state) + : undefined + } + return { + $type: 'app.bsky.feed.defs#postView', + ...post, + } + } + + blockedPost( + uri: string, + authorDid: string, + state: HydrationState, + ): BlockedPost { + return { + $type: 'app.bsky.feed.defs#blockedPost', + uri, + blocked: true, + author: { + did: authorDid, + viewer: this.blockedProfileViewer(authorDid, state), + }, + } + } + + notFoundPost(uri: string): NotFoundPost { + return { + $type: 'app.bsky.feed.defs#notFoundPost', + uri, + notFound: true, + } + } + + reasonRepost( + creatorDid: string, + repost: Repost, + state: HydrationState, + ): ReasonRepost | undefined { + const creator = this.profileBasic(creatorDid, state) + if (!creator) return + return { + $type: 'app.bsky.feed.defs#reasonRepost', + by: creator, + indexedAt: repost.sortedAt.toISOString(), + } + } + + // Threads + // ------------ + + thread( + skele: { anchor: string; uris: string[] }, + state: HydrationState, + opts: { height: number; depth: number }, + ): ThreadViewPost | NotFoundPost | BlockedPost { + const { anchor, uris } = skele + const post = this.post(anchor, state) + const postInfo = state.posts?.get(anchor) + if (!postInfo || !post) return this.notFoundPost(anchor) + if (this.viewerBlockExists(post.author.did, state)) { + return this.blockedPost(anchor, post.author.did, state) + } + const includedPosts = new Set([anchor]) + const childrenByParentUri: Record = {} + uris.forEach((uri) => { + const post = state.posts?.get(uri) + const parentUri = post?.record.reply?.parent.uri + if (!parentUri) return + if (includedPosts.has(uri)) return + includedPosts.add(uri) + childrenByParentUri[parentUri] ??= [] + childrenByParentUri[parentUri].push(uri) + }) + const rootUri = getRootUri(anchor, postInfo) + const violatesThreadGate = postInfo.violatesThreadGate + + return { + $type: 'app.bsky.feed.defs#threadViewPost', + post, + parent: !violatesThreadGate + ? this.threadParent(anchor, rootUri, state, opts.height) + : undefined, + replies: !violatesThreadGate + ? this.threadReplies( + anchor, + rootUri, + childrenByParentUri, + state, + opts.depth, + ) + : undefined, + } + } + + threadParent( + childUri: string, + rootUri: string, + state: HydrationState, + height: number, + ): ThreadViewPost | NotFoundPost | BlockedPost | undefined { + if (height < 1) return undefined + const parentUri = state.posts?.get(childUri)?.record.reply?.parent.uri + if (!parentUri) return undefined + if (state.postBlocks?.get(childUri)?.reply) { + return this.blockedPost(parentUri, creatorFromUri(parentUri), state) + } + const post = this.post(parentUri, state) + const postInfo = state.posts?.get(parentUri) + if (!postInfo || !post) return this.notFoundPost(parentUri) + if (rootUri !== getRootUri(parentUri, postInfo)) return // outside thread boundary + if (this.viewerBlockExists(post.author.did, state)) { + return this.blockedPost(parentUri, post.author.did, state) + } + return { + $type: 'app.bsky.feed.defs#threadViewPost', + post, + parent: this.threadParent(parentUri, rootUri, state, height - 1), + } + } + + threadReplies( + parentUri: string, + rootUri: string, + childrenByParentUri: Record, + state: HydrationState, + depth: number, + ): (ThreadViewPost | NotFoundPost | BlockedPost)[] | undefined { + if (depth < 1) return undefined + const childrenUris = childrenByParentUri[parentUri] ?? [] + return mapDefined(childrenUris, (uri) => { + const postInfo = state.posts?.get(uri) + if (postInfo?.violatesThreadGate) { + return undefined + } + if (state.postBlocks?.get(uri)?.reply) { + return undefined + } + const post = this.post(uri, state) + if (!postInfo || !post) return this.notFoundPost(uri) + if (rootUri !== getRootUri(uri, postInfo)) return // outside thread boundary + if (this.viewerBlockExists(post.author.did, state)) { + return this.blockedPost(uri, post.author.did, state) + } + return { + $type: 'app.bsky.feed.defs#threadViewPost', + post, + replies: this.threadReplies( + uri, + rootUri, + childrenByParentUri, + state, + depth - 1, + ), + } + }) + } + + // Embeds + // ------------ + + embed( + postUri: string, + embed: Embed | { $type: string }, + state: HydrationState, + depth: number, + ): EmbedView | undefined { + if (isImagesEmbed(embed)) { + return this.imagesEmbed(creatorFromUri(postUri), embed) + } else if (isExternalEmbed(embed)) { + return this.externalEmbed(creatorFromUri(postUri), embed) + } else if (isRecordEmbed(embed)) { + return this.recordEmbed(postUri, embed, state, depth) + } else if (isRecordWithMedia(embed)) { + return this.recordWithMediaEmbed(postUri, embed, state, depth) + } else { + return undefined + } + } + + imagesEmbed(did: string, embed: ImagesEmbed): ImagesEmbedView { + const imgViews = embed.images.map((img) => ({ + thumb: this.imgUriBuilder.getPresetUri( + 'feed_thumbnail', + did, + cidFromBlobJson(img.image), + ), + fullsize: this.imgUriBuilder.getPresetUri( + 'feed_fullsize', + did, + cidFromBlobJson(img.image), + ), + alt: img.alt, + aspectRatio: img.aspectRatio, + })) + return { + $type: 'app.bsky.embed.images#view', + images: imgViews, + } + } + + externalEmbed(did: string, embed: ExternalEmbed): ExternalEmbedView { + const { uri, title, description, thumb } = embed.external + return { + $type: 'app.bsky.embed.external#view', + external: { + uri, + title, + description, + thumb: thumb + ? this.imgUriBuilder.getPresetUri( + 'feed_thumbnail', + did, + cidFromBlobJson(thumb), + ) + : undefined, + }, + } + } + + embedNotFound(uri: string): { $type: string; record: EmbedNotFound } { + return { + $type: 'app.bsky.embed.record#view', + record: { + $type: 'app.bsky.embed.record#viewNotFound', + uri, + notFound: true, + }, + } + } + + embedBlocked( + uri: string, + state: HydrationState, + ): { $type: string; record: EmbedBlocked } { + const creator = creatorFromUri(uri) + return { + $type: 'app.bsky.embed.record#view', + record: { + $type: 'app.bsky.embed.record#viewBlocked', + uri, + blocked: true, + author: { + did: creator, + viewer: this.blockedProfileViewer(creator, state), + }, + }, + } + } + + embedPostView( + uri: string, + state: HydrationState, + depth: number, + ): PostEmbedView | undefined { + const postView = this.post(uri, state, depth) + if (!postView) return + return { + $type: 'app.bsky.embed.record#viewRecord', + uri: postView.uri, + cid: postView.cid, + author: postView.author, + value: postView.record, + labels: postView.labels, + indexedAt: postView.indexedAt, + embeds: depth > 1 ? undefined : postView.embed ? [postView.embed] : [], + } + } + + recordEmbed( + postUri: string, + embed: RecordEmbed, + state: HydrationState, + depth: number, + withTypeTag = true, + ): RecordEmbedView { + const uri = embed.record.uri + const parsedUri = new AtUri(uri) + if ( + this.viewerBlockExists(parsedUri.hostname, state) || + state.postBlocks?.get(postUri)?.embed + ) { + return this.embedBlocked(uri, state) + } + + if (parsedUri.collection === ids.AppBskyFeedPost) { + const view = this.embedPostView(uri, state, depth) + if (!view) return this.embedNotFound(uri) + return this.recordEmbedWrapper(view, withTypeTag) + } else if (parsedUri.collection === ids.AppBskyFeedGenerator) { + const view = this.feedGenerator(uri, state) + if (!view) return this.embedNotFound(uri) + view.$type = 'app.bsky.feed.defs#generatorView' + return this.recordEmbedWrapper(view, withTypeTag) + } else if (parsedUri.collection === ids.AppBskyGraphList) { + const view = this.list(uri, state) + if (!view) return this.embedNotFound(uri) + view.$type = 'app.bsky.graph.defs#listView' + return this.recordEmbedWrapper(view, withTypeTag) + } + return this.embedNotFound(uri) + } + + private recordEmbedWrapper( + record: RecordEmbedViewInternal, + withTypeTag: boolean, + ): RecordEmbedView { + return { + $type: withTypeTag ? 'app.bsky.embed.record#view' : undefined, + record, + } + } + + recordWithMediaEmbed( + postUri: string, + embed: RecordWithMedia, + state: HydrationState, + depth: number, + ): RecordWithMediaView | undefined { + const creator = creatorFromUri(postUri) + let mediaEmbed: ImagesEmbedView | ExternalEmbedView + if (isImagesEmbed(embed.media)) { + mediaEmbed = this.imagesEmbed(creator, embed.media) + } else if (isExternalEmbed(embed.media)) { + mediaEmbed = this.externalEmbed(creator, embed.media) + } else { + return + } + return { + $type: 'app.bsky.embed.recordWithMedia#view', + media: mediaEmbed, + record: this.recordEmbed(postUri, embed.record, state, depth, false), + } + } + + userReplyDisabled(uri: string, state: HydrationState): boolean | undefined { + const post = state.posts?.get(uri) + if (post?.violatesThreadGate) { + return true + } + const rootUriStr: string = post?.record.reply?.root.uri ?? uri + const gate = state.threadgates?.get(postToGateUri(rootUriStr))?.record + if (!gate || !state.viewer) { + return undefined + } + const rootPost = state.posts?.get(rootUriStr)?.record + const ownerDid = new AtUri(rootUriStr).hostname + const { + canReply, + allowFollowing, + allowListUris = [], + } = parseThreadGate(state.viewer, ownerDid, rootPost ?? null, gate) + if (canReply) { + return false + } + if (allowFollowing && state.profileViewers?.get(ownerDid)?.followedBy) { + return false + } + for (const listUri of allowListUris) { + const list = state.listViewers?.get(listUri) + if (list?.viewerInList) { + return false + } + } + return true + } + + notification( + notif: Notification, + lastSeenAt: string | undefined, + state: HydrationState, + ): NotificationView | undefined { + if (!notif.timestamp || !notif.reason) return + const uri = new AtUri(notif.uri) + const authorDid = uri.hostname + const author = this.profile(authorDid, state) + if (!author) return + let recordInfo: RecordInfo> | null | undefined + if (uri.collection === ids.AppBskyFeedPost) { + recordInfo = state.posts?.get(notif.uri) + } else if (uri.collection === ids.AppBskyFeedLike) { + recordInfo = state.likes?.get(notif.uri) + } else if (uri.collection === ids.AppBskyFeedRepost) { + recordInfo = state.reposts?.get(notif.uri) + } else if (uri.collection === ids.AppBskyGraphFollow) { + recordInfo = state.follows?.get(notif.uri) + } + if (!recordInfo) return + const labels = state.labels?.get(notif.uri) ?? [] + const selfLabels = this.selfLabels({ + uri: notif.uri, + cid: recordInfo.cid, + record: recordInfo.record, + }) + const indexedAt = notif.timestamp.toDate().toISOString() + return { + uri: notif.uri, + cid: recordInfo.cid, + author, + reason: notif.reason, + reasonSubject: notif.reasonSubject || undefined, + record: recordInfo.record, + // @NOTE works with a hack in listNotifications so that when there's no last-seen time, + // the user's first notification is marked unread, and all previous read. in this case, + // the last seen time will be equal to the first notification's indexed time. + isRead: lastSeenAt ? lastSeenAt > indexedAt : true, + indexedAt: notif.timestamp.toDate().toISOString(), + labels: [...labels, ...selfLabels], + } + } +} + +const postToGateUri = (uri: string) => { + const aturi = new AtUri(uri) + if (aturi.collection === ids.AppBskyFeedPost) { + aturi.collection = ids.AppBskyFeedThreadgate + } + return aturi.toString() +} + +const getRootUri = (uri: string, post: Post): string => { + return post.record.reply?.root.uri ?? uri +} diff --git a/packages/bsky/src/views/types.ts b/packages/bsky/src/views/types.ts new file mode 100644 index 00000000000..8c5a3deb026 --- /dev/null +++ b/packages/bsky/src/views/types.ts @@ -0,0 +1,72 @@ +import { + Main as ImagesEmbed, + View as ImagesEmbedView, +} from '../lexicon/types/app/bsky/embed/images' +import { + Main as ExternalEmbed, + View as ExternalEmbedView, +} from '../lexicon/types/app/bsky/embed/external' +import { + Main as RecordEmbed, + View as RecordEmbedView, + ViewBlocked as EmbedBlocked, + ViewNotFound as EmbedNotFound, + ViewRecord as PostEmbedView, +} from '../lexicon/types/app/bsky/embed/record' +import { + Main as RecordWithMedia, + View as RecordWithMediaView, +} from '../lexicon/types/app/bsky/embed/recordWithMedia' +import { + BlockedPost, + GeneratorView, + NotFoundPost, + PostView, +} from '../lexicon/types/app/bsky/feed/defs' +import { ListView } from '../lexicon/types/app/bsky/graph/defs' + +export type { + Main as ImagesEmbed, + View as ImagesEmbedView, +} from '../lexicon/types/app/bsky/embed/images' +export { isMain as isImagesEmbed } from '../lexicon/types/app/bsky/embed/images' +export type { + Main as ExternalEmbed, + View as ExternalEmbedView, +} from '../lexicon/types/app/bsky/embed/external' +export { isMain as isExternalEmbed } from '../lexicon/types/app/bsky/embed/external' +export type { + Main as RecordEmbed, + View as RecordEmbedView, + ViewBlocked as EmbedBlocked, + ViewNotFound as EmbedNotFound, + ViewRecord as PostEmbedView, +} from '../lexicon/types/app/bsky/embed/record' +export { isMain as isRecordEmbed } from '../lexicon/types/app/bsky/embed/record' +export type { + Main as RecordWithMedia, + View as RecordWithMediaView, +} from '../lexicon/types/app/bsky/embed/recordWithMedia' +export { isMain as isRecordWithMedia } from '../lexicon/types/app/bsky/embed/recordWithMedia' +export type { View as RecordWithMediaEmbedView } from '../lexicon/types/app/bsky/embed/recordWithMedia' +export type { + BlockedPost, + GeneratorView, + NotFoundPost, + PostView, +} from '../lexicon/types/app/bsky/feed/defs' +export type { ListView } from '../lexicon/types/app/bsky/graph/defs' + +export type { Notification as NotificationView } from '../lexicon/types/app/bsky/notification/listNotifications' + +export type Embed = ImagesEmbed | ExternalEmbed | RecordEmbed | RecordWithMedia + +export type EmbedView = + | ImagesEmbedView + | ExternalEmbedView + | RecordEmbedView + | RecordWithMediaView + +export type MaybePostView = PostView | NotFoundPost | BlockedPost + +export type RecordEmbedViewInternal = PostEmbedView | GeneratorView | ListView diff --git a/packages/bsky/src/views/util.ts b/packages/bsky/src/views/util.ts new file mode 100644 index 00000000000..6f63945ef0a --- /dev/null +++ b/packages/bsky/src/views/util.ts @@ -0,0 +1,64 @@ +import { AtUri } from '@atproto/syntax' +import { BlobRef } from '@atproto/lexicon' +import { Record as PostRecord } from '../lexicon/types/app/bsky/feed/post' +import { + Record as GateRecord, + isFollowingRule, + isListRule, + isMentionRule, +} from '../lexicon/types/app/bsky/feed/threadgate' +import { isMention } from '../lexicon/types/app/bsky/richtext/facet' + +export const creatorFromUri = (uri: string): string => { + return new AtUri(uri).hostname +} + +export const parseThreadGate = ( + replierDid: string, + ownerDid: string, + rootPost: PostRecord | null, + gate: GateRecord | null, +): ParsedThreadGate => { + if (replierDid === ownerDid) { + return { canReply: true } + } + // if gate.allow is unset then *any* reply is allowed, if it is an empty array then *no* reply is allowed + if (!gate || !gate.allow) { + return { canReply: true } + } + + const allowMentions = !!gate.allow.find(isMentionRule) + const allowFollowing = !!gate.allow.find(isFollowingRule) + const allowListUris = gate.allow?.filter(isListRule).map((item) => item.list) + + // check mentions first since it's quick and synchronous + if (allowMentions) { + const isMentioned = rootPost?.facets?.some((facet) => { + return facet.features.some( + (item) => isMention(item) && item.did === replierDid, + ) + }) + if (isMentioned) { + return { canReply: true, allowMentions, allowFollowing, allowListUris } + } + } + return { allowMentions, allowFollowing, allowListUris } +} + +type ParsedThreadGate = { + canReply?: boolean + allowMentions?: boolean + allowFollowing?: boolean + allowListUris?: string[] +} + +export const cidFromBlobJson = (json: BlobRef) => { + if (json instanceof BlobRef) { + return json.ref.toString() + } + // @NOTE below handles the fact that parseRecordBytes() produces raw json rather than lexicon values + if (json['$type'] === 'blob') { + return (json['ref']?.['$link'] ?? '') as string + } + return (json['cid'] ?? '') as string +} diff --git a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap index 4ac043c9b88..8aea15ddfc8 100644 --- a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap @@ -65,9 +65,11 @@ Object { "cid": "cids(2)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "description": "its me!", "did": "user(3)", "displayName": "ali", "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(3)", @@ -938,13 +940,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(4)@jpeg", + "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": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", + "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", }, ], }, @@ -952,8 +954,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1223,13 +1225,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(4)@jpeg", + "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": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", + "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", }, ], }, @@ -1237,8 +1239,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1457,9 +1459,11 @@ Object { "cid": "cids(0)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/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)", @@ -1505,9 +1509,11 @@ Object { "cid": "cids(0)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/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)", @@ -1533,23 +1539,23 @@ Object { "muted": false, }, }, - "description": "Provides all feed candidates", + "description": "Provides even-indexed feed candidates", "did": "user(0)", - "displayName": "All", + "displayName": "Even", "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 2, + "likeCount": 0, "uri": "record(0)", - "viewer": Object { - "like": "record(4)", - }, + "viewer": Object {}, }, Object { "cid": "cids(3)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/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)", @@ -1575,13 +1581,15 @@ Object { "muted": false, }, }, - "description": "Provides even-indexed feed candidates", + "description": "Provides all feed candidates", "did": "user(0)", - "displayName": "Even", + "displayName": "All", "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 0, - "uri": "record(5)", - "viewer": Object {}, + "likeCount": 2, + "uri": "record(4)", + "viewer": Object { + "like": "record(5)", + }, }, ], } @@ -1589,14 +1597,17 @@ Object { exports[`feed generation getSuggestedFeeds returns list of suggested feed generators 1`] = ` Object { + "cursor": "4", "feeds": Array [ Object { "cid": "cids(0)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/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)", @@ -1636,9 +1647,11 @@ Object { "cid": "cids(3)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/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)", @@ -1676,9 +1689,11 @@ Object { "cid": "cids(4)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/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)", diff --git a/packages/bsky/tests/_util.ts b/packages/bsky/tests/_util.ts index 8d39a0f9c2c..a86d47a04f6 100644 --- a/packages/bsky/tests/_util.ts +++ b/packages/bsky/tests/_util.ts @@ -49,7 +49,7 @@ export const forSnapshot = (obj: unknown) => { return constantDate } } - if (str.match(/^\d+::bafy/)) { + if (str.match(/^\d+__bafy/)) { return constantKeysetCursor } if (str.match(/\/img\/[^/]+\/.+\/did:plc:[^/]+\/[^/]+@[\w]+$/)) { @@ -110,7 +110,7 @@ export function take( } export const constantDate = new Date(0).toISOString() -export const constantKeysetCursor = '0000000000000::bafycid' +export const constantKeysetCursor = '0000000000000__bafycid' const mapLeafValues = (obj: unknown, fn: (val: unknown) => unknown) => { if (Array.isArray(obj)) { diff --git a/packages/bsky/tests/admin/admin-auth.test.ts b/packages/bsky/tests/admin/admin-auth.test.ts index ff00d0906b0..cb13b58897a 100644 --- a/packages/bsky/tests/admin/admin-auth.test.ts +++ b/packages/bsky/tests/admin/admin-auth.test.ts @@ -27,15 +27,30 @@ describe('admin auth', () => { bskyDid = network.bsky.ctx.cfg.serverDid modServiceKey = await Secp256k1Keypair.create() - const origResolve = network.bsky.ctx.idResolver.did.resolveAtprotoKey - network.bsky.ctx.idResolver.did.resolveAtprotoKey = async ( + const origResolve = network.bsky.dataplane.idResolver.did.resolve + network.bsky.dataplane.idResolver.did.resolve = async function ( did: string, forceRefresh?: boolean, - ) => { + ) { if (did === modServiceDid || did === altModDid) { - return modServiceKey.did() + return { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/multikey/v1', + 'https://w3id.org/security/suites/secp256k1-2019/v1', + ], + id: did, + verificationMethod: [ + { + id: `${did}#atproto`, + type: 'Multikey', + controller: did, + publicKeyMultibase: modServiceKey.did().replace('did:key:', ''), + }, + ], + } } - return origResolve(did, forceRefresh) + return origResolve.call(this, did, forceRefresh) } agent = network.bsky.getClient() @@ -70,9 +85,7 @@ describe('admin auth', () => { ) const res = await agent.api.com.atproto.admin.getSubjectStatus( - { - did: repoSubject.did, - }, + { did: repoSubject.did }, headers, ) expect(res.data.subject.did).toBe(repoSubject.did) diff --git a/packages/bsky/tests/admin/moderation.test.ts b/packages/bsky/tests/admin/moderation.test.ts index 6b01bfbbcb6..2f6caa8d17d 100644 --- a/packages/bsky/tests/admin/moderation.test.ts +++ b/packages/bsky/tests/admin/moderation.test.ts @@ -55,18 +55,18 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.bsky.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { did: repoSubject.did, }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.bsky.adminAuthHeaders() }, ) expect(res.data.subject.did).toEqual(sc.dids.bob) expect(res.data.takedown?.applied).toBe(true) - expect(res.data.takedown?.ref).toBe('test-repo') + // expect(res.data.takedown?.ref).toBe('test-repo') @TODO add these checks back in once takedown refs make it into dataplane }) it('restores takendown accounts', async () => { @@ -77,14 +77,14 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.bsky.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { did: repoSubject.did, }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.bsky.adminAuthHeaders() }, ) expect(res.data.subject.did).toEqual(sc.dids.bob) expect(res.data.takedown?.applied).toBe(false) @@ -99,18 +99,18 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.bsky.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { uri: recordSubject.uri, }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.bsky.adminAuthHeaders() }, ) expect(res.data.subject.uri).toEqual(recordSubject.uri) expect(res.data.takedown?.applied).toBe(true) - expect(res.data.takedown?.ref).toBe('test-record') + // expect(res.data.takedown?.ref).toBe('test-record') }) it('restores takendown records', async () => { @@ -121,55 +121,27 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.bsky.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { uri: recordSubject.uri, }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.bsky.adminAuthHeaders() }, ) expect(res.data.subject.uri).toEqual(recordSubject.uri) expect(res.data.takedown?.applied).toBe(false) expect(res.data.takedown?.ref).toBeUndefined() }) - it('does not allow non-full moderators to update subject state', async () => { - const subject = { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - } - const attemptTakedownTriage = - agent.api.com.atproto.admin.updateSubjectStatus( - { - subject, - takedown: { applied: true }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('triage'), - }, - ) - await expect(attemptTakedownTriage).rejects.toThrow( - 'Must be a full moderator to update subject state', - ) - const res = await agent.api.com.atproto.admin.getSubjectStatus( - { - did: subject.did, - }, - { headers: network.bsky.adminAuthHeaders('moderator') }, - ) - expect(res.data.takedown?.applied).toBe(false) - }) - describe('blob takedown', () => { let blobUri: string let imageUri: string beforeAll(async () => { blobUri = `${network.bsky.url}/blob/${blobSubject.did}/${blobSubject.cid}` - imageUri = network.bsky.ctx.imgUriBuilder + imageUri = network.bsky.ctx.views.imgUriBuilder .getPresetUri('feed_thumbnail', blobSubject.did, blobSubject.cid) .replace(network.bsky.ctx.cfg.publicUrl || '', network.bsky.url) // Warm image server cache @@ -194,12 +166,12 @@ describe('moderation', () => { did: blobSubject.did, blob: blobSubject.cid, }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.bsky.adminAuthHeaders() }, ) expect(res.data.subject.did).toEqual(blobSubject.did) expect(res.data.subject.cid).toEqual(blobSubject.cid) expect(res.data.takedown?.applied).toBe(true) - expect(res.data.takedown?.ref).toBe('test-blob') + // expect(res.data.takedown?.ref).toBe('test-blob') }) it('prevents resolution of blob', async () => { @@ -211,12 +183,6 @@ describe('moderation', () => { }) }) - it('prevents image blob from being served, even when cached.', async () => { - const fetchImage = await fetch(imageUri) - expect(fetchImage.status).toEqual(404) - expect(await fetchImage.json()).toEqual({ message: 'Image not found' }) - }) - it('restores blob when takedown is removed', async () => { await agent.api.com.atproto.admin.updateSubjectStatus( { diff --git a/packages/bsky/tests/auth.test.ts b/packages/bsky/tests/auth.test.ts index e08049fa84c..d0903174a2b 100644 --- a/packages/bsky/tests/auth.test.ts +++ b/packages/bsky/tests/auth.test.ts @@ -22,7 +22,8 @@ describe('auth', () => { await network.close() }) - it('handles signing key change for service auth.', async () => { + // @TODO invalidations do not originate from appview frontends: requires identity event on the repo stream. + it.skip('handles signing key change for service auth.', async () => { const issuer = sc.dids.alice const attemptWithKey = async (keypair: Keypair) => { const jwt = await createServiceJwt({ diff --git a/packages/bsky/tests/auto-moderator/fixtures/hiveai_resp_example.json b/packages/bsky/tests/auto-moderator/fixtures/hiveai_resp_example.json deleted file mode 100644 index 2315fa9d0c0..00000000000 --- a/packages/bsky/tests/auto-moderator/fixtures/hiveai_resp_example.json +++ /dev/null @@ -1,401 +0,0 @@ -{ - "id": "02122580-c37f-11ed-81d2-000000000000", - "code": 200, - "project_id": 12345, - "user_id": 12345, - "created_on": "2023-03-15T22:16:18.408Z", - "status": [ - { - "status": { - "code": "0", - "message": "SUCCESS" - }, - "response": { - "input": { - "id": "02122580-c37f-11ed-81d2-000000000000", - "charge": 0.003, - "model": "mod55_dense", - "model_version": 1, - "model_type": "CATEGORIZATION", - "created_on": "2023-03-15T22:16:18.136Z", - "media": { - "url": null, - "filename": "bafkreiam7k6mvkyuoybq4ynhljvj5xa75sdbhjbolzjf5j2udx7vj5gnsy", - "type": "PHOTO", - "mime_type": "jpeg", - "mimetype": "image/jpeg", - "width": 800, - "height": 800, - "num_frames": 1, - "duration": 0 - }, - "user_id": 12345, - "project_id": 12345, - "config_version": 1, - "config_tag": "default" - }, - "output": [ - { - "time": 0, - "classes": [ - { - "class": "general_not_nsfw_not_suggestive", - "score": 0.9998097218132356 - }, - { - "class": "general_nsfw", - "score": 8.857344804177162e-5 - }, - { - "class": "general_suggestive", - "score": 0.00010170473872266839 - }, - { - "class": "no_female_underwear", - "score": 0.9999923079040384 - }, - { - "class": "yes_female_underwear", - "score": 7.692095961599136e-6 - }, - { - "class": "no_male_underwear", - "score": 0.9999984904867634 - }, - { - "class": "yes_male_underwear", - "score": 1.5095132367094679e-6 - }, - { - "class": "no_sex_toy", - "score": 0.9999970970762551 - }, - { - "class": "yes_sex_toy", - "score": 2.9029237450490604e-6 - }, - { - "class": "no_female_nudity", - "score": 0.9999739028909301 - }, - { - "class": "yes_female_nudity", - "score": 2.60971090699536e-5 - }, - { - "class": "no_male_nudity", - "score": 0.9999711373083747 - }, - { - "class": "yes_male_nudity", - "score": 2.8862691625255323e-5 - }, - { - "class": "no_female_swimwear", - "score": 0.9999917609899659 - }, - { - "class": "yes_female_swimwear", - "score": 8.239010034025379e-6 - }, - { - "class": "no_male_shirtless", - "score": 0.9999583350744331 - }, - { - "class": "yes_male_shirtless", - "score": 4.166492556688088e-5 - }, - { - "class": "no_text", - "score": 0.9958378716447616 - }, - { - "class": "text", - "score": 0.0041621283552384265 - }, - { - "class": "animated", - "score": 0.46755478950048235 - }, - { - "class": "hybrid", - "score": 0.0011440363434524984 - }, - { - "class": "natural", - "score": 0.5313011741560651 - }, - { - "class": "animated_gun", - "score": 2.0713000782979496e-5 - }, - { - "class": "gun_in_hand", - "score": 1.5844730446534659e-6 - }, - { - "class": "gun_not_in_hand", - "score": 1.0338973818006654e-6 - }, - { - "class": "no_gun", - "score": 0.9999766686287906 - }, - { - "class": "culinary_knife_in_hand", - "score": 3.8063500083369785e-6 - }, - { - "class": "culinary_knife_not_in_hand", - "score": 7.94057948996249e-7 - }, - { - "class": "knife_in_hand", - "score": 4.5578955723278505e-7 - }, - { - "class": "knife_not_in_hand", - "score": 3.842124714748908e-7 - }, - { - "class": "no_knife", - "score": 0.999994559590014 - }, - { - "class": "a_little_bloody", - "score": 2.1317745626539786e-7 - }, - { - "class": "no_blood", - "score": 0.9999793341236429 - }, - { - "class": "other_blood", - "score": 2.0322054269591763e-5 - }, - { - "class": "very_bloody", - "score": 1.306446309561673e-7 - }, - { - "class": "no_pills", - "score": 0.9999989592376954 - }, - { - "class": "yes_pills", - "score": 1.0407623044588633e-6 - }, - { - "class": "no_smoking", - "score": 0.9999939101969173 - }, - { - "class": "yes_smoking", - "score": 6.089803082758281e-6 - }, - { - "class": "illicit_injectables", - "score": 6.925695592003094e-7 - }, - { - "class": "medical_injectables", - "score": 8.587808234452378e-7 - }, - { - "class": "no_injectables", - "score": 0.9999984486496174 - }, - { - "class": "no_nazi", - "score": 0.9999987449628097 - }, - { - "class": "yes_nazi", - "score": 1.2550371902234279e-6 - }, - { - "class": "no_kkk", - "score": 0.999999762417549 - }, - { - "class": "yes_kkk", - "score": 2.3758245111050425e-7 - }, - { - "class": "no_middle_finger", - "score": 0.9999881515231847 - }, - { - "class": "yes_middle_finger", - "score": 1.184847681536747e-5 - }, - { - "class": "no_terrorist", - "score": 0.9999998870793229 - }, - { - "class": "yes_terrorist", - "score": 1.1292067715380635e-7 - }, - { - "class": "no_overlay_text", - "score": 0.9996453363440359 - }, - { - "class": "yes_overlay_text", - "score": 0.0003546636559640924 - }, - { - "class": "no_sexual_activity", - "score": 0.9999563580374798 - }, - { - "class": "yes_sexual_activity", - "score": 0.99, - "realScore": 4.364196252012032e-5 - }, - { - "class": "hanging", - "score": 3.6435135762510905e-7 - }, - { - "class": "no_hanging_no_noose", - "score": 0.9999980779196416 - }, - { - "class": "noose", - "score": 1.5577290007796094e-6 - }, - { - "class": "no_realistic_nsfw", - "score": 0.9999944341007805 - }, - { - "class": "yes_realistic_nsfw", - "score": 5.565899219571182e-6 - }, - { - "class": "animated_corpse", - "score": 5.276802046755426e-7 - }, - { - "class": "human_corpse", - "score": 2.5449360984211012e-8 - }, - { - "class": "no_corpse", - "score": 0.9999994468704343 - }, - { - "class": "no_self_harm", - "score": 0.9999994515625507 - }, - { - "class": "yes_self_harm", - "score": 5.484374493605692e-7 - }, - { - "class": "no_drawing", - "score": 0.9978276028816608 - }, - { - "class": "yes_drawing", - "score": 0.0021723971183392485 - }, - { - "class": "no_emaciated_body", - "score": 0.9999998146500432 - }, - { - "class": "yes_emaciated_body", - "score": 1.853499568724518e-7 - }, - { - "class": "no_child_present", - "score": 0.9999970498515446 - }, - { - "class": "yes_child_present", - "score": 2.950148455380443e-6 - }, - { - "class": "no_sexual_intent", - "score": 0.9999963861546292 - }, - { - "class": "yes_sexual_intent", - "score": 3.613845370766111e-6 - }, - { - "class": "animal_genitalia_and_human", - "score": 2.255472023465222e-8 - }, - { - "class": "animal_genitalia_only", - "score": 4.6783185199931176e-7 - }, - { - "class": "animated_animal_genitalia", - "score": 6.707857419436447e-7 - }, - { - "class": "no_animal_genitalia", - "score": 0.9999988388276858 - }, - { - "class": "no_gambling", - "score": 0.9999960939687145 - }, - { - "class": "yes_gambling", - "score": 3.906031285604864e-6 - }, - { - "class": "no_undressed", - "score": 0.99999923356218 - }, - { - "class": "yes_undressed", - "score": 7.664378199789045e-7 - }, - { - "class": "no_confederate", - "score": 0.9999925456900376 - }, - { - "class": "yes_confederate", - "score": 7.454309962453175e-6 - }, - { - "class": "animated_alcohol", - "score": 1.8109949948066074e-6 - }, - { - "class": "no_alcohol", - "score": 0.9999916620957963 - }, - { - "class": "yes_alcohol", - "score": 5.88781463445443e-6 - }, - { - "class": "yes_drinking_alcohol", - "score": 6.390945746578106e-7 - }, - { - "class": "no_religious_icon", - "score": 0.9999862158580689 - }, - { - "class": "yes_religious_icon", - "score": 1.3784141931119298e-5 - } - ] - } - ] - } - } - ], - "from_cache": false -} diff --git a/packages/bsky/tests/auto-moderator/hive.test.ts b/packages/bsky/tests/auto-moderator/hive.test.ts deleted file mode 100644 index 3a5cef45a37..00000000000 --- a/packages/bsky/tests/auto-moderator/hive.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import fs from 'fs/promises' -import * as hive from '../../src/auto-moderator/hive' - -describe('labeling', () => { - it('correctly parses hive responses', async () => { - const exampleRespBytes = await fs.readFile( - 'tests/auto-moderator/fixtures/hiveai_resp_example.json', - ) - const exampleResp = JSON.parse(exampleRespBytes.toString()) - const classes = hive.respToClasses(exampleResp) - expect(classes.length).toBeGreaterThan(10) - - const labels = hive.summarizeLabels(classes) - expect(labels).toEqual(['porn']) - }) -}) diff --git a/packages/bsky/tests/auto-moderator/labeler.test.ts b/packages/bsky/tests/auto-moderator/labeler.test.ts deleted file mode 100644 index b735ebb28b2..00000000000 --- a/packages/bsky/tests/auto-moderator/labeler.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { TestNetwork, usersSeed } from '@atproto/dev-env' -import { AtUri, BlobRef } from '@atproto/api' -import { Readable } from 'stream' -import { AutoModerator } from '../../src/auto-moderator' -import IndexerContext from '../../src/indexer/context' -import { cidForRecord } from '@atproto/repo' -import { TID } from '@atproto/common' -import { CID } from 'multiformats/cid' -import { ImgLabeler } from '../../src/auto-moderator/hive' -import { TestOzone } from '@atproto/dev-env/src/ozone' - -// 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 - let ozone: TestOzone - let autoMod: AutoModerator - let ctx: IndexerContext - let badBlob1: BlobRef - let badBlob2: BlobRef - let goodBlob: BlobRef - let alice: string - const postUri = () => AtUri.make(alice, 'app.bsky.feed.post', TID.nextStr()) - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_labeler', - }) - ozone = network.ozone - ctx = network.bsky.indexer.ctx - const pdsCtx = network.pds.ctx - autoMod = ctx.autoMod - autoMod.imgLabeler = new TestImgLabeler() - const sc = network.getSeedClient() - await usersSeed(sc) - await network.processAll() - alice = sc.dids.alice - const storeBlob = (bytes: Uint8Array) => { - return pdsCtx.actorStore.transact(alice, async (store) => { - const blobRef = await store.repo.blob.addUntetheredBlob( - 'image/jpeg', - Readable.from([bytes], { objectMode: false }), - ) - const preparedBlobRef = { - cid: blobRef.ref, - mimeType: 'image/jpeg', - constraints: {}, - } - await store.repo.blob.verifyBlobAndMakePermanent(preparedBlobRef) - await store.repo.blob.associateBlob(preparedBlobRef, postUri()) - return blobRef - }) - } - const bytes1 = new Uint8Array([1, 2, 3, 4]) - const bytes2 = new Uint8Array([5, 6, 7, 8]) - const bytes3 = new Uint8Array([4, 3, 2, 1]) - badBlob1 = await storeBlob(bytes1) - badBlob2 = await storeBlob(bytes2) - goodBlob = await storeBlob(bytes3) - badCid1 = badBlob1.ref - badCid2 = badBlob2.ref - }) - - afterAll(async () => { - await network.close() - }) - - const getLabels = async (subject: string) => { - return ozone.ctx.db.db - .selectFrom('label') - .selectAll() - .where('uri', '=', subject) - .execute() - } - - it('labels text in posts', async () => { - const post = { - $type: 'app.bsky.feed.post', - text: 'blah blah label_me', - createdAt: new Date().toISOString(), - } - const cid = await cidForRecord(post) - const uri = postUri() - autoMod.processRecord(uri, cid, post) - await network.processAll() - const labels = await getLabels(uri.toString()) - expect(labels.length).toBe(1) - expect(labels[0]).toMatchObject({ - src: ozone.ctx.cfg.service.did, - uri: uri.toString(), - cid: cid.toString(), - val: 'test-label', - neg: false, - }) - - // Verify that along with applying the labels, we are also leaving trace of the label as moderation event - // Temporarily assign an instance of moderation service to the autoMod so that we can validate label event - const modSrvc = ozone.ctx.modService(ozone.ctx.db) - const { events } = await modSrvc.getEvents({ - includeAllUserRecords: false, - subject: uri.toString(), - limit: 10, - types: [], - }) - expect(events.length).toBe(1) - expect(events[0]).toMatchObject({ - action: 'com.atproto.admin.defs#modEventLabel', - subjectUri: uri.toString(), - createLabelVals: 'test-label', - negateLabelVals: null, - comment: `[AutoModerator]: Applying labels`, - createdBy: network.bsky.indexer.ctx.cfg.serverDid, - }) - }) - - it('labels embeds in posts', async () => { - const post = { - $type: 'app.bsky.feed.post', - text: 'blah blah', - embed: { - $type: 'app.bsky.embed.images', - images: [ - { - image: badBlob1, - alt: 'img', - }, - { - image: badBlob2, - alt: 'label_me_2', - }, - { - image: goodBlob, - alt: 'img', - }, - ], - }, - createdAt: new Date().toISOString(), - } - const uri = postUri() - const cid = await cidForRecord(post) - autoMod.processRecord(uri, cid, post) - await autoMod.processAll() - const dbLabels = await getLabels(uri.toString()) - const labels = dbLabels.map((row) => row.val).sort() - expect(labels).toEqual( - ['test-label', 'test-label-2', 'img-label', 'other-img-label'].sort(), - ) - }) -}) - -class TestImgLabeler implements ImgLabeler { - async labelImg(_did: string, cid: CID): Promise { - if (cid.equals(badCid1)) { - return ['img-label'] - } - if (cid.equals(badCid2)) { - return ['other-img-label'] - } - return [] - } -} diff --git a/packages/bsky/tests/blob-resolver.test.ts b/packages/bsky/tests/blob-resolver.test.ts index e428c70ca08..985f347f7c2 100644 --- a/packages/bsky/tests/blob-resolver.test.ts +++ b/packages/bsky/tests/blob-resolver.test.ts @@ -1,6 +1,6 @@ import axios, { AxiosInstance } from 'axios' import { CID } from 'multiformats/cid' -import { verifyCidForBytes } from '@atproto/common' +import { cidForCbor, verifyCidForBytes } from '@atproto/common' import { TestNetwork, basicSeed } from '@atproto/dev-env' import { randomBytes } from '@atproto/crypto' @@ -17,7 +17,6 @@ describe('blob resolver', () => { const sc = network.getSeedClient() await basicSeed(sc) await network.processAll() - await network.bsky.processAll() fileDid = sc.dids.carol fileCid = sc.posts[fileDid][0].images[0].image.ref client = axios.create({ @@ -45,8 +44,9 @@ describe('blob resolver', () => { }) it('404s on missing blob.', async () => { + const badCid = await cidForCbor({ unknown: true }) const { data, status } = await client.get( - `/blob/did:plc:unknown/${fileCid.toString()}`, + `/blob/${fileDid}/${badCid.toString()}`, ) expect(status).toEqual(404) expect(data).toEqual({ @@ -55,6 +55,17 @@ describe('blob resolver', () => { }) }) + it('404s on missing identity.', async () => { + const { data, status } = await client.get( + `/blob/did:plc:unknown/${fileCid.toString()}`, + ) + expect(status).toEqual(404) + expect(data).toEqual({ + error: 'NotFoundError', + message: 'Origin not found', + }) + }) + it('400s on invalid did.', async () => { const { data, status } = await client.get( `/blob/did::/${fileCid.toString()}`, diff --git a/packages/bsky/tests/daemon.test.ts b/packages/bsky/tests/daemon.test.ts deleted file mode 100644 index cb3c7058cff..00000000000 --- a/packages/bsky/tests/daemon.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import assert from 'assert' -import { AtUri } from '@atproto/api' -import { TestNetwork, usersSeed } from '@atproto/dev-env' -import { BskyDaemon, DaemonConfig, PrimaryDatabase } from '../src' -import { countAll, excluded } from '../src/db/util' -import { NotificationsDaemon } from '../src/daemon/notifications' -import { - BEFORE_LAST_SEEN_DAYS, - BEFORE_LATEST_UNREAD_DAYS, - UNREAD_KEPT_COUNT, -} from '../src/services/util/notification' - -describe('daemon', () => { - let network: TestNetwork - let daemon: BskyDaemon - let db: PrimaryDatabase - let actors: { did: string }[] = [] - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_daemon', - }) - db = network.bsky.ctx.db.getPrimary() - daemon = BskyDaemon.create({ - db, - cfg: new DaemonConfig({ - version: network.bsky.ctx.cfg.version, - dbPostgresUrl: network.bsky.ctx.cfg.dbPrimaryPostgresUrl, - dbPostgresSchema: network.bsky.ctx.cfg.dbPostgresSchema, - }), - }) - const sc = network.getSeedClient() - await usersSeed(sc) - await network.processAll() - actors = await db.db.selectFrom('actor').selectAll().execute() - }) - - afterAll(async () => { - await network.close() - }) - - describe('notifications daemon', () => { - it('processes all dids', async () => { - for (const { did } of actors) { - await Promise.all([ - setLastSeen(daemon.ctx.db, { did }), - createNotifications(daemon.ctx.db, { - did, - daysAgo: 2 * BEFORE_LAST_SEEN_DAYS, - count: 1, - }), - ]) - } - await expect(countNotifications(db)).resolves.toBe(actors.length) - await runNotifsOnce(daemon.notifications) - await expect(countNotifications(db)).resolves.toBe(0) - }) - - it('removes read notifications older than threshold.', async () => { - const { did } = actors[0] - const lastSeenDaysAgo = 10 - await Promise.all([ - setLastSeen(daemon.ctx.db, { did, daysAgo: lastSeenDaysAgo }), - // read, delete - createNotifications(daemon.ctx.db, { - did, - daysAgo: lastSeenDaysAgo + BEFORE_LAST_SEEN_DAYS + 1, - count: 2, - }), - // read, keep - createNotifications(daemon.ctx.db, { - did, - daysAgo: lastSeenDaysAgo + BEFORE_LAST_SEEN_DAYS - 1, - count: 3, - }), - // unread, keep - createNotifications(daemon.ctx.db, { - did, - daysAgo: lastSeenDaysAgo - 1, - count: 4, - }), - ]) - await expect(countNotifications(db)).resolves.toBe(9) - await runNotifsOnce(daemon.notifications) - await expect(countNotifications(db)).resolves.toBe(7) - await clearNotifications(db) - }) - - it('removes unread notifications older than threshold.', async () => { - const { did } = actors[0] - await Promise.all([ - setLastSeen(daemon.ctx.db, { - did, - daysAgo: 2 * BEFORE_LATEST_UNREAD_DAYS, // all are unread - }), - createNotifications(daemon.ctx.db, { - did, - daysAgo: 0, - count: 1, - }), - createNotifications(daemon.ctx.db, { - did, - daysAgo: BEFORE_LATEST_UNREAD_DAYS - 1, - count: 99, - }), - createNotifications(daemon.ctx.db, { - did, - daysAgo: BEFORE_LATEST_UNREAD_DAYS + 1, - count: 400, - }), - ]) - await expect(countNotifications(db)).resolves.toBe(UNREAD_KEPT_COUNT) - await runNotifsOnce(daemon.notifications) - // none removed when within UNREAD_KEPT_COUNT - await expect(countNotifications(db)).resolves.toBe(UNREAD_KEPT_COUNT) - // add one more, tip over UNREAD_KEPT_COUNT - await createNotifications(daemon.ctx.db, { - did, - daysAgo: BEFORE_LATEST_UNREAD_DAYS + 1, - count: 1, - }) - await runNotifsOnce(daemon.notifications) - // removed all older than BEFORE_LATEST_UNREAD_DAYS - await expect(countNotifications(db)).resolves.toBe(100) - await clearNotifications(db) - }) - }) - - const runNotifsOnce = async (notifsDaemon: NotificationsDaemon) => { - assert(!notifsDaemon.running, 'notifications daemon is already running') - notifsDaemon.run({ forever: false, batchSize: 2 }) - await notifsDaemon.running - } - - const setLastSeen = async ( - db: PrimaryDatabase, - opts: { did: string; daysAgo?: number }, - ) => { - const { did, daysAgo = 0 } = opts - const lastSeenAt = new Date() - lastSeenAt.setDate(lastSeenAt.getDate() - daysAgo) - await db.db - .insertInto('actor_state') - .values({ did, lastSeenNotifs: lastSeenAt.toISOString() }) - .onConflict((oc) => - oc.column('did').doUpdateSet({ - lastSeenNotifs: excluded(db.db, 'lastSeenNotifs'), - }), - ) - .execute() - } - - const createNotifications = async ( - db: PrimaryDatabase, - opts: { - did: string - count: number - daysAgo: number - }, - ) => { - const { did, count, daysAgo } = opts - const sortAt = new Date() - sortAt.setDate(sortAt.getDate() - daysAgo) - await db.db - .insertInto('notification') - .values( - [...Array(count)].map(() => ({ - did, - author: did, - reason: 'none', - recordCid: 'bafycid', - recordUri: AtUri.make(did, 'invalid.collection', 'self').toString(), - sortAt: sortAt.toISOString(), - })), - ) - .execute() - } - - const clearNotifications = async (db: PrimaryDatabase) => { - await db.db.deleteFrom('notification').execute() - } - - const countNotifications = async (db: PrimaryDatabase) => { - const { count } = await db.db - .selectFrom('notification') - .select(countAll.as('count')) - .executeTakeFirstOrThrow() - return count - } -}) diff --git a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap b/packages/bsky/tests/data-plane/__snapshots__/indexing.test.ts.snap similarity index 98% rename from packages/bsky/tests/__snapshots__/indexing.test.ts.snap rename to packages/bsky/tests/data-plane/__snapshots__/indexing.test.ts.snap index 142866aeebd..e927f120505 100644 --- a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap +++ b/packages/bsky/tests/data-plane/__snapshots__/indexing.test.ts.snap @@ -17,7 +17,7 @@ Array [ }, }, Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "feed": Array [ Object { "post": Object { @@ -113,7 +113,7 @@ Array [ "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(3)", "val": "test-label", }, @@ -121,7 +121,7 @@ Array [ "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(3)", "val": "test-label-2", }, @@ -207,7 +207,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(4)", + "did": "user(3)", "handle": "dan.test", "labels": Array [], "viewer": Object { @@ -223,7 +223,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(4)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -317,7 +317,7 @@ Array [ "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(6)", "val": "test-label", }, @@ -413,10 +413,10 @@ Array [ ], }, Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "follows": Array [ Object { - "did": "user(4)", + "did": "user(3)", "handle": "dan.test", "labels": Array [], "viewer": Object { diff --git a/packages/bsky/tests/db.test.ts b/packages/bsky/tests/data-plane/db.test.ts similarity index 65% rename from packages/bsky/tests/db.test.ts rename to packages/bsky/tests/data-plane/db.test.ts index 28008f9897e..1a095906a98 100644 --- a/packages/bsky/tests/db.test.ts +++ b/packages/bsky/tests/data-plane/db.test.ts @@ -1,20 +1,17 @@ -import { once } from 'events' import { sql } from 'kysely' 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' +import { Database } from '../../src' describe('db', () => { let network: TestNetwork - let db: PrimaryDatabase + let db: Database beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_db', }) - db = network.bsky.ctx.db.getPrimary() + db = network.bsky.db }) afterAll(async () => { @@ -189,80 +186,4 @@ describe('db', () => { expect(res.length).toBe(0) }) }) - - describe('Leader', () => { - it('allows leaders to run sequentially.', async () => { - const task = async () => { - await wait(25) - return 'complete' - } - const leader1 = new Leader(707, db) - const leader2 = new Leader(707, db) - const leader3 = new Leader(707, db) - const result1 = await leader1.run(task) - await wait(5) // Short grace period for pg to close session - const result2 = await leader2.run(task) - await wait(5) - const result3 = await leader3.run(task) - await wait(5) - const result4 = await leader3.run(task) - expect([result1, result2, result3, result4]).toEqual([ - { ran: true, result: 'complete' }, - { ran: true, result: 'complete' }, - { ran: true, result: 'complete' }, - { ran: true, result: 'complete' }, - ]) - }) - - it('only allows one leader at a time.', async () => { - const task = async () => { - await wait(75) - return 'complete' - } - const results = await Promise.all([ - new Leader(717, db).run(task), - new Leader(717, db).run(task), - new Leader(717, db).run(task), - ]) - const byRan = (a, b) => Number(a.ran) - Number(b.ran) - expect(results.sort(byRan)).toEqual([ - { ran: false }, - { ran: false }, - { ran: true, result: 'complete' }, - ]) - }) - - it('leaders with different ids do not conflict.', async () => { - const task = async () => { - await wait(75) - return 'complete' - } - const results = await Promise.all([ - new Leader(727, db).run(task), - new Leader(728, db).run(task), - new Leader(729, db).run(task), - ]) - expect(results).toEqual([ - { ran: true, result: 'complete' }, - { ran: true, result: 'complete' }, - { ran: true, result: 'complete' }, - ]) - }) - - it('supports abort.', async () => { - const task = async (ctx: { signal: AbortSignal }) => { - wait(10).then(abort) - return await Promise.race([ - wait(50), - once(ctx.signal, 'abort').then(() => ctx.signal.reason), - ]) - } - const leader = new Leader(737, db) - const abort = () => { - leader.session?.abortController.abort(new Error('Oops!')) - } - const result = await leader.run(task) - expect(result).toEqual({ ran: true, result: new Error('Oops!') }) - }) - }) }) diff --git a/packages/bsky/tests/duplicate-records.test.ts b/packages/bsky/tests/data-plane/duplicate-records.test.ts similarity index 75% rename from packages/bsky/tests/duplicate-records.test.ts rename to packages/bsky/tests/data-plane/duplicate-records.test.ts index 9c7617bd668..da7287893ba 100644 --- a/packages/bsky/tests/duplicate-records.test.ts +++ b/packages/bsky/tests/data-plane/duplicate-records.test.ts @@ -2,23 +2,19 @@ import { AtUri } from '@atproto/syntax' 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' +import * as lex from '../../src/lexicon/lexicons' +import { Database } from '../../src' describe('duplicate record', () => { let network: TestNetwork let did: string - let db: PrimaryDatabase - let services: Services + let db: Database beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_duplicates', }) - db = network.bsky.indexer.ctx.db - services = network.bsky.indexer.ctx.services + db = network.bsky.db did = 'did:example:alice' }) @@ -51,21 +47,25 @@ describe('duplicate record', () => { } const uri = AtUri.make(did, coll, TID.nextStr()) const cid = await cidForCbor(repost) - await services - .indexing(db) - .indexRecord(uri, cid, repost, WriteOpAction.Create, repost.createdAt) + await network.bsky.sub.indexingSvc.indexRecord( + uri, + cid, + repost, + WriteOpAction.Create, + repost.createdAt, + ) uris.push(uri) } let count = await countRecords(db, 'repost') expect(count).toBe(1) - await services.indexing(db).deleteRecord(uris[0], false) + await network.bsky.sub.indexingSvc.deleteRecord(uris[0], false) count = await countRecords(db, 'repost') expect(count).toBe(1) - await services.indexing(db).deleteRecord(uris[1], true) + await network.bsky.sub.indexingSvc.deleteRecord(uris[1], true) count = await countRecords(db, 'repost') expect(count).toBe(0) @@ -87,16 +87,20 @@ describe('duplicate record', () => { } const uri = AtUri.make(did, coll, TID.nextStr()) const cid = await cidForCbor(like) - await services - .indexing(db) - .indexRecord(uri, cid, like, WriteOpAction.Create, like.createdAt) + await network.bsky.sub.indexingSvc.indexRecord( + uri, + cid, + like, + WriteOpAction.Create, + like.createdAt, + ) uris.push(uri) } let count = await countRecords(db, 'like') expect(count).toBe(1) - await services.indexing(db).deleteRecord(uris[0], false) + await network.bsky.sub.indexingSvc.deleteRecord(uris[0], false) count = await countRecords(db, 'like') expect(count).toBe(1) @@ -107,7 +111,7 @@ describe('duplicate record', () => { .executeTakeFirst() expect(got?.uri).toEqual(uris[1].toString()) - await services.indexing(db).deleteRecord(uris[1], true) + await network.bsky.sub.indexingSvc.deleteRecord(uris[1], true) count = await countRecords(db, 'like') expect(count).toBe(0) @@ -124,21 +128,25 @@ describe('duplicate record', () => { } const uri = AtUri.make(did, coll, TID.nextStr()) const cid = await cidForCbor(follow) - await services - .indexing(db) - .indexRecord(uri, cid, follow, WriteOpAction.Create, follow.createdAt) + await network.bsky.sub.indexingSvc.indexRecord( + uri, + cid, + follow, + WriteOpAction.Create, + follow.createdAt, + ) uris.push(uri) } let count = await countRecords(db, 'follow') expect(count).toBe(1) - await services.indexing(db).deleteRecord(uris[0], false) + await network.bsky.sub.indexingSvc.deleteRecord(uris[0], false) count = await countRecords(db, 'follow') expect(count).toBe(1) - await services.indexing(db).deleteRecord(uris[1], true) + await network.bsky.sub.indexingSvc.deleteRecord(uris[1], true) count = await countRecords(db, 'follow') expect(count).toBe(0) diff --git a/packages/bsky/tests/handle-invalidation.test.ts b/packages/bsky/tests/data-plane/handle-invalidation.test.ts similarity index 94% rename from packages/bsky/tests/handle-invalidation.test.ts rename to packages/bsky/tests/data-plane/handle-invalidation.test.ts index 70ac7c29a09..8469a8507ef 100644 --- a/packages/bsky/tests/handle-invalidation.test.ts +++ b/packages/bsky/tests/data-plane/handle-invalidation.test.ts @@ -25,8 +25,8 @@ describe('handle invalidation', () => { alice = sc.dids.alice bob = sc.dids.bob - const origResolve = network.bsky.indexer.ctx.idResolver.handle.resolve - network.bsky.indexer.ctx.idResolver.handle.resolve = async ( + const origResolve = network.bsky.dataplane.idResolver.handle.resolve + network.bsky.dataplane.idResolver.handle.resolve = async ( handle: string, ) => { if (mockHandles[handle] === null) { @@ -44,9 +44,8 @@ describe('handle invalidation', () => { const backdateIndexedAt = async (did: string) => { const TWO_DAYS_AGO = new Date(Date.now() - 2 * DAY).toISOString() - await network.bsky.ctx.db - .getPrimary() - .db.updateTable('actor') + await network.bsky.db.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/data-plane/indexing.test.ts similarity index 84% rename from packages/bsky/tests/indexing.test.ts rename to packages/bsky/tests/data-plane/indexing.test.ts index 3a5a12b7ac6..f97495c6b32 100644 --- a/packages/bsky/tests/indexing.test.ts +++ b/packages/bsky/tests/data-plane/indexing.test.ts @@ -12,15 +12,16 @@ import AtpAgent, { AppBskyGraphFollow, } from '@atproto/api' import { TestNetwork, SeedClient, usersSeed, basicSeed } from '@atproto/dev-env' -import { forSnapshot } from './_util' -import { ids } from '../src/lexicon/lexicons' -import { Database } from '../src/db' +import { forSnapshot } from '../_util' +import { ids } from '../../src/lexicon/lexicons' +import { Database } from '../../src/data-plane/server/db' describe('indexing', () => { let network: TestNetwork let agent: AtpAgent let pdsAgent: AtpAgent let sc: SeedClient + let db: Database beforeAll(async () => { network = await TestNetwork.create({ @@ -29,12 +30,11 @@ describe('indexing', () => { agent = network.bsky.getClient() pdsAgent = network.pds.getClient() sc = network.getSeedClient() + db = network.bsky.db await usersSeed(sc) // Data in tests is not processed from subscription await network.processAll() - await network.bsky.ingester.sub.destroy() - await network.bsky.indexer.sub.destroy() - await network.bsky.processAll() + await network.bsky.sub.destroy() }) afterAll(async () => { @@ -42,7 +42,6 @@ describe('indexing', () => { }) it('indexes posts.', async () => { - const { db, services } = network.bsky.indexer.ctx const createdAt = new Date().toISOString() const createRecord = await prepareCreate({ did: sc.dids.alice, @@ -93,7 +92,7 @@ describe('indexing', () => { }) // Create - await services.indexing(db).indexRecord(...createRecord) + await network.bsky.sub.indexingSvc.indexRecord(...createRecord) const getAfterCreate = await agent.api.app.bsky.feed.getPostThread( { uri: uri.toString() }, @@ -103,7 +102,7 @@ describe('indexing', () => { const createNotifications = await getNotifications(db, uri) // Update - await services.indexing(db).indexRecord(...updateRecord) + await network.bsky.sub.indexingSvc.indexRecord(...updateRecord) const getAfterUpdate = await agent.api.app.bsky.feed.getPostThread( { uri: uri.toString() }, @@ -113,7 +112,7 @@ describe('indexing', () => { const updateNotifications = await getNotifications(db, uri) // Delete - await services.indexing(db).deleteRecord(...deleteRecord) + await network.bsky.sub.indexingSvc.deleteRecord(...deleteRecord) const getAfterDelete = agent.api.app.bsky.feed.getPostThread( { uri: uri.toString() }, @@ -132,7 +131,6 @@ describe('indexing', () => { }) it('indexes profiles.', async () => { - const { db, services } = network.bsky.indexer.ctx const createRecord = await prepareCreate({ did: sc.dids.dan, collection: ids.AppBskyActorProfile, @@ -159,7 +157,7 @@ describe('indexing', () => { }) // Create - await services.indexing(db).indexRecord(...createRecord) + await network.bsky.sub.indexingSvc.indexRecord(...createRecord) const getAfterCreate = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.dan }, @@ -168,7 +166,7 @@ describe('indexing', () => { expect(forSnapshot(getAfterCreate.data)).toMatchSnapshot() // Update - await services.indexing(db).indexRecord(...updateRecord) + await network.bsky.sub.indexingSvc.indexRecord(...updateRecord) const getAfterUpdate = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.dan }, @@ -177,7 +175,7 @@ describe('indexing', () => { expect(forSnapshot(getAfterUpdate.data)).toMatchSnapshot() // Delete - await services.indexing(db).deleteRecord(...deleteRecord) + await network.bsky.sub.indexingSvc.deleteRecord(...deleteRecord) const getAfterDelete = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.dan }, @@ -187,7 +185,6 @@ describe('indexing', () => { }) it('handles post aggregations out of order.', async () => { - const { db, services } = network.bsky.indexer.ctx const createdAt = new Date().toISOString() const originalPost = await prepareCreate({ did: sc.dids.alice, @@ -234,11 +231,11 @@ describe('indexing', () => { } as AppBskyFeedRepost.Record, }) // reply, like, and repost indexed orior to the original post - await services.indexing(db).indexRecord(...reply) - await services.indexing(db).indexRecord(...like) - await services.indexing(db).indexRecord(...repost) - await services.indexing(db).indexRecord(...originalPost) - await network.bsky.processAll() + await network.bsky.sub.indexingSvc.indexRecord(...reply) + await network.bsky.sub.indexingSvc.indexRecord(...like) + await network.bsky.sub.indexingSvc.indexRecord(...repost) + await network.bsky.sub.indexingSvc.indexRecord(...originalPost) + await network.bsky.sub.background.processAll() const agg = await db.db .selectFrom('post_agg') .selectAll() @@ -258,14 +255,13 @@ describe('indexing', () => { rkey: uri.rkey, }) } - await services.indexing(db).deleteRecord(...del(reply[0])) - await services.indexing(db).deleteRecord(...del(like[0])) - await services.indexing(db).deleteRecord(...del(repost[0])) - await services.indexing(db).deleteRecord(...del(originalPost[0])) + await network.bsky.sub.indexingSvc.deleteRecord(...del(reply[0])) + await network.bsky.sub.indexingSvc.deleteRecord(...del(like[0])) + await network.bsky.sub.indexingSvc.deleteRecord(...del(repost[0])) + await network.bsky.sub.indexingSvc.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({ @@ -323,13 +319,12 @@ describe('indexing', () => { } 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() + await network.bsky.sub.indexingSvc.indexRecord(...originalPost) + await network.bsky.sub.indexingSvc.indexRecord(...ownLike) + await network.bsky.sub.indexingSvc.indexRecord(...ownRepost) + await network.bsky.sub.indexingSvc.indexRecord(...aliceLike) + await network.bsky.sub.indexingSvc.indexRecord(...aliceRepost) + await network.bsky.sub.background.processAll() const { data: { notifications }, @@ -354,15 +349,14 @@ describe('indexing', () => { }) } - 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])) + await network.bsky.sub.indexingSvc.deleteRecord(...del(ownLike[0])) + await network.bsky.sub.indexingSvc.deleteRecord(...del(ownRepost[0])) + await network.bsky.sub.indexingSvc.deleteRecord(...del(aliceLike[0])) + await network.bsky.sub.indexingSvc.deleteRecord(...del(aliceRepost[0])) + await network.bsky.sub.indexingSvc.deleteRecord(...del(originalPost[0])) }) it('handles profile aggregations out of order.', async () => { - const { db, services } = network.bsky.indexer.ctx const createdAt = new Date().toISOString() const unknownDid = 'did:example:unknown' const follow = await prepareCreate({ @@ -374,8 +368,8 @@ describe('indexing', () => { createdAt, } as AppBskyGraphFollow.Record, }) - await services.indexing(db).indexRecord(...follow) - await network.bsky.processAll() + await network.bsky.sub.indexingSvc.indexRecord(...follow) + await network.bsky.sub.background.processAll() const agg = await db.db .selectFrom('profile_agg') .select(['did', 'followersCount']) @@ -393,22 +387,19 @@ describe('indexing', () => { rkey: uri.rkey, }) } - await services.indexing(db).deleteRecord(...del(follow[0])) + await network.bsky.sub.indexingSvc.deleteRecord(...del(follow[0])) }) describe('indexRepo', () => { beforeAll(async () => { - network.bsky.indexer.sub.resume() - network.bsky.ingester.sub.resume() + network.bsky.sub.run() await basicSeed(sc, false) await network.processAll() - await network.bsky.ingester.sub.destroy() - await network.bsky.indexer.sub.destroy() - await network.bsky.processAll() + await network.bsky.sub.destroy() + await network.bsky.sub.background.processAll() }) it('preserves indexes when no record changes.', async () => { - const { db, services } = network.bsky.indexer.ctx // Mark originals const { data: origProfile } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, @@ -427,8 +418,8 @@ describe('indexing', () => { 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() + await network.bsky.sub.indexingSvc.indexRepo(sc.dids.alice, commit.cid) + await network.bsky.sub.background.processAll() // Check const { data: profile } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, @@ -448,7 +439,6 @@ describe('indexing', () => { }) it('updates indexes when records change.', async () => { - const { db, services } = network.bsky.indexer.ctx // Update profile await pdsAgent.api.com.atproto.repo.putRecord( { @@ -472,8 +462,8 @@ describe('indexing', () => { 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() + await network.bsky.sub.indexingSvc.indexRepo(sc.dids.alice, commit.cid) + await network.bsky.sub.background.processAll() // Check const { data: profile } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, @@ -495,7 +485,6 @@ describe('indexing', () => { }) it('skips invalid records.', async () => { - const { db, services } = network.bsky.indexer.ctx const { accountManager } = network.pds.ctx // const { db: pdsDb, services: pdsServices } = network.pds.ctx // Create a good and a bad post record @@ -531,7 +520,7 @@ describe('indexing', () => { await pdsAgent.api.com.atproto.sync.getLatestCommit({ did: sc.dids.alice, }) - await services.indexing(db).indexRepo(sc.dids.alice, commit.cid) + await network.bsky.sub.indexingSvc.indexRepo(sc.dids.alice, commit.cid) // Check const getGoodPost = agent.api.app.bsky.feed.getPostThread( { uri: writes[0].uri.toString(), depth: 0 }, @@ -556,7 +545,6 @@ describe('indexing', () => { } it('indexes handle for a fresh did', async () => { - const { db, services } = network.bsky.indexer.ctx const now = new Date().toISOString() const sessionAgent = new AtpAgent({ service: network.pds.url }) const { @@ -567,12 +555,11 @@ describe('indexing', () => { password: 'password', }) await expect(getIndexedHandle(did)).rejects.toThrow('Profile not found') - await services.indexing(db).indexHandle(did, now) + await network.bsky.sub.indexingSvc.indexHandle(did, now) await expect(getIndexedHandle(did)).resolves.toEqual('did1.test') }) it('reindexes handle for existing did when forced', async () => { - const { db, services } = network.bsky.indexer.ctx const now = new Date().toISOString() const sessionAgent = new AtpAgent({ service: network.pds.url }) const { @@ -582,19 +569,18 @@ describe('indexing', () => { handle: 'did2.test', password: 'password', }) - await services.indexing(db).indexHandle(did, now) + await network.bsky.sub.indexingSvc.indexHandle(did, now) await expect(getIndexedHandle(did)).resolves.toEqual('did2.test') await sessionAgent.com.atproto.identity.updateHandle({ handle: 'did2-updated.test', }) - await services.indexing(db).indexHandle(did, now) + await network.bsky.sub.indexingSvc.indexHandle(did, now) await expect(getIndexedHandle(did)).resolves.toEqual('did2.test') // Didn't update, not forced - await services.indexing(db).indexHandle(did, now, true) + await network.bsky.sub.indexingSvc.indexHandle(did, now, true) await expect(getIndexedHandle(did)).resolves.toEqual('did2-updated.test') }) it('handles profile aggregations out of order', async () => { - const { db, services } = network.bsky.indexer.ctx const now = new Date().toISOString() const sessionAgent = new AtpAgent({ service: network.pds.url }) const { @@ -613,9 +599,9 @@ describe('indexing', () => { createdAt: now, } as AppBskyGraphFollow.Record, }) - await services.indexing(db).indexRecord(...follow) - await services.indexing(db).indexHandle(did, now) - await network.bsky.processAll() + await network.bsky.sub.indexingSvc.indexRecord(...follow) + await network.bsky.sub.indexingSvc.indexHandle(did, now) + await network.bsky.sub.background.processAll() const agg = await db.db .selectFrom('profile_agg') .select(['did', 'followersCount']) @@ -630,13 +616,12 @@ describe('indexing', () => { describe('tombstoneActor', () => { it('does not unindex actor when they are still being hosted by their pds', async () => { - 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) }, ) // Attempt indexing tombstone - await services.indexing(db).tombstoneActor(sc.dids.alice) + await network.bsky.sub.indexingSvc.tombstoneActor(sc.dids.alice) const { data: profileAfter } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, { headers: await network.serviceHeaders(sc.dids.bob) }, @@ -645,7 +630,6 @@ describe('indexing', () => { }) it('unindexes actor when they are no longer hosted by their pds', async () => { - const { db, services } = network.bsky.indexer.ctx const { alice } = sc.dids const getProfileBefore = agent.api.app.bsky.actor.getProfile( { actor: alice }, @@ -664,7 +648,7 @@ describe('indexing', () => { }) await network.pds.ctx.backgroundQueue.processAll() // Index tombstone - await services.indexing(db).tombstoneActor(alice) + await network.bsky.sub.indexingSvc.tombstoneActor(alice) const getProfileAfter = agent.api.app.bsky.actor.getProfile( { actor: alice }, { headers: await network.serviceHeaders(sc.dids.bob) }, diff --git a/packages/bsky/tests/subscription/repo.test.ts b/packages/bsky/tests/data-plane/subscription/repo.test.ts similarity index 83% rename from packages/bsky/tests/subscription/repo.test.ts rename to packages/bsky/tests/data-plane/subscription/repo.test.ts index fe910c85603..3a02b2fa61c 100644 --- a/packages/bsky/tests/subscription/repo.test.ts +++ b/packages/bsky/tests/data-plane/subscription/repo.test.ts @@ -4,14 +4,13 @@ import { CommitData } from '@atproto/repo' import { PreparedWrite } from '@atproto/pds/src/repo' import * as sequencer from '@atproto/pds/src/sequencer' import { cborDecode, cborEncode } from '@atproto/common' -import { DatabaseSchemaType } from '../../src/db/database-schema' -import { ids } from '../../src/lexicon/lexicons' -import { forSnapshot } from '../_util' -import { AppContext, Database } from '../../src' +import { DatabaseSchemaType } from '../../../src/data-plane/server/db/database-schema' +import { ids } from '../../../src/lexicon/lexicons' +import { forSnapshot } from '../../_util' +import { Database } from '../../../src' describe('sync', () => { let network: TestNetwork - let ctx: AppContext let pdsAgent: AtpAgent let sc: SeedClient @@ -19,7 +18,6 @@ describe('sync', () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_subscription_repo', }) - ctx = network.bsky.ctx pdsAgent = network.pds.getClient() sc = network.getSeedClient() await basicSeed(sc) @@ -30,7 +28,7 @@ describe('sync', () => { }) it('indexes permit history being replayed.', async () => { - const db = ctx.db.getPrimary() + const { db } = network.bsky // Generate some modifications and dupes const { alice, bob, carol, dan } = sc.dids @@ -62,16 +60,12 @@ describe('sync', () => { const originalTableDump = await getTableDump() // Reprocess repos via sync subscription, on top of existing indices - 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) + await network.bsky.sub.destroy() + // Hard reset of state + network.bsky.sub.cursor = 0 + network.bsky.sub.seenSeq = null // Boot streams back up - network.bsky.indexer.sub.resume() - network.bsky.ingester.sub.resume() + network.bsky.sub.run() await network.processAll() // Permissive of indexedAt times changing @@ -102,7 +96,7 @@ describe('sync', () => { }) await network.processAll() // confirm jack was indexed as an actor despite the bad event - const actors = await dumpTable(ctx.db.getPrimary(), 'actor', ['did']) + const actors = await dumpTable(network.bsky.db, 'actor', ['did']) expect(actors.map((a) => a.handle)).toContain('jack.test') network.pds.ctx.sequencer.sequenceCommit = sequenceCommitOrig }) diff --git a/packages/bsky/tests/subscription/util.test.ts b/packages/bsky/tests/data-plane/subscription/util.test.ts similarity index 98% rename from packages/bsky/tests/subscription/util.test.ts rename to packages/bsky/tests/data-plane/subscription/util.test.ts index 497532f643b..0aba097c334 100644 --- a/packages/bsky/tests/subscription/util.test.ts +++ b/packages/bsky/tests/data-plane/subscription/util.test.ts @@ -3,8 +3,8 @@ import { ConsecutiveList, LatestQueue, PartitionedQueue, -} from '../../src/subscription/util' -import { randomStr } from '../../../crypto/src' +} from '../../../src/data-plane/server/subscription/util' +import { randomStr } from '../../../../crypto/src' describe('subscription utils', () => { describe('ConsecutiveList', () => { diff --git a/packages/bsky/tests/did-cache.test.ts b/packages/bsky/tests/did-cache.test.ts deleted file mode 100644 index 20114779fff..00000000000 --- a/packages/bsky/tests/did-cache.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { TestNetwork, SeedClient, usersSeed } from '@atproto/dev-env' -import { IdResolver } from '@atproto/identity' -import DidRedisCache from '../src/did-cache' -import { wait } from '@atproto/common' -import { Redis } from '../src' - -describe('did cache', () => { - let network: TestNetwork - let sc: SeedClient - let idResolver: IdResolver - let redis: Redis - let didCache: DidRedisCache - - let alice: string - let bob: string - let carol: string - let dan: string - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_did_cache', - }) - idResolver = network.bsky.indexer.ctx.idResolver - redis = network.bsky.indexer.ctx.redis - didCache = network.bsky.indexer.ctx.didCache - sc = network.getSeedClient() - await usersSeed(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('caches dids on lookup', async () => { - await didCache.processAll() - const docs = await Promise.all([ - idResolver.did.cache?.checkCache(alice), - idResolver.did.cache?.checkCache(bob), - idResolver.did.cache?.checkCache(carol), - idResolver.did.cache?.checkCache(dan), - ]) - expect(docs.length).toBe(4) - expect(docs[0]?.doc.id).toEqual(alice) - expect(docs[1]?.doc.id).toEqual(bob) - expect(docs[2]?.doc.id).toEqual(carol) - expect(docs[3]?.doc.id).toEqual(dan) - }) - - it('clears cache and repopulates', async () => { - await Promise.all([ - idResolver.did.cache?.clearEntry(alice), - idResolver.did.cache?.clearEntry(bob), - idResolver.did.cache?.clearEntry(carol), - idResolver.did.cache?.clearEntry(dan), - ]) - const docsCleared = await Promise.all([ - idResolver.did.cache?.checkCache(alice), - idResolver.did.cache?.checkCache(bob), - idResolver.did.cache?.checkCache(carol), - idResolver.did.cache?.checkCache(dan), - ]) - expect(docsCleared).toEqual([null, null, null, null]) - - await Promise.all([ - idResolver.did.resolve(alice), - idResolver.did.resolve(bob), - idResolver.did.resolve(carol), - idResolver.did.resolve(dan), - ]) - await didCache.processAll() - - const docs = await Promise.all([ - idResolver.did.cache?.checkCache(alice), - idResolver.did.cache?.checkCache(bob), - idResolver.did.cache?.checkCache(carol), - idResolver.did.cache?.checkCache(dan), - ]) - expect(docs.length).toBe(4) - expect(docs[0]?.doc.id).toEqual(alice) - expect(docs[1]?.doc.id).toEqual(bob) - expect(docs[2]?.doc.id).toEqual(carol) - expect(docs[3]?.doc.id).toEqual(dan) - }) - - it('accurately reports expired dids & refreshes the cache', async () => { - const didCache = new DidRedisCache(redis.withNamespace('did-doc'), { - staleTTL: 1, - maxTTL: 60000, - }) - const shortCacheResolver = new IdResolver({ - plcUrl: network.bsky.ctx.cfg.didPlcUrl, - didCache, - }) - const doc = await shortCacheResolver.did.resolve(alice) - await didCache.processAll() - // let's mess with alice's doc so we know what we're getting - await didCache.cacheDid(alice, { ...doc, id: 'did:example:alice' }) - await wait(5) - - // first check the cache & see that we have the stale value - const cached = await shortCacheResolver.did.cache?.checkCache(alice) - expect(cached?.stale).toBe(true) - expect(cached?.doc.id).toEqual('did:example:alice') - // see that the resolver gives us the stale value while it revalidates - const staleGet = await shortCacheResolver.did.resolve(alice) - expect(staleGet?.id).toEqual('did:example:alice') - await didCache.processAll() - - // since it revalidated, ensure we have the new value - const updatedCache = await shortCacheResolver.did.cache?.checkCache(alice) - expect(updatedCache?.doc.id).toEqual(alice) - const updatedGet = await shortCacheResolver.did.resolve(alice) - expect(updatedGet?.id).toEqual(alice) - await didCache.destroy() - }) - - it('does not return expired dids & refreshes the cache', async () => { - const didCache = new DidRedisCache(redis.withNamespace('did-doc'), { - staleTTL: 0, - maxTTL: 1, - }) - const shortExpireResolver = new IdResolver({ - plcUrl: network.bsky.ctx.cfg.didPlcUrl, - didCache, - }) - const doc = await shortExpireResolver.did.resolve(alice) - await didCache.processAll() - - // again, we mess with the cached doc so we get something different - await didCache.cacheDid(alice, { ...doc, id: 'did:example:alice' }) - await wait(5) - - // see that the resolver does not return expired value & instead force refreshes - const staleGet = await shortExpireResolver.did.resolve(alice) - expect(staleGet?.id).toEqual(alice) - await didCache.destroy() - }) -}) diff --git a/packages/bsky/tests/feed-generation.test.ts b/packages/bsky/tests/feed-generation.test.ts index 5e63025ff7b..44f3760808b 100644 --- a/packages/bsky/tests/feed-generation.test.ts +++ b/packages/bsky/tests/feed-generation.test.ts @@ -1,3 +1,6 @@ +import assert from 'assert' +import { XRPCError } from '@atproto/xrpc' +import { AuthRequiredError } from '@atproto/xrpc-server' import { TID } from '@atproto/common' import { AtUri, AtpAgent } from '@atproto/api' import { @@ -16,9 +19,6 @@ import { SkeletonFeedPost, } from '../src/lexicon/types/app/bsky/feed/defs' import { forSnapshot, paginateAll } from './_util' -import { AuthRequiredError } from '@atproto/xrpc-server' -import assert from 'assert' -import { XRPCError } from '@atproto/xrpc' describe('feed generation', () => { let network: TestNetwork @@ -74,9 +74,8 @@ describe('feed generation', () => { { uri: feedUriBadPagination.toString(), order: 3 }, { uri: primeUri.toString(), order: 4 }, ] - await network.bsky.ctx.db - .getPrimary() - .db.insertInto('suggested_feed') + await network.bsky.db.db + .insertInto('suggested_feed') .values(feedSuggestions) .execute() }) @@ -151,23 +150,10 @@ describe('feed generation', () => { sc.getHeaders(alice), ) await network.processAll() - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: prime.uri, - cid: prime.cid, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownRecord({ + recordUri: prime.uri, + }) + feedUriAll = all.uri feedUriAllRef = new RecordRef(all.uri, all.cid) feedUriEven = even.uri @@ -319,7 +305,6 @@ describe('feed generation', () => { sc.getHeaders(sc.dids.bob), ) await network.processAll() - await network.bsky.processAll() // now take it offline await bobFg.close() @@ -359,15 +344,24 @@ describe('feed generation', () => { describe('getPopularFeedGenerators', () => { it('gets popular feed generators', async () => { - const resEven = - await agent.api.app.bsky.unspecced.getPopularFeedGenerators( - {}, - { headers: await network.serviceHeaders(sc.dids.bob) }, - ) - expect(resEven.data.feeds.map((f) => f.likeCount)).toEqual([ - 2, 0, 0, 0, 0, + const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( + {}, + { headers: await network.serviceHeaders(sc.dids.bob) }, + ) + expect(res.data.feeds.map((f) => f.uri)).not.toContain(feedUriPrime) // taken-down + expect(res.data.feeds.map((f) => f.uri)).toEqual([ + feedUriAll, + feedUriEven, + feedUriBadPagination, ]) - expect(resEven.data.feeds.map((f) => f.uri)).not.toContain(feedUriPrime) // taken-down + }) + + it('searches feed generators', async () => { + const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( + { query: 'all' }, + { headers: await network.serviceHeaders(sc.dids.bob) }, + ) + expect(res.data.feeds.map((f) => f.uri)).toEqual([feedUriAll]) }) it('paginates', async () => { @@ -376,7 +370,6 @@ describe('feed generation', () => { {}, { headers: await network.serviceHeaders(sc.dids.bob) }, ) - const resOne = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( { limit: 2 }, diff --git a/packages/bsky/tests/image/server.test.ts b/packages/bsky/tests/image/server.test.ts index ee4d668945d..5ccd3dbd70a 100644 --- a/packages/bsky/tests/image/server.test.ts +++ b/packages/bsky/tests/image/server.test.ts @@ -18,7 +18,6 @@ describe('image processing server', () => { const sc = network.getSeedClient() await basicSeed(sc) await network.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/image/uri.test.ts b/packages/bsky/tests/image/uri.test.ts index 60586d23f6b..137e2359706 100644 --- a/packages/bsky/tests/image/uri.test.ts +++ b/packages/bsky/tests/image/uri.test.ts @@ -14,16 +14,20 @@ describe('image uri builder', () => { }) it('generates paths.', () => { - expect(ImageUriBuilder.getPath({ preset: 'banner', did, cid })).toEqual( - `/banner/plain/${did}/${cid.toString()}@jpeg`, - ) expect( - ImageUriBuilder.getPath({ preset: 'feed_thumbnail', did, cid }), + ImageUriBuilder.getPath({ preset: 'banner', did, cid: cid.toString() }), + ).toEqual(`/banner/plain/${did}/${cid.toString()}@jpeg`) + expect( + ImageUriBuilder.getPath({ + preset: 'feed_thumbnail', + did, + cid: cid.toString(), + }), ).toEqual(`/feed_thumbnail/plain/${did}/${cid.toString()}@jpeg`) }) it('generates uris.', () => { - expect(uriBuilder.getPresetUri('banner', did, cid)).toEqual( + expect(uriBuilder.getPresetUri('banner', did, cid.toString())).toEqual( `https://example.com/img/banner/plain/${did}/${cid.toString()}@jpeg`, ) expect( @@ -38,7 +42,7 @@ describe('image uri builder', () => { ImageUriBuilder.getOptions(`/banner/plain/${did}/${cid.toString()}@png`), ).toEqual({ did: 'did:plc:xyz', - cid, + cid: cid.toString(), fit: 'cover', format: 'png', height: 1000, @@ -52,7 +56,7 @@ describe('image uri builder', () => { ), ).toEqual({ did: 'did:plc:xyz', - cid, + cid: cid.toString(), fit: 'inside', format: 'jpeg', height: 2000, diff --git a/packages/bsky/tests/notification-server.test.ts b/packages/bsky/tests/notification-server.test.ts deleted file mode 100644 index 0efd1e448b4..00000000000 --- a/packages/bsky/tests/notification-server.test.ts +++ /dev/null @@ -1,270 +0,0 @@ -import AtpAgent, { AtUri } from '@atproto/api' -import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' -import { - CourierNotificationServer, - GorushNotificationServer, -} from '../src/notifications' -import { Database } from '../src' -import { createCourierClient } from '../src/courier' - -describe('notification server', () => { - let network: TestNetwork - let agent: AtpAgent - let pdsAgent: AtpAgent - let sc: SeedClient - let notifServer: GorushNotificationServer - - // account dids, for convenience - let alice: string - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_notification_server', - }) - agent = network.bsky.getClient() - pdsAgent = network.pds.getClient() - sc = network.getSeedClient() - await basicSeed(sc) - await network.processAll() - await network.bsky.processAll() - alice = sc.dids.alice - notifServer = new GorushNotificationServer( - network.bsky.ctx.db.getPrimary(), - 'http://mock', - ) - }) - - afterAll(async () => { - await network.close() - }) - - describe('registerPush', () => { - 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 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 views = await notifServer.getNotificationViews([notif]) - if (!views.length) - throw new Error('no notification display attributes found') - expect(views[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 views = await notifServer.getNotificationViews([ - notif, - flippedNotif, - ]) - expect(views.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 views = await notifServer.getNotificationViews([notif]) - expect(views.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 views = await notifServer.getNotificationViews([notif]) - expect(views.length).toBe(0) - await pdsAgent.api.app.bsky.graph.unmuteActorList( - { list: listRef.uri }, - { headers: sc.getHeaders(alice), encoding: 'application/json' }, - ) - }) - }) - - describe('GorushNotificationServer', () => { - it('gets user tokens from db', async () => { - const tokens = await notifServer.getTokensByDid([alice]) - expect(tokens[alice][0].token).toEqual('123') - }) - - 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, - notif /* second one will get dropped by rate limit */, - ] - const prepared = await notifServer.prepareNotifications(notifAsArray) - expect(prepared).toEqual([ - { - collapse_id: 'like', - collapse_key: 'like', - 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', - }, - ]) - }) - }) - - describe('CourierNotificationServer', () => { - 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 courierNotifServer = new CourierNotificationServer( - db, - createCourierClient({ baseUrl: 'http://mock', httpVersion: '2' }), - ) - const prepared = await courierNotifServer.prepareNotifications([notif]) - expect(prepared[0]?.id).toBeTruthy() - expect(prepared.map((p) => p.toJson())).toEqual([ - { - id: prepared[0].id, // already ensured it exists - recipientDid: notif.did, - title: 'bobby liked your post', - message: 'again', - collapseKey: 'like', - timestamp: notif.sortAt, - // this is missing, appears to be a quirk of toJson() - // alwaysDeliver: false, - additional: { - reason: notif.reason, - uri: notif.recordUri, - subject: notif.reasonSubject, - }, - }, - ]) - }) - }) - - 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/bsky/tests/pipeline/backpressure.test.ts b/packages/bsky/tests/pipeline/backpressure.test.ts deleted file mode 100644 index 87e01b8cc89..00000000000 --- a/packages/bsky/tests/pipeline/backpressure.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { wait } from '@atproto/common' -import { - BskyIndexers, - TestNetworkNoAppView, - getIndexers, - getIngester, - processAll, - SeedClient, - basicSeed, -} from '@atproto/dev-env' -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]], - }) - sc = network.getSeedClient() - 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 deleted file mode 100644 index 8d90f9fea8f..00000000000 --- a/packages/bsky/tests/pipeline/reingest.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - TestNetworkNoAppView, - SeedClient, - getIngester, - ingestAll, - basicSeed, -} from '@atproto/dev-env' -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, - }) - sc = network.getSeedClient() - 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 deleted file mode 100644 index 2c7470fc06d..00000000000 --- a/packages/bsky/tests/pipeline/repartition.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - BskyIndexers, - TestNetworkNoAppView, - SeedClient, - getIndexers, - getIngester, - ingestAll, - processAll, - usersSeed, -} from '@atproto/dev-env' -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 - }) - sc = network.getSeedClient() - 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/reprocessing.test.ts b/packages/bsky/tests/reprocessing.test.ts deleted file mode 100644 index fd9199379c7..00000000000 --- a/packages/bsky/tests/reprocessing.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import axios from 'axios' -import { AtUri } from '@atproto/syntax' -import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' -import { Database } from '../src/db' - -describe('reprocessing', () => { - let network: TestNetwork - let sc: SeedClient - let alice: string - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_reprocessing', - }) - sc = network.getSeedClient() - 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/bsky/tests/server.test.ts b/packages/bsky/tests/server.test.ts index 157b352136f..3cc0257a4eb 100644 --- a/packages/bsky/tests/server.test.ts +++ b/packages/bsky/tests/server.test.ts @@ -3,11 +3,10 @@ import express from 'express' import axios, { AxiosError } from 'axios' import { TestNetwork, basicSeed } from '@atproto/dev-env' import { handler as errorHandler } from '../src/error' -import { Database } from '../src' +import { once } from 'events' describe('server', () => { let network: TestNetwork - let db: Database let alice: string beforeAll(async () => { @@ -18,7 +17,6 @@ describe('server', () => { await basicSeed(sc) await network.processAll() alice = sc.dids.alice - db = network.bsky.ctx.db.getPrimary() }) afterAll(async () => { @@ -56,7 +54,7 @@ describe('server', () => { it('healthcheck succeeds when database is available.', async () => { const { data, status } = await axios.get(`${network.bsky.url}/xrpc/_health`) expect(status).toEqual(200) - expect(data).toEqual({ version: '0.0.0' }) + expect(data).toEqual({ version: 'unknown' }) }) // TODO(bsky) check on a different endpoint that accepts json, currently none. @@ -107,10 +105,9 @@ describe('server', () => { expect(res.headers['content-encoding']).toBeUndefined() }) - it('healthcheck fails when database is unavailable.', async () => { - await network.bsky.ingester.sub.destroy() - await network.bsky.indexer.sub.destroy() - await db.close() + it('healthcheck fails when dataplane is unavailable.', async () => { + const { port } = network.bsky.dataplane.server.address() as AddressInfo + await network.bsky.dataplane.destroy() let error: AxiosError try { await axios.get(`${network.bsky.url}/xrpc/_health`) @@ -121,10 +118,14 @@ describe('server', () => { } else { throw err } + } finally { + // restart dataplane server to allow test suite to cleanup + network.bsky.dataplane.server.listen(port) + await once(network.bsky.dataplane.server, 'listening') } expect(error.response?.status).toEqual(503) expect(error.response?.data).toEqual({ - version: '0.0.0', + version: 'unknown', error: 'Service Unavailable', }) }) diff --git a/packages/bsky/tests/subscription/mutes.test.ts b/packages/bsky/tests/subscription/mutes.test.ts deleted file mode 100644 index 9b3f194050b..00000000000 --- a/packages/bsky/tests/subscription/mutes.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import AtpAgent from '@atproto/api' -import { wait } from '@atproto/common' -import { TestNetwork, SeedClient, basicSeed, TestBsync } from '@atproto/dev-env' -import assert from 'assert' - -describe('sync mutes', () => { - let network: TestNetwork - let bsync: TestBsync - let pdsAgent: AtpAgent - let sc: SeedClient - - beforeAll(async () => { - assert(process.env.DB_POSTGRES_URL) - bsync = await TestBsync.create({ - dbSchema: 'bsync_subscription_mutes', - dbUrl: process.env.DB_POSTGRES_URL, - }) - network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_subscription_mutes', - bsky: { - bsyncUrl: bsync.url, - bsyncApiKey: [...bsync.ctx.cfg.auth.apiKeys][0], - bsyncHttpVersion: '1.1', - bsyncOnlyMutes: true, - ingester: { - bsyncUrl: bsync.url, - bsyncApiKey: [...bsync.ctx.cfg.auth.apiKeys][0], - bsyncHttpVersion: '1.1', - }, - }, - }) - pdsAgent = network.pds.getClient() - sc = network.getSeedClient() - await basicSeed(sc) - }) - - afterAll(async () => { - await network.close() - await bsync.close() - }) - - it('mutes and unmutes actors.', async () => { - await pdsAgent.api.app.bsky.graph.muteActor( - { actor: sc.dids.alice }, - { headers: sc.getHeaders(sc.dids.bob), encoding: 'application/json' }, - ) - await pdsAgent.api.app.bsky.graph.muteActor( - { actor: sc.dids.carol }, - { headers: sc.getHeaders(sc.dids.bob), encoding: 'application/json' }, - ) - await pdsAgent.api.app.bsky.graph.muteActor( - { actor: sc.dids.dan }, - { headers: sc.getHeaders(sc.dids.bob), encoding: 'application/json' }, - ) - await processAllMuteOps(network, bsync) - const { data: mutes1 } = await pdsAgent.api.app.bsky.graph.getMutes( - {}, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(mutes1.mutes.map((mute) => mute.did)).toEqual([ - sc.dids.dan, - sc.dids.carol, - sc.dids.alice, - ]) - await pdsAgent.api.app.bsky.graph.unmuteActor( - { actor: sc.dids.carol }, - { headers: sc.getHeaders(sc.dids.bob), encoding: 'application/json' }, - ) - await processAllMuteOps(network, bsync) - const { data: mutes2 } = await pdsAgent.api.app.bsky.graph.getMutes( - {}, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(mutes2.mutes.map((mute) => mute.did)).toEqual([ - sc.dids.dan, - sc.dids.alice, - ]) - }) - - it('mutes and unmutes lists.', async () => { - // create lists - const list1 = await pdsAgent.api.app.bsky.graph.list.create( - { repo: sc.dids.bob }, - { - name: 'mod list 1', - purpose: 'app.bsky.graph.defs#modlist', - createdAt: new Date().toISOString(), - }, - sc.getHeaders(sc.dids.bob), - ) - const list2 = await pdsAgent.api.app.bsky.graph.list.create( - { repo: sc.dids.bob }, - { - name: 'mod list 2', - purpose: 'app.bsky.graph.defs#modlist', - createdAt: new Date().toISOString(), - }, - sc.getHeaders(sc.dids.bob), - ) - const list3 = await pdsAgent.api.app.bsky.graph.list.create( - { repo: sc.dids.bob }, - { - name: 'mod list 3', - purpose: 'app.bsky.graph.defs#modlist', - createdAt: new Date().toISOString(), - }, - sc.getHeaders(sc.dids.bob), - ) - await network.processAll() - await pdsAgent.api.app.bsky.graph.muteActorList( - { list: list1.uri }, - { headers: sc.getHeaders(sc.dids.bob), encoding: 'application/json' }, - ) - await pdsAgent.api.app.bsky.graph.muteActorList( - { list: list2.uri }, - { headers: sc.getHeaders(sc.dids.bob), encoding: 'application/json' }, - ) - await pdsAgent.api.app.bsky.graph.muteActorList( - { list: list3.uri }, - { headers: sc.getHeaders(sc.dids.bob), encoding: 'application/json' }, - ) - await processAllMuteOps(network, bsync) - const { data: listmutes1 } = await pdsAgent.api.app.bsky.graph.getListMutes( - {}, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(listmutes1.lists.map((list) => list.uri)).toEqual([ - list3.uri, - list2.uri, - list1.uri, - ]) - await pdsAgent.api.app.bsky.graph.unmuteActorList( - { list: list2.uri }, - { headers: sc.getHeaders(sc.dids.bob), encoding: 'application/json' }, - ) - await processAllMuteOps(network, bsync) - const { data: listmutes2 } = await pdsAgent.api.app.bsky.graph.getListMutes( - {}, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(listmutes2.lists.map((list) => list.uri)).toEqual([ - list3.uri, - list1.uri, - ]) - }) -}) - -async function processAllMuteOps(network: TestNetwork, bsync: TestBsync) { - const getBsyncCursor = async () => { - const result = await bsync.ctx.db.db - .selectFrom('mute_op') - .orderBy('id', 'desc') - .select('id') - .limit(1) - .executeTakeFirst() - return result?.id.toString() ?? null - } - assert(network.bsky.ingester.ctx.muteSubscription) - let total = 0 - while ( - (await getBsyncCursor()) !== - network.bsky.ingester.ctx.muteSubscription.cursor - ) { - if (total > 5000) { - throw new Error('timeout while processing mute ops') - } - await wait(50) - total += 50 - } -} 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 37478713bd9..6112f126d97 100644 --- a/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap @@ -89,7 +89,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(3)", "val": "test-label", }, @@ -97,7 +97,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(3)", "val": "test-label-2", }, @@ -221,7 +221,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(4)", "handle": "dan.test", "labels": Array [], "viewer": Object { @@ -237,7 +237,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(6)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -332,7 +332,7 @@ Array [ "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(6)", "val": "test-label", }, @@ -498,7 +498,7 @@ Array [ "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", + "src": "did:example:labeler", "uri": "record(0)", "val": "test-label", }, @@ -506,7 +506,7 @@ Array [ "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", + "src": "did:example:labeler", "uri": "record(0)", "val": "test-label-2", }, @@ -552,8 +552,8 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", + "did": "user(2)", "displayName": "ali", "handle": "alice.test", "labels": Array [ @@ -561,7 +561,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "user(2)", "uri": "record(4)", "val": "self-label-a", }, @@ -569,7 +569,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "user(2)", "uri": "record(4)", "val": "self-label-b", }, @@ -600,8 +600,8 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", + "did": "user(2)", "displayName": "ali", "handle": "alice.test", "labels": Array [ @@ -609,7 +609,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "user(2)", "uri": "record(4)", "val": "self-label-a", }, @@ -617,7 +617,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "user(2)", "uri": "record(4)", "val": "self-label-b", }, @@ -746,13 +746,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", + "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", }, ], }, @@ -760,8 +760,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1037,13 +1037,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", + "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", }, ], }, @@ -1051,8 +1051,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1236,7 +1236,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(4)", "val": "test-label", }, @@ -1244,7 +1244,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(4)", "val": "test-label-2", }, @@ -1415,7 +1415,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(6)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1432,13 +1432,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", + "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": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(8)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(8)@jpeg", + "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", }, ], }, @@ -1674,7 +1674,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label", }, @@ -1682,7 +1682,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label-2", }, @@ -1812,7 +1812,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(4)", "handle": "dan.test", "labels": Array [], "viewer": Object { @@ -1827,7 +1827,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(6)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1920,7 +1920,7 @@ Array [ "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(8)", "val": "test-label", }, diff --git a/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap index a3cfb905dc9..c5ff586f5c2 100644 --- a/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap @@ -103,7 +103,7 @@ Object { "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(0)", "val": "test-label", }, @@ -288,7 +288,7 @@ Object { exports[`pds views with blocking from block lists returns a users own list blocks 1`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "lists": Array [ Object { "cid": "cids(0)", @@ -383,7 +383,7 @@ Object { exports[`pds views with blocking from block lists returns lists associated with a user 1`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "lists": Array [ Object { "cid": "cids(0)", @@ -477,7 +477,7 @@ Object { exports[`pds views with blocking from block lists returns the contents of a list 1`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "items": Array [ Object { "subject": Object { diff --git a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap index d5ecf9b2c7e..751dc15b3ac 100644 --- a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap @@ -103,7 +103,7 @@ Object { "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(0)", "val": "test-label", }, @@ -301,7 +301,7 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(7)", "val": "test-label", }, @@ -309,7 +309,7 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(7)", "val": "test-label-2", }, diff --git a/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap b/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap index dfb5ff2ecb3..fcd5003475b 100644 --- a/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap @@ -2,7 +2,7 @@ exports[`pds follow views fetches followers 1`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "followers": Array [ Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", @@ -83,7 +83,7 @@ Object { exports[`pds follow views fetches followers 2`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "followers": Array [ Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", @@ -134,7 +134,7 @@ Object { exports[`pds follow views fetches followers 3`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "followers": Array [ Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", @@ -200,7 +200,7 @@ Object { exports[`pds follow views fetches followers 4`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "followers": Array [ Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", @@ -236,7 +236,7 @@ Object { exports[`pds follow views fetches followers 5`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "followers": Array [ Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", @@ -287,7 +287,7 @@ Object { exports[`pds follow views fetches follows 1`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "follows": Array [ Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", @@ -368,7 +368,7 @@ Object { exports[`pds follow views fetches follows 2`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "follows": Array [ Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", @@ -419,7 +419,7 @@ Object { exports[`pds follow views fetches follows 3`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "follows": Array [ Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", @@ -455,7 +455,7 @@ Object { exports[`pds follow views fetches follows 4`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "follows": Array [ Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", @@ -521,7 +521,7 @@ Object { exports[`pds follow views fetches follows 5`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "follows": Array [ Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", @@ -589,11 +589,6 @@ Object { "did": "user(2)", "following": "record(2)", }, - Object { - "$type": "app.bsky.graph.defs#notFoundActor", - "actor": "did:example:fake", - "notFound": true, - }, ], } `; diff --git a/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap b/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap index 426467a3fa7..c9ad1536f85 100644 --- a/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap @@ -2,7 +2,7 @@ exports[`pds like views fetches post likes 1`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "likes": Array [ Object { "actor": Object { @@ -72,7 +72,7 @@ Object { exports[`pds like views fetches reply likes 1`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "likes": Array [ Object { "actor": Object { diff --git a/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap b/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap index 790cc5db4e6..f7887147f39 100644 --- a/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap @@ -90,7 +90,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label", }, @@ -98,7 +98,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label-2", }, @@ -221,7 +221,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label", }, @@ -229,7 +229,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label-2", }, @@ -408,7 +408,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(4)", "handle": "dan.test", "labels": Array [], "viewer": Object { @@ -423,7 +423,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(6)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -516,7 +516,7 @@ Array [ "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(8)", "val": "test-label", }, diff --git a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap index 8b46475eafe..4cd94ec2efc 100644 --- a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap @@ -38,9 +38,11 @@ Object { "cid": "cids(4)", "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)", @@ -240,7 +242,7 @@ Object { "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(9)", "val": "test-label", }, @@ -248,7 +250,7 @@ Object { "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(9)", "val": "test-label-2", }, @@ -297,7 +299,7 @@ Object { exports[`bsky views with mutes from mute lists returns a users own list mutes 1`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "lists": Array [ Object { "cid": "cids(0)", @@ -390,7 +392,7 @@ Object { exports[`bsky views with mutes from mute lists returns lists associated with a user 1`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "lists": Array [ Object { "cid": "cids(0)", @@ -483,7 +485,7 @@ Object { exports[`bsky views with mutes from mute lists returns the contents of a list 1`] = ` Object { - "cursor": "0000000000000::bafycid", + "cursor": "0000000000000__bafycid", "items": Array [ Object { "subject": Object { diff --git a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap index 655d7b62cb6..90919028294 100644 --- a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap @@ -217,7 +217,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label", }, @@ -225,7 +225,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label-2", }, diff --git a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap index e2ac2d587c0..9a3d1d088b8 100644 --- a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap @@ -250,7 +250,7 @@ Array [ "cid": "cids(12)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(14)", "val": "test-label", }, @@ -258,7 +258,7 @@ Array [ "cid": "cids(12)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(14)", "val": "test-label-2", }, @@ -380,7 +380,7 @@ Array [ }, "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "repost", "reasonSubject": "record(2)", @@ -407,7 +407,7 @@ Array [ }, "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "repost", "reasonSubject": "record(4)", @@ -434,7 +434,7 @@ Array [ }, "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "mention", "record": Object { @@ -478,7 +478,7 @@ Array [ }, "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "like", "reasonSubject": "record(4)", @@ -506,7 +506,7 @@ Array [ }, "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "follow", "record": Object { @@ -565,7 +565,7 @@ Array [ }, "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "reply", "reasonSubject": "record(4)", @@ -600,7 +600,7 @@ Array [ }, "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "like", "reasonSubject": "record(13)", @@ -628,7 +628,7 @@ Array [ }, "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "like", "reasonSubject": "record(4)", @@ -660,7 +660,7 @@ Array [ }, "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "follow", "record": Object { @@ -688,13 +688,13 @@ Array [ }, "cid": "cids(15)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [ Object { "cid": "cids(15)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(17)", "val": "test-label", }, @@ -702,7 +702,7 @@ Array [ "cid": "cids(15)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(17)", "val": "test-label-2", }, @@ -760,7 +760,7 @@ Array [ }, "cid": "cids(17)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "like", "reasonSubject": "record(13)", @@ -792,7 +792,7 @@ Array [ }, "cid": "cids(18)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "like", "reasonSubject": "record(4)", @@ -871,7 +871,7 @@ Array [ }, "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "follow", "record": Object { @@ -915,13 +915,13 @@ Array [ }, "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [ Object { "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(4)", "val": "test-label", }, diff --git a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap index dd71dc9010d..41a448f63eb 100644 --- a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap @@ -54,7 +54,6 @@ Object { "like": "record(7)", }, }, - "replies": Array [], }, "post": Object { "author": Object { @@ -85,7 +84,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label", }, @@ -93,7 +92,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label-2", }, @@ -135,7 +134,6 @@ Object { "uri": "record(5)", "viewer": Object {}, }, - "replies": Array [], }, "post": Object { "author": Object { @@ -319,7 +317,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(7)", "val": "test-label", }, @@ -327,7 +325,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(7)", "val": "test-label-2", }, @@ -557,7 +555,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(7)", "val": "test-label", }, @@ -565,7 +563,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(7)", "val": "test-label-2", }, @@ -1073,6 +1071,11 @@ Object { }, }, "replies": Array [ + Object { + "$type": "app.bsky.feed.defs#notFoundPost", + "notFound": true, + "uri": "record(5)", + }, Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { @@ -1104,16 +1107,16 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", - "uri": "record(5)", + "src": "did:example:labeler", + "uri": "record(6)", "val": "test-label", }, Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", - "uri": "record(5)", + "src": "did:example:labeler", + "uri": "record(6)", "val": "test-label-2", }, ], @@ -1151,7 +1154,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object {}, }, "replies": Array [ @@ -1198,7 +1201,7 @@ Object { "reply": Object { "parent": Object { "cid": "cids(3)", - "uri": "record(5)", + "uri": "record(6)", }, "root": Object { "cid": "cids(0)", @@ -1209,9 +1212,9 @@ Object { }, "replyCount": 0, "repostCount": 2, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object { - "repost": "record(7)", + "repost": "record(8)", }, }, "replies": Array [], @@ -1304,7 +1307,7 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label", }, @@ -1312,7 +1315,7 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label-2", }, diff --git a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap index 0817313a331..32c41656733 100644 --- a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap @@ -278,7 +278,7 @@ Array [ "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(5)", "val": "test-label", }, @@ -853,7 +853,7 @@ Array [ "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(8)", "val": "test-label", }, @@ -880,8 +880,8 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(1)@jpeg", - "did": "user(5)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1000,13 +1000,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(11)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(11)@jpeg", + "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": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(12)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(12)@jpeg", + "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", }, ], }, @@ -1014,8 +1014,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(1)@jpeg", - "did": "user(5)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1034,9 +1034,9 @@ Array [ "cid": "cids(13)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(15)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(15)", @@ -1058,9 +1058,9 @@ Array [ "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(14)", - "val": "kind", + "val": "test-label-3", }, ], "likeCount": 2, @@ -1116,8 +1116,8 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(1)@jpeg", - "did": "user(5)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1135,9 +1135,9 @@ Array [ "cid": "cids(13)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", + "src": "did:example:labeler", "uri": "record(15)", - "val": "kind", + "val": "test-label-3", }, ], "likeCount": 0, @@ -1324,7 +1324,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(3)", "val": "test-label", }, @@ -1332,7 +1332,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(3)", "val": "test-label-2", }, @@ -1497,7 +1497,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(6)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1516,13 +1516,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", + "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": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(8)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(8)@jpeg", + "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", }, ], }, @@ -1549,9 +1549,9 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(11)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(11)", @@ -1574,9 +1574,9 @@ Array [ "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(8)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(8)", @@ -1660,7 +1660,7 @@ Array [ "reason": Object { "$type": "app.bsky.feed.defs#reasonRepost", "by": Object { - "did": "user(6)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1760,7 +1760,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(3)", "val": "test-label", }, @@ -1768,7 +1768,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(3)", "val": "test-label-2", }, @@ -1859,7 +1859,7 @@ Array [ Object { "post": Object { "author": Object { - "did": "user(6)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2016,7 +2016,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(3)", "val": "test-label", }, @@ -2024,7 +2024,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(3)", "val": "test-label-2", }, @@ -2209,7 +2209,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(6)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2226,9 +2226,9 @@ Array [ "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(8)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(8)", @@ -2313,7 +2313,7 @@ Array [ "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(13)", "val": "test-label", }, @@ -2430,7 +2430,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(6)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2449,13 +2449,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", + "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": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(8)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(8)@jpeg", + "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", }, ], }, @@ -2482,9 +2482,9 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(11)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(11)", @@ -2507,9 +2507,9 @@ Array [ "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(8)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(8)", @@ -2621,7 +2621,7 @@ Array [ Object { "post": Object { "author": Object { - "did": "user(6)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2639,13 +2639,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", + "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": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(8)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(8)@jpeg", + "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", }, ], }, @@ -2673,9 +2673,9 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(11)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(11)", @@ -2697,9 +2697,9 @@ Array [ "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(8)", - "val": "kind", + "val": "test-label-3", }, ], "likeCount": 2, @@ -2774,9 +2774,9 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(11)", - "val": "kind", + "val": "test-label-3", }, ], "likeCount": 0, @@ -2898,13 +2898,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(3)@jpeg", + "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", }, ], }, @@ -2929,9 +2929,9 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(4)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(4)", @@ -2954,9 +2954,9 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(2)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(2)", @@ -3055,7 +3055,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3139,7 +3139,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(10)", "val": "test-label", }, @@ -3147,7 +3147,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(10)", "val": "test-label-2", }, @@ -3192,7 +3192,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3279,7 +3279,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3327,7 +3327,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3404,7 +3404,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(10)", "val": "test-label", }, @@ -3412,7 +3412,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(10)", "val": "test-label-2", }, @@ -3458,7 +3458,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3506,7 +3506,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3556,7 +3556,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3623,9 +3623,9 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(2)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(2)", @@ -3710,7 +3710,7 @@ Array [ "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(13)", "val": "test-label", }, @@ -3767,7 +3767,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3833,13 +3833,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(3)@jpeg", + "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", }, ], }, @@ -3865,9 +3865,9 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(4)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(4)", @@ -3889,9 +3889,9 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(2)", - "val": "kind", + "val": "test-label-3", }, ], "likeCount": 2, @@ -3964,9 +3964,9 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(4)", - "val": "kind", + "val": "test-label-3", }, ], "likeCount": 0, @@ -3988,7 +3988,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4088,13 +4088,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(3)@jpeg", + "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", }, ], }, @@ -4120,9 +4120,9 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(2)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(2)", @@ -4145,9 +4145,9 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(1)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(1)", @@ -4247,7 +4247,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4332,7 +4332,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(10)", "val": "test-label", }, @@ -4340,7 +4340,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(10)", "val": "test-label-2", }, @@ -4385,7 +4385,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4471,7 +4471,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4519,7 +4519,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4569,7 +4569,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4634,9 +4634,9 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(1)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(1)", @@ -4721,7 +4721,7 @@ Array [ "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(13)", "val": "test-label", }, @@ -4750,7 +4750,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4815,13 +4815,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(3)@jpeg", + "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", }, ], }, @@ -4848,9 +4848,9 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(2)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(2)", @@ -4872,9 +4872,9 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "did:example:labeler", "uri": "record(1)", - "val": "kind", + "val": "test-label-3", }, ], "likeCount": 2, @@ -4928,7 +4928,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -5096,7 +5096,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(4)", "val": "test-label", }, @@ -5104,7 +5104,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(4)", "val": "test-label-2", }, @@ -5289,7 +5289,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(4)", "val": "test-label", }, @@ -5297,7 +5297,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(4)", "val": "test-label-2", }, @@ -5484,7 +5484,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(6)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -5501,13 +5501,13 @@ Array [ "images": Array [ Object { "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", + "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": "../dev-env/src/seed/img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(9)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(9)@jpeg", + "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", }, ], }, @@ -5533,9 +5533,9 @@ Array [ "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(12)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(12)", @@ -5558,9 +5558,9 @@ Array [ "cid": "cids(8)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(11)", - "val": "kind", + "val": "test-label-3", }, ], "uri": "record(11)", @@ -5689,9 +5689,9 @@ Array [ "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(5)", + "src": "did:example:labeler", "uri": "record(12)", - "val": "kind", + "val": "test-label-3", }, ], "likeCount": 0, diff --git a/packages/bsky/tests/views/actor-search.test.ts b/packages/bsky/tests/views/actor-search.test.ts index c0e862de249..e7c4390edc2 100644 --- a/packages/bsky/tests/views/actor-search.test.ts +++ b/packages/bsky/tests/views/actor-search.test.ts @@ -19,11 +19,11 @@ describe.skip('pds actor search views', () => { sc = network.getSeedClient() await wait(50) // allow pending sub to be established - await network.bsky.ingester.sub.destroy() + await network.bsky.sub.destroy() await usersBulkSeed(sc) // Skip did/handle resolution for expediency - const db = network.bsky.ctx.db.getPrimary() + const { db } = network.bsky const now = new Date().toISOString() await db.db .insertInto('actor') @@ -38,9 +38,8 @@ describe.skip('pds actor search views', () => { .execute() // Process remaining profiles - network.bsky.ingester.sub.resume() + network.bsky.sub.run() await network.processAll(50000) - await network.bsky.processAll() headers = await network.serviceHeaders(Object.values(sc.dids)[0]) }) @@ -238,22 +237,9 @@ describe.skip('pds actor search views', () => { }) it('search blocks by actor takedown', async () => { - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids['cara-wiegand69.test'], - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.server.ctx.dataplane.takedownActor({ + did: sc.dids['cara-wiegand69.test'], + }) const result = await agent.api.app.bsky.actor.searchActorsTypeahead( { term: 'car' }, { headers }, diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 4db9ee49028..160608744a6 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -138,7 +138,7 @@ describe('pds author feed views', () => { ) }) - it('blocked by actor takedown.', async () => { + it('non-admins blocked by actor takedown.', async () => { const { data: preBlock } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: alice }, { headers: await network.serviceHeaders(carol) }, @@ -146,45 +146,26 @@ describe('pds author feed views', () => { expect(preBlock.feed.length).toBeGreaterThan(0) - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownActor({ + did: alice, + }) - const attempt = agent.api.app.bsky.feed.getAuthorFeed( + const attemptAsUser = agent.api.app.bsky.feed.getAuthorFeed( { actor: alice }, { headers: await network.serviceHeaders(carol) }, ) - await expect(attempt).rejects.toThrow('Profile not found') + await expect(attemptAsUser).rejects.toThrow('Profile not found') - // Cleanup - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, + const attemptAsAdmin = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: network.bsky.adminAuthHeaders() }, ) + expect(attemptAsAdmin.data.feed.length).toEqual(preBlock.feed.length) + + // Cleanup + await network.bsky.ctx.dataplane.untakedownActor({ + did: alice, + }) }) it('blocked by record takedown.', async () => { @@ -197,49 +178,35 @@ describe('pds author feed views', () => { const post = preBlock.feed[0].post - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uri, - cid: post.cid, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownRecord({ + recordUri: post.uri, + }) - const { data: postBlock } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: await network.serviceHeaders(carol) }, + const [{ data: postBlockAsUser }, { data: postBlockAsAdmin }] = + await Promise.all([ + agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: await network.serviceHeaders(carol) }, + ), + agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: network.bsky.adminAuthHeaders() }, + ), + ]) + + expect(postBlockAsUser.feed.length).toEqual(preBlock.feed.length - 1) + expect(postBlockAsUser.feed.map((item) => item.post.uri)).not.toContain( + post.uri, + ) + expect(postBlockAsAdmin.feed.length).toEqual(preBlock.feed.length) + expect(postBlockAsAdmin.feed.map((item) => item.post.uri)).toContain( + post.uri, ) - - expect(postBlock.feed.length).toEqual(preBlock.feed.length - 1) - expect(postBlock.feed.map((item) => item.post.uri)).not.toContain(post.uri) // Cleanup - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uri, - cid: post.cid, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.untakedownRecord({ + recordUri: post.uri, + }) }) it('can filter by posts_with_media', async () => { diff --git a/packages/bsky/tests/views/blocks.test.ts b/packages/bsky/tests/views/blocks.test.ts index 2f45477c664..ceff6f57392 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -111,7 +111,7 @@ describe('pds views with blocking', () => { expect(forSnapshot(thread)).toMatchSnapshot() }) - it('loads blocked reply as anchor with no parent', async () => { + it('loads blocked reply as anchor with blocked parent', async () => { const { data: thread } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: carolReplyToDan.ref.uriStr }, { headers: await network.serviceHeaders(alice) }, @@ -120,7 +120,10 @@ describe('pds views with blocking', () => { throw new Error('Expected thread view post') } expect(thread.thread.post.uri).toEqual(carolReplyToDan.ref.uriStr) - expect(thread.thread.parent).toBeUndefined() + expect(thread.thread.parent).toMatchObject({ + $type: 'app.bsky.feed.defs#blockedPost', + uri: sc.posts[dan][0].ref.uriStr, + }) }) it('blocks thread parent', async () => { @@ -409,10 +412,9 @@ describe('pds views with blocking', () => { { headers: await network.serviceHeaders(alice) }, ) assert(isThreadViewPost(unblock.thread)) - expect(unblock.thread.replies?.map(getThreadPostUri)).toEqual([ - carolReplyToDan.ref.uriStr, - aliceReplyToDan.ref.uriStr, - ]) + expect(unblock.thread.replies?.map(getThreadPostUri).sort()).toEqual( + [aliceReplyToDan.ref.uriStr, carolReplyToDan.ref.uriStr].sort(), + ) // block then reply danBlockCarol = await pdsAgent.api.app.bsky.graph.block.create( diff --git a/packages/bsky/tests/views/follows.test.ts b/packages/bsky/tests/views/follows.test.ts index d0f640c6813..2331a0e990c 100644 --- a/packages/bsky/tests/views/follows.test.ts +++ b/packages/bsky/tests/views/follows.test.ts @@ -18,7 +18,6 @@ describe('pds follow views', () => { sc = network.getSeedClient() await followsSeed(sc) await network.processAll() - await network.bsky.processAll() alice = sc.dids.alice }) @@ -119,22 +118,9 @@ describe('pds follow views', () => { }) it('blocks followers by actor takedown', async () => { - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.dan, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownActor({ + did: sc.dids.dan, + }) const aliceFollowers = await agent.api.app.bsky.graph.getFollowers( { actor: sc.dids.alice }, @@ -145,21 +131,9 @@ describe('pds follow views', () => { sc.dids.dan, ) - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.dan, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.untakedownActor({ + did: sc.dids.dan, + }) }) it('fetches follows', async () => { @@ -252,22 +226,9 @@ describe('pds follow views', () => { }) it('blocks follows by actor takedown', async () => { - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.dan, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownActor({ + did: sc.dids.dan, + }) const aliceFollows = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.alice }, @@ -278,30 +239,18 @@ describe('pds follow views', () => { sc.dids.dan, ) - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.dan, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.untakedownActor({ + did: sc.dids.dan, + }) }) it('fetches relationships between users', async () => { const res = await agent.api.app.bsky.graph.getRelationships({ actor: sc.dids.bob, - others: [sc.dids.alice, sc.dids.bob, sc.dids.carol, 'did:example:fake'], + others: [sc.dids.alice, sc.dids.bob, sc.dids.carol], }) expect(res.data.actor).toEqual(sc.dids.bob) - expect(res.data.relationships.length).toBe(4) + expect(res.data.relationships.length).toBe(3) expect(forSnapshot(res.data)).toMatchSnapshot() }) }) diff --git a/packages/bsky/tests/views/list-feed.test.ts b/packages/bsky/tests/views/list-feed.test.ts index 4951a6d6a23..f7b68b8db7a 100644 --- a/packages/bsky/tests/views/list-feed.test.ts +++ b/packages/bsky/tests/views/list-feed.test.ts @@ -111,22 +111,9 @@ describe('list feed views', () => { }) it('blocks posts by actor takedown', async () => { - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: bob, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownActor({ + did: bob, + }) const res = await agent.api.app.bsky.feed.getListFeed({ list: listRef.uriStr, @@ -135,42 +122,16 @@ describe('list feed views', () => { expect(hasBob).toBe(false) // Cleanup - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: bob, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.untakedownActor({ + did: bob, + }) }) it('blocks posts by record takedown.', async () => { const postRef = sc.replies[bob][0].ref // Post and reply parent - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownRecord({ + recordUri: postRef.uriStr, + }) const res = await agent.api.app.bsky.feed.getListFeed({ list: listRef.uriStr, @@ -181,21 +142,8 @@ describe('list feed views', () => { expect(hasPost).toBe(false) // Cleanup - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.untakedownRecord({ + recordUri: postRef.uriStr, + }) }) }) diff --git a/packages/bsky/tests/views/mute-lists.test.ts b/packages/bsky/tests/views/mute-lists.test.ts index c366b9bf390..3f90586f681 100644 --- a/packages/bsky/tests/views/mute-lists.test.ts +++ b/packages/bsky/tests/views/mute-lists.test.ts @@ -92,7 +92,6 @@ describe('bsky views with mutes from mute lists', () => { }) it('uses a list for mutes', async () => { - // @TODO proxy through appview await agent.api.app.bsky.graph.muteActorList( { list: listUri, @@ -196,7 +195,6 @@ 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.processAll() const res = await agent.api.app.bsky.actor.getSuggestions( { diff --git a/packages/bsky/tests/views/mutes.test.ts b/packages/bsky/tests/views/mutes.test.ts index 8e26770ef23..b9d276b975e 100644 --- a/packages/bsky/tests/views/mutes.test.ts +++ b/packages/bsky/tests/views/mutes.test.ts @@ -228,6 +228,6 @@ describe('mute views', () => { encoding: 'application/json', }, ) - await expect(promise).rejects.toThrow('Cannot mute oneself') + await expect(promise).rejects.toThrow() // @TODO check error message w/ grpc error passthru }) }) diff --git a/packages/bsky/tests/views/notifications.test.ts b/packages/bsky/tests/views/notifications.test.ts index 376ead163fc..d9b88d0bffe 100644 --- a/packages/bsky/tests/views/notifications.test.ts +++ b/packages/bsky/tests/views/notifications.test.ts @@ -19,7 +19,6 @@ describe('notification views', () => { sc = network.getSeedClient() await basicSeed(sc) await network.processAll() - await network.bsky.processAll() alice = sc.dids.alice }) @@ -72,7 +71,6 @@ describe('notification views', () => { 'indeed', ) await network.processAll() - await network.bsky.processAll() const notifCountAlice = await agent.api.app.bsky.notification.getUnreadCount( @@ -96,7 +94,6 @@ 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.processAll() const notifsAlice = await agent.api.app.bsky.notification.listNotifications( {}, @@ -132,7 +129,7 @@ describe('notification views', () => { expect(notifs.length).toBe(13) const readStates = notifs.map((notif) => notif.isRead) - expect(readStates).toEqual(notifs.map(() => false)) + expect(readStates).toEqual(notifs.map((_, i) => i !== 0)) // only first appears unread expect(forSnapshot(sort(notifs))).toMatchSnapshot() }) @@ -224,7 +221,7 @@ describe('notification views', () => { expect(notifs.length).toBe(13) const readStates = notifs.map((notif) => notif.isRead) - expect(readStates).toEqual(notifs.map((n) => n.indexedAt <= seenAt)) + expect(readStates).toEqual(notifs.map((n) => n.indexedAt < seenAt)) // reset last-seen await agent.api.app.bsky.notification.updateSeen( { seenAt: new Date(0).toISOString() }, @@ -240,23 +237,9 @@ describe('notification views', () => { const postRef2 = sc.posts[sc.dids.dan][1].ref // Mention await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ), + network.bsky.ctx.dataplane.takedownRecord({ + recordUri: postRef.uriStr, + }), ), ) @@ -277,22 +260,9 @@ describe('notification views', () => { // Cleanup await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ), + network.bsky.ctx.dataplane.untakedownRecord({ + recordUri: postRef.uriStr, + }), ), ) }) @@ -300,7 +270,7 @@ describe('notification views', () => { it('fails open on clearly bad cursor.', async () => { const { data: notifs } = await agent.api.app.bsky.notification.listNotifications( - { cursor: 'bad' }, + { cursor: '90210::bafycid' }, { headers: await network.serviceHeaders(alice) }, ) expect(notifs).toEqual({ notifications: [] }) diff --git a/packages/bsky/tests/views/profile.test.ts b/packages/bsky/tests/views/profile.test.ts index ddd484b3b31..8c83c3e49aa 100644 --- a/packages/bsky/tests/views/profile.test.ts +++ b/packages/bsky/tests/views/profile.test.ts @@ -183,93 +183,20 @@ describe('pds profile views', () => { }) it('blocked by actor takedown', async () => { - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) - const promise = agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: await network.serviceHeaders(bob) }, - ) - - await expect(promise).rejects.toThrow('Account has been taken down') - - // Cleanup - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) - }) - - it('blocked by actor suspension', async () => { - await pdsAgent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventTakedown', - durationInHours: 1, - }, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) - await network.processAll() + await network.bsky.ctx.dataplane.takedownActor({ + did: alice, + }) const promise = agent.api.app.bsky.actor.getProfile( { actor: alice }, { headers: await network.serviceHeaders(bob) }, ) - await expect(promise).rejects.toThrow( - 'Account has been temporarily suspended', - ) + await expect(promise).rejects.toThrow('Account has been suspended') // Cleanup - await pdsAgent.api.com.atproto.admin.emitModerationEvent( - { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ) - await network.processAll() + await network.bsky.ctx.dataplane.untakedownActor({ + did: alice, + }) }) async function updateProfile(did: string, record: Record) { diff --git a/packages/bsky/tests/views/suggested-follows.test.ts b/packages/bsky/tests/views/suggested-follows.test.ts index ff9bec4539c..5d39f3f90ef 100644 --- a/packages/bsky/tests/views/suggested-follows.test.ts +++ b/packages/bsky/tests/views/suggested-follows.test.ts @@ -16,7 +16,6 @@ describe('suggested follows', () => { sc = network.getSeedClient() await likesSeed(sc) await network.processAll() - await network.bsky.processAll() const suggestions = [ { did: sc.dids.alice, order: 1 }, @@ -26,9 +25,8 @@ describe('suggested follows', () => { { did: sc.dids.fred, order: 5 }, { did: sc.dids.gina, order: 6 }, ] - await network.bsky.ctx.db - .getPrimary() - .db.insertInto('suggested_follow') + await network.bsky.db.db + .insertInto('suggested_follow') .values(suggestions) .execute() }) diff --git a/packages/bsky/tests/views/suggestions.test.ts b/packages/bsky/tests/views/suggestions.test.ts index bcae515ffb3..49bc5858a34 100644 --- a/packages/bsky/tests/views/suggestions.test.ts +++ b/packages/bsky/tests/views/suggestions.test.ts @@ -15,7 +15,6 @@ describe('pds user search views', () => { sc = network.getSeedClient() await basicSeed(sc) await network.processAll() - await network.bsky.processAll() const suggestions = [ { did: sc.dids.alice, order: 1 }, @@ -24,9 +23,8 @@ describe('pds user search views', () => { { did: sc.dids.dan, order: 4 }, ] - await network.bsky.ctx.db - .getPrimary() - .db.insertInto('suggested_follow') + await network.bsky.db.db + .insertInto('suggested_follow') .values(suggestions) .execute() }) @@ -61,21 +59,21 @@ describe('pds user search views', () => { it('paginates', async () => { const result1 = await agent.api.app.bsky.actor.getSuggestions( - { limit: 1 }, + { limit: 2 }, { headers: await network.serviceHeaders(sc.dids.carol) }, ) expect(result1.data.actors.length).toBe(1) expect(result1.data.actors[0].handle).toEqual('bob.test') const result2 = await agent.api.app.bsky.actor.getSuggestions( - { limit: 1, cursor: result1.data.cursor }, + { limit: 2, cursor: result1.data.cursor }, { headers: await network.serviceHeaders(sc.dids.carol) }, ) expect(result2.data.actors.length).toBe(1) expect(result2.data.actors[0].handle).toEqual('dan.test') const result3 = await agent.api.app.bsky.actor.getSuggestions( - { limit: 1, cursor: result2.data.cursor }, + { limit: 2, cursor: result2.data.cursor }, { headers: await network.serviceHeaders(sc.dids.carol) }, ) expect(result3.data.actors.length).toBe(0) @@ -110,9 +108,8 @@ describe('pds user search views', () => { subjectType: 'feed', }, ] - await network.bsky.ctx.db - .getPrimary() - .db.insertInto('tagged_suggestion') + await network.bsky.db.db + .insertInto('tagged_suggestion') .values(suggestions) .execute() const res = await agent.api.app.bsky.unspecced.getTaggedSuggestions() diff --git a/packages/bsky/tests/views/thread.test.ts b/packages/bsky/tests/views/thread.test.ts index 88f7db4c573..c3496f7cf50 100644 --- a/packages/bsky/tests/views/thread.test.ts +++ b/packages/bsky/tests/views/thread.test.ts @@ -113,7 +113,6 @@ describe('pds thread views', () => { ) indexes.aliceReplyReply = sc.replies[alice].length - 1 await network.processAll() - await network.bsky.processAll() const thread1 = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr }, @@ -123,7 +122,6 @@ describe('pds thread views', () => { await sc.deletePost(bob, sc.replies[bob][indexes.bobReply].ref.uri) await network.processAll() - await network.bsky.processAll() const thread2 = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr }, @@ -138,6 +136,56 @@ describe('pds thread views', () => { expect(forSnapshot(thread3.data.thread)).toMatchSnapshot() }) + it('omits parents and replies w/ different root than anchor post.', async () => { + const badRoot = sc.posts[alice][0] + const goodRoot = await sc.post(alice, 'good root') + const goodReply1 = await sc.reply( + alice, + goodRoot.ref, + goodRoot.ref, + 'good reply 1', + ) + const goodReply2 = await sc.reply( + alice, + goodRoot.ref, + goodReply1.ref, + 'good reply 2', + ) + const badReply = await sc.reply( + alice, + badRoot.ref, + goodReply1.ref, + 'bad reply', + ) + await network.processAll() + // good reply doesn't have replies w/ different root + const { data: goodReply1Thread } = + await agent.api.app.bsky.feed.getPostThread( + { uri: goodReply1.ref.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + assert(isThreadViewPost(goodReply1Thread.thread)) + assert(isThreadViewPost(goodReply1Thread.thread.parent)) + expect(goodReply1Thread.thread.parent.post.uri).toEqual(goodRoot.ref.uriStr) + expect( + goodReply1Thread.thread.replies?.map((r) => { + assert(isThreadViewPost(r)) + return r.post.uri + }), + ).toEqual([ + goodReply2.ref.uriStr, // does not contain badReply + ]) + expect(goodReply1Thread.thread.parent.replies).toBeUndefined() + // bad reply doesn't have a parent, which would have a different root + const { data: badReplyThread } = + await agent.api.app.bsky.feed.getPostThread( + { uri: badReply.ref.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + assert(isThreadViewPost(badReplyThread.thread)) + expect(badReplyThread.thread.parent).toBeUndefined() // is not goodReply1 + }) + it('reflects self-labels', async () => { const { data: thread } = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][0].ref.uriStr }, @@ -163,22 +211,9 @@ describe('pds thread views', () => { describe('takedown', () => { it('blocks post by actor', async () => { - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownActor({ + did: alice, + }) // Same as shallow post thread test, minus alice const promise = agent.api.app.bsky.feed.getPostThread( @@ -191,40 +226,15 @@ describe('pds thread views', () => { ) // Cleanup - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.untakedownActor({ + did: alice, + }) }) it('blocks replies by actor', async () => { - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: carol, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownActor({ + did: carol, + }) // Same as deep post thread test, minus carol const thread = await agent.api.app.bsky.feed.getPostThread( @@ -235,40 +245,15 @@ describe('pds thread views', () => { expect(forSnapshot(thread.data.thread)).toMatchSnapshot() // Cleanup - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: carol, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.untakedownActor({ + did: carol, + }) }) it('blocks ancestors by actor', async () => { - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: bob, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownActor({ + did: bob, + }) // Same as ancestor post thread test, minus bob const thread = await agent.api.app.bsky.feed.getPostThread( @@ -279,42 +264,16 @@ describe('pds thread views', () => { expect(forSnapshot(thread.data.thread)).toMatchSnapshot() // Cleanup - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: bob, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.untakedownActor({ + did: bob, + }) }) it('blocks post by record', async () => { const postRef = sc.posts[alice][1].ref - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownRecord({ + recordUri: postRef.uriStr, + }) const promise = agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: postRef.uriStr }, @@ -326,22 +285,9 @@ describe('pds thread views', () => { ) // Cleanup - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.untakedownRecord({ + recordUri: postRef.uriStr, + }) }) it('blocks ancestors by record', async () => { @@ -352,23 +298,9 @@ describe('pds thread views', () => { const parent = threadPreTakedown.data.thread.parent?.['post'] - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: parent.uri, - cid: parent.cid, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.takedownRecord({ + recordUri: parent.uri, + }) // Same as ancestor post thread test, minus parent post const thread = await agent.api.app.bsky.feed.getPostThread( @@ -379,22 +311,9 @@ describe('pds thread views', () => { expect(forSnapshot(thread.data.thread)).toMatchSnapshot() // Cleanup - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: parent.uri, - cid: parent.cid, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ) + await network.bsky.ctx.dataplane.untakedownRecord({ + recordUri: parent.uri, + }) }) it('blocks replies by record', async () => { @@ -407,23 +326,9 @@ describe('pds thread views', () => { await Promise.all( [post1, post2].map((post) => - agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uri, - cid: post.cid, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ), + network.bsky.ctx.dataplane.takedownRecord({ + recordUri: post.uri, + }), ), ) @@ -438,25 +343,9 @@ describe('pds thread views', () => { // Cleanup await Promise.all( [post1, post2].map((post) => - agent.api.com.atproto.admin.updateSubjectStatus( - { - event: { - $type: 'com.atproto.admin.defs#modEventReverseTakedown', - }, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uri, - cid: post.cid, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ), + network.bsky.ctx.dataplane.untakedownRecord({ + recordUri: post.uri, + }), ), ) }) diff --git a/packages/bsky/tests/views/threadgating.test.ts b/packages/bsky/tests/views/threadgating.test.ts index feb10dfbadd..cd154cedd83 100644 --- a/packages/bsky/tests/views/threadgating.test.ts +++ b/packages/bsky/tests/views/threadgating.test.ts @@ -308,8 +308,9 @@ describe('views with thread gating', () => { assert(isThreadViewPost(reply1)) assert(isThreadViewPost(reply2)) expect(otherReplies.length).toEqual(0) - expect(reply1.post.uri).toEqual(danReply.ref.uriStr) - expect(reply2.post.uri).toEqual(aliceReply.ref.uriStr) + expect([reply1.post.uri, reply2.post.uri].sort()).toEqual( + [danReply.ref.uriStr, aliceReply.ref.uriStr].sort(), + ) }) it('applies gate for unknown list rule.', async () => { @@ -418,8 +419,9 @@ describe('views with thread gating', () => { assert(isThreadViewPost(reply1)) assert(isThreadViewPost(reply2)) expect(otherReplies.length).toEqual(0) - expect(reply1.post.uri).toEqual(danReply.ref.uriStr) - expect(reply2.post.uri).toEqual(aliceReply.ref.uriStr) + expect([reply1.post.uri, reply2.post.uri].sort()).toEqual( + [aliceReply.ref.uriStr, danReply.ref.uriStr].sort(), + ) }) it('applies gate for missing rules, takes no action.', async () => { diff --git a/packages/bsky/tests/views/timeline.test.ts b/packages/bsky/tests/views/timeline.test.ts index dd9b89535c8..f697f02e033 100644 --- a/packages/bsky/tests/views/timeline.test.ts +++ b/packages/bsky/tests/views/timeline.test.ts @@ -2,8 +2,10 @@ import assert from 'assert' import AtpAgent from '@atproto/api' import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { forSnapshot, getOriginator, paginateAll } from '../_util' -import { FeedAlgorithm } from '../../src/api/app/bsky/util/feed' import { FeedViewPost } from '../../src/lexicon/types/app/bsky/feed/defs' +import { Database } from '../../src' + +const REVERSE_CHRON = 'reverse-chronological' describe('timeline views', () => { let network: TestNetwork @@ -28,26 +30,18 @@ describe('timeline views', () => { bob = sc.dids.bob carol = sc.dids.carol dan = sc.dids.dan - // Label posts as "kind" to check labels on embed views - const labelPostA = sc.posts[bob][0].ref - const labelPostB = sc.posts[carol][0].ref - await network.bsky.ctx.services - .label(network.bsky.ctx.db.getPrimary()) - .formatAndCreate( - network.ozone.ctx.cfg.service.did, - labelPostA.uriStr, - labelPostA.cidStr, - { create: ['kind'] }, - ) - await network.bsky.ctx.services - .label(network.bsky.ctx.db.getPrimary()) - .formatAndCreate( - network.ozone.ctx.cfg.service.did, - labelPostB.uriStr, - labelPostB.cidStr, - { create: ['kind'] }, - ) - await network.bsky.processAll() + // covers label hydration on embeds + const { db } = network.bsky + await createLabel(db, { + val: 'test-label-3', + uri: sc.posts[bob][0].ref.uriStr, + cid: sc.posts[bob][0].ref.cidStr, + }) + await createLabel(db, { + val: 'test-label-3', + uri: sc.posts[carol][0].ref.uriStr, + cid: sc.posts[carol][0].ref.cidStr, + }) }) afterAll(async () => { @@ -67,7 +61,7 @@ describe('timeline views', () => { } const aliceTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, + { algorithm: REVERSE_CHRON }, { headers: await network.serviceHeaders(alice), }, @@ -77,7 +71,7 @@ describe('timeline views', () => { aliceTL.data.feed.forEach(expectOriginatorFollowedBy(alice)) const bobTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, + { algorithm: REVERSE_CHRON }, { headers: await network.serviceHeaders(bob), }, @@ -87,7 +81,7 @@ describe('timeline views', () => { bobTL.data.feed.forEach(expectOriginatorFollowedBy(bob)) const carolTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, + { algorithm: REVERSE_CHRON }, { headers: await network.serviceHeaders(carol), }, @@ -97,7 +91,7 @@ describe('timeline views', () => { carolTL.data.feed.forEach(expectOriginatorFollowedBy(carol)) const danTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, + { algorithm: REVERSE_CHRON }, { headers: await network.serviceHeaders(dan), }, @@ -115,7 +109,7 @@ describe('timeline views', () => { }, ) const reverseChronologicalTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, + { algorithm: REVERSE_CHRON }, { headers: await network.serviceHeaders(alice), }, @@ -128,7 +122,7 @@ describe('timeline views', () => { const paginator = async (cursor?: string) => { const res = await agent.api.app.bsky.feed.getTimeline( { - algorithm: FeedAlgorithm.ReverseChronological, + algorithm: REVERSE_CHRON, cursor, limit: 4, }, @@ -144,7 +138,7 @@ describe('timeline views', () => { const full = await agent.api.app.bsky.feed.getTimeline( { - algorithm: FeedAlgorithm.ReverseChronological, + algorithm: REVERSE_CHRON, }, { headers: await network.serviceHeaders(carol) }, ) @@ -196,27 +190,12 @@ describe('timeline views', () => { it('blocks posts, reposts, replies by actor takedown', async () => { await Promise.all( [bob, carol].map((did) => - agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ), + network.bsky.ctx.dataplane.takedownActor({ did }), ), ) const aliceTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, + { algorithm: REVERSE_CHRON }, { headers: await network.serviceHeaders(alice) }, ) @@ -225,21 +204,7 @@ describe('timeline views', () => { // Cleanup await Promise.all( [bob, carol].map((did) => - agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ), + network.bsky.ctx.dataplane.untakedownActor({ did }), ), ) }) @@ -249,28 +214,14 @@ describe('timeline views', () => { const postRef2 = sc.replies[bob][0].ref // Post and reply parent await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - takedown: { - applied: true, - ref: 'test', - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ), + network.bsky.ctx.dataplane.takedownRecord({ + recordUri: postRef.uriStr, + }), ), ) const aliceTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, + { algorithm: REVERSE_CHRON }, { headers: await network.serviceHeaders(alice) }, ) @@ -279,31 +230,35 @@ describe('timeline views', () => { // Cleanup await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - takedown: { - applied: false, - }, - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), - }, - ), + network.bsky.ctx.dataplane.untakedownRecord({ + recordUri: postRef.uriStr, + }), ), ) }) it('fails open on clearly bad cursor.', async () => { const { data: timeline } = await agent.api.app.bsky.feed.getTimeline( - { cursor: 'bad' }, + { cursor: '90210::bafycid' }, { headers: await network.serviceHeaders(alice) }, ) expect(timeline).toEqual({ feed: [] }) }) }) + +const createLabel = async ( + db: Database, + opts: { uri: string; cid: string; val: string }, +) => { + await db.db + .insertInto('label') + .values({ + uri: opts.uri, + cid: opts.cid, + val: opts.val, + cts: new Date().toISOString(), + neg: false, + src: 'did:example:labeler', + }) + .execute() +} diff --git a/packages/bsky/tsconfig.json b/packages/bsky/tsconfig.json index 3f6ca1c27ec..45283e8f73b 100644 --- a/packages/bsky/tsconfig.json +++ b/packages/bsky/tsconfig.json @@ -11,11 +11,10 @@ { "path": "../api/tsconfig.build.json" }, { "path": "../common/tsconfig.build.json" }, { "path": "../crypto/tsconfig.build.json" }, - { "path": "../identifier/tsconfig.build.json" }, { "path": "../lexicon/tsconfig.build.json" }, { "path": "../lex-cli/tsconfig.build.json" }, { "path": "../repo/tsconfig.build.json" }, - { "path": "../uri/tsconfig.build.json" }, + { "path": "../syntax/tsconfig.build.json" }, { "path": "../xrpc-server/tsconfig.build.json" } ] } diff --git a/packages/common-web/src/arrays.ts b/packages/common-web/src/arrays.ts index 51598fc86f1..36c2f0dcb27 100644 --- a/packages/common-web/src/arrays.ts +++ b/packages/common-web/src/arrays.ts @@ -1,3 +1,10 @@ +export const keyBy = (arr: T[], key: string): Record => { + return arr.reduce((acc, cur) => { + acc[cur[key]] = cur + return acc + }, {} as Record) +} + export const mapDefined = ( arr: T[], fn: (obj: T) => S | undefined, diff --git a/packages/common-web/src/did-doc.ts b/packages/common-web/src/did-doc.ts index c2a05b796d3..541e10d0937 100644 --- a/packages/common-web/src/did-doc.ts +++ b/packages/common-web/src/did-doc.ts @@ -44,6 +44,11 @@ export const getSigningKey = ( publicKeyMultibase: found.publicKeyMultibase, } } +export const getSigningDidKey = (doc: DidDocument): string | undefined => { + const parsed = getSigningKey(doc) + if (!parsed) return + return `did:key:${parsed.publicKeyMultibase}` +} export const getPdsEndpoint = (doc: DidDocument): string | undefined => { return getServiceEndpoint(doc, { diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md index 8d2dd8efae3..dd5af8ef139 100644 --- a/packages/dev-env/CHANGELOG.md +++ b/packages/dev-env/CHANGELOG.md @@ -1,5 +1,55 @@ # @atproto/dev-env +## 0.2.33 + +### Patch Changes + +- Updated dependencies [[`514aab92d`](https://github.com/bluesky-social/atproto/commit/514aab92d26acd43859285f46318e386846522b1)]: + - @atproto/api@0.10.1 + - @atproto/bsky@0.0.33 + - @atproto/ozone@0.0.12 + - @atproto/pds@0.4.1 + +## 0.2.32 + +### Patch Changes + +- Updated dependencies [[`b60719480`](https://github.com/bluesky-social/atproto/commit/b60719480f5f00bffd074a40e8ddc03aa93d137d), [`4c511b3d9`](https://github.com/bluesky-social/atproto/commit/4c511b3d9de41ffeae3fc11db941e7df04f4468a)]: + - @atproto/api@0.10.0 + - @atproto/bsky@0.0.32 + - @atproto/ozone@0.0.11 + - @atproto/pds@0.4.0 + +## 0.2.31 + +### Patch Changes + +- Updated dependencies [[`f79cc6339`](https://github.com/bluesky-social/atproto/commit/f79cc63390ae9dbd47a4ff5d694eec25b78b788e)]: + - @atproto/api@0.9.8 + - @atproto/ozone@0.0.10 + - @atproto/bsky@0.0.31 + - @atproto/pds@0.3.19 + +## 0.2.30 + +### Patch Changes + +- Updated dependencies [[`8c94979f7`](https://github.com/bluesky-social/atproto/commit/8c94979f73fc5057449e24e66ef2e09b0e17e55b)]: + - @atproto/api@0.9.7 + - @atproto/bsky@0.0.30 + - @atproto/pds@0.3.18 + - @atproto/ozone@0.0.9 + +## 0.2.29 + +### Patch Changes + +- Updated dependencies [[`e4ec7af03`](https://github.com/bluesky-social/atproto/commit/e4ec7af03608949fc3b00a845f547a77599b5ad0)]: + - @atproto/api@0.9.6 + - @atproto/bsky@0.0.29 + - @atproto/ozone@0.0.8 + - @atproto/pds@0.3.17 + ## 0.2.28 ### Patch Changes diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index c291aaf61fc..ee5165e9273 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.2.28", + "version": "0.2.33", "license": "MIT", "description": "Local development environment helper for atproto development", "keywords": [ @@ -39,7 +39,7 @@ "@did-plc/lib": "^0.0.1", "@did-plc/server": "^0.0.1", "axios": "^0.27.2", - "better-sqlite3": "^7.6.2", + "better-sqlite3": "^9.4.0", "chalk": "^5.0.1", "dotenv": "^16.0.3", "express": "^4.18.2", diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 4548cd45c41..461ef4d07df 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -1,23 +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, MINUTE, SECOND, wait } from '@atproto/common-web' import { AtpAgent } from '@atproto/api' -import { Secp256k1Keypair, randomIntFromSeed } from '@atproto/crypto' +import { Secp256k1Keypair } 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' import { ADMIN_PASSWORD, MOD_PASSWORD, TRIAGE_PASSWORD } from './const' +import { BackgroundQueue } from '@atproto/bsky/src/data-plane/server/background' export class TestBsky { constructor( public url: string, public port: number, + public db: bsky.Database, public server: bsky.BskyAppView, - public indexer: bsky.BskyIndexer, - public ingester: bsky.BskyIngester, + public dataplane: bsky.DataPlaneServer, + public bsync: bsky.MockBsync, + public sub: bsky.RepoSubscription, ) {} static async create(cfg: BskyConfig): Promise { @@ -34,39 +33,43 @@ export class TestBsky { signer: serviceKeypair, }) + // shared across server, ingester, and indexer in order to share pool, avoid too many pg connections. + const db = new bsky.Database({ + url: cfg.dbPostgresUrl, + schema: cfg.dbPostgresSchema, + poolSize: 10, + }) + + const dataplanePort = await getPort() + const dataplane = await bsky.DataPlaneServer.create( + db, + dataplanePort, + cfg.plcUrl, + ) + + const bsyncPort = await getPort() + const bsync = await bsky.MockBsync.create(db, bsyncPort) + const config = new bsky.ServerConfig({ - version: '0.0.0', + version: 'unknown', port, didPlcUrl: cfg.plcUrl, publicUrl: 'https://bsky.public.url', serverDid, - didCacheStaleTTL: HOUR, - didCacheMaxTTL: DAY, - labelCacheStaleTTL: 30 * SECOND, - labelCacheMaxTTL: MINUTE, + dataplaneUrls: [`http://localhost:${dataplanePort}`], + dataplaneHttpVersion: '1.1', + bsyncUrl: `http://localhost:${bsyncPort}`, + bsyncHttpVersion: '1.1', + courierUrl: 'https://fake.example', modServiceDid: cfg.modServiceDid ?? 'did:example:invalidMod', + labelsFromIssuerDids: ['did:example:labeler'], // this did is also used as the labeler in seeds ...cfg, - // Each test suite gets its own lock id for the repo subscription - adminPassword: ADMIN_PASSWORD, - moderatorPassword: MOD_PASSWORD, - triagePassword: TRIAGE_PASSWORD, - feedGenDid: 'did:example:feedGen', - rateLimitsEnabled: false, - }) - - // shared across server, ingester, and indexer in order to share pool, avoid too many pg connections. - const db = new bsky.DatabaseCoordinator({ - schema: cfg.dbPostgresSchema, - primary: { - url: cfg.dbPrimaryPostgresUrl, - poolSize: 10, - }, - replicas: [], + adminPasswords: [ADMIN_PASSWORD, MOD_PASSWORD, TRIAGE_PASSWORD], }) // Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..." - const migrationDb = new bsky.PrimaryDatabase({ - url: cfg.dbPrimaryPostgresUrl, + const migrationDb = new bsky.Database({ + url: cfg.dbPostgresUrl, schema: cfg.dbPostgresSchema, }) if (cfg.migration) { @@ -76,285 +79,52 @@ export class TestBsky { } await migrationDb.close() - const ns = cfg.dbPostgresSchema - ? await randomIntFromSeed(cfg.dbPostgresSchema, 1000000) - : undefined - assert(config.redisHost) - const redisCache = new bsky.Redis({ - host: config.redisHost, - namespace: `ns${ns}`, - db: 1, - }) - // api server const server = bsky.BskyAppView.create({ - db, - redis: redisCache, config, - imgInvalidator: cfg.imgInvalidator, signingKey: serviceKeypair, }) - // indexer - const indexerCfg = new bsky.IndexerConfig({ - version: '0.0.0', - serverDid, - didCacheStaleTTL: HOUR, - didCacheMaxTTL: DAY, - redisHost: cfg.redisHost, - dbPostgresUrl: cfg.dbPrimaryPostgresUrl, - dbPostgresSchema: cfg.dbPostgresSchema, - didPlcUrl: cfg.plcUrl, - labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, - imgUriEndpoint: 'img.example.com', - moderationPushUrl: - cfg.indexer?.moderationPushUrl ?? 'https://modservice.invalid', - indexerPartitionIds: [0], - indexerNamespace: `ns${ns}`, - indexerSubLockId: uniqueLockId(), - indexerPort: await getPort(), - ingesterPartitionCount: 1, - pushNotificationEndpoint: 'https://push.bsky.app/api/push', - ...(cfg.indexer ?? {}), - }) - assert(indexerCfg.redisHost) - const indexerRedis = new bsky.Redis({ - host: indexerCfg.redisHost, - namespace: `ns${ns}`, - }) - const indexer = bsky.BskyIndexer.create({ - cfg: indexerCfg, - db: db.getPrimary(), - redis: indexerRedis, - redisCache, - }) - // ingester - const ingesterCfg = new bsky.IngesterConfig({ - version: '0.0.0', - redisHost: cfg.redisHost, - dbPostgresUrl: cfg.dbPrimaryPostgresUrl, - dbPostgresSchema: cfg.dbPostgresSchema, - repoProvider: cfg.repoProvider, - labelProvider: cfg.labelProvider, - ingesterNamespace: `ns${ns}`, - ingesterSubLockId: uniqueLockId(), - ingesterPartitionCount: 1, - ...(cfg.ingester ?? {}), - }) - assert(ingesterCfg.redisHost) - const ingesterRedis = new bsky.Redis({ - host: ingesterCfg.redisHost, - namespace: ingesterCfg.ingesterNamespace, - }) - const ingester = bsky.BskyIngester.create({ - cfg: ingesterCfg, - db: db.getPrimary(), - redis: ingesterRedis, + const sub = new bsky.RepoSubscription({ + service: cfg.repoProvider, + db, + idResolver: dataplane.idResolver, + background: new BackgroundQueue(db), }) - await ingester.start() - await indexer.start() - await server.start() - // manually process labels in dev-env (in network.processAll) - ingester.ctx.labelSubscription?.destroy() + await server.start() + sub.run() - return new TestBsky(url, port, server, indexer, ingester) + return new TestBsky(url, port, db, server, dataplane, bsync, sub) } get ctx(): bsky.AppContext { return this.server.ctx } - get sub() { - return this.indexer.sub - } - getClient() { 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 + adminAuth(): string { + const [password] = this.ctx.cfg.adminPasswords return ( 'Basic ' + ui8.toString(ui8.fromString(`admin:${password}`, 'utf8'), 'base64pad') ) } - adminAuthHeaders(role?: 'admin' | 'moderator' | 'triage') { + adminAuthHeaders() { return { - authorization: this.adminAuth(role), + authorization: this.adminAuth(), } } - async processAll() { - await Promise.all([ - this.ctx.backgroundQueue.processAll(), - this.indexer.ctx.backgroundQueue.processAll(), - ]) - } - async close() { - await this.server.destroy({ skipDb: true, skipRedis: true }) - await this.ingester.destroy({ skipDb: true }) - await this.indexer.destroy() // closes shared db & redis - } -} - -// 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, 1000000) : 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://'), - labelProvider: 'http://labeler.invalid', - ingesterSubLockId: uniqueLockId(), - ingesterPartitionCount: config.ingesterPartitionCount ?? 1, - ingesterNamespace: `ns${ns}`, - ...config, - }) - const db = new bsky.PrimaryDatabase({ - url: cfg.dbPostgresUrl, - schema: cfg.dbPostgresSchema, - }) - assert(cfg.redisHost) - const redis = new bsky.Redis({ - host: cfg.redisHost, - namespace: cfg.ingesterNamespace, - }) - await db.migrateToLatestOrThrow() - const ingester = await bsky.BskyIngester.create({ cfg, db, redis }) - await ingester.ctx.labelSubscription?.destroy() - return ingester -} - -// get multiple indexers for separate partitions, sharing db and redis instance. -export async function getIndexers( - network: TestNetworkNoAppView, - opts: Partial & { - name: string - partitionIdsByIndexer: number[][] - }, -): Promise { - const { name, ...config } = opts - const ns = name ? await randomIntFromSeed(name, 1000000) : undefined - const baseCfg: bsky.IndexerConfigValues = { - version: '0.0.0', - serverDid: 'did:example:bsky', - didCacheStaleTTL: HOUR, - didCacheMaxTTL: DAY, - 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, - imgUriEndpoint: '', - indexerPartitionIds: [0], - indexerNamespace: `ns${ns}`, - ingesterPartitionCount: config.ingesterPartitionCount ?? 1, - moderationPushUrl: config.moderationPushUrl ?? 'https://modservice.invalid', - ...config, - } - const db = new bsky.PrimaryDatabase({ - url: baseCfg.dbPostgresUrl, - schema: baseCfg.dbPostgresSchema, - }) - assert(baseCfg.redisHost) - const redis = new bsky.Redis({ - host: baseCfg.redisHost, - namespace: baseCfg.indexerNamespace, - }) - const redisCache = new bsky.Redis({ - host: baseCfg.redisHost, - namespace: baseCfg.indexerNamespace, - db: 1, - }) - - 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, redisCache }) - }), - ) - 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, -) { - 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, -) { - const sequencer = network.pds.ctx.sequencer - await network.pds.processAll() - // eslint-disable-next-line no-constant-condition - while (true) { - await wait(50) - // check ingester - const [ingesterCursor, curr] = await Promise.all([ - ingester.sub.getCursor(), - sequencer.curr(), - ]) - const ingesterCaughtUp = curr !== null && ingesterCursor === curr - if (ingesterCaughtUp) return + await this.server.destroy() + await this.bsync.destroy() + await this.dataplane.destroy() + await this.sub.destroy() + await this.db.close() } } diff --git a/packages/dev-env/src/mock/index.ts b/packages/dev-env/src/mock/index.ts index ab818dfc24c..f115a1112ef 100644 --- a/packages/dev-env/src/mock/index.ts +++ b/packages/dev-env/src/mock/index.ts @@ -1,5 +1,6 @@ import { AtUri } from '@atproto/syntax' import AtpAgent from '@atproto/api' +import { Database } from '@atproto/bsky' import { REASONSPAM, REASONOTHER, @@ -186,28 +187,16 @@ export async function generateMockSetup(env: TestNetwork) { }, ) - const ctx = env.bsky.ctx - if (ctx) { - const labelSrvc = ctx.services.label(ctx.db.getPrimary()) - await labelSrvc.createLabels([ - { - src: env.ozone.ctx.cfg.service.did, - uri: labeledPost.uri, - cid: labeledPost.cid, - val: 'nudity', - neg: false, - cts: new Date().toISOString(), - }, - { - src: env.ozone.ctx.cfg.service.did, - uri: filteredPost.uri, - cid: filteredPost.cid, - val: 'dmca-violation', - neg: false, - cts: new Date().toISOString(), - }, - ]) - } + await createLabel(env.bsky.db, { + uri: labeledPost.uri, + cid: labeledPost.cid, + val: 'nudity', + }) + await createLabel(env.bsky.db, { + uri: filteredPost.uri, + cid: filteredPost.cid, + val: 'dmca-violation', + }) // a set of replies for (let i = 0; i < 100; i++) { @@ -341,3 +330,20 @@ export async function generateMockSetup(env: TestNetwork) { function ucfirst(str: string): string { return str.at(0)?.toUpperCase() + str.slice(1) } + +const createLabel = async ( + db: Database, + opts: { uri: string; cid: string; val: string }, +) => { + await db.db + .insertInto('label') + .values({ + uri: opts.uri, + cid: opts.cid, + val: opts.val, + cts: new Date().toISOString(), + neg: false, + src: 'did:example:labeler', + }) + .execute() +} diff --git a/packages/dev-env/src/network-no-appview.ts b/packages/dev-env/src/network-no-appview.ts index 44701ece35e..3272245280c 100644 --- a/packages/dev-env/src/network-no-appview.ts +++ b/packages/dev-env/src/network-no-appview.ts @@ -32,7 +32,7 @@ export class TestNetworkNoAppView { return fg } - getSeedClient(): SeedClient { + getSeedClient(): SeedClient { const agent = this.pds.getClient() return new SeedClient(this, agent) } diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index 4e29c3c9384..c90e2c181f5 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -56,16 +56,11 @@ export class TestNetwork extends TestNetworkNoAppView { plcUrl: plc.url, pdsPort, repoProvider: `ws://localhost:${pdsPort}`, - labelProvider: `http://localhost:${ozonePort}`, dbPostgresSchema: `appview_${dbPostgresSchema}`, - dbPrimaryPostgresUrl: dbPostgresUrl, + dbPostgresUrl, redisHost, modServiceDid: ozoneDid, ...params.bsky, - indexer: { - ...params.bsky?.indexer, - moderationPushUrl: `http://admin:${ADMIN_PASSWORD}@localhost:${ozonePort}`, - }, }) const pds = await TestPds.create({ @@ -98,13 +93,12 @@ export class TestNetwork extends TestNetworkNoAppView { } async processFullSubscription(timeout = 5000) { - const sub = this.bsky.indexer.sub + const sub = this.bsky.sub const start = Date.now() const lastSeq = await this.pds.ctx.sequencer.curr() if (!lastSeq) return while (Date.now() - start < timeout) { - const partitionState = sub.partitions.get(0) - if (partitionState?.cursor >= lastSeq) { + if (sub.seenSeq !== null && sub.seenSeq >= lastSeq) { // has seen last seq, just need to wait for it to finish processing await sub.repoQueue.main.onIdle() return @@ -117,9 +111,7 @@ export class TestNetwork extends TestNetworkNoAppView { async processAll(timeout?: number) { await this.pds.processAll() await this.processFullSubscription(timeout) - await this.bsky.processAll() - await this.ozone.processAll() - await this.bsky.ingester.ctx.labelSubscription?.fetchLabels() + await this.bsky.sub.background.processAll() } async serviceHeaders(did: string, aud?: string) { diff --git a/packages/dev-env/src/seed/basic.ts b/packages/dev-env/src/seed/basic.ts index 47c299dce45..45583813afb 100644 --- a/packages/dev-env/src/seed/basic.ts +++ b/packages/dev-env/src/seed/basic.ts @@ -1,7 +1,13 @@ -import { SeedClient } from './client' import usersSeed from './users' +import { TestBsky } from '../bsky' +import { TestNetwork } from '../network' +import { TestNetworkNoAppView } from '../network-no-appview' +import { SeedClient } from './client' -export default async (sc: SeedClient, users = true) => { +export default async ( + sc: SeedClient, + users = true, +) => { if (users) await usersSeed(sc) const alice = sc.dids.alice @@ -129,6 +135,25 @@ export default async (sc: SeedClient, users = true) => { await sc.repost(dan, sc.posts[alice][1].ref) await sc.repost(dan, alicesReplyToBob.ref) + if (sc.network instanceof TestNetwork) { + const bsky = sc.network.bsky + await createLabel(bsky, { + val: 'test-label', + uri: sc.posts[alice][2].ref.uriStr, + cid: sc.posts[alice][2].ref.cidStr, + }) + await createLabel(bsky, { + val: 'test-label', + uri: sc.replies[bob][0].ref.uriStr, + cid: sc.replies[bob][0].ref.cidStr, + }) + await createLabel(bsky, { + val: 'test-label-2', + uri: sc.replies[bob][0].ref.uriStr, + cid: sc.replies[bob][0].ref.cidStr, + }) + } + return sc } @@ -144,3 +169,20 @@ export const replies = { bob: ['hear that label_me label_me_2'], carol: ['of course'], } + +const createLabel = async ( + bsky: TestBsky, + opts: { uri: string; cid: string; val: string }, +) => { + await bsky.db.db + .insertInto('label') + .values({ + uri: opts.uri, + cid: opts.cid, + val: opts.val, + cts: new Date().toISOString(), + neg: false, + src: 'did:example:labeler', // this did is also configured on labelsFromIssuerDids + }) + .execute() +} diff --git a/packages/dev-env/src/seed/client.ts b/packages/dev-env/src/seed/client.ts index 5b7a614228f..984115731dd 100644 --- a/packages/dev-env/src/seed/client.ts +++ b/packages/dev-env/src/seed/client.ts @@ -46,7 +46,9 @@ export class RecordRef { } } -export class SeedClient { +export class SeedClient< + Network extends TestNetworkNoAppView = TestNetworkNoAppView, +> { accounts: Record< string, { @@ -82,7 +84,7 @@ export class SeedClient { > dids: Record - constructor(public network: TestNetworkNoAppView, public agent: AtpAgent) { + constructor(public network: Network, public agent: AtpAgent) { this.accounts = {} this.profiles = {} this.follows = {} diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index 04a46732b5a..4ba979d59fa 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -2,7 +2,6 @@ import * as pds from '@atproto/pds' import * as bsky from '@atproto/bsky' import * as bsync from '@atproto/bsync' import * as ozone from '@atproto/ozone' -import { ImageInvalidator } from '@atproto/bsky' import { ExportableKeypair } from '@atproto/crypto' export type PlcConfig = { @@ -18,14 +17,11 @@ export type PdsConfig = Partial & { export type BskyConfig = Partial & { plcUrl: string repoProvider: string - labelProvider: string - dbPrimaryPostgresUrl: string + dbPostgresUrl: string + dbPostgresSchema: string redisHost: string pdsPort: number - imgInvalidator?: ImageInvalidator migration?: string - indexer?: Partial - ingester?: Partial } export type BsyncConfig = Partial & { diff --git a/packages/dev-env/src/util.ts b/packages/dev-env/src/util.ts index 7d6091023f6..679ca89c7a8 100644 --- a/packages/dev-env/src/util.ts +++ b/packages/dev-env/src/util.ts @@ -7,7 +7,7 @@ export const mockNetworkUtilities = (pds: TestPds, bsky?: TestBsky) => { mockResolvers(pds.ctx.idResolver, pds) if (bsky) { mockResolvers(bsky.ctx.idResolver, pds) - mockResolvers(bsky.indexer.ctx.idResolver, pds) + mockResolvers(bsky.dataplane.idResolver, pds) } } diff --git a/packages/identity/src/did/atproto-data.ts b/packages/identity/src/did/atproto-data.ts index c03f76ef598..c0cd9829739 100644 --- a/packages/identity/src/did/atproto-data.ts +++ b/packages/identity/src/did/atproto-data.ts @@ -20,7 +20,13 @@ export { export const getKey = (doc: DidDocument): string | undefined => { const key = getSigningKey(doc) if (!key) return undefined + return getDidKeyFromMultibase(key) +} +export const getDidKeyFromMultibase = (key: { + type: string + publicKeyMultibase: string +}): string | undefined => { const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase) let didKey: string | undefined = undefined if (key.type === 'EcdsaSecp256r1VerificationKey2019') { diff --git a/packages/lex-cli/src/codegen/server.ts b/packages/lex-cli/src/codegen/server.ts index 13e6ecd8e87..f220ed315c4 100644 --- a/packages/lex-cli/src/codegen/server.ts +++ b/packages/lex-cli/src/codegen/server.ts @@ -408,7 +408,7 @@ function genServerXrpcMethod( file.addImportDeclaration({ moduleSpecifier: '@atproto/xrpc-server', - namedImports: [{ name: 'HandlerAuth' }], + namedImports: [{ name: 'HandlerAuth' }, { name: 'HandlerPipeThrough' }], }) //= export interface HandlerInput {...} if (def.type === 'procedure' && def.input?.encoding) { @@ -452,6 +452,7 @@ function genServerXrpcMethod( name: 'HandlerSuccess', isExported: true, }) + if (def.output.encoding) { handlerSuccess.addProperty({ name: 'encoding', @@ -502,7 +503,9 @@ function genServerXrpcMethod( file.addTypeAlias({ isExported: true, name: 'HandlerOutput', - type: `HandlerError | ${hasHandlerSuccess ? 'HandlerSuccess' : 'void'}`, + type: `HandlerError | ${ + hasHandlerSuccess ? 'HandlerSuccess | HandlerPipeThrough' : 'void' + }`, }) file.addTypeAlias({ diff --git a/packages/ozone/CHANGELOG.md b/packages/ozone/CHANGELOG.md index 79ab4db32ab..ca4d4954d97 100644 --- a/packages/ozone/CHANGELOG.md +++ b/packages/ozone/CHANGELOG.md @@ -1,5 +1,42 @@ # @atproto/ozone +## 0.0.12 + +### Patch Changes + +- Updated dependencies [[`514aab92d`](https://github.com/bluesky-social/atproto/commit/514aab92d26acd43859285f46318e386846522b1)]: + - @atproto/api@0.10.1 + +## 0.0.11 + +### Patch Changes + +- Updated dependencies [[`b60719480`](https://github.com/bluesky-social/atproto/commit/b60719480f5f00bffd074a40e8ddc03aa93d137d), [`4c511b3d9`](https://github.com/bluesky-social/atproto/commit/4c511b3d9de41ffeae3fc11db941e7df04f4468a)]: + - @atproto/api@0.10.0 + +## 0.0.10 + +### Patch Changes + +- [#2192](https://github.com/bluesky-social/atproto/pull/2192) [`f79cc6339`](https://github.com/bluesky-social/atproto/commit/f79cc63390ae9dbd47a4ff5d694eec25b78b788e) Thanks [@foysalit](https://github.com/foysalit)! - Tag event on moderation subjects and allow filtering events and subjects by tags + +- Updated dependencies [[`f79cc6339`](https://github.com/bluesky-social/atproto/commit/f79cc63390ae9dbd47a4ff5d694eec25b78b788e)]: + - @atproto/api@0.9.8 + +## 0.0.9 + +### Patch Changes + +- Updated dependencies [[`8c94979f7`](https://github.com/bluesky-social/atproto/commit/8c94979f73fc5057449e24e66ef2e09b0e17e55b)]: + - @atproto/api@0.9.7 + +## 0.0.8 + +### Patch Changes + +- Updated dependencies [[`e4ec7af03`](https://github.com/bluesky-social/atproto/commit/e4ec7af03608949fc3b00a845f547a77599b5ad0)]: + - @atproto/api@0.9.6 + ## 0.0.7 ### Patch Changes diff --git a/packages/ozone/package.json b/packages/ozone/package.json index 41e3556e567..e76dc644f10 100644 --- a/packages/ozone/package.json +++ b/packages/ozone/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/ozone", - "version": "0.0.7", + "version": "0.0.12", "license": "MIT", "description": "Backend service for moderating the Bluesky network.", "keywords": [ @@ -34,9 +34,9 @@ "@atproto/api": "workspace:^", "@atproto/common": "workspace:^", "@atproto/crypto": "workspace:^", - "@atproto/syntax": "workspace:^", "@atproto/identity": "workspace:^", "@atproto/lexicon": "workspace:^", + "@atproto/syntax": "workspace:^", "@atproto/xrpc-server": "workspace:^", "@did-plc/lib": "^0.0.1", "compression": "^1.7.4", diff --git a/packages/ozone/src/api/admin/emitModerationEvent.ts b/packages/ozone/src/api/admin/emitModerationEvent.ts index eb1bc71a180..ef4c5fd2822 100644 --- a/packages/ozone/src/api/admin/emitModerationEvent.ts +++ b/packages/ozone/src/api/admin/emitModerationEvent.ts @@ -7,6 +7,7 @@ import { isModEventTakedown, } from '../../lexicon/types/com/atproto/admin/defs' import { subjectFromInput } from '../../mod-service/subject' +import { ModerationLangService } from '../../mod-service/lang' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.emitModerationEvent({ @@ -26,17 +27,23 @@ export default function (server: Server, ctx: AppContext) { // apply access rules - // if less than moderator access then can not takedown an account - if (!access.moderator && isTakedownEvent && subject.isRepo()) { - throw new AuthRequiredError( - 'Must be a full moderator to perform an account takedown', - ) - } // if less than moderator access then can only take ack and escalation actions - if (!access.moderator && (isTakedownEvent || isReverseTakedownEvent)) { - throw new AuthRequiredError( - 'Must be a full moderator to take this type of action', - ) + if (isTakedownEvent || isReverseTakedownEvent) { + if (!access.moderator) { + throw new AuthRequiredError( + 'Must be a full moderator to take this type of action', + ) + } + + // Non admins should not be able to take down feed generators + if ( + !access.admin && + subject.recordPath?.includes('app.bsky.feed.generator/') + ) { + throw new AuthRequiredError( + 'Must be a full admin to take this type of action on feed generators', + ) + } } // if less than moderator access then can not apply labels if (!access.moderator && isLabelEvent) { @@ -51,17 +58,21 @@ export default function (server: Server, ctx: AppContext) { } if (isTakedownEvent || isReverseTakedownEvent) { - const isSubjectTakendown = await moderationService.isSubjectTakendown( - subject, - ) + const status = await moderationService.getStatus(subject) - if (isSubjectTakendown && isTakedownEvent) { + if (status?.takendown && isTakedownEvent) { throw new InvalidRequestError(`Subject is already taken down`) } - if (!isSubjectTakendown && isReverseTakedownEvent) { + if (!status?.takendown && isReverseTakedownEvent) { throw new InvalidRequestError(`Subject is not taken down`) } + + if (status?.takendown && isReverseTakedownEvent && subject.isRecord()) { + // due to the way blob status is modeled, we should reverse takedown on all + // blobs for the record being restored, which aren't taken down on another record. + subject.blobCids = status.blobCids ?? [] + } } const moderationEvent = await db.transaction(async (dbTxn) => { @@ -73,10 +84,21 @@ export default function (server: Server, ctx: AppContext) { createdBy, }) + const moderationLangService = new ModerationLangService(moderationTxn) + await moderationLangService.tagSubjectWithLang({ + subject, + createdBy: ctx.cfg.service.did, + subjectStatus: result.subjectStatus, + }) + if (subject.isRepo()) { if (isTakedownEvent) { - const isSuspend = !!result.durationInHours - await moderationTxn.takedownRepo(subject, result.id, isSuspend) + const isSuspend = !!result.event.durationInHours + await moderationTxn.takedownRepo( + subject, + result.event.id, + isSuspend, + ) } else if (isReverseTakedownEvent) { await moderationTxn.reverseTakedownRepo(subject) } @@ -84,7 +106,7 @@ export default function (server: Server, ctx: AppContext) { if (subject.isRecord()) { if (isTakedownEvent) { - await moderationTxn.takedownRecord(subject, result.id) + await moderationTxn.takedownRecord(subject, result.event.id) } else if (isReverseTakedownEvent) { await moderationTxn.reverseTakedownRecord(subject) } @@ -92,20 +114,20 @@ export default function (server: Server, ctx: AppContext) { if (isLabelEvent) { await moderationTxn.formatAndCreateLabels( - result.subjectUri ?? result.subjectDid, - result.subjectCid, + result.event.subjectUri ?? result.event.subjectDid, + result.event.subjectCid, { - create: result.createLabelVals?.length - ? result.createLabelVals.split(' ') + create: result.event.createLabelVals?.length + ? result.event.createLabelVals.split(' ') : undefined, - negate: result.negateLabelVals?.length - ? result.negateLabelVals.split(' ') + negate: result.event.negateLabelVals?.length + ? result.event.negateLabelVals.split(' ') : undefined, }, ) } - return result + return result.event }) return { diff --git a/packages/ozone/src/api/admin/queryModerationEvents.ts b/packages/ozone/src/api/admin/queryModerationEvents.ts index 4c0cbdd1500..670cda96cbc 100644 --- a/packages/ozone/src/api/admin/queryModerationEvents.ts +++ b/packages/ozone/src/api/admin/queryModerationEvents.ts @@ -13,7 +13,16 @@ export default function (server: Server, ctx: AppContext) { sortDirection = 'desc', types, includeAllUserRecords = false, + hasComment, + comment, createdBy, + createdAfter, + createdBefore, + addedLabels = [], + removedLabels = [], + addedTags = [], + removedTags = [], + reportTypes, } = params const db = ctx.db const modService = ctx.modService(db) @@ -25,6 +34,15 @@ export default function (server: Server, ctx: AppContext) { cursor, sortDirection, includeAllUserRecords, + hasComment, + comment, + createdAfter, + createdBefore, + addedLabels, + addedTags, + removedLabels, + removedTags, + reportTypes, }) return { encoding: 'application/json', diff --git a/packages/ozone/src/api/admin/queryModerationStatuses.ts b/packages/ozone/src/api/admin/queryModerationStatuses.ts index fc935e5917a..fc491339ffa 100644 --- a/packages/ozone/src/api/admin/queryModerationStatuses.ts +++ b/packages/ozone/src/api/admin/queryModerationStatuses.ts @@ -22,6 +22,8 @@ export default function (server: Server, ctx: AppContext) { includeMuted = false, limit = 50, cursor, + tags = [], + excludeTags = [], } = params const db = ctx.db const modService = ctx.modService(db) @@ -41,6 +43,8 @@ export default function (server: Server, ctx: AppContext) { sortField, limit, cursor, + tags, + excludeTags, }) const subjectStatuses = results.statuses.map((status) => modService.views.formatSubjectStatus(status), diff --git a/packages/ozone/src/api/index.ts b/packages/ozone/src/api/index.ts index 49a9d70e1fa..54e52ffe292 100644 --- a/packages/ozone/src/api/index.ts +++ b/packages/ozone/src/api/index.ts @@ -8,6 +8,8 @@ import getRepo from './admin/getRepo' import queryModerationStatuses from './admin/queryModerationStatuses' import queryModerationEvents from './admin/queryModerationEvents' import getModerationEvent from './admin/getModerationEvent' +import queryLabels from './label/queryLabels' +import subscribeLabels from './label/subscribeLabels' import fetchLabels from './temp/fetchLabels' import createCommunicationTemplate from './admin/createCommunicationTemplate' import updateCommunicationTemplate from './admin/updateCommunicationTemplate' @@ -27,6 +29,8 @@ export default function (server: Server, ctx: AppContext) { getModerationEvent(server, ctx) queryModerationEvents(server, ctx) queryModerationStatuses(server, ctx) + queryLabels(server, ctx) + subscribeLabels(server, ctx) fetchLabels(server, ctx) listCommunicationTemplates(server, ctx) createCommunicationTemplate(server, ctx) diff --git a/packages/ozone/src/api/label/queryLabels.ts b/packages/ozone/src/api/label/queryLabels.ts new file mode 100644 index 00000000000..6de6380d194 --- /dev/null +++ b/packages/ozone/src/api/label/queryLabels.ts @@ -0,0 +1,58 @@ +import { Server } from '../../lexicon' +import AppContext from '../../context' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { sql } from 'kysely' +import { formatLabel } from '../../mod-service/util' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.label.queryLabels(async ({ params }) => { + const { uriPatterns, sources, limit, cursor } = params + let builder = ctx.db.db.selectFrom('label').selectAll().limit(limit) + // if includes '*', then we don't need a where clause + if (!uriPatterns.includes('*')) { + builder = builder.where((qb) => { + // starter where clause that is always false so that we can chain `orWhere`s + qb = qb.where(sql`1 = 0`) + for (const pattern of uriPatterns) { + // if no '*', then we're looking for an exact match + if (!pattern.includes('*')) { + qb = qb.orWhere('uri', '=', pattern) + } else { + if (pattern.indexOf('*') < pattern.length - 1) { + throw new InvalidRequestError(`invalid pattern: ${pattern}`) + } + const searchPattern = pattern + .slice(0, -1) + .replaceAll('%', '') // sanitize search pattern + .replaceAll('_', '\\_') // escape any underscores + qb = qb.orWhere('uri', 'like', `${searchPattern}%`) + } + } + return qb + }) + } + if (sources && sources.length > 0) { + builder = builder.where('src', 'in', sources) + } + if (cursor) { + const cursorId = parseInt(cursor, 10) + if (isNaN(cursorId)) { + throw new InvalidRequestError('invalid cursor') + } + builder = builder.where('id', '>', cursorId) + } + + const res = await builder.execute() + + const labels = res.map((l) => formatLabel(l)) + const resCursor = res.at(-1)?.id.toString(10) + + return { + encoding: 'application/json', + body: { + cursor: resCursor, + labels, + }, + } + }) +} diff --git a/packages/ozone/src/api/label/subscribeLabels.ts b/packages/ozone/src/api/label/subscribeLabels.ts new file mode 100644 index 00000000000..701405f9dad --- /dev/null +++ b/packages/ozone/src/api/label/subscribeLabels.ts @@ -0,0 +1,25 @@ +import { Server } from '../../lexicon' +import AppContext from '../../context' +import Outbox from '../../sequencer/outbox' +import { InvalidRequestError } from '@atproto/xrpc-server' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.label.subscribeLabels(async function* ({ + params, + signal, + }) { + const { cursor } = params + const outbox = new Outbox(ctx.sequencer) + + if (cursor !== undefined) { + const curr = await ctx.sequencer.curr() + if (cursor > (curr ?? 0)) { + throw new InvalidRequestError('Cursor in the future.', 'FutureCursor') + } + } + + for await (const evt of outbox.events(cursor, signal)) { + yield { $type: 'com.atproto.label.subscribeLabels#labels', ...evt } + } + }) +} diff --git a/packages/ozone/src/api/moderation/createReport.ts b/packages/ozone/src/api/moderation/createReport.ts index 6ede6dcd0e4..e87b957e8d0 100644 --- a/packages/ozone/src/api/moderation/createReport.ts +++ b/packages/ozone/src/api/moderation/createReport.ts @@ -4,6 +4,7 @@ import { getReasonType } from './util' import { subjectFromInput } from '../../mod-service/subject' import { REASONAPPEAL } from '../../lexicon/types/com/atproto/moderation/defs' import { ForbiddenError } from '@atproto/xrpc-server' +import { ModerationLangService } from '../../mod-service/lang' export default function (server: Server, ctx: AppContext) { server.com.atproto.moderation.createReport({ @@ -23,12 +24,22 @@ export default function (server: Server, ctx: AppContext) { const db = ctx.db const report = await db.transaction(async (dbTxn) => { const moderationTxn = ctx.modService(dbTxn) - return moderationTxn.report({ - reasonType: getReasonType(reasonType), - reason, + const { event: reportEvent, subjectStatus } = + await moderationTxn.report({ + reasonType: getReasonType(reasonType), + reason, + subject, + reportedBy: requester || ctx.cfg.service.did, + }) + + const moderationLangService = new ModerationLangService(moderationTxn) + await moderationLangService.tagSubjectWithLang({ subject, - reportedBy: requester || ctx.cfg.service.did, + subjectStatus, + createdBy: ctx.cfg.service.did, }) + + return reportEvent }) const body = ctx.modService(db).views.formatReport(report) diff --git a/packages/ozone/src/api/moderation/util.ts b/packages/ozone/src/api/moderation/util.ts index 040007d5e79..e64229891a5 100644 --- a/packages/ozone/src/api/moderation/util.ts +++ b/packages/ozone/src/api/moderation/util.ts @@ -62,4 +62,6 @@ const eventTypes = new Set([ 'com.atproto.admin.defs#modEventUnmute', 'com.atproto.admin.defs#modEventReverseTakedown', 'com.atproto.admin.defs#modEventEmail', + 'com.atproto.admin.defs#modEventResolveAppeal', + 'com.atproto.admin.defs#modEventTag', ]) diff --git a/packages/ozone/src/api/temp/fetchLabels.ts b/packages/ozone/src/api/temp/fetchLabels.ts index f11cb2028bb..fd0331487d1 100644 --- a/packages/ozone/src/api/temp/fetchLabels.ts +++ b/packages/ozone/src/api/temp/fetchLabels.ts @@ -1,5 +1,6 @@ import { Server } from '../../lexicon' import AppContext from '../../context' +import { formatLabel } from '../../mod-service/util' import { UNSPECCED_TAKEDOWN_BLOBS_LABEL, UNSPECCED_TAKEDOWN_LABEL, @@ -28,10 +29,7 @@ export default function (server: Server, ctx: AppContext) { .limit(limit) .execute() - const labels = labelRes.map((l) => ({ - ...l, - cid: l.cid === '' ? undefined : l.cid, - })) + const labels = labelRes.map((l) => formatLabel(l)) return { encoding: 'application/json', diff --git a/packages/ozone/src/config/config.ts b/packages/ozone/src/config/config.ts index 62b76ac5935..a219dba8d5a 100644 --- a/packages/ozone/src/config/config.ts +++ b/packages/ozone/src/config/config.ts @@ -19,6 +19,9 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => { const dbCfg: OzoneConfig['db'] = { postgresUrl: env.dbPostgresUrl, postgresSchema: env.dbPostgresSchema, + poolSize: env.dbPoolSize, + poolMaxUses: env.dbPoolMaxUses, + poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs, } assert(env.appviewUrl) @@ -73,6 +76,9 @@ export type ServiceConfig = { export type DatabaseConfig = { postgresUrl: string postgresSchema?: string + poolSize?: number + poolMaxUses?: number + poolIdleTimeoutMs?: number } export type AppviewConfig = { diff --git a/packages/ozone/src/config/env.ts b/packages/ozone/src/config/env.ts index b9574708ff9..1a93ad2b855 100644 --- a/packages/ozone/src/config/env.ts +++ b/packages/ozone/src/config/env.ts @@ -13,6 +13,9 @@ export const readEnv = (): OzoneEnvironment => { pdsDid: envStr('OZONE_PDS_DID'), dbPostgresUrl: envStr('OZONE_DB_POSTGRES_URL'), dbPostgresSchema: envStr('OZONE_DB_POSTGRES_SCHEMA'), + dbPoolSize: envInt('OZONE_DB_POOL_SIZE'), + dbPoolMaxUses: envInt('OZONE_DB_POOL_MAX_USES'), + dbPoolIdleTimeoutMs: envInt('OZONE_DB_POOL_IDLE_TIMEOUT_MS'), didPlcUrl: envStr('OZONE_DID_PLC_URL'), cdnPaths: envList('OZONE_CDN_PATHS'), adminPassword: envStr('OZONE_ADMIN_PASSWORD'), @@ -34,6 +37,9 @@ export type OzoneEnvironment = { pdsDid?: string dbPostgresUrl?: string dbPostgresSchema?: string + dbPoolSize?: number + dbPoolMaxUses?: number + dbPoolIdleTimeoutMs?: number didPlcUrl?: string cdnPaths?: string[] adminPassword?: string diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts index 5419bac436c..e65a6af2185 100644 --- a/packages/ozone/src/context.ts +++ b/packages/ozone/src/context.ts @@ -10,6 +10,7 @@ import * as auth from './auth' import { BackgroundQueue } from './background' import assert from 'assert' import { EventPusher } from './daemon' +import Sequencer from './sequencer/sequencer' import { CommunicationTemplateService, CommunicationTemplateServiceCreator, @@ -27,6 +28,7 @@ export type AppContextOptions = { idResolver: IdResolver imgInvalidator?: ImageInvalidator backgroundQueue: BackgroundQueue + sequencer: Sequencer } export class AppContext { @@ -40,6 +42,9 @@ export class AppContext { const db = new Database({ url: cfg.db.postgresUrl, schema: cfg.db.postgresSchema, + poolSize: cfg.db.poolSize, + poolMaxUses: cfg.db.poolMaxUses, + poolIdleTimeoutMs: cfg.db.poolIdleTimeoutMs, }) const signingKey = await Secp256k1Keypair.import(secrets.signingKeyHex) const appviewAgent = new AtpAgent({ service: cfg.appview.url }) @@ -78,6 +83,8 @@ export class AppContext { plcUrl: cfg.identity.plcUrl, }) + const sequencer = new Sequencer(db) + return new AppContext( { db, @@ -89,6 +96,7 @@ export class AppContext { signingKey, idResolver, backgroundQueue, + sequencer, ...(overrides ?? {}), }, secrets, @@ -143,6 +151,10 @@ export class AppContext { return this.opts.backgroundQueue } + get sequencer(): Sequencer { + return this.opts.sequencer + } + get authVerifier() { return auth.authVerifier(this.idResolver, { aud: this.cfg.service.did }) } diff --git a/packages/ozone/src/db/migrations/20231219T205730722Z-init.ts b/packages/ozone/src/db/migrations/20231219T205730722Z-init.ts index f636f40a3f4..e08c80686d2 100644 --- a/packages/ozone/src/db/migrations/20231219T205730722Z-init.ts +++ b/packages/ozone/src/db/migrations/20231219T205730722Z-init.ts @@ -68,13 +68,19 @@ export async function up(db: Kysely): Promise { // Label await db.schema .createTable('label') + .addColumn('id', 'bigserial', (col) => col.primaryKey()) .addColumn('src', 'varchar', (col) => col.notNull()) .addColumn('uri', 'varchar', (col) => col.notNull()) .addColumn('cid', 'varchar', (col) => col.notNull()) .addColumn('val', 'varchar', (col) => col.notNull()) .addColumn('neg', 'boolean', (col) => col.notNull()) .addColumn('cts', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('label_pkey', ['src', 'uri', 'cid', 'val']) + .execute() + await db.schema + .createIndex('unique_label_idx') + .unique() + .on('label') + .columns(['src', 'uri', 'cid', 'val']) .execute() await db.schema .createIndex('label_uri_index') diff --git a/packages/ozone/src/db/migrations/20240201T051104136Z-mod-event-blobs.ts b/packages/ozone/src/db/migrations/20240201T051104136Z-mod-event-blobs.ts new file mode 100644 index 00000000000..21b5e893b23 --- /dev/null +++ b/packages/ozone/src/db/migrations/20240201T051104136Z-mod-event-blobs.ts @@ -0,0 +1,15 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('moderation_event') + .addColumn('subjectBlobCids', 'jsonb') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema + .alterTable('moderation_event') + .dropColumn('subjectBlobCids') + .execute() +} diff --git a/packages/ozone/src/db/migrations/20240208T213404429Z-add-tags-column-to-moderation-subject.ts b/packages/ozone/src/db/migrations/20240208T213404429Z-add-tags-column-to-moderation-subject.ts new file mode 100644 index 00000000000..0c323ace91c --- /dev/null +++ b/packages/ozone/src/db/migrations/20240208T213404429Z-add-tags-column-to-moderation-subject.ts @@ -0,0 +1,31 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('moderation_event') + .addColumn('addedTags', 'jsonb') + .execute() + await db.schema + .alterTable('moderation_event') + .addColumn('removedTags', 'jsonb') + .execute() + await db.schema + .alterTable('moderation_subject_status') + .addColumn('tags', 'jsonb') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema + .alterTable('moderation_event') + .dropColumn('addedTags') + .execute() + await db.schema + .alterTable('moderation_event') + .dropColumn('removedTags') + .execute() + await db.schema + .alterTable('moderation_subject_status') + .dropColumn('tags') + .execute() +} diff --git a/packages/ozone/src/db/migrations/index.ts b/packages/ozone/src/db/migrations/index.ts index d00857bf575..1a823f860c5 100644 --- a/packages/ozone/src/db/migrations/index.ts +++ b/packages/ozone/src/db/migrations/index.ts @@ -4,3 +4,5 @@ export * as _20231219T205730722Z from './20231219T205730722Z-init' export * as _20240116T085607200Z from './20240116T085607200Z-communication-template' +export * as _20240201T051104136Z from './20240201T051104136Z-mod-event-blobs' +export * as _20240208T213404429Z from './20240208T213404429Z-add-tags-column-to-moderation-subject' diff --git a/packages/ozone/src/db/schema/label.ts b/packages/ozone/src/db/schema/label.ts index 0c8a398a7db..f50a6119ab3 100644 --- a/packages/ozone/src/db/schema/label.ts +++ b/packages/ozone/src/db/schema/label.ts @@ -1,6 +1,9 @@ +import { Generated, Selectable } from 'kysely' + export const tableName = 'label' export interface Label { + id: Generated src: string uri: string cid: string @@ -9,4 +12,8 @@ export interface Label { cts: string } +export type LabelRow = Selectable