diff --git a/.changeset/eleven-walls-cover.md b/.changeset/eleven-walls-cover.md new file mode 100644 index 00000000000..249dc417699 --- /dev/null +++ b/.changeset/eleven-walls-cover.md @@ -0,0 +1,5 @@ +--- +"@atproto/bsky": patch +--- + +Allow using a handle as "actor" param in app.bsky.graph.getLists diff --git a/.changeset/giant-starfishes-fry.md b/.changeset/giant-starfishes-fry.md deleted file mode 100644 index 8f20fdb00cc..00000000000 --- a/.changeset/giant-starfishes-fry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@atproto/xrpc-server": patch ---- - -Expose the request context for AuthVerifier and StreamAuthVerifier as distinct types diff --git a/.changeset/happy-eggs-swim.md b/.changeset/happy-eggs-swim.md deleted file mode 100644 index e7da6125c16..00000000000 --- a/.changeset/happy-eggs-swim.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@atproto/pds": patch ---- - -Properly authenticate OAuth requests in catch all handler. diff --git a/.changeset/hot-cows-scream.md b/.changeset/hot-cows-scream.md deleted file mode 100644 index 3460ae59b59..00000000000 --- a/.changeset/hot-cows-scream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@atproto/oauth-client": patch ---- - -Add CustomEvent ponyfill for enviroments that don't provide it diff --git a/.changeset/old-tips-sneeze.md b/.changeset/old-tips-sneeze.md new file mode 100644 index 00000000000..680ef5afdb5 --- /dev/null +++ b/.changeset/old-tips-sneeze.md @@ -0,0 +1,5 @@ +--- +"@atproto/bsky": patch +--- + +Add missing dev-dependency diff --git a/.changeset/short-cougars-relate.md b/.changeset/short-cougars-relate.md new file mode 100644 index 00000000000..18e356a3b57 --- /dev/null +++ b/.changeset/short-cougars-relate.md @@ -0,0 +1,5 @@ +--- +"@atproto/ozone": patch +--- + +Add proper typings diff --git a/.github/workflows/build-and-push-bsky-ghcr.yaml b/.github/workflows/build-and-push-bsky-ghcr.yaml index 42483b86f4f..5ac49497642 100644 --- a/.github/workflows/build-and-push-bsky-ghcr.yaml +++ b/.github/workflows/build-and-push-bsky-ghcr.yaml @@ -3,7 +3,7 @@ on: push: branches: - main - - divy/mod-full-thread + - bsky-tweaks env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} diff --git a/.github/workflows/build-and-push-ozone-aws.yaml b/.github/workflows/build-and-push-ozone-aws.yaml index 53f95c5b731..4cce9a36c2d 100644 --- a/.github/workflows/build-and-push-ozone-aws.yaml +++ b/.github/workflows/build-and-push-ozone-aws.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - divy/ozone-passthru 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-pds-ghcr.yaml b/.github/workflows/build-and-push-pds-ghcr.yaml index c35b3fcc5df..8626816acfd 100644 --- a/.github/workflows/build-and-push-pds-ghcr.yaml +++ b/.github/workflows/build-and-push-pds-ghcr.yaml @@ -3,7 +3,7 @@ on: push: branches: - main - - divy/starter-packs + - msieben/micro-optimizations env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 2a09bb2fb71..3e68aa0c1d1 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -20,8 +20,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v4 - with: - version: 8 - uses: actions/setup-node@v3 with: node-version: 18 diff --git a/.github/workflows/repo.yaml b/.github/workflows/repo.yaml index 0f5859551a7..005bacb421b 100644 --- a/.github/workflows/repo.yaml +++ b/.github/workflows/repo.yaml @@ -18,7 +18,6 @@ jobs: - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: 8 run_install: false - uses: actions/setup-node@v4 with: @@ -45,7 +44,6 @@ jobs: - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: 8 run_install: false - uses: actions/setup-node@v4 with: @@ -66,7 +64,6 @@ jobs: - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: 8 run_install: false - uses: actions/setup-node@v4 with: diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000000..3c032078a4a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 diff --git a/Makefile b/Makefile index ee6338e8cf2..d7424038a49 100644 --- a/Makefile +++ b/Makefile @@ -51,4 +51,4 @@ deps: ## Installs dependent libs using 'pnpm install' nvm-setup: ## Use NVM to install and activate node+pnpm nvm install 18 nvm use 18 - npm install --global pnpm + corepack enable diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index 6ba7aaa734a..784bce8a42e 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -91,6 +91,10 @@ "labels": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } + }, + "pinnedPost": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" } } }, @@ -419,6 +423,15 @@ "type": "array", "maxLength": 1000, "items": { "type": "string", "maxLength": 100 } + }, + "nuxs": { + "description": "Storage for NUXs the user has encountered.", + "type": "array", + "maxLength": 100, + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#nux" + } } } }, @@ -429,6 +442,32 @@ "properties": { "guide": { "type": "string", "maxLength": 100 } } + }, + "nux": { + "type": "object", + "description": "A new user experiences (NUX) storage object", + "required": ["id", "completed"], + "properties": { + "id": { + "type": "string", + "maxLength": 100 + }, + "completed": { + "type": "boolean", + "default": false + }, + "data": { + "description": "Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.", + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "expiresAt": { + "type": "string", + "format": "datetime", + "description": "The date and time at which the NUX will expire and should be considered completed." + } + } } } } diff --git a/lexicons/app/bsky/actor/profile.json b/lexicons/app/bsky/actor/profile.json index 4363a02d2cd..911d7a05692 100644 --- a/lexicons/app/bsky/actor/profile.json +++ b/lexicons/app/bsky/actor/profile.json @@ -41,6 +41,10 @@ "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "pinnedPost": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, "createdAt": { "type": "string", "format": "datetime" } } } diff --git a/lexicons/app/bsky/embed/defs.json b/lexicons/app/bsky/embed/defs.json new file mode 100644 index 00000000000..57ffc03a679 --- /dev/null +++ b/lexicons/app/bsky/embed/defs.json @@ -0,0 +1,15 @@ +{ + "lexicon": 1, + "id": "app.bsky.embed.defs", + "defs": { + "aspectRatio": { + "type": "object", + "description": "width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.", + "required": ["width", "height"], + "properties": { + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 } + } + } + } +} diff --git a/lexicons/app/bsky/embed/images.json b/lexicons/app/bsky/embed/images.json index 7de9d7a862d..c6b6fa20b5d 100644 --- a/lexicons/app/bsky/embed/images.json +++ b/lexicons/app/bsky/embed/images.json @@ -27,16 +27,10 @@ "type": "string", "description": "Alt text description of the image, for accessibility." }, - "aspectRatio": { "type": "ref", "ref": "#aspectRatio" } - } - }, - "aspectRatio": { - "type": "object", - "description": "width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.", - "required": ["width", "height"], - "properties": { - "width": { "type": "integer", "minimum": 1 }, - "height": { "type": "integer", "minimum": 1 } + "aspectRatio": { + "type": "ref", + "ref": "app.bsky.embed.defs#aspectRatio" + } } }, "view": { @@ -68,7 +62,10 @@ "type": "string", "description": "Alt text description of the image, for accessibility." }, - "aspectRatio": { "type": "ref", "ref": "#aspectRatio" } + "aspectRatio": { + "type": "ref", + "ref": "app.bsky.embed.defs#aspectRatio" + } } } } diff --git a/lexicons/app/bsky/embed/record.json b/lexicons/app/bsky/embed/record.json index 412aa0ff511..a73200a3299 100644 --- a/lexicons/app/bsky/embed/record.json +++ b/lexicons/app/bsky/embed/record.json @@ -20,6 +20,7 @@ "#viewRecord", "#viewNotFound", "#viewBlocked", + "#viewDetached", "app.bsky.feed.defs#generatorView", "app.bsky.graph.defs#listView", "app.bsky.labeler.defs#labelerView", @@ -49,12 +50,14 @@ "replyCount": { "type": "integer" }, "repostCount": { "type": "integer" }, "likeCount": { "type": "integer" }, + "quoteCount": { "type": "integer" }, "embeds": { "type": "array", "items": { "type": "union", "refs": [ "app.bsky.embed.images#view", + "app.bsky.embed.video#view", "app.bsky.embed.external#view", "app.bsky.embed.record#view", "app.bsky.embed.recordWithMedia#view" @@ -80,6 +83,14 @@ "blocked": { "type": "boolean", "const": true }, "author": { "type": "ref", "ref": "app.bsky.feed.defs#blockedAuthor" } } + }, + "viewDetached": { + "type": "object", + "required": ["uri", "detached"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "detached": { "type": "boolean", "const": true } + } } } } diff --git a/lexicons/app/bsky/embed/recordWithMedia.json b/lexicons/app/bsky/embed/recordWithMedia.json index 46145464fe2..5baf68502e5 100644 --- a/lexicons/app/bsky/embed/recordWithMedia.json +++ b/lexicons/app/bsky/embed/recordWithMedia.json @@ -13,7 +13,11 @@ }, "media": { "type": "union", - "refs": ["app.bsky.embed.images", "app.bsky.embed.external"] + "refs": [ + "app.bsky.embed.images", + "app.bsky.embed.video", + "app.bsky.embed.external" + ] } } }, @@ -27,7 +31,11 @@ }, "media": { "type": "union", - "refs": ["app.bsky.embed.images#view", "app.bsky.embed.external#view"] + "refs": [ + "app.bsky.embed.images#view", + "app.bsky.embed.video#view", + "app.bsky.embed.external#view" + ] } } } diff --git a/lexicons/app/bsky/embed/video.json b/lexicons/app/bsky/embed/video.json new file mode 100644 index 00000000000..da58ded8bb4 --- /dev/null +++ b/lexicons/app/bsky/embed/video.json @@ -0,0 +1,66 @@ +{ + "lexicon": 1, + "id": "app.bsky.embed.video", + "description": "A video embedded in a Bluesky record (eg, a post).", + "defs": { + "main": { + "type": "object", + "required": ["video"], + "properties": { + "video": { + "type": "blob", + "accept": ["video/mp4"], + "maxSize": 50000000 + }, + "captions": { + "type": "array", + "items": { "type": "ref", "ref": "#caption" }, + "maxLength": 20 + }, + "alt": { + "type": "string", + "description": "Alt text description of the video, for accessibility.", + "maxGraphemes": 1000, + "maxLength": 10000 + }, + "aspectRatio": { + "type": "ref", + "ref": "app.bsky.embed.defs#aspectRatio" + } + } + }, + "caption": { + "type": "object", + "required": ["lang", "file"], + "properties": { + "lang": { + "type": "string", + "format": "language" + }, + "file": { + "type": "blob", + "accept": ["text/vtt"], + "maxSize": 20000 + } + } + }, + "view": { + "type": "object", + "required": ["cid", "playlist"], + "properties": { + "cid": { "type": "string", "format": "cid" }, + "playlist": { "type": "string", "format": "uri" }, + "thumbnail": { "type": "string", "format": "uri" }, + "alt": { + "type": "string", + "maxGraphemes": 1000, + "maxLength": 10000 + }, + "aspectRatio": { + "type": "ref", + "ref": "app.bsky.embed.defs#aspectRatio" + } + } + } + } +} diff --git a/lexicons/app/bsky/feed/defs.json b/lexicons/app/bsky/feed/defs.json index e2ecff210cf..341f7fbd02b 100644 --- a/lexicons/app/bsky/feed/defs.json +++ b/lexicons/app/bsky/feed/defs.json @@ -17,6 +17,7 @@ "type": "union", "refs": [ "app.bsky.embed.images#view", + "app.bsky.embed.video#view", "app.bsky.embed.external#view", "app.bsky.embed.record#view", "app.bsky.embed.recordWithMedia#view" @@ -25,6 +26,7 @@ "replyCount": { "type": "integer" }, "repostCount": { "type": "integer" }, "likeCount": { "type": "integer" }, + "quoteCount": { "type": "integer" }, "indexedAt": { "type": "string", "format": "datetime" }, "viewer": { "type": "ref", "ref": "#viewerState" }, "labels": { @@ -41,7 +43,9 @@ "repost": { "type": "string", "format": "at-uri" }, "like": { "type": "string", "format": "at-uri" }, "threadMuted": { "type": "boolean" }, - "replyDisabled": { "type": "boolean" } + "replyDisabled": { "type": "boolean" }, + "embeddingDisabled": { "type": "boolean" }, + "pinned": { "type": "boolean" } } }, "feedViewPost": { @@ -50,7 +54,7 @@ "properties": { "post": { "type": "ref", "ref": "#postView" }, "reply": { "type": "ref", "ref": "#replyRef" }, - "reason": { "type": "union", "refs": ["#reasonRepost"] }, + "reason": { "type": "union", "refs": ["#reasonRepost", "#reasonPin"] }, "feedContext": { "type": "string", "description": "Context provided by feed generator that may be passed back alongside interactions.", @@ -85,6 +89,10 @@ "indexedAt": { "type": "string", "format": "datetime" } } }, + "reasonPin": { + "type": "object", + "properties": {} + }, "threadViewPost": { "type": "object", "required": ["post"], @@ -168,7 +176,10 @@ "required": ["post"], "properties": { "post": { "type": "string", "format": "at-uri" }, - "reason": { "type": "union", "refs": ["#skeletonReasonRepost"] }, + "reason": { + "type": "union", + "refs": ["#skeletonReasonRepost", "#skeletonReasonPin"] + }, "feedContext": { "type": "string", "description": "Context that will be passed through to client and may be passed to feed generator back alongside interactions.", @@ -183,6 +194,10 @@ "repost": { "type": "string", "format": "at-uri" } } }, + "skeletonReasonPin": { + "type": "object", + "properties": {} + }, "threadgateView": { "type": "object", "properties": { diff --git a/lexicons/app/bsky/feed/getActorLikes.json b/lexicons/app/bsky/feed/getActorLikes.json index 22f8ed984ac..f7c0224b0a5 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. Does not require auth.", + "description": "Get a list of posts liked by an actor. Requires auth, actor must be the requesting account.", "parameters": { "type": "params", "required": ["actor"], diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json index 90e4d1a7708..19e99b174ac 100644 --- a/lexicons/app/bsky/feed/getAuthorFeed.json +++ b/lexicons/app/bsky/feed/getAuthorFeed.json @@ -27,6 +27,10 @@ "posts_and_author_threads" ], "default": "posts_with_replies" + }, + "includePins": { + "type": "boolean", + "default": false } } }, diff --git a/lexicons/app/bsky/feed/getPostThread.json b/lexicons/app/bsky/feed/getPostThread.json index 89e99d9c6d7..7efab15f1e6 100644 --- a/lexicons/app/bsky/feed/getPostThread.json +++ b/lexicons/app/bsky/feed/getPostThread.json @@ -43,6 +43,10 @@ "app.bsky.feed.defs#notFoundPost", "app.bsky.feed.defs#blockedPost" ] + }, + "threadgate": { + "type": "ref", + "ref": "app.bsky.feed.defs#threadgateView" } } } diff --git a/lexicons/app/bsky/feed/getQuotes.json b/lexicons/app/bsky/feed/getQuotes.json new file mode 100644 index 00000000000..4ea8b30e488 --- /dev/null +++ b/lexicons/app/bsky/feed/getQuotes.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "app.bsky.feed.getQuotes", + "defs": { + "main": { + "type": "query", + "description": "Get a list of quotes for a given post.", + "parameters": { + "type": "params", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) of post record" + }, + "cid": { + "type": "string", + "format": "cid", + "description": "If supplied, filters to quotes of specific version (by CID) of the post record." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri", "posts"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "cursor": { "type": "string" }, + "posts": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#postView" + } + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/feed/post.json b/lexicons/app/bsky/feed/post.json index b9b236b4f81..9893a355946 100644 --- a/lexicons/app/bsky/feed/post.json +++ b/lexicons/app/bsky/feed/post.json @@ -31,6 +31,7 @@ "type": "union", "refs": [ "app.bsky.embed.images", + "app.bsky.embed.video", "app.bsky.embed.external", "app.bsky.embed.record", "app.bsky.embed.recordWithMedia" diff --git a/lexicons/app/bsky/feed/postgate.json b/lexicons/app/bsky/feed/postgate.json new file mode 100644 index 00000000000..e442e10ebd8 --- /dev/null +++ b/lexicons/app/bsky/feed/postgate.json @@ -0,0 +1,45 @@ +{ + "lexicon": 1, + "id": "app.bsky.feed.postgate", + "defs": { + "main": { + "type": "record", + "key": "tid", + "description": "Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository.", + "record": { + "type": "object", + "required": ["post", "createdAt"], + "properties": { + "createdAt": { "type": "string", "format": "datetime" }, + "post": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to the post record." + }, + "detachedEmbeddingUris": { + "type": "array", + "maxLength": 50, + "items": { + "type": "string", + "format": "at-uri" + }, + "description": "List of AT-URIs embedding this post that the author has detached from." + }, + "embeddingRules": { + "type": "array", + "maxLength": 5, + "items": { + "type": "union", + "refs": ["#disableRule"] + } + } + } + } + }, + "disableRule": { + "type": "object", + "description": "Disables embedding of this post.", + "properties": {} + } + } +} diff --git a/lexicons/app/bsky/feed/threadgate.json b/lexicons/app/bsky/feed/threadgate.json index ff258da4d30..2da5a771db9 100644 --- a/lexicons/app/bsky/feed/threadgate.json +++ b/lexicons/app/bsky/feed/threadgate.json @@ -5,7 +5,7 @@ "main": { "type": "record", "key": "tid", - "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..", + "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"], @@ -23,7 +23,16 @@ "refs": ["#mentionRule", "#followingRule", "#listRule"] } }, - "createdAt": { "type": "string", "format": "datetime" } + "createdAt": { "type": "string", "format": "datetime" }, + "hiddenReplies": { + "type": "array", + "maxLength": 50, + "items": { + "type": "string", + "format": "at-uri" + }, + "description": "List of hidden reply URIs." + } } } }, diff --git a/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json b/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json index 5b0cfdebb70..49d6a0b2e06 100644 --- a/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json +++ b/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json @@ -24,6 +24,11 @@ "type": "ref", "ref": "app.bsky.actor.defs#profileView" } + }, + "isFallback": { + "type": "boolean", + "description": "If true, response has fallen-back to generic results, and is not scoped using relativeToDid", + "default": false } } } diff --git a/lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json b/lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json index 7211c6c05da..42edf3af0e6 100644 --- a/lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json +++ b/lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json @@ -40,6 +40,11 @@ "type": "ref", "ref": "app.bsky.unspecced.defs#skeletonSearchActor" } + }, + "relativeToDid": { + "type": "string", + "format": "did", + "description": "DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer." } } } diff --git a/lexicons/app/bsky/video/defs.json b/lexicons/app/bsky/video/defs.json new file mode 100644 index 00000000000..8c846ccbc91 --- /dev/null +++ b/lexicons/app/bsky/video/defs.json @@ -0,0 +1,28 @@ +{ + "lexicon": 1, + "id": "app.bsky.video.defs", + "defs": { + "jobStatus": { + "type": "object", + "required": ["jobId", "did", "state"], + "properties": { + "jobId": { "type": "string" }, + "did": { "type": "string", "format": "did" }, + "state": { + "type": "string", + "description": "The state of the video processing job. All values not listed as a known value indicate that the job is in process.", + "knownValues": ["JOB_STATE_COMPLETED", "JOB_STATE_FAILED"] + }, + "progress": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Progress within the current processing state." + }, + "blob": { "type": "blob" }, + "error": { "type": "string" }, + "message": { "type": "string" } + } + } + } +} diff --git a/lexicons/app/bsky/video/getJobStatus.json b/lexicons/app/bsky/video/getJobStatus.json new file mode 100644 index 00000000000..ff3076f2a0c --- /dev/null +++ b/lexicons/app/bsky/video/getJobStatus.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "app.bsky.video.getJobStatus", + "defs": { + "main": { + "type": "query", + "description": "Get status details for a video processing job.", + "parameters": { + "type": "params", + "required": ["jobId"], + "properties": { + "jobId": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["jobStatus"], + "properties": { + "jobStatus": { + "type": "ref", + "ref": "app.bsky.video.defs#jobStatus" + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/video/getUploadLimits.json b/lexicons/app/bsky/video/getUploadLimits.json new file mode 100644 index 00000000000..bcba18e5ef7 --- /dev/null +++ b/lexicons/app/bsky/video/getUploadLimits.json @@ -0,0 +1,24 @@ +{ + "lexicon": 1, + "id": "app.bsky.video.getUploadLimits", + "defs": { + "main": { + "type": "query", + "description": "Get video upload limits for the authenticated user.", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["canUpload"], + "properties": { + "canUpload": { "type": "boolean" }, + "remainingDailyVideos": { "type": "integer" }, + "remainingDailyBytes": { "type": "integer" }, + "message": { "type": "string" }, + "error": { "type": "string" } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/video/uploadVideo.json b/lexicons/app/bsky/video/uploadVideo.json new file mode 100644 index 00000000000..8ffc26462d2 --- /dev/null +++ b/lexicons/app/bsky/video/uploadVideo.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "app.bsky.video.uploadVideo", + "defs": { + "main": { + "type": "procedure", + "description": "Upload a video to be processed then stored on the PDS.", + "input": { + "encoding": "video/mp4" + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["jobStatus"], + "properties": { + "jobStatus": { + "type": "ref", + "ref": "app.bsky.video.defs#jobStatus" + } + } + } + } + } + } +} diff --git a/lexicons/com/atproto/repo/applyWrites.json b/lexicons/com/atproto/repo/applyWrites.json index 427fc84c4a5..26dd96c5346 100644 --- a/lexicons/com/atproto/repo/applyWrites.json +++ b/lexicons/com/atproto/repo/applyWrites.json @@ -18,8 +18,7 @@ }, "validate": { "type": "boolean", - "default": true, - "description": "Can be set to 'false' to skip Lexicon schema validation of record data, for all operations." + "description": "Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons." }, "writes": { "type": "array", @@ -37,6 +36,27 @@ } } }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [], + "properties": { + "commit": { + "type": "ref", + "ref": "com.atproto.repo.defs#commitMeta" + }, + "results": { + "type": "array", + "items": { + "type": "union", + "refs": ["#createResult", "#updateResult", "#deleteResult"], + "closed": true + } + } + } + } + }, "errors": [ { "name": "InvalidSwap", @@ -72,6 +92,35 @@ "collection": { "type": "string", "format": "nsid" }, "rkey": { "type": "string" } } + }, + "createResult": { + "type": "object", + "required": ["uri", "cid"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "validationStatus": { + "type": "string", + "knownValues": ["valid", "unknown"] + } + } + }, + "updateResult": { + "type": "object", + "required": ["uri", "cid"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "validationStatus": { + "type": "string", + "knownValues": ["valid", "unknown"] + } + } + }, + "deleteResult": { + "type": "object", + "required": [], + "properties": {} } } } diff --git a/lexicons/com/atproto/repo/createRecord.json b/lexicons/com/atproto/repo/createRecord.json index 185f5250850..7290085711f 100644 --- a/lexicons/com/atproto/repo/createRecord.json +++ b/lexicons/com/atproto/repo/createRecord.json @@ -28,8 +28,7 @@ }, "validate": { "type": "boolean", - "default": true, - "description": "Can be set to 'false' to skip Lexicon schema validation of record data." + "description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons." }, "record": { "type": "unknown", @@ -50,7 +49,15 @@ "required": ["uri", "cid"], "properties": { "uri": { "type": "string", "format": "at-uri" }, - "cid": { "type": "string", "format": "cid" } + "cid": { "type": "string", "format": "cid" }, + "commit": { + "type": "ref", + "ref": "com.atproto.repo.defs#commitMeta" + }, + "validationStatus": { + "type": "string", + "knownValues": ["valid", "unknown"] + } } } }, diff --git a/lexicons/com/atproto/repo/defs.json b/lexicons/com/atproto/repo/defs.json new file mode 100644 index 00000000000..0f5128fb9e4 --- /dev/null +++ b/lexicons/com/atproto/repo/defs.json @@ -0,0 +1,14 @@ +{ + "lexicon": 1, + "id": "com.atproto.repo.defs", + "defs": { + "commitMeta": { + "type": "object", + "required": ["cid", "rev"], + "properties": { + "cid": { "type": "string", "format": "cid" }, + "rev": { "type": "string" } + } + } + } +} diff --git a/lexicons/com/atproto/repo/deleteRecord.json b/lexicons/com/atproto/repo/deleteRecord.json index 65b9f8f9536..fb9b90b720d 100644 --- a/lexicons/com/atproto/repo/deleteRecord.json +++ b/lexicons/com/atproto/repo/deleteRecord.json @@ -38,6 +38,18 @@ } } }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "commit": { + "type": "ref", + "ref": "com.atproto.repo.defs#commitMeta" + } + } + } + }, "errors": [{ "name": "InvalidSwap" }] } } diff --git a/lexicons/com/atproto/repo/getRecord.json b/lexicons/com/atproto/repo/getRecord.json index 5d8bb173470..261c1cdf7aa 100644 --- a/lexicons/com/atproto/repo/getRecord.json +++ b/lexicons/com/atproto/repo/getRecord.json @@ -38,7 +38,8 @@ "value": { "type": "unknown" } } } - } + }, + "errors": [{ "name": "RecordNotFound" }] } } } diff --git a/lexicons/com/atproto/repo/putRecord.json b/lexicons/com/atproto/repo/putRecord.json index 51f11c0f13f..9a841f6a8df 100644 --- a/lexicons/com/atproto/repo/putRecord.json +++ b/lexicons/com/atproto/repo/putRecord.json @@ -29,8 +29,7 @@ }, "validate": { "type": "boolean", - "default": true, - "description": "Can be set to 'false' to skip Lexicon schema validation of record data." + "description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons." }, "record": { "type": "unknown", @@ -56,7 +55,15 @@ "required": ["uri", "cid"], "properties": { "uri": { "type": "string", "format": "at-uri" }, - "cid": { "type": "string", "format": "cid" } + "cid": { "type": "string", "format": "cid" }, + "commit": { + "type": "ref", + "ref": "com.atproto.repo.defs#commitMeta" + }, + "validationStatus": { + "type": "string", + "knownValues": ["valid", "unknown"] + } } } }, diff --git a/lexicons/tools/ozone/communication/createTemplate.json b/lexicons/tools/ozone/communication/createTemplate.json index 175a55f9d03..902a46447f1 100644 --- a/lexicons/tools/ozone/communication/createTemplate.json +++ b/lexicons/tools/ozone/communication/createTemplate.json @@ -23,6 +23,11 @@ "type": "string", "description": "Subject of the message, used in emails." }, + "lang": { + "type": "string", + "format": "language", + "description": "Message language." + }, "createdBy": { "type": "string", "format": "did", @@ -37,7 +42,8 @@ "type": "ref", "ref": "tools.ozone.communication.defs#templateView" } - } + }, + "errors": [{ "name": "DuplicateTemplateName" }] } } } diff --git a/lexicons/tools/ozone/communication/defs.json b/lexicons/tools/ozone/communication/defs.json index 89d28bdf690..fe61156960a 100644 --- a/lexicons/tools/ozone/communication/defs.json +++ b/lexicons/tools/ozone/communication/defs.json @@ -25,6 +25,11 @@ "description": "Subject of the message, used in emails." }, "disabled": { "type": "boolean" }, + "lang": { + "type": "string", + "format": "language", + "description": "Message language." + }, "lastUpdatedBy": { "type": "string", "format": "did", diff --git a/lexicons/tools/ozone/communication/updateTemplate.json b/lexicons/tools/ozone/communication/updateTemplate.json index 153453a875e..fc2b3f68845 100644 --- a/lexicons/tools/ozone/communication/updateTemplate.json +++ b/lexicons/tools/ozone/communication/updateTemplate.json @@ -19,6 +19,11 @@ "type": "string", "description": "Name of the template." }, + "lang": { + "type": "string", + "format": "language", + "description": "Message language." + }, "contentMarkdown": { "type": "string", "description": "Content of the template, markdown supported, can contain variable placeholders." @@ -44,7 +49,8 @@ "type": "ref", "ref": "tools.ozone.communication.defs#templateView" } - } + }, + "errors": [{ "name": "DuplicateTemplateName" }] } } } diff --git a/lexicons/tools/ozone/moderation/defs.json b/lexicons/tools/ozone/moderation/defs.json index 83299d760b4..9bfabb0a9f6 100644 --- a/lexicons/tools/ozone/moderation/defs.json +++ b/lexicons/tools/ozone/moderation/defs.json @@ -210,6 +210,10 @@ "durationInHours": { "type": "integer", "description": "Indicates how long the takedown should be in effect before automatically expiring." + }, + "acknowledgeAccountSubjects": { + "type": "boolean", + "description": "If true, all other reports on content authored by this account will be resolved (acknowledged)." } } }, diff --git a/lexicons/tools/ozone/moderation/emitEvent.json b/lexicons/tools/ozone/moderation/emitEvent.json index 76590ac61e5..4692dfe29ba 100644 --- a/lexicons/tools/ozone/moderation/emitEvent.json +++ b/lexicons/tools/ozone/moderation/emitEvent.json @@ -25,6 +25,7 @@ "tools.ozone.moderation.defs#modEventMuteReporter", "tools.ozone.moderation.defs#modEventUnmuteReporter", "tools.ozone.moderation.defs#modEventReverseTakedown", + "tools.ozone.moderation.defs#modEventResolveAppeal", "tools.ozone.moderation.defs#modEventEmail", "tools.ozone.moderation.defs#modEventTag" ] diff --git a/lexicons/tools/ozone/moderation/getRecords.json b/lexicons/tools/ozone/moderation/getRecords.json new file mode 100644 index 00000000000..12dc1f68536 --- /dev/null +++ b/lexicons/tools/ozone/moderation/getRecords.json @@ -0,0 +1,43 @@ +{ + "lexicon": 1, + "id": "tools.ozone.moderation.getRecords", + "defs": { + "main": { + "type": "query", + "description": "Get details about some records.", + "parameters": { + "type": "params", + "required": ["uris"], + "properties": { + "uris": { + "type": "array", + "maxLength": 100, + "items": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["records"], + "properties": { + "records": { + "type": "array", + "items": { + "type": "union", + "refs": [ + "tools.ozone.moderation.defs#recordViewDetail", + "tools.ozone.moderation.defs#recordViewNotFound" + ] + } + } + } + } + } + } + } +} diff --git a/lexicons/tools/ozone/moderation/getRepos.json b/lexicons/tools/ozone/moderation/getRepos.json new file mode 100644 index 00000000000..3124db568a0 --- /dev/null +++ b/lexicons/tools/ozone/moderation/getRepos.json @@ -0,0 +1,43 @@ +{ + "lexicon": 1, + "id": "tools.ozone.moderation.getRepos", + "defs": { + "main": { + "type": "query", + "description": "Get details about some repositories.", + "parameters": { + "type": "params", + "required": ["dids"], + "properties": { + "dids": { + "type": "array", + "maxLength": 100, + "items": { + "type": "string", + "format": "did" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["repos"], + "properties": { + "repos": { + "type": "array", + "items": { + "type": "union", + "refs": [ + "tools.ozone.moderation.defs#repoViewDetail", + "tools.ozone.moderation.defs#repoViewNotFound" + ] + } + } + } + } + } + } + } +} diff --git a/lexicons/tools/ozone/moderation/queryStatuses.json b/lexicons/tools/ozone/moderation/queryStatuses.json index 81fc1e38f33..1eed947892e 100644 --- a/lexicons/tools/ozone/moderation/queryStatuses.json +++ b/lexicons/tools/ozone/moderation/queryStatuses.json @@ -8,7 +8,15 @@ "parameters": { "type": "params", "properties": { - "subject": { "type": "string", "format": "uri" }, + "includeAllUserRecords": { + "type": "boolean", + "description": "All subjects belonging to the account specified in the 'subject' param will be returned." + }, + "subject": { + "type": "string", + "format": "uri", + "description": "The subject to get the status for." + }, "comment": { "type": "string", "description": "Search subjects by keyword from comments" @@ -47,7 +55,10 @@ }, "ignoreSubjects": { "type": "array", - "items": { "type": "string", "format": "uri" } + "items": { + "type": "string", + "format": "uri" + } }, "lastReviewedBy": { "type": "string", @@ -80,13 +91,19 @@ }, "tags": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "excludeTags": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, - "cursor": { "type": "string" } + "cursor": { + "type": "string" + } } }, "output": { @@ -95,7 +112,9 @@ "type": "object", "required": ["subjectStatuses"], "properties": { - "cursor": { "type": "string" }, + "cursor": { + "type": "string" + }, "subjectStatuses": { "type": "array", "items": { diff --git a/lexicons/tools/ozone/signature/defs.json b/lexicons/tools/ozone/signature/defs.json new file mode 100644 index 00000000000..6c7297ebabd --- /dev/null +++ b/lexicons/tools/ozone/signature/defs.json @@ -0,0 +1,14 @@ +{ + "lexicon": 1, + "id": "tools.ozone.signature.defs", + "defs": { + "sigDetail": { + "type": "object", + "required": ["property", "value"], + "properties": { + "property": { "type": "string" }, + "value": { "type": "string" } + } + } + } +} diff --git a/lexicons/tools/ozone/signature/findCorrelation.json b/lexicons/tools/ozone/signature/findCorrelation.json new file mode 100644 index 00000000000..f803f25cd3b --- /dev/null +++ b/lexicons/tools/ozone/signature/findCorrelation.json @@ -0,0 +1,39 @@ +{ + "lexicon": 1, + "id": "tools.ozone.signature.findCorrelation", + "defs": { + "main": { + "type": "query", + "description": "Find all correlated threat signatures between 2 or more accounts.", + "parameters": { + "type": "params", + "required": ["dids"], + "properties": { + "dids": { + "type": "array", + "items": { + "type": "string", + "format": "did" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["details"], + "properties": { + "details": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.ozone.signature.defs#sigDetail" + } + } + } + } + } + } + } +} diff --git a/lexicons/tools/ozone/signature/findRelatedAccounts.json b/lexicons/tools/ozone/signature/findRelatedAccounts.json new file mode 100644 index 00000000000..f5e4c7b057c --- /dev/null +++ b/lexicons/tools/ozone/signature/findRelatedAccounts.json @@ -0,0 +1,61 @@ +{ + "lexicon": 1, + "id": "tools.ozone.signature.findRelatedAccounts", + "defs": { + "main": { + "type": "query", + "description": "Get accounts that share some matching threat signatures with the root account.", + "parameters": { + "type": "params", + "required": ["did"], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "cursor": { "type": "string" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["accounts"], + "properties": { + "cursor": { "type": "string" }, + "accounts": { + "type": "array", + "items": { + "type": "ref", + "ref": "#relatedAccount" + } + } + } + } + } + }, + "relatedAccount": { + "type": "object", + "required": ["account"], + "properties": { + "account": { + "type": "ref", + "ref": "com.atproto.admin.defs#accountView" + }, + "similarities": { + "type": "array", + "items": { + "type": "ref", + "ref": "tools.ozone.signature.defs#sigDetail" + } + } + } + } + } +} diff --git a/lexicons/tools/ozone/signature/searchAccounts.json b/lexicons/tools/ozone/signature/searchAccounts.json new file mode 100644 index 00000000000..328d7787696 --- /dev/null +++ b/lexicons/tools/ozone/signature/searchAccounts.json @@ -0,0 +1,46 @@ +{ + "lexicon": 1, + "id": "tools.ozone.signature.searchAccounts", + "defs": { + "main": { + "type": "query", + "description": "Search for accounts that match one or more threat signature values.", + "parameters": { + "type": "params", + "required": ["values"], + "properties": { + "values": { + "type": "array", + "items": { + "type": "string" + } + }, + "cursor": { "type": "string" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["accounts"], + "properties": { + "cursor": { "type": "string" }, + "accounts": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.admin.defs#accountView" + } + } + } + } + } + } + } +} diff --git a/package.json b/package.json index 644fff5ef8c..e990ffde7d0 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "engines": { "node": ">=18" }, + "packageManager": "pnpm@8.15.9", "scripts": { "lint:fix": "pnpm lint --fix", "lint": "eslint . --ext .ts,.js", @@ -35,7 +36,7 @@ "@swc/core": "^1.3.42", "@swc/jest": "^0.2.24", "@types/jest": "^28.1.4", - "@types/node": "^18.19.24", + "@types/node": "^18.19.50", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", "dotenv": "^16.0.3", diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index aaa2e2aa1a0..25ff2e61573 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,88 @@ # @atproto/api +## 0.13.11 + +### Patch Changes + +- [#2857](https://github.com/bluesky-social/atproto/pull/2857) [`a0531ce42`](https://github.com/bluesky-social/atproto/commit/a0531ce429f5139cb0e2cc19aa9b338599947e44) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds support for muting words within link cards attached to `RecordWithMedia` embeds. + +## 0.13.10 + +### Patch Changes + +- [#2855](https://github.com/bluesky-social/atproto/pull/2855) [`df14df522`](https://github.com/bluesky-social/atproto/commit/df14df522bb7986e56ee1f6a0f5d862e1ea6f4d5) Thanks [@dholms](https://github.com/dholms)! - Add tools.ozone.signature lexicons + +## 0.13.9 + +### Patch Changes + +- [#2836](https://github.com/bluesky-social/atproto/pull/2836) [`a2bad977a`](https://github.com/bluesky-social/atproto/commit/a2bad977a8d941b4075ea3ffee3d6f7a0c0f467c) Thanks [@foysalit](https://github.com/foysalit)! - Add getRepos and getRecords endpoints for bulk fetching + +## 0.13.8 + +### Patch Changes + +- [#2771](https://github.com/bluesky-social/atproto/pull/2771) [`2676206e4`](https://github.com/bluesky-social/atproto/commit/2676206e422233fefbf2d9d182e8d462f0957c93) Thanks [@mozzius](https://github.com/mozzius)! - Add pinned posts to profile record and getAuthorFeed + +- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6), [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94)]: + - @atproto/xrpc@0.6.3 + - @atproto/common-web@0.3.1 + - @atproto/lexicon@0.4.2 + +## 0.13.7 + +### Patch Changes + +- [#2807](https://github.com/bluesky-social/atproto/pull/2807) [`e6bd5aecc`](https://github.com/bluesky-social/atproto/commit/e6bd5aecce7954d60e5fb263297e697ab7aab98e) Thanks [@foysalit](https://github.com/foysalit)! - Introduce a acknowledgeAccountSubjects flag on takedown event to ack all subjects from the author that need review + +- [#2810](https://github.com/bluesky-social/atproto/pull/2810) [`33aa0c722`](https://github.com/bluesky-social/atproto/commit/33aa0c722226a18215af0ae1833c7c552fc7aaa7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add NUX API + +- Updated dependencies [[`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]: + - @atproto/xrpc@0.6.2 + +## 0.13.6 + +### Patch Changes + +- [#2780](https://github.com/bluesky-social/atproto/pull/2780) [`e4d41d66f`](https://github.com/bluesky-social/atproto/commit/e4d41d66fa4757a696363f39903562458967b63d) Thanks [@foysalit](https://github.com/foysalit)! - Add language property to communication templates + +## 0.13.5 + +### Patch Changes + +- [#2751](https://github.com/bluesky-social/atproto/pull/2751) [`80ada8f47`](https://github.com/bluesky-social/atproto/commit/80ada8f47628f55f3074cd16a52857e98d117e14) Thanks [@devinivy](https://github.com/devinivy)! - Lexicons and support for video embeds within bsky posts. + +## 0.13.4 + +### Patch Changes + +- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Drop use of `AtpBaseClient` class + +- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose the `CredentialSession` class that can be used to instantiate both `Agent` and `XrpcClient`, while internally managing credential based (username/password) sessions. + +- [`bbca17bc5`](https://github.com/bluesky-social/atproto/commit/bbca17bc5388e0b2af26fb107347c8ab507ee42f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Deprecate Agent.accountDid in favor of Agent.assertDid + +- [#2737](https://github.com/bluesky-social/atproto/pull/2737) [`a8e1f9000`](https://github.com/bluesky-social/atproto/commit/a8e1f9000d9617c4df9d9f0e74ae0e0b73fcfd66) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `threadgate: ThreadgateView` to response from `getPostThread` + +- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `Agent` is no longer an abstract class. Instead it can be instantiated using object implementing a new `SessionManager` interface. If your project extends `Agent` and overrides the constructor or any method implementations, consider that you may want to call them from `super`. + +- Updated dependencies [[`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]: + - @atproto/xrpc@0.6.1 + +## 0.13.3 + +### Patch Changes + +- [#2735](https://github.com/bluesky-social/atproto/pull/2735) [`4ab248354`](https://github.com/bluesky-social/atproto/commit/4ab2483547d5dabfba88ed4419a4f374bbd7cae7) Thanks [@haileyok](https://github.com/haileyok)! - add `quoteCount` to embed view + +## 0.13.2 + +### Patch Changes + +- [#2658](https://github.com/bluesky-social/atproto/pull/2658) [`2a0c088cc`](https://github.com/bluesky-social/atproto/commit/2a0c088cc5d502ca70da9612a261186aa2f2e1fb) Thanks [@haileyok](https://github.com/haileyok)! - Adds `app.bsky.feed.getQuotes` lexicon and handlers + +- [#2675](https://github.com/bluesky-social/atproto/pull/2675) [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds `postgate` records to power quote gating and detached quote posts, plus `hiddenReplies` to the `threadgate` record. + ## 0.13.1 ### Patch Changes diff --git a/packages/api/OAUTH.md b/packages/api/OAUTH.md index 911e4f8569a..e60c59f914d 100644 --- a/packages/api/OAUTH.md +++ b/packages/api/OAUTH.md @@ -40,7 +40,7 @@ Here is an example client metadata. "tos_uri": "https://example.com/tos", "policy_uri": "https://example.com/policy", "redirect_uris": ["https://example.com/callback"], - "scope": "offline_access", + "scope": "atproto", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none", @@ -73,8 +73,7 @@ Here is an example client metadata. the user during the authentication process. - If you don't want or need the user to stay authenticated for long periods - (better for security), you can remove the `offline_access` scope, and - `refresh_token` from the `grant_types`. + (better for security), you can remove `refresh_token` from the `grant_types`. > [!NOTE] > @@ -168,6 +167,7 @@ ngrok as the `client_id`: Replace the content of the `src/app.ts` file, with the following content: ```typescript +import { Agent } from '@atproto/api' import { BrowserOAuthClient } from '@atproto/oauth-client-browser' async function main() { @@ -200,19 +200,28 @@ following code: ```typescript const result = await oauthClient.init() -const agent = result?.agent + +if (result) { + if ('state' in result) { + console.log('The user was just redirected back from the authorization page') + } + + console.log(`The user is currently signed in as ${result.session.did}`) +} + +const session = result?.session // TO BE CONTINUED ``` At this point you can detect if the user is already authenticated or not (by -checking if `agent` is `undefined`). +checking if `session` is `undefined`). Let's initiate an authentication flow if the user is not authenticated. Replace the `// TO BE CONTINUED` comment with the following code: ```typescript -if (!agent) { +if (!session) { const handle = prompt('Enter your atproto handle to authenticate') if (!handle) throw new Error('Authentication process canceled by the user') @@ -234,14 +243,16 @@ if (!agent) { // TO BE CONTINUED ``` -At this point in the script, the user **will** be authenticated. API calls can -be made using the `agent`. The `agent` is an instance of a sub-class of the -`Agent` from `@atproto/api`. Let's make a simple call to the API to retrieve the -user's profile. Replace the `// TO BE CONTINUED` comment with the following -code: +At this point in the script, the user **will** be authenticated. Authenticated +API calls can be made using the `session`. The `session` can be used to instantiate the +`Agent` class from `@atproto/api`. Let's make a simple call to the API to +retrieve the user's profile. Replace the `// TO BE CONTINUED` comment with the +following code: ```typescript -if (agent) { +if (session) { + const agent = new Agent(session) + const fetchProfile = async () => { const profile = await agent.getProfile({ actor: agent.did }) return profile.data @@ -263,7 +274,7 @@ if (agent) { document.body.appendChild(logoutBtn) logoutBtn.textContent = 'Logout' logoutBtn.onclick = async () => { - await oauthAgent.signOut() + await session.signOut() window.location.reload() } diff --git a/packages/api/README.md b/packages/api/README.md index fb4607deb1d..3d8fd5e7385 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -35,7 +35,7 @@ manage sessions: #### App password based session management -Username / password based authentication van be performed using the `AtpAgent` +Username / password based authentication can be performed using the `AtpAgent` class. > [!CAUTION] @@ -88,17 +88,21 @@ are available: Lower lever; compatible with most JS engines. Every `@atproto/oauth-client-*` implementation has a different way to obtain an -OAuth based API agent instance. Here is an example restoring a previously -saved session: +`OAuthSession` instance that can be used to instantiate an `Agent` (from +`@atproto/api`). Here is an example restoring a previously saved session: ```typescript +import { Agent } from '@atproto/api' import { OAuthClient } from '@atproto/oauth-client' const oauthClient = new OAuthClient({ // ... }) -const agent = await oauthClient.restore('did:plc:123') +const oauthSession = await oauthClient.restore('did:plc:123') + +// Instantiate the api Agent using an OAuthSession +const agent = new Agent(oauthSession) ``` ### API calls @@ -106,6 +110,10 @@ const agent = await oauthClient.restore('did:plc:123') The agent includes methods for many common operations, including: ```typescript +// The DID of the user currently authenticated (or undefined) +agent.did +agent.accountDid // Throws if the user is not authenticated + // Feeds and content await agent.getTimeline(params, opts) await agent.getAuthorFeed(params, opts) @@ -151,11 +159,13 @@ await agent.updateSeenNotifications() await agent.resolveHandle(params, opts) await agent.updateHandle(params, opts) -// Session management (OAuth based agent instances have a different set of methods) +// Legacy: Session management should be performed through the SessionManager +// rather than the Agent instance. if (agent instanceof AtpAgent) { - await agent.createAccount(params) - await agent.login(params) - await agent.resumeSession(session) + // AtpAgent instances support using different sessions during their lifetime + await agent.createAccount({ ... }) // session a + await agent.login({ ... }) // session b + await agent.resumeSession(savedSession) // session c } ``` diff --git a/packages/api/package.json b/packages/api/package.json index 6c804d7cdbe..ec0e8ee0a90 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.13.1", + "version": "0.13.11", "license": "MIT", "description": "Client library for atproto and Bluesky", "keywords": [ @@ -28,7 +28,8 @@ "@atproto/xrpc": "workspace:^", "await-lock": "^2.2.2", "multiformats": "^9.9.0", - "tlds": "^1.234.0" + "tlds": "^1.234.0", + "zod": "^3.23.8" }, "devDependencies": { "@atproto/lex-cli": "workspace:^", diff --git a/packages/api/src/agent.ts b/packages/api/src/agent.ts index c2e6d27d75b..4f2ce6da015 100644 --- a/packages/api/src/agent.ts +++ b/packages/api/src/agent.ts @@ -1,20 +1,20 @@ import { TID } from '@atproto/common-web' import { AtUri, ensureValidDid } from '@atproto/syntax' -import { - buildFetchHandler, - FetchHandler, - FetchHandlerOptions, -} from '@atproto/xrpc' +import { buildFetchHandler, FetchHandler, XrpcClient } from '@atproto/xrpc' import AwaitLock from 'await-lock' import { AppBskyActorDefs, AppBskyActorProfile, AppBskyFeedPost, AppBskyLabelerDefs, - AtpBaseClient, + AppNS, + ChatNS, ComAtprotoRepoPutRecord, + ComNS, + ToolsNS, } from './client/index' -import { MutedWord } from './client/types/app/bsky/actor/defs' +import { schemas } from './client/lexicons' +import { MutedWord, Nux } from './client/types/app/bsky/actor/defs' import { BSKY_LABELER_DID } from './const' import { interpretLabelValueDefinitions } from './moderation' import { DEFAULT_LABEL_SETTINGS } from './moderation/const/labels' @@ -23,6 +23,7 @@ import { LabelPreference, ModerationPrefs, } from './moderation/types' +import { SessionManager } from './session-manager' import { AtpAgentGlobalOpts, AtprotoServiceType, @@ -39,6 +40,7 @@ import { sanitizeMutedWordValue, savedFeedsToUriArrays, validateSavedFeed, + validateNux, } from './util' const FEED_VIEW_PREF_DEFAULTS = { @@ -68,14 +70,13 @@ export type { FetchHandler } /** * An {@link Agent} is an {@link AtpBaseClient} with the following * additional features: - * - Abstract session management utilities * - AT Protocol labelers configuration utilities * - AT Protocol proxy configuration utilities * - Cloning utilities * - `app.bsky` syntactic sugar * - `com.atproto` syntactic sugar */ -export abstract class Agent extends AtpBaseClient { +export class Agent extends XrpcClient { //#region Static configuration /** @@ -94,8 +95,18 @@ export abstract class Agent extends AtpBaseClient { //#endregion - constructor(fetchHandlerOpts: FetchHandler | FetchHandlerOptions) { - const fetchHandler = buildFetchHandler(fetchHandlerOpts) + com = new ComNS(this) + app = new AppNS(this) + chat = new ChatNS(this) + tools = new ToolsNS(this) + + /** @deprecated use `this` instead */ + get xrpc(): XrpcClient { + return this + } + + constructor(readonly sessionManager: SessionManager) { + const fetchHandler = buildFetchHandler(sessionManager) super((url, init) => { const headers = new Headers(init?.headers) @@ -118,16 +129,20 @@ export abstract class Agent extends AtpBaseClient { ) return fetchHandler(url, { ...init, headers }) - }) + }, schemas) } //#region Cloning utilities - abstract clone(): Agent + clone(): Agent { + return this.copyInto(new Agent(this.sessionManager)) + } copyInto(inst: T): T { inst.configureLabelers(this.labelers) inst.configureProxy(this.proxy ?? null) + inst.clearHeaders() + for (const [key, value] of this.headers) inst.setHeader(key, value) return inst } @@ -185,12 +200,19 @@ export abstract class Agent extends AtpBaseClient { /** * Get the authenticated user's DID, if any. */ - abstract readonly did?: string + get did() { + return this.sessionManager.did + } + + /** @deprecated Use {@link Agent.assertDid} instead */ + get accountDid() { + return this.assertDid + } /** * Get the authenticated user's DID, or throw an error if not authenticated. */ - public get accountDid(): string { + get assertDid(): string { this.assertAuthenticated() return this.did } @@ -549,6 +571,7 @@ export abstract class Agent extends AtpBaseClient { bskyAppState: { queuedNudges: [], activeProgressGuide: undefined, + nuxs: [], }, } const res = await this.app.bsky.actor.getPreferences({}) @@ -653,6 +676,7 @@ export abstract class Agent extends AtpBaseClient { const { $type, ...v } = pref prefs.bskyAppState.queuedNudges = v.queuedNudges || [] prefs.bskyAppState.activeProgressGuide = v.activeProgressGuide + prefs.bskyAppState.nuxs = v.nuxs || [] } } @@ -1353,6 +1377,82 @@ export abstract class Agent extends AtpBaseClient { }) } + /** + * Insert or update a NUX in user prefs + */ + async bskyAppUpsertNux(nux: Nux) { + validateNux(nux) + + await this.updatePreferences((prefs: AppBskyActorDefs.Preferences) => { + let bskyAppStatePref: AppBskyActorDefs.BskyAppStatePref = prefs.findLast( + (pref) => + AppBskyActorDefs.isBskyAppStatePref(pref) && + AppBskyActorDefs.validateBskyAppStatePref(pref).success, + ) + + bskyAppStatePref = bskyAppStatePref || {} + bskyAppStatePref.nuxs = bskyAppStatePref.nuxs || [] + + const existing = bskyAppStatePref.nuxs?.find((n) => { + return n.id === nux.id + }) + + let next: AppBskyActorDefs.Nux + + if (existing) { + next = { + id: existing.id, + completed: nux.completed, + data: nux.data, + expiresAt: nux.expiresAt, + } + } else { + next = nux + } + + // remove duplicates and append + bskyAppStatePref.nuxs = bskyAppStatePref.nuxs + .filter((n) => n.id !== nux.id) + .concat(next) + + return prefs + .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p)) + .concat([ + { + ...bskyAppStatePref, + $type: 'app.bsky.actor.defs#bskyAppStatePref', + }, + ]) + }) + } + + /** + * Removes NUXs from user preferences. + */ + async bskyAppRemoveNuxs(ids: string[]) { + await this.updatePreferences((prefs: AppBskyActorDefs.Preferences) => { + let bskyAppStatePref: AppBskyActorDefs.BskyAppStatePref = prefs.findLast( + (pref) => + AppBskyActorDefs.isBskyAppStatePref(pref) && + AppBskyActorDefs.validateBskyAppStatePref(pref).success, + ) + + bskyAppStatePref = bskyAppStatePref || {} + bskyAppStatePref.nuxs = (bskyAppStatePref.nuxs || []).filter((nux) => { + return !ids.includes(nux.id) + }) + + return prefs + .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p)) + .concat([ + { + ...bskyAppStatePref, + $type: 'app.bsky.actor.defs#bskyAppStatePref', + }, + ]) + }) + } + //- Private methods #prefsLock = new AwaitLock() diff --git a/packages/api/src/atp-agent.ts b/packages/api/src/atp-agent.ts index 1495c9423e3..0d4129dc6d3 100644 --- a/packages/api/src/atp-agent.ts +++ b/packages/api/src/atp-agent.ts @@ -4,16 +4,18 @@ import { Gettable, ResponseType, XRPCError, - combineHeaders, + XrpcClient, errorResponseBody, } from '@atproto/xrpc' import { Agent } from './agent' import { - AtpBaseClient, ComAtprotoServerCreateAccount, ComAtprotoServerCreateSession, ComAtprotoServerGetSession, + ComAtprotoServerNS, } from './client' +import { schemas } from './client/lexicons' +import { SessionManager } from './session-manager' import { AtpAgentLoginOpts, AtpPersistSessionHandler, @@ -32,92 +34,44 @@ export type AtpAgentOptions = { } /** - * An {@link AtpAgent} extends the {@link Agent} abstract class by - * implementing password based session management. + * A wrapper around the {@link Agent} class that uses credential based session + * management. This class also exposes most of the session management methods + * directly. + * + * This class will be deprecated in the near future. Use {@link Agent} directly + * with a {@link CredentialSession} instead: + * + * ```ts + * const session = new CredentialSession({ + * service: new URL('https://example.com'), + * }) + * + * const agent = new Agent(session) + * ``` */ export class AtpAgent extends Agent { - public readonly headers: Map> - public readonly sessionManager: SessionManager - - constructor(options: AtpAgentOptions | SessionManager) { - super(async (url: string, init?: RequestInit): Promise => { - // wait for any active session-refreshes to finish - await this.sessionManager.refreshSessionPromise - - const initialHeaders = combineHeaders(init?.headers, this.headers) - const reqInit = { ...init, headers: initialHeaders } - - const initialUri = new URL(url, this.dispatchUrl) - const initialReq = new Request(initialUri, reqInit) - - const initialToken = this.session?.accessJwt - if (!initialToken || initialReq.headers.has('authorization')) { - return (0, this.sessionManager.fetch)(initialReq) - } - - initialReq.headers.set('authorization', `Bearer ${initialToken}`) - const initialRes = await (0, this.sessionManager.fetch)(initialReq) - - if (!this.session?.refreshJwt) { - return initialRes - } - const isExpiredToken = await isErrorResponse( - initialRes, - [400], - ['ExpiredToken'], - ) - - if (!isExpiredToken) { - return initialRes + readonly sessionManager: CredentialSession + + constructor(options: AtpAgentOptions | CredentialSession) { + const sessionManager = + options instanceof CredentialSession + ? options + : new CredentialSession( + new URL(options.service), + options.fetch, + options.persistSession, + ) + + super(sessionManager) + + // This assignment is already being done in the super constructor, but we + // need to do it here to make TypeScript happy. + this.sessionManager = sessionManager + + if (!(options instanceof CredentialSession) && options.headers) { + for (const [key, value] of options.headers) { + this.setHeader(key, value) } - - try { - await this.sessionManager.refreshSession() - } catch { - return initialRes - } - - if (reqInit?.signal?.aborted) { - return initialRes - } - - // The stream was already consumed. We cannot retry the request. A solution - // would be to tee() the input stream but that would bufferize the entire - // stream in memory which can lead to memory starvation. Instead, we will - // return the original response and let the calling code handle retries. - if (ReadableStream && reqInit.body instanceof ReadableStream) { - return initialRes - } - - // Return initial "ExpiredToken" response if the session was not refreshed. - const updatedToken = this.session?.accessJwt - if (!updatedToken || updatedToken === initialToken) { - return initialRes - } - - // Make sure the initial request is cancelled to avoid leaking resources - // (NodeJS 👀): https://undici.nodejs.org/#/?id=garbage-collection - await initialRes.body?.cancel() - - // We need to re-compute the URI in case the PDS endpoint has changed - const updatedUri = new URL(url, this.dispatchUrl) - const updatedReq = new Request(updatedUri, reqInit) - - updatedReq.headers.set('authorization', `Bearer ${updatedToken}`) - - return await (0, this.sessionManager.fetch)(updatedReq) - }) - - if (options instanceof SessionManager) { - this.headers = new Map() - this.sessionManager = options - } else { - this.headers = new Map(options.headers) - this.sessionManager = new SessionManager( - new URL(options.service), - options.fetch, - options.persistSession, - ) } } @@ -125,36 +79,16 @@ export class AtpAgent extends Agent { return this.copyInto(new AtpAgent(this.sessionManager)) } - copyInto(inst: T): T { - if (inst instanceof AtpAgent) { - for (const [key] of inst.headers) { - inst.unsetHeader(key) - } - for (const [key, value] of this.headers) { - inst.setHeader(key, value) - } - } - return super.copyInto(inst) - } - - setHeader(key: string, value: Gettable): void { - this.headers.set(key.toLowerCase(), value) - } - - unsetHeader(key: string): void { - this.headers.delete(key.toLowerCase()) - } - get session() { return this.sessionManager.session } get hasSession() { - return !!this.session + return this.sessionManager.hasSession } get did() { - return this.session?.did + return this.sessionManager.did } get serviceUrl() { @@ -166,7 +100,7 @@ export class AtpAgent extends Agent { } get dispatchUrl() { - return this.pdsUrl || this.serviceUrl + return this.sessionManager.dispatchUrl } /** @deprecated use {@link serviceUrl} instead */ @@ -186,7 +120,7 @@ export class AtpAgent extends Agent { ) } - /** @deprecated This will be removed in OAuthAtpAgent */ + /** @deprecated use {@link AtpAgent.serviceUrl} instead */ getServiceUrl() { return this.serviceUrl } @@ -216,23 +150,34 @@ export class AtpAgent extends Agent { } /** - * Private class meant to be used by clones of {@link AtpAgent} so they can - * share the same session across multiple instances (with different - * proxying/labelers/headers options). + * Credentials (username / password) based session manager. Instances of this + * class will typically be used as the session manager for an {@link AtpAgent}. + * They can also be used with an {@link XrpcClient}, if you want to use you + * own Lexicons. */ -class SessionManager { +export class CredentialSession implements SessionManager { public pdsUrl?: URL // The PDS URL, driven by the did doc public session?: AtpSessionData public refreshSessionPromise: Promise | undefined /** - * Private {@link AtpBaseClient} used to perform session management API + * Private {@link ComAtprotoServerNS} used to perform session management API * calls on the service endpoint. Calls performed by this agent will not be - * authenticated using the user's session. + * authenticated using the user's session to allow proper manual configuration + * of the headers when performing session management operations. */ - protected api = new AtpBaseClient((url, init) => { - return (0, this.fetch)(new URL(url, this.serviceUrl), init) - }) + protected server = new ComAtprotoServerNS( + // Note that the use of the codegen "schemas" (to instantiate `this.api`), + // as well as the use of `ComAtprotoServerNS` will cause this class to + // reference (way) more code than it actually needs. It is not possible, + // with the current state of the codegen, to generate a client that only + // includes the methods that are actually used by this class. This is a + // known limitation that should be addressed in a future version of the + // codegen. + new XrpcClient((url, init) => { + return (0, this.fetch)(new URL(url, this.serviceUrl), init) + }, schemas), + ) constructor( public readonly serviceUrl: URL, @@ -240,6 +185,18 @@ class SessionManager { protected readonly persistSession?: AtpPersistSessionHandler, ) {} + get did() { + return this.session?.did + } + + get dispatchUrl() { + return this.pdsUrl || this.serviceUrl + } + + get hasSession() { + return !!this.session + } + /** * Sets a WhatWG "fetch()" function to be used for making HTTP requests. */ @@ -247,6 +204,71 @@ class SessionManager { this.fetch = fetch } + async fetchHandler(url: string, init?: RequestInit): Promise { + // wait for any active session-refreshes to finish + await this.refreshSessionPromise + + const initialUri = new URL(url, this.dispatchUrl) + const initialReq = new Request(initialUri, init) + + const initialToken = this.session?.accessJwt + if (!initialToken || initialReq.headers.has('authorization')) { + return (0, this.fetch)(initialReq) + } + + initialReq.headers.set('authorization', `Bearer ${initialToken}`) + const initialRes = await (0, this.fetch)(initialReq) + + if (!this.session?.refreshJwt) { + return initialRes + } + const isExpiredToken = await isErrorResponse( + initialRes, + [400], + ['ExpiredToken'], + ) + + if (!isExpiredToken) { + return initialRes + } + + try { + await this.refreshSession() + } catch { + return initialRes + } + + if (init?.signal?.aborted) { + return initialRes + } + + // The stream was already consumed. We cannot retry the request. A solution + // would be to tee() the input stream but that would bufferize the entire + // stream in memory which can lead to memory starvation. Instead, we will + // return the original response and let the calling code handle retries. + if (ReadableStream && init?.body instanceof ReadableStream) { + return initialRes + } + + // Return initial "ExpiredToken" response if the session was not refreshed. + const updatedToken = this.session?.accessJwt + if (!updatedToken || updatedToken === initialToken) { + return initialRes + } + + // Make sure the initial request is cancelled to avoid leaking resources + // (NodeJS 👀): https://undici.nodejs.org/#/?id=garbage-collection + await initialRes.body?.cancel() + + // We need to re-compute the URI in case the PDS endpoint has changed + const updatedUri = new URL(url, this.dispatchUrl) + const updatedReq = new Request(updatedUri, init) + + updatedReq.headers.set('authorization', `Bearer ${updatedToken}`) + + return await (0, this.fetch)(updatedReq) + } + /** * Create a new account and hydrate its session in this agent. */ @@ -255,7 +277,7 @@ class SessionManager { opts?: ComAtprotoServerCreateAccount.CallOptions, ): Promise { try { - const res = await this.api.com.atproto.server.createAccount(data, opts) + const res = await this.server.createAccount(data, opts) this.session = { accessJwt: res.data.accessJwt, refreshJwt: res.data.refreshJwt, @@ -283,7 +305,7 @@ class SessionManager { opts: AtpAgentLoginOpts, ): Promise { try { - const res = await this.api.com.atproto.server.createSession({ + const res = await this.server.createSession({ identifier: opts.identifier, password: opts.password, authFactorToken: opts.authFactorToken, @@ -312,7 +334,7 @@ class SessionManager { async logout(): Promise { if (this.session) { try { - await this.api.com.atproto.server.deleteSession(undefined, { + await this.server.deleteSession(undefined, { headers: { authorization: `Bearer ${this.session.accessJwt}`, }, @@ -335,7 +357,7 @@ class SessionManager { this.session = session try { - const res = await this.api.com.atproto.server + const res = await this.server .getSession(undefined, { headers: { authorization: `Bearer ${session.accessJwt}` }, }) @@ -346,15 +368,14 @@ class SessionManager { session.refreshJwt ) { try { - const res = await this.api.com.atproto.server.refreshSession( - undefined, - { headers: { authorization: `Bearer ${session.refreshJwt}` } }, - ) + const res = await this.server.refreshSession(undefined, { + headers: { authorization: `Bearer ${session.refreshJwt}` }, + }) session.accessJwt = res.data.accessJwt session.refreshJwt = res.data.refreshJwt - return this.api.com.atproto.server.getSession(undefined, { + return this.server.getSession(undefined, { headers: { authorization: `Bearer ${session.accessJwt}` }, }) } catch { @@ -425,7 +446,7 @@ class SessionManager { } try { - const res = await this.api.com.atproto.server.refreshSession(undefined, { + const res = await this.server.refreshSession(undefined, { headers: { authorization: `Bearer ${this.session.refreshJwt}` }, }) // succeeded, update the session diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 50d054c1339..ef839da17d8 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -32,6 +32,7 @@ import * as ComAtprotoModerationCreateReport from './types/com/atproto/moderatio import * as ComAtprotoModerationDefs from './types/com/atproto/moderation/defs' import * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites' import * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord' +import * as ComAtprotoRepoDefs from './types/com/atproto/repo/defs' 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' @@ -92,10 +93,12 @@ import * as AppBskyActorProfile from './types/app/bsky/actor/profile' import * as AppBskyActorPutPreferences from './types/app/bsky/actor/putPreferences' import * as AppBskyActorSearchActors from './types/app/bsky/actor/searchActors' import * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searchActorsTypeahead' +import * as AppBskyEmbedDefs from './types/app/bsky/embed/defs' import * as AppBskyEmbedExternal from './types/app/bsky/embed/external' import * as AppBskyEmbedImages from './types/app/bsky/embed/images' import * as AppBskyEmbedRecord from './types/app/bsky/embed/record' import * as AppBskyEmbedRecordWithMedia from './types/app/bsky/embed/recordWithMedia' +import * as AppBskyEmbedVideo from './types/app/bsky/embed/video' import * as AppBskyFeedDefs from './types/app/bsky/feed/defs' import * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator' import * as AppBskyFeedGenerator from './types/app/bsky/feed/generator' @@ -110,11 +113,13 @@ import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' import * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed' import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' +import * as AppBskyFeedGetQuotes from './types/app/bsky/feed/getQuotes' import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' import * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds' import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' import * as AppBskyFeedLike from './types/app/bsky/feed/like' import * as AppBskyFeedPost from './types/app/bsky/feed/post' +import * as AppBskyFeedPostgate from './types/app/bsky/feed/postgate' import * as AppBskyFeedRepost from './types/app/bsky/feed/repost' import * as AppBskyFeedSearchPosts from './types/app/bsky/feed/searchPosts' import * as AppBskyFeedSendInteractions from './types/app/bsky/feed/sendInteractions' @@ -161,6 +166,10 @@ import * as AppBskyUnspeccedGetSuggestionsSkeleton from './types/app/bsky/unspec import * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions' import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton' import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton' +import * as AppBskyVideoDefs from './types/app/bsky/video/defs' +import * as AppBskyVideoGetJobStatus from './types/app/bsky/video/getJobStatus' +import * as AppBskyVideoGetUploadLimits from './types/app/bsky/video/getUploadLimits' +import * as AppBskyVideoUploadVideo from './types/app/bsky/video/uploadVideo' import * as ChatBskyActorDeclaration from './types/chat/bsky/actor/declaration' import * as ChatBskyActorDefs from './types/chat/bsky/actor/defs' import * as ChatBskyActorDeleteAccount from './types/chat/bsky/actor/deleteAccount' @@ -190,11 +199,17 @@ import * as ToolsOzoneModerationDefs from './types/tools/ozone/moderation/defs' import * as ToolsOzoneModerationEmitEvent from './types/tools/ozone/moderation/emitEvent' import * as ToolsOzoneModerationGetEvent from './types/tools/ozone/moderation/getEvent' import * as ToolsOzoneModerationGetRecord from './types/tools/ozone/moderation/getRecord' +import * as ToolsOzoneModerationGetRecords from './types/tools/ozone/moderation/getRecords' import * as ToolsOzoneModerationGetRepo from './types/tools/ozone/moderation/getRepo' +import * as ToolsOzoneModerationGetRepos from './types/tools/ozone/moderation/getRepos' import * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation/queryEvents' import * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses' import * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos' import * as ToolsOzoneServerGetConfig from './types/tools/ozone/server/getConfig' +import * as ToolsOzoneSignatureDefs from './types/tools/ozone/signature/defs' +import * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation' +import * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts' +import * as ToolsOzoneSignatureSearchAccounts from './types/tools/ozone/signature/searchAccounts' import * as ToolsOzoneTeamAddMember from './types/tools/ozone/team/addMember' import * as ToolsOzoneTeamDefs from './types/tools/ozone/team/defs' import * as ToolsOzoneTeamDeleteMember from './types/tools/ozone/team/deleteMember' @@ -229,6 +244,7 @@ export * as ComAtprotoModerationCreateReport from './types/com/atproto/moderatio export * as ComAtprotoModerationDefs from './types/com/atproto/moderation/defs' export * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites' export * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord' +export * as ComAtprotoRepoDefs from './types/com/atproto/repo/defs' 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' @@ -289,10 +305,12 @@ export * as AppBskyActorProfile from './types/app/bsky/actor/profile' export * as AppBskyActorPutPreferences from './types/app/bsky/actor/putPreferences' export * as AppBskyActorSearchActors from './types/app/bsky/actor/searchActors' export * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searchActorsTypeahead' +export * as AppBskyEmbedDefs from './types/app/bsky/embed/defs' export * as AppBskyEmbedExternal from './types/app/bsky/embed/external' export * as AppBskyEmbedImages from './types/app/bsky/embed/images' export * as AppBskyEmbedRecord from './types/app/bsky/embed/record' export * as AppBskyEmbedRecordWithMedia from './types/app/bsky/embed/recordWithMedia' +export * as AppBskyEmbedVideo from './types/app/bsky/embed/video' export * as AppBskyFeedDefs from './types/app/bsky/feed/defs' export * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator' export * as AppBskyFeedGenerator from './types/app/bsky/feed/generator' @@ -307,11 +325,13 @@ export * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' export * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed' export * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' export * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' +export * as AppBskyFeedGetQuotes from './types/app/bsky/feed/getQuotes' export * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' export * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds' export * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' export * as AppBskyFeedLike from './types/app/bsky/feed/like' export * as AppBskyFeedPost from './types/app/bsky/feed/post' +export * as AppBskyFeedPostgate from './types/app/bsky/feed/postgate' export * as AppBskyFeedRepost from './types/app/bsky/feed/repost' export * as AppBskyFeedSearchPosts from './types/app/bsky/feed/searchPosts' export * as AppBskyFeedSendInteractions from './types/app/bsky/feed/sendInteractions' @@ -358,6 +378,10 @@ export * as AppBskyUnspeccedGetSuggestionsSkeleton from './types/app/bsky/unspec export * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions' export * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton' export * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton' +export * as AppBskyVideoDefs from './types/app/bsky/video/defs' +export * as AppBskyVideoGetJobStatus from './types/app/bsky/video/getJobStatus' +export * as AppBskyVideoGetUploadLimits from './types/app/bsky/video/getUploadLimits' +export * as AppBskyVideoUploadVideo from './types/app/bsky/video/uploadVideo' export * as ChatBskyActorDeclaration from './types/chat/bsky/actor/declaration' export * as ChatBskyActorDefs from './types/chat/bsky/actor/defs' export * as ChatBskyActorDeleteAccount from './types/chat/bsky/actor/deleteAccount' @@ -387,11 +411,17 @@ export * as ToolsOzoneModerationDefs from './types/tools/ozone/moderation/defs' export * as ToolsOzoneModerationEmitEvent from './types/tools/ozone/moderation/emitEvent' export * as ToolsOzoneModerationGetEvent from './types/tools/ozone/moderation/getEvent' export * as ToolsOzoneModerationGetRecord from './types/tools/ozone/moderation/getRecord' +export * as ToolsOzoneModerationGetRecords from './types/tools/ozone/moderation/getRecords' export * as ToolsOzoneModerationGetRepo from './types/tools/ozone/moderation/getRepo' +export * as ToolsOzoneModerationGetRepos from './types/tools/ozone/moderation/getRepos' export * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation/queryEvents' export * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses' export * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos' export * as ToolsOzoneServerGetConfig from './types/tools/ozone/server/getConfig' +export * as ToolsOzoneSignatureDefs from './types/tools/ozone/signature/defs' +export * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation' +export * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts' +export * as ToolsOzoneSignatureSearchAccounts from './types/tools/ozone/signature/searchAccounts' export * as ToolsOzoneTeamAddMember from './types/tools/ozone/team/addMember' export * as ToolsOzoneTeamDefs from './types/tools/ozone/team/defs' export * as ToolsOzoneTeamDeleteMember from './types/tools/ozone/team/deleteMember' @@ -844,12 +874,11 @@ export class ComAtprotoRepoNS { params?: ComAtprotoRepoGetRecord.QueryParams, opts?: ComAtprotoRepoGetRecord.CallOptions, ): Promise { - return this._client.call( - 'com.atproto.repo.getRecord', - params, - undefined, - opts, - ) + return this._client + .call('com.atproto.repo.getRecord', params, undefined, opts) + .catch((e) => { + throw ComAtprotoRepoGetRecord.toKnownErr(e) + }) } importRepo( @@ -1417,6 +1446,7 @@ export class AppBskyNS { notification: AppBskyNotificationNS richtext: AppBskyRichtextNS unspecced: AppBskyUnspeccedNS + video: AppBskyVideoNS constructor(client: XrpcClient) { this._client = client @@ -1428,6 +1458,7 @@ export class AppBskyNS { this.notification = new AppBskyNotificationNS(client) this.richtext = new AppBskyRichtextNS(client) this.unspecced = new AppBskyUnspeccedNS(client) + this.video = new AppBskyVideoNS(client) } } @@ -1599,6 +1630,7 @@ export class AppBskyFeedNS { generator: GeneratorRecord like: LikeRecord post: PostRecord + postgate: PostgateRecord repost: RepostRecord threadgate: ThreadgateRecord @@ -1607,6 +1639,7 @@ export class AppBskyFeedNS { this.generator = new GeneratorRecord(client) this.like = new LikeRecord(client) this.post = new PostRecord(client) + this.postgate = new PostgateRecord(client) this.repost = new RepostRecord(client) this.threadgate = new ThreadgateRecord(client) } @@ -1739,6 +1772,13 @@ export class AppBskyFeedNS { return this._client.call('app.bsky.feed.getPosts', params, undefined, opts) } + getQuotes( + params?: AppBskyFeedGetQuotes.QueryParams, + opts?: AppBskyFeedGetQuotes.CallOptions, + ): Promise { + return this._client.call('app.bsky.feed.getQuotes', params, undefined, opts) + } + getRepostedBy( params?: AppBskyFeedGetRepostedBy.QueryParams, opts?: AppBskyFeedGetRepostedBy.CallOptions, @@ -1982,6 +2022,67 @@ export class PostRecord { } } +export class PostgateRecord { + _client: XrpcClient + + constructor(client: XrpcClient) { + this._client = client + } + + async list( + params: Omit, + ): Promise<{ + cursor?: string + records: { uri: string; value: AppBskyFeedPostgate.Record }[] + }> { + const res = await this._client.call('com.atproto.repo.listRecords', { + collection: 'app.bsky.feed.postgate', + ...params, + }) + return res.data + } + + async get( + params: Omit, + ): Promise<{ uri: string; cid: string; value: AppBskyFeedPostgate.Record }> { + const res = await this._client.call('com.atproto.repo.getRecord', { + collection: 'app.bsky.feed.postgate', + ...params, + }) + return res.data + } + + async create( + params: Omit< + ComAtprotoRepoCreateRecord.InputSchema, + 'collection' | 'record' + >, + record: AppBskyFeedPostgate.Record, + headers?: Record, + ): Promise<{ uri: string; cid: string }> { + record.$type = 'app.bsky.feed.postgate' + const res = await this._client.call( + 'com.atproto.repo.createRecord', + undefined, + { collection: 'app.bsky.feed.postgate', ...params, record }, + { encoding: 'application/json', headers }, + ) + return res.data + } + + async delete( + params: Omit, + headers?: Record, + ): Promise { + await this._client.call( + 'com.atproto.repo.deleteRecord', + undefined, + { collection: 'app.bsky.feed.postgate', ...params }, + { headers }, + ) + } +} + export class RepostRecord { _client: XrpcClient @@ -2945,6 +3046,45 @@ export class AppBskyUnspeccedNS { } } +export class AppBskyVideoNS { + _client: XrpcClient + + constructor(client: XrpcClient) { + this._client = client + } + + getJobStatus( + params?: AppBskyVideoGetJobStatus.QueryParams, + opts?: AppBskyVideoGetJobStatus.CallOptions, + ): Promise { + return this._client.call( + 'app.bsky.video.getJobStatus', + params, + undefined, + opts, + ) + } + + getUploadLimits( + params?: AppBskyVideoGetUploadLimits.QueryParams, + opts?: AppBskyVideoGetUploadLimits.CallOptions, + ): Promise { + return this._client.call( + 'app.bsky.video.getUploadLimits', + params, + undefined, + opts, + ) + } + + uploadVideo( + data?: AppBskyVideoUploadVideo.InputSchema, + opts?: AppBskyVideoUploadVideo.CallOptions, + ): Promise { + return this._client.call('app.bsky.video.uploadVideo', opts?.qp, data, opts) + } +} + export class ChatNS { _client: XrpcClient bsky: ChatBskyNS @@ -3264,6 +3404,7 @@ export class ToolsOzoneNS { communication: ToolsOzoneCommunicationNS moderation: ToolsOzoneModerationNS server: ToolsOzoneServerNS + signature: ToolsOzoneSignatureNS team: ToolsOzoneTeamNS constructor(client: XrpcClient) { @@ -3271,6 +3412,7 @@ export class ToolsOzoneNS { this.communication = new ToolsOzoneCommunicationNS(client) this.moderation = new ToolsOzoneModerationNS(client) this.server = new ToolsOzoneServerNS(client) + this.signature = new ToolsOzoneSignatureNS(client) this.team = new ToolsOzoneTeamNS(client) } } @@ -3286,12 +3428,11 @@ export class ToolsOzoneCommunicationNS { data?: ToolsOzoneCommunicationCreateTemplate.InputSchema, opts?: ToolsOzoneCommunicationCreateTemplate.CallOptions, ): Promise { - return this._client.call( - 'tools.ozone.communication.createTemplate', - opts?.qp, - data, - opts, - ) + return this._client + .call('tools.ozone.communication.createTemplate', opts?.qp, data, opts) + .catch((e) => { + throw ToolsOzoneCommunicationCreateTemplate.toKnownErr(e) + }) } deleteTemplate( @@ -3322,12 +3463,11 @@ export class ToolsOzoneCommunicationNS { data?: ToolsOzoneCommunicationUpdateTemplate.InputSchema, opts?: ToolsOzoneCommunicationUpdateTemplate.CallOptions, ): Promise { - return this._client.call( - 'tools.ozone.communication.updateTemplate', - opts?.qp, - data, - opts, - ) + return this._client + .call('tools.ozone.communication.updateTemplate', opts?.qp, data, opts) + .catch((e) => { + throw ToolsOzoneCommunicationUpdateTemplate.toKnownErr(e) + }) } } @@ -3372,6 +3512,18 @@ export class ToolsOzoneModerationNS { }) } + getRecords( + params?: ToolsOzoneModerationGetRecords.QueryParams, + opts?: ToolsOzoneModerationGetRecords.CallOptions, + ): Promise { + return this._client.call( + 'tools.ozone.moderation.getRecords', + params, + undefined, + opts, + ) + } + getRepo( params?: ToolsOzoneModerationGetRepo.QueryParams, opts?: ToolsOzoneModerationGetRepo.CallOptions, @@ -3383,6 +3535,18 @@ export class ToolsOzoneModerationNS { }) } + getRepos( + params?: ToolsOzoneModerationGetRepos.QueryParams, + opts?: ToolsOzoneModerationGetRepos.CallOptions, + ): Promise { + return this._client.call( + 'tools.ozone.moderation.getRepos', + params, + undefined, + opts, + ) + } + queryEvents( params?: ToolsOzoneModerationQueryEvents.QueryParams, opts?: ToolsOzoneModerationQueryEvents.CallOptions, @@ -3440,6 +3604,50 @@ export class ToolsOzoneServerNS { } } +export class ToolsOzoneSignatureNS { + _client: XrpcClient + + constructor(client: XrpcClient) { + this._client = client + } + + findCorrelation( + params?: ToolsOzoneSignatureFindCorrelation.QueryParams, + opts?: ToolsOzoneSignatureFindCorrelation.CallOptions, + ): Promise { + return this._client.call( + 'tools.ozone.signature.findCorrelation', + params, + undefined, + opts, + ) + } + + findRelatedAccounts( + params?: ToolsOzoneSignatureFindRelatedAccounts.QueryParams, + opts?: ToolsOzoneSignatureFindRelatedAccounts.CallOptions, + ): Promise { + return this._client.call( + 'tools.ozone.signature.findRelatedAccounts', + params, + undefined, + opts, + ) + } + + searchAccounts( + params?: ToolsOzoneSignatureSearchAccounts.QueryParams, + opts?: ToolsOzoneSignatureSearchAccounts.CallOptions, + ): Promise { + return this._client.call( + 'tools.ozone.signature.searchAccounts', + params, + undefined, + opts, + ) + } +} + export class ToolsOzoneTeamNS { _client: XrpcClient diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index de8d86f732d..7b373bca624 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -1254,9 +1254,8 @@ export const schemaDict = { }, validate: { type: 'boolean', - default: true, description: - "Can be set to 'false' to skip Lexicon schema validation of record data, for all operations.", + "Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons.", }, writes: { type: 'array', @@ -1279,6 +1278,31 @@ export const schemaDict = { }, }, }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: [], + properties: { + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + results: { + type: 'array', + items: { + type: 'union', + refs: [ + 'lex:com.atproto.repo.applyWrites#createResult', + 'lex:com.atproto.repo.applyWrites#updateResult', + 'lex:com.atproto.repo.applyWrites#deleteResult', + ], + closed: true, + }, + }, + }, + }, + }, errors: [ { name: 'InvalidSwap', @@ -1336,6 +1360,47 @@ export const schemaDict = { }, }, }, + createResult: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, + }, + }, + updateResult: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, + }, + }, + deleteResult: { + type: 'object', + required: [], + properties: {}, + }, }, }, ComAtprotoRepoCreateRecord: { @@ -1370,9 +1435,8 @@ export const schemaDict = { }, validate: { type: 'boolean', - default: true, description: - "Can be set to 'false' to skip Lexicon schema validation of record data.", + "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.", }, record: { type: 'unknown', @@ -1401,6 +1465,14 @@ export const schemaDict = { type: 'string', format: 'cid', }, + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, }, }, }, @@ -1414,6 +1486,25 @@ export const schemaDict = { }, }, }, + ComAtprotoRepoDefs: { + lexicon: 1, + id: 'com.atproto.repo.defs', + defs: { + commitMeta: { + type: 'object', + required: ['cid', 'rev'], + properties: { + cid: { + type: 'string', + format: 'cid', + }, + rev: { + type: 'string', + }, + }, + }, + }, + }, ComAtprotoRepoDeleteRecord: { lexicon: 1, id: 'com.atproto.repo.deleteRecord', @@ -1458,6 +1549,18 @@ export const schemaDict = { }, }, }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + }, + }, + }, errors: [ { name: 'InvalidSwap', @@ -1583,6 +1686,11 @@ export const schemaDict = { }, }, }, + errors: [ + { + name: 'RecordNotFound', + }, + ], }, }, }, @@ -1778,9 +1886,8 @@ export const schemaDict = { }, validate: { type: 'boolean', - default: true, description: - "Can be set to 'false' to skip Lexicon schema validation of record data.", + "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.", }, record: { type: 'unknown', @@ -1815,6 +1922,14 @@ export const schemaDict = { type: 'string', format: 'cid', }, + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, }, }, }, @@ -4080,6 +4195,10 @@ export const schemaDict = { ref: 'lex:com.atproto.label.defs#label', }, }, + pinnedPost: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, }, }, profileAssociated: { @@ -4462,6 +4581,15 @@ export const schemaDict = { maxLength: 100, }, }, + nuxs: { + description: 'Storage for NUXs the user has encountered.', + type: 'array', + maxLength: 100, + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#nux', + }, + }, }, }, bskyAppProgressGuide: { @@ -4476,6 +4604,34 @@ export const schemaDict = { }, }, }, + nux: { + type: 'object', + description: 'A new user experiences (NUX) storage object', + required: ['id', 'completed'], + properties: { + id: { + type: 'string', + maxLength: 100, + }, + completed: { + type: 'boolean', + default: false, + }, + data: { + description: + 'Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.', + type: 'string', + maxLength: 3000, + maxGraphemes: 300, + }, + expiresAt: { + type: 'string', + format: 'datetime', + description: + 'The date and time at which the NUX will expire and should be considered completed.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { @@ -4665,6 +4821,10 @@ export const schemaDict = { type: 'ref', ref: 'lex:com.atproto.repo.strongRef', }, + pinnedPost: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, createdAt: { type: 'string', format: 'datetime', @@ -4796,6 +4956,28 @@ export const schemaDict = { }, }, }, + AppBskyEmbedDefs: { + lexicon: 1, + id: 'app.bsky.embed.defs', + defs: { + aspectRatio: { + type: 'object', + description: + 'width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.', + required: ['width', 'height'], + properties: { + width: { + type: 'integer', + minimum: 1, + }, + height: { + type: 'integer', + minimum: 1, + }, + }, + }, + }, + }, AppBskyEmbedExternal: { lexicon: 1, id: 'app.bsky.embed.external', @@ -4900,23 +5082,7 @@ export const schemaDict = { }, aspectRatio: { type: 'ref', - ref: 'lex:app.bsky.embed.images#aspectRatio', - }, - }, - }, - aspectRatio: { - type: 'object', - description: - 'width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.', - required: ['width', 'height'], - properties: { - width: { - type: 'integer', - minimum: 1, - }, - height: { - type: 'integer', - minimum: 1, + ref: 'lex:app.bsky.embed.defs#aspectRatio', }, }, }, @@ -4957,7 +5123,7 @@ export const schemaDict = { }, aspectRatio: { type: 'ref', - ref: 'lex:app.bsky.embed.images#aspectRatio', + ref: 'lex:app.bsky.embed.defs#aspectRatio', }, }, }, @@ -4989,6 +5155,7 @@ export const schemaDict = { 'lex:app.bsky.embed.record#viewRecord', 'lex:app.bsky.embed.record#viewNotFound', 'lex:app.bsky.embed.record#viewBlocked', + 'lex:app.bsky.embed.record#viewDetached', 'lex:app.bsky.feed.defs#generatorView', 'lex:app.bsky.graph.defs#listView', 'lex:app.bsky.labeler.defs#labelerView', @@ -5033,12 +5200,16 @@ export const schemaDict = { likeCount: { type: 'integer', }, + quoteCount: { + type: 'integer', + }, embeds: { type: 'array', items: { type: 'union', refs: [ 'lex:app.bsky.embed.images#view', + 'lex:app.bsky.embed.video#view', 'lex:app.bsky.embed.external#view', 'lex:app.bsky.embed.record#view', 'lex:app.bsky.embed.recordWithMedia#view', @@ -5083,6 +5254,20 @@ export const schemaDict = { }, }, }, + viewDetached: { + type: 'object', + required: ['uri', 'detached'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + detached: { + type: 'boolean', + const: true, + }, + }, + }, }, }, AppBskyEmbedRecordWithMedia: { @@ -5101,7 +5286,11 @@ export const schemaDict = { }, media: { type: 'union', - refs: ['lex:app.bsky.embed.images', 'lex:app.bsky.embed.external'], + refs: [ + 'lex:app.bsky.embed.images', + 'lex:app.bsky.embed.video', + 'lex:app.bsky.embed.external', + ], }, }, }, @@ -5117,6 +5306,7 @@ export const schemaDict = { type: 'union', refs: [ 'lex:app.bsky.embed.images#view', + 'lex:app.bsky.embed.video#view', 'lex:app.bsky.embed.external#view', ], }, @@ -5124,6 +5314,85 @@ export const schemaDict = { }, }, }, + AppBskyEmbedVideo: { + lexicon: 1, + id: 'app.bsky.embed.video', + description: 'A video embedded in a Bluesky record (eg, a post).', + defs: { + main: { + type: 'object', + required: ['video'], + properties: { + video: { + type: 'blob', + accept: ['video/mp4'], + maxSize: 50000000, + }, + captions: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.embed.video#caption', + }, + maxLength: 20, + }, + alt: { + type: 'string', + description: + 'Alt text description of the video, for accessibility.', + maxGraphemes: 1000, + maxLength: 10000, + }, + aspectRatio: { + type: 'ref', + ref: 'lex:app.bsky.embed.defs#aspectRatio', + }, + }, + }, + caption: { + type: 'object', + required: ['lang', 'file'], + properties: { + lang: { + type: 'string', + format: 'language', + }, + file: { + type: 'blob', + accept: ['text/vtt'], + maxSize: 20000, + }, + }, + }, + view: { + type: 'object', + required: ['cid', 'playlist'], + properties: { + cid: { + type: 'string', + format: 'cid', + }, + playlist: { + type: 'string', + format: 'uri', + }, + thumbnail: { + type: 'string', + format: 'uri', + }, + alt: { + type: 'string', + maxGraphemes: 1000, + maxLength: 10000, + }, + aspectRatio: { + type: 'ref', + ref: 'lex:app.bsky.embed.defs#aspectRatio', + }, + }, + }, + }, + }, AppBskyFeedDefs: { lexicon: 1, id: 'app.bsky.feed.defs', @@ -5151,6 +5420,7 @@ export const schemaDict = { type: 'union', refs: [ 'lex:app.bsky.embed.images#view', + 'lex:app.bsky.embed.video#view', 'lex:app.bsky.embed.external#view', 'lex:app.bsky.embed.record#view', 'lex:app.bsky.embed.recordWithMedia#view', @@ -5165,6 +5435,9 @@ export const schemaDict = { likeCount: { type: 'integer', }, + quoteCount: { + type: 'integer', + }, indexedAt: { type: 'string', format: 'datetime', @@ -5205,6 +5478,12 @@ export const schemaDict = { replyDisabled: { type: 'boolean', }, + embeddingDisabled: { + type: 'boolean', + }, + pinned: { + type: 'boolean', + }, }, }, feedViewPost: { @@ -5221,7 +5500,10 @@ export const schemaDict = { }, reason: { type: 'union', - refs: ['lex:app.bsky.feed.defs#reasonRepost'], + refs: [ + 'lex:app.bsky.feed.defs#reasonRepost', + 'lex:app.bsky.feed.defs#reasonPin', + ], }, feedContext: { type: 'string', @@ -5273,6 +5555,10 @@ export const schemaDict = { }, }, }, + reasonPin: { + type: 'object', + properties: {}, + }, threadViewPost: { type: 'object', required: ['post'], @@ -5430,7 +5716,10 @@ export const schemaDict = { }, reason: { type: 'union', - refs: ['lex:app.bsky.feed.defs#skeletonReasonRepost'], + refs: [ + 'lex:app.bsky.feed.defs#skeletonReasonRepost', + 'lex:app.bsky.feed.defs#skeletonReasonPin', + ], }, feedContext: { type: 'string', @@ -5450,6 +5739,10 @@ export const schemaDict = { }, }, }, + skeletonReasonPin: { + type: 'object', + properties: {}, + }, threadgateView: { type: 'object', properties: { @@ -5728,7 +6021,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Get a list of posts liked by an actor. Does not require auth.', + 'Get a list of posts liked by an actor. Requires auth, actor must be the requesting account.', parameters: { type: 'params', required: ['actor'], @@ -5815,6 +6108,10 @@ export const schemaDict = { ], default: 'posts_with_replies', }, + includePins: { + type: 'boolean', + default: false, + }, }, }, output: { @@ -6227,6 +6524,10 @@ export const schemaDict = { 'lex:app.bsky.feed.defs#blockedPost', ], }, + threadgate: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#threadgateView', + }, }, }, }, @@ -6280,13 +6581,76 @@ export const schemaDict = { }, }, }, - AppBskyFeedGetRepostedBy: { + AppBskyFeedGetQuotes: { lexicon: 1, - id: 'app.bsky.feed.getRepostedBy', + id: 'app.bsky.feed.getQuotes', defs: { main: { type: 'query', - description: 'Get a list of reposts for a given post.', + description: 'Get a list of quotes for a given post.', + parameters: { + type: 'params', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + description: 'Reference (AT-URI) of post record', + }, + cid: { + type: 'string', + format: 'cid', + description: + 'If supplied, filters to quotes of specific version (by CID) of the post record.', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['uri', 'posts'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + cursor: { + type: 'string', + }, + posts: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#postView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyFeedGetRepostedBy: { + lexicon: 1, + id: 'app.bsky.feed.getRepostedBy', + defs: { + main: { + type: 'query', + description: 'Get a list of reposts for a given post.', parameters: { type: 'params', required: ['uri'], @@ -6505,6 +6869,7 @@ export const schemaDict = { type: 'union', refs: [ 'lex:app.bsky.embed.images', + 'lex:app.bsky.embed.video', 'lex:app.bsky.embed.external', 'lex:app.bsky.embed.record', 'lex:app.bsky.embed.recordWithMedia', @@ -6596,6 +6961,56 @@ export const schemaDict = { }, }, }, + AppBskyFeedPostgate: { + lexicon: 1, + id: 'app.bsky.feed.postgate', + defs: { + main: { + type: 'record', + key: 'tid', + description: + 'Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository.', + record: { + type: 'object', + required: ['post', 'createdAt'], + properties: { + createdAt: { + type: 'string', + format: 'datetime', + }, + post: { + type: 'string', + format: 'at-uri', + description: 'Reference (AT-URI) to the post record.', + }, + detachedEmbeddingUris: { + type: 'array', + maxLength: 50, + items: { + type: 'string', + format: 'at-uri', + }, + description: + 'List of AT-URIs embedding this post that the author has detached from.', + }, + embeddingRules: { + type: 'array', + maxLength: 5, + items: { + type: 'union', + refs: ['lex:app.bsky.feed.postgate#disableRule'], + }, + }, + }, + }, + }, + disableRule: { + type: 'object', + description: 'Disables embedding of this post.', + properties: {}, + }, + }, + }, AppBskyFeedRepost: { lexicon: 1, id: 'app.bsky.feed.repost', @@ -6781,7 +7196,7 @@ export const schemaDict = { type: 'record', key: 'tid', 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 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'], @@ -6807,6 +7222,15 @@ export const schemaDict = { type: 'string', format: 'datetime', }, + hiddenReplies: { + type: 'array', + maxLength: 50, + items: { + type: 'string', + format: 'at-uri', + }, + description: 'List of hidden reply URIs.', + }, }, }, }, @@ -7846,6 +8270,12 @@ export const schemaDict = { ref: 'lex:app.bsky.actor.defs#profileView', }, }, + isFallback: { + type: 'boolean', + description: + 'If true, response has fallen-back to generic results, and is not scoped using relativeToDid', + default: false, + }, }, }, }, @@ -8802,6 +9232,12 @@ export const schemaDict = { ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor', }, }, + relativeToDid: { + type: 'string', + format: 'did', + description: + 'DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer.', + }, }, }, }, @@ -9049,6 +9485,138 @@ export const schemaDict = { }, }, }, + AppBskyVideoDefs: { + lexicon: 1, + id: 'app.bsky.video.defs', + defs: { + jobStatus: { + type: 'object', + required: ['jobId', 'did', 'state'], + properties: { + jobId: { + type: 'string', + }, + did: { + type: 'string', + format: 'did', + }, + state: { + type: 'string', + description: + 'The state of the video processing job. All values not listed as a known value indicate that the job is in process.', + knownValues: ['JOB_STATE_COMPLETED', 'JOB_STATE_FAILED'], + }, + progress: { + type: 'integer', + minimum: 0, + maximum: 100, + description: 'Progress within the current processing state.', + }, + blob: { + type: 'blob', + }, + error: { + type: 'string', + }, + message: { + type: 'string', + }, + }, + }, + }, + }, + AppBskyVideoGetJobStatus: { + lexicon: 1, + id: 'app.bsky.video.getJobStatus', + defs: { + main: { + type: 'query', + description: 'Get status details for a video processing job.', + parameters: { + type: 'params', + required: ['jobId'], + properties: { + jobId: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['jobStatus'], + properties: { + jobStatus: { + type: 'ref', + ref: 'lex:app.bsky.video.defs#jobStatus', + }, + }, + }, + }, + }, + }, + }, + AppBskyVideoGetUploadLimits: { + lexicon: 1, + id: 'app.bsky.video.getUploadLimits', + defs: { + main: { + type: 'query', + description: 'Get video upload limits for the authenticated user.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['canUpload'], + properties: { + canUpload: { + type: 'boolean', + }, + remainingDailyVideos: { + type: 'integer', + }, + remainingDailyBytes: { + type: 'integer', + }, + message: { + type: 'string', + }, + error: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + AppBskyVideoUploadVideo: { + lexicon: 1, + id: 'app.bsky.video.uploadVideo', + defs: { + main: { + type: 'procedure', + description: 'Upload a video to be processed then stored on the PDS.', + input: { + encoding: 'video/mp4', + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['jobStatus'], + properties: { + jobStatus: { + type: 'ref', + ref: 'lex:app.bsky.video.defs#jobStatus', + }, + }, + }, + }, + }, + }, + }, ChatBskyActorDeclaration: { lexicon: 1, id: 'chat.bsky.actor.declaration', @@ -9981,6 +10549,11 @@ export const schemaDict = { type: 'string', description: 'Subject of the message, used in emails.', }, + lang: { + type: 'string', + format: 'language', + description: 'Message language.', + }, createdBy: { type: 'string', format: 'did', @@ -9996,6 +10569,11 @@ export const schemaDict = { ref: 'lex:tools.ozone.communication.defs#templateView', }, }, + errors: [ + { + name: 'DuplicateTemplateName', + }, + ], }, }, }, @@ -10034,6 +10612,11 @@ export const schemaDict = { disabled: { type: 'boolean', }, + lang: { + type: 'string', + format: 'language', + description: 'Message language.', + }, lastUpdatedBy: { type: 'string', format: 'did', @@ -10121,6 +10704,11 @@ export const schemaDict = { type: 'string', description: 'Name of the template.', }, + lang: { + type: 'string', + format: 'language', + description: 'Message language.', + }, contentMarkdown: { type: 'string', description: @@ -10148,6 +10736,11 @@ export const schemaDict = { ref: 'lex:tools.ozone.communication.defs#templateView', }, }, + errors: [ + { + name: 'DuplicateTemplateName', + }, + ], }, }, }, @@ -10410,6 +11003,11 @@ export const schemaDict = { description: 'Indicates how long the takedown should be in effect before automatically expiring.', }, + acknowledgeAccountSubjects: { + type: 'boolean', + description: + 'If true, all other reports on content authored by this account will be resolved (acknowledged).', + }, }, }, modEventReverseTakedown: { @@ -10947,6 +11545,7 @@ export const schemaDict = { 'lex:tools.ozone.moderation.defs#modEventMuteReporter', 'lex:tools.ozone.moderation.defs#modEventUnmuteReporter', 'lex:tools.ozone.moderation.defs#modEventReverseTakedown', + 'lex:tools.ozone.moderation.defs#modEventResolveAppeal', 'lex:tools.ozone.moderation.defs#modEventEmail', 'lex:tools.ozone.moderation.defs#modEventTag', ], @@ -11049,6 +11648,49 @@ export const schemaDict = { }, }, }, + ToolsOzoneModerationGetRecords: { + lexicon: 1, + id: 'tools.ozone.moderation.getRecords', + defs: { + main: { + type: 'query', + description: 'Get details about some records.', + parameters: { + type: 'params', + required: ['uris'], + properties: { + uris: { + type: 'array', + maxLength: 100, + items: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['records'], + properties: { + records: { + type: 'array', + items: { + type: 'union', + refs: [ + 'lex:tools.ozone.moderation.defs#recordViewDetail', + 'lex:tools.ozone.moderation.defs#recordViewNotFound', + ], + }, + }, + }, + }, + }, + }, + }, + }, ToolsOzoneModerationGetRepo: { lexicon: 1, id: 'tools.ozone.moderation.getRepo', @@ -11081,6 +11723,49 @@ export const schemaDict = { }, }, }, + ToolsOzoneModerationGetRepos: { + lexicon: 1, + id: 'tools.ozone.moderation.getRepos', + defs: { + main: { + type: 'query', + description: 'Get details about some repositories.', + parameters: { + type: 'params', + required: ['dids'], + properties: { + dids: { + type: 'array', + maxLength: 100, + items: { + type: 'string', + format: 'did', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['repos'], + properties: { + repos: { + type: 'array', + items: { + type: 'union', + refs: [ + 'lex:tools.ozone.moderation.defs#repoViewDetail', + 'lex:tools.ozone.moderation.defs#repoViewNotFound', + ], + }, + }, + }, + }, + }, + }, + }, + }, ToolsOzoneModerationQueryEvents: { lexicon: 1, id: 'tools.ozone.moderation.queryEvents', @@ -11220,9 +11905,15 @@ export const schemaDict = { parameters: { type: 'params', properties: { + includeAllUserRecords: { + type: 'boolean', + description: + "All subjects belonging to the account specified in the 'subject' param will be returned.", + }, subject: { type: 'string', format: 'uri', + description: 'The subject to get the status for.', }, comment: { type: 'string', @@ -11448,6 +12139,181 @@ export const schemaDict = { }, }, }, + ToolsOzoneSignatureDefs: { + lexicon: 1, + id: 'tools.ozone.signature.defs', + defs: { + sigDetail: { + type: 'object', + required: ['property', 'value'], + properties: { + property: { + type: 'string', + }, + value: { + type: 'string', + }, + }, + }, + }, + }, + ToolsOzoneSignatureFindCorrelation: { + lexicon: 1, + id: 'tools.ozone.signature.findCorrelation', + defs: { + main: { + type: 'query', + description: + 'Find all correlated threat signatures between 2 or more accounts.', + parameters: { + type: 'params', + required: ['dids'], + properties: { + dids: { + type: 'array', + items: { + type: 'string', + format: 'did', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['details'], + properties: { + details: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.signature.defs#sigDetail', + }, + }, + }, + }, + }, + }, + }, + }, + ToolsOzoneSignatureFindRelatedAccounts: { + lexicon: 1, + id: 'tools.ozone.signature.findRelatedAccounts', + defs: { + main: { + type: 'query', + description: + 'Get accounts that share some matching threat signatures with the root account.', + parameters: { + type: 'params', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + cursor: { + type: 'string', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['accounts'], + properties: { + cursor: { + type: 'string', + }, + accounts: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.signature.findRelatedAccounts#relatedAccount', + }, + }, + }, + }, + }, + }, + relatedAccount: { + type: 'object', + required: ['account'], + properties: { + account: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#accountView', + }, + similarities: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:tools.ozone.signature.defs#sigDetail', + }, + }, + }, + }, + }, + }, + ToolsOzoneSignatureSearchAccounts: { + lexicon: 1, + id: 'tools.ozone.signature.searchAccounts', + defs: { + main: { + type: 'query', + description: + 'Search for accounts that match one or more threat signature values.', + parameters: { + type: 'params', + required: ['values'], + properties: { + values: { + type: 'array', + items: { + type: 'string', + }, + }, + cursor: { + type: 'string', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['accounts'], + properties: { + cursor: { + type: 'string', + }, + accounts: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#accountView', + }, + }, + }, + }, + }, + }, + }, + }, ToolsOzoneTeamAddMember: { lexicon: 1, id: 'tools.ozone.team.addMember', @@ -11709,6 +12575,7 @@ export const ids = { ComAtprotoModerationDefs: 'com.atproto.moderation.defs', ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites', ComAtprotoRepoCreateRecord: 'com.atproto.repo.createRecord', + ComAtprotoRepoDefs: 'com.atproto.repo.defs', ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord', ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo', ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord', @@ -11774,10 +12641,12 @@ export const ids = { AppBskyActorPutPreferences: 'app.bsky.actor.putPreferences', AppBskyActorSearchActors: 'app.bsky.actor.searchActors', AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead', + AppBskyEmbedDefs: 'app.bsky.embed.defs', AppBskyEmbedExternal: 'app.bsky.embed.external', AppBskyEmbedImages: 'app.bsky.embed.images', AppBskyEmbedRecord: 'app.bsky.embed.record', AppBskyEmbedRecordWithMedia: 'app.bsky.embed.recordWithMedia', + AppBskyEmbedVideo: 'app.bsky.embed.video', AppBskyFeedDefs: 'app.bsky.feed.defs', AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator', AppBskyFeedGenerator: 'app.bsky.feed.generator', @@ -11792,11 +12661,13 @@ export const ids = { AppBskyFeedGetListFeed: 'app.bsky.feed.getListFeed', AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread', AppBskyFeedGetPosts: 'app.bsky.feed.getPosts', + AppBskyFeedGetQuotes: 'app.bsky.feed.getQuotes', AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy', AppBskyFeedGetSuggestedFeeds: 'app.bsky.feed.getSuggestedFeeds', AppBskyFeedGetTimeline: 'app.bsky.feed.getTimeline', AppBskyFeedLike: 'app.bsky.feed.like', AppBskyFeedPost: 'app.bsky.feed.post', + AppBskyFeedPostgate: 'app.bsky.feed.postgate', AppBskyFeedRepost: 'app.bsky.feed.repost', AppBskyFeedSearchPosts: 'app.bsky.feed.searchPosts', AppBskyFeedSendInteractions: 'app.bsky.feed.sendInteractions', @@ -11849,6 +12720,10 @@ export const ids = { AppBskyUnspeccedSearchActorsSkeleton: 'app.bsky.unspecced.searchActorsSkeleton', AppBskyUnspeccedSearchPostsSkeleton: 'app.bsky.unspecced.searchPostsSkeleton', + AppBskyVideoDefs: 'app.bsky.video.defs', + AppBskyVideoGetJobStatus: 'app.bsky.video.getJobStatus', + AppBskyVideoGetUploadLimits: 'app.bsky.video.getUploadLimits', + AppBskyVideoUploadVideo: 'app.bsky.video.uploadVideo', ChatBskyActorDeclaration: 'chat.bsky.actor.declaration', ChatBskyActorDefs: 'chat.bsky.actor.defs', ChatBskyActorDeleteAccount: 'chat.bsky.actor.deleteAccount', @@ -11882,11 +12757,18 @@ export const ids = { ToolsOzoneModerationEmitEvent: 'tools.ozone.moderation.emitEvent', ToolsOzoneModerationGetEvent: 'tools.ozone.moderation.getEvent', ToolsOzoneModerationGetRecord: 'tools.ozone.moderation.getRecord', + ToolsOzoneModerationGetRecords: 'tools.ozone.moderation.getRecords', ToolsOzoneModerationGetRepo: 'tools.ozone.moderation.getRepo', + ToolsOzoneModerationGetRepos: 'tools.ozone.moderation.getRepos', ToolsOzoneModerationQueryEvents: 'tools.ozone.moderation.queryEvents', ToolsOzoneModerationQueryStatuses: 'tools.ozone.moderation.queryStatuses', ToolsOzoneModerationSearchRepos: 'tools.ozone.moderation.searchRepos', ToolsOzoneServerGetConfig: 'tools.ozone.server.getConfig', + ToolsOzoneSignatureDefs: 'tools.ozone.signature.defs', + ToolsOzoneSignatureFindCorrelation: 'tools.ozone.signature.findCorrelation', + ToolsOzoneSignatureFindRelatedAccounts: + 'tools.ozone.signature.findRelatedAccounts', + ToolsOzoneSignatureSearchAccounts: 'tools.ozone.signature.searchAccounts', ToolsOzoneTeamAddMember: 'tools.ozone.team.addMember', ToolsOzoneTeamDefs: 'tools.ozone.team.defs', ToolsOzoneTeamDeleteMember: 'tools.ozone.team.deleteMember', 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 d6c0de137b0..c6392632de0 100644 --- a/packages/api/src/client/types/app/bsky/actor/defs.ts +++ b/packages/api/src/client/types/app/bsky/actor/defs.ts @@ -7,6 +7,7 @@ import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' import * as AppBskyGraphDefs from '../graph/defs' +import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' export interface ProfileViewBasic { did: string @@ -74,6 +75,7 @@ export interface ProfileViewDetailed { createdAt?: string viewer?: ViewerState labels?: ComAtprotoLabelDefs.Label[] + pinnedPost?: ComAtprotoRepoStrongRef.Main [k: string]: unknown } @@ -469,6 +471,8 @@ export interface BskyAppStatePref { activeProgressGuide?: BskyAppProgressGuide /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */ queuedNudges?: string[] + /** Storage for NUXs the user has encountered. */ + nuxs?: Nux[] [k: string]: unknown } @@ -501,3 +505,24 @@ export function isBskyAppProgressGuide(v: unknown): v is BskyAppProgressGuide { export function validateBskyAppProgressGuide(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#bskyAppProgressGuide', v) } + +/** A new user experiences (NUX) storage object */ +export interface Nux { + id: string + completed: boolean + /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */ + data?: string + /** The date and time at which the NUX will expire and should be considered completed. */ + expiresAt?: string + [k: string]: unknown +} + +export function isNux(v: unknown): v is Nux { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.actor.defs#nux' + ) +} + +export function validateNux(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#nux', v) +} 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 1043682d475..c109e93f9ab 100644 --- a/packages/api/src/client/types/app/bsky/actor/profile.ts +++ b/packages/api/src/client/types/app/bsky/actor/profile.ts @@ -20,6 +20,7 @@ export interface Record { | ComAtprotoLabelDefs.SelfLabels | { $type: string; [k: string]: unknown } joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main + pinnedPost?: ComAtprotoRepoStrongRef.Main createdAt?: string [k: string]: unknown } diff --git a/packages/api/src/client/types/app/bsky/embed/defs.ts b/packages/api/src/client/types/app/bsky/embed/defs.ts new file mode 100644 index 00000000000..b7b753d65f6 --- /dev/null +++ b/packages/api/src/client/types/app/bsky/embed/defs.ts @@ -0,0 +1,26 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +/** width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit. */ +export interface AspectRatio { + width: number + height: number + [k: string]: unknown +} + +export function isAspectRatio(v: unknown): v is AspectRatio { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.defs#aspectRatio' + ) +} + +export function validateAspectRatio(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.defs#aspectRatio', v) +} 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 ddfdf4c156c..886ad7c5c5b 100644 --- a/packages/api/src/client/types/app/bsky/embed/images.ts +++ b/packages/api/src/client/types/app/bsky/embed/images.ts @@ -5,6 +5,7 @@ import { ValidationResult, BlobRef } from '@atproto/lexicon' import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' +import * as AppBskyEmbedDefs from './defs' export interface Main { images: Image[] @@ -28,7 +29,7 @@ export interface Image { image: BlobRef /** Alt text description of the image, for accessibility. */ alt: string - aspectRatio?: AspectRatio + aspectRatio?: AppBskyEmbedDefs.AspectRatio [k: string]: unknown } @@ -42,25 +43,6 @@ export function validateImage(v: unknown): ValidationResult { return lexicons.validate('app.bsky.embed.images#image', v) } -/** width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit. */ -export interface AspectRatio { - width: number - height: number - [k: string]: unknown -} - -export function isAspectRatio(v: unknown): v is AspectRatio { - return ( - isObj(v) && - hasProp(v, '$type') && - v.$type === 'app.bsky.embed.images#aspectRatio' - ) -} - -export function validateAspectRatio(v: unknown): ValidationResult { - return lexicons.validate('app.bsky.embed.images#aspectRatio', v) -} - export interface View { images: ViewImage[] [k: string]: unknown @@ -83,7 +65,7 @@ export interface ViewImage { fullsize: string /** Alt text description of the image, for accessibility. */ alt: string - aspectRatio?: AspectRatio + aspectRatio?: AppBskyEmbedDefs.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 0ee45d9c44d..a3744c29246 100644 --- a/packages/api/src/client/types/app/bsky/embed/record.ts +++ b/packages/api/src/client/types/app/bsky/embed/record.ts @@ -12,6 +12,7 @@ import * as AppBskyLabelerDefs from '../labeler/defs' import * as AppBskyActorDefs from '../actor/defs' import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' import * as AppBskyEmbedImages from './images' +import * as AppBskyEmbedVideo from './video' import * as AppBskyEmbedExternal from './external' import * as AppBskyEmbedRecordWithMedia from './recordWithMedia' @@ -38,6 +39,7 @@ export interface View { | ViewRecord | ViewNotFound | ViewBlocked + | ViewDetached | AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView | AppBskyLabelerDefs.LabelerView @@ -66,8 +68,10 @@ export interface ViewRecord { replyCount?: number repostCount?: number likeCount?: number + quoteCount?: number embeds?: ( | AppBskyEmbedImages.View + | AppBskyEmbedVideo.View | AppBskyEmbedExternal.View | View | AppBskyEmbedRecordWithMedia.View @@ -125,3 +129,21 @@ export function isViewBlocked(v: unknown): v is ViewBlocked { export function validateViewBlocked(v: unknown): ValidationResult { return lexicons.validate('app.bsky.embed.record#viewBlocked', v) } + +export interface ViewDetached { + uri: string + detached: true + [k: string]: unknown +} + +export function isViewDetached(v: unknown): v is ViewDetached { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.record#viewDetached' + ) +} + +export function validateViewDetached(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.record#viewDetached', v) +} diff --git a/packages/api/src/client/types/app/bsky/embed/recordWithMedia.ts b/packages/api/src/client/types/app/bsky/embed/recordWithMedia.ts index 000ff6cda16..2b2b3ae6251 100644 --- a/packages/api/src/client/types/app/bsky/embed/recordWithMedia.ts +++ b/packages/api/src/client/types/app/bsky/embed/recordWithMedia.ts @@ -7,12 +7,14 @@ import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' import * as AppBskyEmbedRecord from './record' import * as AppBskyEmbedImages from './images' +import * as AppBskyEmbedVideo from './video' import * as AppBskyEmbedExternal from './external' export interface Main { record: AppBskyEmbedRecord.Main media: | AppBskyEmbedImages.Main + | AppBskyEmbedVideo.Main | AppBskyEmbedExternal.Main | { $type: string; [k: string]: unknown } [k: string]: unknown @@ -35,6 +37,7 @@ export interface View { record: AppBskyEmbedRecord.View media: | AppBskyEmbedImages.View + | AppBskyEmbedVideo.View | AppBskyEmbedExternal.View | { $type: string; [k: string]: unknown } [k: string]: unknown diff --git a/packages/api/src/client/types/app/bsky/embed/video.ts b/packages/api/src/client/types/app/bsky/embed/video.ts new file mode 100644 index 00000000000..2be451b85ed --- /dev/null +++ b/packages/api/src/client/types/app/bsky/embed/video.ts @@ -0,0 +1,67 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as AppBskyEmbedDefs from './defs' + +export interface Main { + video: BlobRef + captions?: Caption[] + /** Alt text description of the video, for accessibility. */ + alt?: string + aspectRatio?: AppBskyEmbedDefs.AspectRatio + [k: string]: unknown +} + +export function isMain(v: unknown): v is Main { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.embed.video#main' || + v.$type === 'app.bsky.embed.video') + ) +} + +export function validateMain(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.video#main', v) +} + +export interface Caption { + lang: string + file: BlobRef + [k: string]: unknown +} + +export function isCaption(v: unknown): v is Caption { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.video#caption' + ) +} + +export function validateCaption(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.video#caption', v) +} + +export interface View { + cid: string + playlist: string + thumbnail?: string + alt?: string + aspectRatio?: AppBskyEmbedDefs.AspectRatio + [k: string]: unknown +} + +export function isView(v: unknown): v is View { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.embed.video#view' + ) +} + +export function validateView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.video#view', v) +} 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 0497b353f2a..bc50067c1da 100644 --- a/packages/api/src/client/types/app/bsky/feed/defs.ts +++ b/packages/api/src/client/types/app/bsky/feed/defs.ts @@ -7,6 +7,7 @@ import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' import * as AppBskyActorDefs from '../actor/defs' import * as AppBskyEmbedImages from '../embed/images' +import * as AppBskyEmbedVideo from '../embed/video' import * as AppBskyEmbedExternal from '../embed/external' import * as AppBskyEmbedRecord from '../embed/record' import * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia' @@ -21,6 +22,7 @@ export interface PostView { record: {} embed?: | AppBskyEmbedImages.View + | AppBskyEmbedVideo.View | AppBskyEmbedExternal.View | AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View @@ -28,6 +30,7 @@ export interface PostView { replyCount?: number repostCount?: number likeCount?: number + quoteCount?: number indexedAt: string viewer?: ViewerState labels?: ComAtprotoLabelDefs.Label[] @@ -51,6 +54,8 @@ export interface ViewerState { like?: string threadMuted?: boolean replyDisabled?: boolean + embeddingDisabled?: boolean + pinned?: boolean [k: string]: unknown } @@ -69,7 +74,7 @@ export function validateViewerState(v: unknown): ValidationResult { export interface FeedViewPost { post: PostView reply?: ReplyRef - reason?: ReasonRepost | { $type: string; [k: string]: unknown } + reason?: ReasonRepost | ReasonPin | { $type: string; [k: string]: unknown } /** Context provided by feed generator that may be passed back alongside interactions. */ feedContext?: string [k: string]: unknown @@ -130,6 +135,22 @@ export function validateReasonRepost(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#reasonRepost', v) } +export interface ReasonPin { + [k: string]: unknown +} + +export function isReasonPin(v: unknown): v is ReasonPin { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#reasonPin' + ) +} + +export function validateReasonPin(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#reasonPin', v) +} + export interface ThreadViewPost { post: PostView parent?: @@ -261,7 +282,10 @@ export function validateGeneratorViewerState(v: unknown): ValidationResult { export interface SkeletonFeedPost { post: string - reason?: SkeletonReasonRepost | { $type: string; [k: string]: unknown } + reason?: + | SkeletonReasonRepost + | SkeletonReasonPin + | { $type: string; [k: string]: unknown } /** Context that will be passed through to client and may be passed to feed generator back alongside interactions. */ feedContext?: string [k: string]: unknown @@ -296,6 +320,22 @@ export function validateSkeletonReasonRepost(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#skeletonReasonRepost', v) } +export interface SkeletonReasonPin { + [k: string]: unknown +} + +export function isSkeletonReasonPin(v: unknown): v is SkeletonReasonPin { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#skeletonReasonPin' + ) +} + +export function validateSkeletonReasonPin(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#skeletonReasonPin', v) +} + export interface ThreadgateView { uri?: string cid?: 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 63f930e857c..77319f28d40 100644 --- a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts @@ -19,6 +19,7 @@ export interface QueryParams { | 'posts_with_media' | 'posts_and_author_threads' | (string & {}) + includePins?: boolean } export type InputSchema = undefined 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 930db4c1766..df012607df1 100644 --- a/packages/api/src/client/types/app/bsky/feed/getPostThread.ts +++ b/packages/api/src/client/types/app/bsky/feed/getPostThread.ts @@ -25,6 +25,7 @@ export interface OutputSchema { | AppBskyFeedDefs.NotFoundPost | AppBskyFeedDefs.BlockedPost | { $type: string; [k: string]: unknown } + threadgate?: AppBskyFeedDefs.ThreadgateView [k: string]: unknown } diff --git a/packages/api/src/client/types/app/bsky/feed/getQuotes.ts b/packages/api/src/client/types/app/bsky/feed/getQuotes.ts new file mode 100644 index 00000000000..aafc487cd43 --- /dev/null +++ b/packages/api/src/client/types/app/bsky/feed/getQuotes.ts @@ -0,0 +1,43 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + /** Reference (AT-URI) of post record */ + uri: string + /** If supplied, filters to quotes of specific version (by CID) of the post record. */ + cid?: string + limit?: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + uri: string + cid?: string + cursor?: string + posts: AppBskyFeedDefs.PostView[] + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/api/src/client/types/app/bsky/feed/post.ts b/packages/api/src/client/types/app/bsky/feed/post.ts index 0de5192af77..e358409c0e0 100644 --- a/packages/api/src/client/types/app/bsky/feed/post.ts +++ b/packages/api/src/client/types/app/bsky/feed/post.ts @@ -7,6 +7,7 @@ import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' import * as AppBskyRichtextFacet from '../richtext/facet' import * as AppBskyEmbedImages from '../embed/images' +import * as AppBskyEmbedVideo from '../embed/video' import * as AppBskyEmbedExternal from '../embed/external' import * as AppBskyEmbedRecord from '../embed/record' import * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia' @@ -23,6 +24,7 @@ export interface Record { reply?: ReplyRef embed?: | AppBskyEmbedImages.Main + | AppBskyEmbedVideo.Main | AppBskyEmbedExternal.Main | AppBskyEmbedRecord.Main | AppBskyEmbedRecordWithMedia.Main diff --git a/packages/api/src/client/types/app/bsky/feed/postgate.ts b/packages/api/src/client/types/app/bsky/feed/postgate.ts new file mode 100644 index 00000000000..aad8496699a --- /dev/null +++ b/packages/api/src/client/types/app/bsky/feed/postgate.ts @@ -0,0 +1,47 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface Record { + createdAt: string + /** Reference (AT-URI) to the post record. */ + post: string + /** List of AT-URIs embedding this post that the author has detached from. */ + detachedEmbeddingUris?: string[] + embeddingRules?: (DisableRule | { $type: string; [k: string]: unknown })[] + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.feed.postgate#main' || + v.$type === 'app.bsky.feed.postgate') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.postgate#main', v) +} + +/** Disables embedding of this post. */ +export interface DisableRule { + [k: string]: unknown +} + +export function isDisableRule(v: unknown): v is DisableRule { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.postgate#disableRule' + ) +} + +export function validateDisableRule(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.postgate#disableRule', v) +} diff --git a/packages/api/src/client/types/app/bsky/feed/threadgate.ts b/packages/api/src/client/types/app/bsky/feed/threadgate.ts index cc8c05a78ec..558027fb8e9 100644 --- a/packages/api/src/client/types/app/bsky/feed/threadgate.ts +++ b/packages/api/src/client/types/app/bsky/feed/threadgate.ts @@ -16,6 +16,8 @@ export interface Record { | { $type: string; [k: string]: unknown } )[] createdAt: string + /** List of hidden reply URIs. */ + hiddenReplies?: string[] [k: string]: unknown } diff --git a/packages/api/src/client/types/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/api/src/client/types/app/bsky/graph/getSuggestedFollowsByActor.ts index 94156e8134b..4747afb19fc 100644 --- a/packages/api/src/client/types/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/api/src/client/types/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -16,6 +16,8 @@ export type InputSchema = undefined export interface OutputSchema { suggestions: AppBskyActorDefs.ProfileView[] + /** If true, response has fallen-back to generic results, and is not scoped using relativeToDid */ + isFallback: boolean [k: string]: unknown } diff --git a/packages/api/src/client/types/app/bsky/unspecced/getSuggestionsSkeleton.ts b/packages/api/src/client/types/app/bsky/unspecced/getSuggestionsSkeleton.ts index 6900ec882ea..6ed519ef212 100644 --- a/packages/api/src/client/types/app/bsky/unspecced/getSuggestionsSkeleton.ts +++ b/packages/api/src/client/types/app/bsky/unspecced/getSuggestionsSkeleton.ts @@ -22,6 +22,8 @@ export type InputSchema = undefined export interface OutputSchema { cursor?: string actors: AppBskyUnspeccedDefs.SkeletonSearchActor[] + /** DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer. */ + relativeToDid?: string [k: string]: unknown } diff --git a/packages/api/src/client/types/app/bsky/video/defs.ts b/packages/api/src/client/types/app/bsky/video/defs.ts new file mode 100644 index 00000000000..a7ec84316b6 --- /dev/null +++ b/packages/api/src/client/types/app/bsky/video/defs.ts @@ -0,0 +1,32 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface JobStatus { + jobId: string + did: string + /** The state of the video processing job. All values not listed as a known value indicate that the job is in process. */ + state: 'JOB_STATE_COMPLETED' | 'JOB_STATE_FAILED' | (string & {}) + /** Progress within the current processing state. */ + progress?: number + blob?: BlobRef + error?: string + message?: string + [k: string]: unknown +} + +export function isJobStatus(v: unknown): v is JobStatus { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.video.defs#jobStatus' + ) +} + +export function validateJobStatus(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.video.defs#jobStatus', v) +} diff --git a/packages/api/src/client/types/app/bsky/video/getJobStatus.ts b/packages/api/src/client/types/app/bsky/video/getJobStatus.ts new file mode 100644 index 00000000000..0e9638311c0 --- /dev/null +++ b/packages/api/src/client/types/app/bsky/video/getJobStatus.ts @@ -0,0 +1,35 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as AppBskyVideoDefs from './defs' + +export interface QueryParams { + jobId: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + jobStatus: AppBskyVideoDefs.JobStatus + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/api/src/client/types/app/bsky/video/getUploadLimits.ts b/packages/api/src/client/types/app/bsky/video/getUploadLimits.ts new file mode 100644 index 00000000000..4a2f13617b3 --- /dev/null +++ b/packages/api/src/client/types/app/bsky/video/getUploadLimits.ts @@ -0,0 +1,36 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, 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 { + canUpload: boolean + remainingDailyVideos?: number + remainingDailyBytes?: number + message?: string + error?: string + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/api/src/client/types/app/bsky/video/uploadVideo.ts b/packages/api/src/client/types/app/bsky/video/uploadVideo.ts new file mode 100644 index 00000000000..f51ba897bbe --- /dev/null +++ b/packages/api/src/client/types/app/bsky/video/uploadVideo.ts @@ -0,0 +1,35 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as AppBskyVideoDefs from './defs' + +export interface QueryParams {} + +export type InputSchema = string | Uint8Array | Blob + +export interface OutputSchema { + jobStatus: AppBskyVideoDefs.JobStatus + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap + qp?: QueryParams + encoding?: 'video/mp4' +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} 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 23023fa8482..edebf199a44 100644 --- a/packages/api/src/client/types/com/atproto/repo/applyWrites.ts +++ b/packages/api/src/client/types/com/atproto/repo/applyWrites.ts @@ -6,13 +6,14 @@ import { ValidationResult, BlobRef } from '@atproto/lexicon' import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' +import * as ComAtprotoRepoDefs from './defs' export interface QueryParams {} export interface InputSchema { /** The handle or DID of the repo (aka, current account). */ repo: string - /** Can be set to 'false' to skip Lexicon schema validation of record data, for all operations. */ + /** Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons. */ 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. */ @@ -20,6 +21,12 @@ export interface InputSchema { [k: string]: unknown } +export interface OutputSchema { + commit?: ComAtprotoRepoDefs.CommitMeta + results?: (CreateResult | UpdateResult | DeleteResult)[] + [k: string]: unknown +} + export interface CallOptions { signal?: AbortSignal headers?: HeadersMap @@ -30,6 +37,7 @@ export interface CallOptions { export interface Response { success: boolean headers: HeadersMap + data: OutputSchema } export class InvalidSwapError extends XRPCError { @@ -104,3 +112,57 @@ export function isDelete(v: unknown): v is Delete { export function validateDelete(v: unknown): ValidationResult { return lexicons.validate('com.atproto.repo.applyWrites#delete', v) } + +export interface CreateResult { + uri: string + cid: string + validationStatus?: 'valid' | 'unknown' | (string & {}) + [k: string]: unknown +} + +export function isCreateResult(v: unknown): v is CreateResult { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.applyWrites#createResult' + ) +} + +export function validateCreateResult(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.applyWrites#createResult', v) +} + +export interface UpdateResult { + uri: string + cid: string + validationStatus?: 'valid' | 'unknown' | (string & {}) + [k: string]: unknown +} + +export function isUpdateResult(v: unknown): v is UpdateResult { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.applyWrites#updateResult' + ) +} + +export function validateUpdateResult(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.applyWrites#updateResult', v) +} + +export interface DeleteResult { + [k: string]: unknown +} + +export function isDeleteResult(v: unknown): v is DeleteResult { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.applyWrites#deleteResult' + ) +} + +export function validateDeleteResult(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.applyWrites#deleteResult', v) +} 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 18bd74e0f5d..b0f921ef986 100644 --- a/packages/api/src/client/types/com/atproto/repo/createRecord.ts +++ b/packages/api/src/client/types/com/atproto/repo/createRecord.ts @@ -6,6 +6,7 @@ import { ValidationResult, BlobRef } from '@atproto/lexicon' import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' +import * as ComAtprotoRepoDefs from './defs' export interface QueryParams {} @@ -16,7 +17,7 @@ export interface InputSchema { collection: string /** The Record Key. */ rkey?: string - /** Can be set to 'false' to skip Lexicon schema validation of record data. */ + /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */ validate?: boolean /** The record itself. Must contain a $type field. */ record: {} @@ -28,6 +29,8 @@ export interface InputSchema { export interface OutputSchema { uri: string cid: string + commit?: ComAtprotoRepoDefs.CommitMeta + validationStatus?: 'valid' | 'unknown' | (string & {}) [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/repo/defs.ts b/packages/api/src/client/types/com/atproto/repo/defs.ts new file mode 100644 index 00000000000..d4dfaceabf3 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/repo/defs.ts @@ -0,0 +1,25 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface CommitMeta { + cid: string + rev: string + [k: string]: unknown +} + +export function isCommitMeta(v: unknown): v is CommitMeta { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.defs#commitMeta' + ) +} + +export function validateCommitMeta(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.defs#commitMeta', v) +} 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 5ce29976c95..ed06cbc83fc 100644 --- a/packages/api/src/client/types/com/atproto/repo/deleteRecord.ts +++ b/packages/api/src/client/types/com/atproto/repo/deleteRecord.ts @@ -6,6 +6,7 @@ import { ValidationResult, BlobRef } from '@atproto/lexicon' import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' +import * as ComAtprotoRepoDefs from './defs' export interface QueryParams {} @@ -23,6 +24,11 @@ export interface InputSchema { [k: string]: unknown } +export interface OutputSchema { + commit?: ComAtprotoRepoDefs.CommitMeta + [k: string]: unknown +} + export interface CallOptions { signal?: AbortSignal headers?: HeadersMap @@ -33,6 +39,7 @@ export interface CallOptions { export interface Response { success: boolean headers: HeadersMap + data: OutputSchema } export class InvalidSwapError extends XRPCError { 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 765a229408b..85013fb7719 100644 --- a/packages/api/src/client/types/com/atproto/repo/getRecord.ts +++ b/packages/api/src/client/types/com/atproto/repo/getRecord.ts @@ -38,6 +38,16 @@ export interface Response { data: OutputSchema } +export class RecordNotFoundError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers, { cause: src }) + } +} + export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'RecordNotFound') return new RecordNotFoundError(e) + } + return e } 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 deb7a65d257..9cadf9a2c4d 100644 --- a/packages/api/src/client/types/com/atproto/repo/putRecord.ts +++ b/packages/api/src/client/types/com/atproto/repo/putRecord.ts @@ -6,6 +6,7 @@ import { ValidationResult, BlobRef } from '@atproto/lexicon' import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' +import * as ComAtprotoRepoDefs from './defs' export interface QueryParams {} @@ -16,7 +17,7 @@ export interface InputSchema { collection: string /** The Record Key. */ rkey: string - /** Can be set to 'false' to skip Lexicon schema validation of record data. */ + /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */ validate?: boolean /** The record to write. */ record: {} @@ -30,6 +31,8 @@ export interface InputSchema { export interface OutputSchema { uri: string cid: string + commit?: ComAtprotoRepoDefs.CommitMeta + validationStatus?: 'valid' | 'unknown' | (string & {}) [k: string]: unknown } diff --git a/packages/api/src/client/types/tools/ozone/communication/createTemplate.ts b/packages/api/src/client/types/tools/ozone/communication/createTemplate.ts index 6931479a734..e7d22b33604 100644 --- a/packages/api/src/client/types/tools/ozone/communication/createTemplate.ts +++ b/packages/api/src/client/types/tools/ozone/communication/createTemplate.ts @@ -17,6 +17,8 @@ export interface InputSchema { contentMarkdown: string /** Subject of the message, used in emails. */ subject: string + /** Message language. */ + lang?: string /** DID of the user who is creating the template. */ createdBy?: string [k: string]: unknown @@ -37,6 +39,17 @@ export interface Response { data: OutputSchema } +export class DuplicateTemplateNameError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers, { cause: src }) + } +} + export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'DuplicateTemplateName') + return new DuplicateTemplateNameError(e) + } + return e } diff --git a/packages/api/src/client/types/tools/ozone/communication/defs.ts b/packages/api/src/client/types/tools/ozone/communication/defs.ts index 9384d63664d..4cf61252d7a 100644 --- a/packages/api/src/client/types/tools/ozone/communication/defs.ts +++ b/packages/api/src/client/types/tools/ozone/communication/defs.ts @@ -15,6 +15,8 @@ export interface TemplateView { /** Subject of the message, used in emails. */ contentMarkdown: string disabled: boolean + /** Message language. */ + lang?: string /** DID of the user who last updated the template. */ lastUpdatedBy: string createdAt: string diff --git a/packages/api/src/client/types/tools/ozone/communication/updateTemplate.ts b/packages/api/src/client/types/tools/ozone/communication/updateTemplate.ts index 6ddf4741017..5d6184ff5f4 100644 --- a/packages/api/src/client/types/tools/ozone/communication/updateTemplate.ts +++ b/packages/api/src/client/types/tools/ozone/communication/updateTemplate.ts @@ -15,6 +15,8 @@ export interface InputSchema { id: string /** Name of the template. */ name?: string + /** Message language. */ + lang?: string /** Content of the template, markdown supported, can contain variable placeholders. */ contentMarkdown?: string /** Subject of the message, used in emails. */ @@ -40,6 +42,17 @@ export interface Response { data: OutputSchema } +export class DuplicateTemplateNameError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers, { cause: src }) + } +} + export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'DuplicateTemplateName') + return new DuplicateTemplateNameError(e) + } + return e } diff --git a/packages/api/src/client/types/tools/ozone/moderation/defs.ts b/packages/api/src/client/types/tools/ozone/moderation/defs.ts index a973c298a7f..1e3d00a0cc7 100644 --- a/packages/api/src/client/types/tools/ozone/moderation/defs.ts +++ b/packages/api/src/client/types/tools/ozone/moderation/defs.ts @@ -162,6 +162,8 @@ export interface ModEventTakedown { comment?: string /** Indicates how long the takedown should be in effect before automatically expiring. */ durationInHours?: number + /** If true, all other reports on content authored by this account will be resolved (acknowledged). */ + acknowledgeAccountSubjects?: boolean [k: string]: unknown } diff --git a/packages/api/src/client/types/tools/ozone/moderation/emitEvent.ts b/packages/api/src/client/types/tools/ozone/moderation/emitEvent.ts index 0781bef97f5..bc3495c5ab7 100644 --- a/packages/api/src/client/types/tools/ozone/moderation/emitEvent.ts +++ b/packages/api/src/client/types/tools/ozone/moderation/emitEvent.ts @@ -25,6 +25,7 @@ export interface InputSchema { | ToolsOzoneModerationDefs.ModEventMuteReporter | ToolsOzoneModerationDefs.ModEventUnmuteReporter | ToolsOzoneModerationDefs.ModEventReverseTakedown + | ToolsOzoneModerationDefs.ModEventResolveAppeal | ToolsOzoneModerationDefs.ModEventEmail | ToolsOzoneModerationDefs.ModEventTag | { $type: string; [k: string]: unknown } diff --git a/packages/api/src/client/types/tools/ozone/moderation/getRecords.ts b/packages/api/src/client/types/tools/ozone/moderation/getRecords.ts new file mode 100644 index 00000000000..1d7b8d669b6 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/moderation/getRecords.ts @@ -0,0 +1,39 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ToolsOzoneModerationDefs from './defs' + +export interface QueryParams { + uris: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + records: ( + | ToolsOzoneModerationDefs.RecordViewDetail + | ToolsOzoneModerationDefs.RecordViewNotFound + | { $type: string; [k: string]: unknown } + )[] + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/api/src/client/types/tools/ozone/moderation/getRepos.ts b/packages/api/src/client/types/tools/ozone/moderation/getRepos.ts new file mode 100644 index 00000000000..17de1d06b35 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/moderation/getRepos.ts @@ -0,0 +1,39 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ToolsOzoneModerationDefs from './defs' + +export interface QueryParams { + dids: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + repos: ( + | ToolsOzoneModerationDefs.RepoViewDetail + | ToolsOzoneModerationDefs.RepoViewNotFound + | { $type: string; [k: string]: unknown } + )[] + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/api/src/client/types/tools/ozone/moderation/queryStatuses.ts b/packages/api/src/client/types/tools/ozone/moderation/queryStatuses.ts index 49cbc21f6e8..ebb80d746ff 100644 --- a/packages/api/src/client/types/tools/ozone/moderation/queryStatuses.ts +++ b/packages/api/src/client/types/tools/ozone/moderation/queryStatuses.ts @@ -9,6 +9,9 @@ import { CID } from 'multiformats/cid' import * as ToolsOzoneModerationDefs from './defs' export interface QueryParams { + /** All subjects belonging to the account specified in the 'subject' param will be returned. */ + includeAllUserRecords?: boolean + /** The subject to get the status for. */ subject?: string /** Search subjects by keyword from comments */ comment?: string diff --git a/packages/api/src/client/types/tools/ozone/signature/defs.ts b/packages/api/src/client/types/tools/ozone/signature/defs.ts new file mode 100644 index 00000000000..1ab8f12a45b --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/signature/defs.ts @@ -0,0 +1,25 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface SigDetail { + property: string + value: string + [k: string]: unknown +} + +export function isSigDetail(v: unknown): v is SigDetail { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.signature.defs#sigDetail' + ) +} + +export function validateSigDetail(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.signature.defs#sigDetail', v) +} diff --git a/packages/api/src/client/types/tools/ozone/signature/findCorrelation.ts b/packages/api/src/client/types/tools/ozone/signature/findCorrelation.ts new file mode 100644 index 00000000000..f04a5c735d2 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/signature/findCorrelation.ts @@ -0,0 +1,35 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ToolsOzoneSignatureDefs from './defs' + +export interface QueryParams { + dids: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + details: ToolsOzoneSignatureDefs.SigDetail[] + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/api/src/client/types/tools/ozone/signature/findRelatedAccounts.ts b/packages/api/src/client/types/tools/ozone/signature/findRelatedAccounts.ts new file mode 100644 index 00000000000..b817bbef60e --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/signature/findRelatedAccounts.ts @@ -0,0 +1,60 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs' +import * as ToolsOzoneSignatureDefs from './defs' + +export interface QueryParams { + did: string + cursor?: string + limit?: number +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + accounts: RelatedAccount[] + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} + +export interface RelatedAccount { + account: ComAtprotoAdminDefs.AccountView + similarities?: ToolsOzoneSignatureDefs.SigDetail[] + [k: string]: unknown +} + +export function isRelatedAccount(v: unknown): v is RelatedAccount { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.signature.findRelatedAccounts#relatedAccount' + ) +} + +export function validateRelatedAccount(v: unknown): ValidationResult { + return lexicons.validate( + 'tools.ozone.signature.findRelatedAccounts#relatedAccount', + v, + ) +} diff --git a/packages/api/src/client/types/tools/ozone/signature/searchAccounts.ts b/packages/api/src/client/types/tools/ozone/signature/searchAccounts.ts new file mode 100644 index 00000000000..70e37b77458 --- /dev/null +++ b/packages/api/src/client/types/tools/ozone/signature/searchAccounts.ts @@ -0,0 +1,38 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { HeadersMap, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs' + +export interface QueryParams { + values: string[] + cursor?: string + limit?: number +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + accounts: ComAtprotoAdminDefs.AccountView[] + [k: string]: unknown +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index b482f66baee..c92323bc88a 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -25,10 +25,13 @@ export { LABELS, DEFAULT_LABEL_SETTINGS } from './moderation/const/labels' export { Agent } from './agent' export { AtpAgent, type AtpAgentOptions } from './atp-agent' +export { CredentialSession } from './atp-agent' export { BskyAgent } from './bsky-agent' -/** @deprecated */ -export { AtpAgent as default } from './atp-agent' +export { + /** @deprecated */ + AtpAgent as default, +} from './atp-agent' // Expose a copy to prevent alteration of the internal Lexicon instance used by // the AtpBaseClient class. diff --git a/packages/api/src/moderation/subjects/post.ts b/packages/api/src/moderation/subjects/post.ts index 5d6e5c765a9..f17ec40293f 100644 --- a/packages/api/src/moderation/subjects/post.ts +++ b/packages/api/src/moderation/subjects/post.ts @@ -321,6 +321,20 @@ function checkMutedWords( } } } + + if (AppBskyEmbedExternal.isView(subject.embed.media)) { + const { external } = subject.embed.media + if ( + hasMutedWord({ + mutedWords, + text: external.title + ' ' + external.description, + languages: [], + actor: embedAuthor, + }) + ) { + return true + } + } } } return false diff --git a/packages/api/src/session-manager.ts b/packages/api/src/session-manager.ts new file mode 100644 index 00000000000..029115947e6 --- /dev/null +++ b/packages/api/src/session-manager.ts @@ -0,0 +1,5 @@ +import { FetchHandlerObject } from '@atproto/xrpc' + +export interface SessionManager extends FetchHandlerObject { + readonly did?: string +} diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index f8878ec1581..582f9a70c15 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -106,5 +106,6 @@ export interface BskyPreferences { bskyAppState: { queuedNudges: string[] activeProgressGuide: AppBskyActorDefs.BskyAppProgressGuide | undefined + nuxs: AppBskyActorDefs.Nux[] } } diff --git a/packages/api/src/util.ts b/packages/api/src/util.ts index 196952ff557..145ffba7fbe 100644 --- a/packages/api/src/util.ts +++ b/packages/api/src/util.ts @@ -1,6 +1,8 @@ import { AtUri } from '@atproto/syntax' import { TID } from '@atproto/common-web' +import zod from 'zod' +import { Nux } from './client/types/app/bsky/actor/defs' import { AppBskyActorDefs } from './client' export function sanitizeMutedWordValue(value: string) { @@ -94,3 +96,16 @@ export const asDid = (value: string): Did => { if (isDid(value)) return value throw new TypeError(`Invalid DID: ${value}`) } + +export const nuxSchema = zod + .object({ + id: zod.string().max(64), + completed: zod.boolean(), + data: zod.string().max(300).optional(), + expiresAt: zod.string().datetime().optional(), + }) + .strict() + +export function validateNux(nux: Nux) { + nuxSchema.parse(nux) +} diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/atp-agent.test.ts similarity index 95% rename from packages/api/tests/bsky-agent.test.ts rename to packages/api/tests/atp-agent.test.ts index 491ade3545f..4da2db2d174 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/atp-agent.test.ts @@ -3,7 +3,7 @@ import { TID } from '@atproto/common-web' import { AppBskyActorDefs, AppBskyActorProfile, - BskyAgent, + AtpAgent, ComAtprotoRepoPutRecord, DEFAULT_LABEL_SETTINGS, } from '../src' @@ -27,7 +27,7 @@ describe('agent', () => { }) const getProfileDisplayName = async ( - agent: BskyAgent, + agent: AtpAgent, ): Promise => { try { const res = await agent.app.bsky.actor.profile.get({ @@ -41,14 +41,14 @@ describe('agent', () => { } it('clones correctly', () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) const agent2 = agent.clone() - expect(agent2 instanceof BskyAgent).toBeTruthy() + expect(agent2 instanceof AtpAgent).toBeTruthy() expect(agent.service).toEqual(agent2.service) }) it('upsertProfile correctly creates and updates profiles.', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user1.test', @@ -80,7 +80,7 @@ describe('agent', () => { }) it('upsertProfile correctly handles CAS failures.', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user2.test', email: 'user2@test.com', @@ -118,7 +118,7 @@ describe('agent', () => { }) it('upsertProfile wont endlessly retry CAS failures.', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user3.test', email: 'user3@test.com', @@ -146,7 +146,7 @@ describe('agent', () => { }) it('upsertProfile validates the record.', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user4.test', email: 'user4@test.com', @@ -163,7 +163,7 @@ describe('agent', () => { describe('app', () => { it('should retrieve the api app', () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) expect(agent.api).toBe(agent) expect(agent.app).toBeDefined() }) @@ -171,70 +171,70 @@ describe('agent', () => { describe('post', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await expect(agent.post({ text: 'foo' })).rejects.toThrow('Not logged in') }) }) describe('deletePost', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await expect(agent.deletePost('foo')).rejects.toThrow('Not logged in') }) }) describe('like', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await expect(agent.like('foo', 'bar')).rejects.toThrow('Not logged in') }) }) describe('deleteLike', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await expect(agent.deleteLike('foo')).rejects.toThrow('Not logged in') }) }) describe('repost', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await expect(agent.repost('foo', 'bar')).rejects.toThrow('Not logged in') }) }) describe('deleteRepost', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await expect(agent.deleteRepost('foo')).rejects.toThrow('Not logged in') }) }) describe('follow', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await expect(agent.follow('foo')).rejects.toThrow('Not logged in') }) }) describe('deleteFollow', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await expect(agent.deleteFollow('foo')).rejects.toThrow('Not logged in') }) }) describe('preferences methods', () => { it('gets and sets preferences correctly', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user5.test', email: 'user5@test.com', password: 'password', }) - const DEFAULT_LABELERS = BskyAgent.appLabelers.map((did) => ({ + const DEFAULT_LABELERS = AtpAgent.appLabelers.map((did) => ({ did, labels: {}, })) @@ -276,6 +276,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -317,6 +318,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -358,6 +360,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -399,6 +402,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -444,6 +448,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -492,6 +497,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -540,6 +546,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -588,6 +595,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -636,6 +644,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -684,6 +693,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -738,6 +748,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -786,6 +797,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -834,6 +846,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -882,6 +895,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -930,6 +944,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -985,6 +1000,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -1040,6 +1056,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -1095,6 +1112,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) @@ -1150,12 +1168,13 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) }) it('resolves duplicates correctly', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user6.test', @@ -1299,7 +1318,7 @@ describe('agent', () => { porn: 'warn', }, labelers: [ - ...BskyAgent.appLabelers.map((did) => ({ did, labels: {} })), + ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })), { did: 'did:plc:first-labeler', labels: {}, @@ -1332,6 +1351,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1356,7 +1376,7 @@ describe('agent', () => { porn: 'warn', }, labelers: [ - ...BskyAgent.appLabelers.map((did) => ({ did, labels: {} })), + ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })), { did: 'did:plc:first-labeler', labels: {}, @@ -1389,6 +1409,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1414,7 +1435,7 @@ describe('agent', () => { porn: 'ignore', }, labelers: [ - ...BskyAgent.appLabelers.map((did) => ({ did, labels: {} })), + ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })), { did: 'did:plc:first-labeler', labels: {}, @@ -1447,6 +1468,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1472,7 +1494,7 @@ describe('agent', () => { porn: 'ignore', }, labelers: [ - ...BskyAgent.appLabelers.map((did) => ({ did, labels: {} })), + ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })), { did: 'did:plc:first-labeler', labels: {}, @@ -1501,6 +1523,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1526,7 +1549,7 @@ describe('agent', () => { porn: 'ignore', }, labelers: [ - ...BskyAgent.appLabelers.map((did) => ({ did, labels: {} })), + ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })), { did: 'did:plc:first-labeler', labels: {}, @@ -1555,6 +1578,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1580,7 +1604,7 @@ describe('agent', () => { porn: 'ignore', }, labelers: [ - ...BskyAgent.appLabelers.map((did) => ({ did, labels: {} })), + ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })), { did: 'did:plc:first-labeler', labels: {}, @@ -1609,6 +1633,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two'], + nuxs: [], }, }) @@ -1646,7 +1671,7 @@ describe('agent', () => { porn: 'ignore', }, labelers: [ - ...BskyAgent.appLabelers.map((did) => ({ did, labels: {} })), + ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })), { did: 'did:plc:first-labeler', labels: {}, @@ -1675,6 +1700,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: ['two', 'three'], + nuxs: [], }, }) @@ -1747,10 +1773,10 @@ describe('agent', () => { }) describe('muted words', () => { - let agent: BskyAgent + let agent: AtpAgent beforeAll(async () => { - agent = new BskyAgent({ service: network.pds.url }) + agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user7.test', email: 'user7@test.com', @@ -2170,10 +2196,10 @@ describe('agent', () => { }) describe('legacy muted words', () => { - let agent: BskyAgent + let agent: AtpAgent async function updatePreferences( - agent: BskyAgent, + agent: AtpAgent, cb: ( prefs: AppBskyActorDefs.Preferences, ) => AppBskyActorDefs.Preferences | false, @@ -2226,7 +2252,7 @@ describe('agent', () => { } beforeAll(async () => { - agent = new BskyAgent({ service: network.pds.url }) + agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user7-1.test', email: 'user7-1@test.com', @@ -2348,11 +2374,11 @@ describe('agent', () => { }) describe('hidden posts', () => { - let agent: BskyAgent + let agent: AtpAgent const postUri = 'at://did:plc:fake/app.bsky.feed.post/fake' beforeAll(async () => { - agent = new BskyAgent({ service: network.pds.url }) + agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user8.test', email: 'user8@test.com', @@ -2385,13 +2411,13 @@ describe('agent', () => { }) describe(`saved feeds v2`, () => { - let agent: BskyAgent + let agent: AtpAgent let i = 0 const feedUri = () => `at://bob.com/app.bsky.feed.generator/${i++}` const listUri = () => `at://bob.com/app.bsky.graph.list/${i++}` beforeAll(async () => { - agent = new BskyAgent({ service: network.pds.url }) + agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user9.test', email: 'user9@test.com', @@ -2859,12 +2885,12 @@ describe('agent', () => { }) describe(`saved feeds v2: migration scenarios`, () => { - let agent: BskyAgent + let agent: AtpAgent let i = 0 const feedUri = () => `at://bob.com/app.bsky.feed.generator/${i++}` beforeAll(async () => { - agent = new BskyAgent({ service: network.pds.url }) + agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user10.test', email: 'user10@test.com', @@ -3179,7 +3205,7 @@ describe('agent', () => { describe('queued nudges', () => { it('queueNudges & dismissNudges', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user11.test', email: 'user11@test.com', @@ -3210,7 +3236,7 @@ describe('agent', () => { describe('guided tours', () => { it('setActiveProgressGuide', async () => { - const agent = new BskyAgent({ service: network.pds.url }) + const agent = new AtpAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user12.test', @@ -3246,6 +3272,71 @@ describe('agent', () => { }) }) + describe('nuxs', () => { + let agent: AtpAgent + + const nux = { + id: 'a', + completed: false, + data: '{}', + expiresAt: new Date(Date.now() + 6e3).toISOString(), + } + + beforeAll(async () => { + agent = new AtpAgent({ service: network.pds.url }) + + await agent.createAccount({ + handle: 'nuxs.test', + email: 'nuxs@test.com', + password: 'password', + }) + }) + + it('bskyAppUpsertNux', async () => { + // never duplicates + await agent.bskyAppUpsertNux(nux) + await agent.bskyAppUpsertNux(nux) + await agent.bskyAppUpsertNux(nux) + + const prefs = await agent.getPreferences() + const nuxs = prefs.bskyAppState.nuxs + + expect(nuxs.length).toEqual(1) + expect(nuxs.find((n) => n.id === nux.id)).toEqual(nux) + }) + + it('bskyAppUpsertNux completed', async () => { + // never duplicates + await agent.bskyAppUpsertNux({ + ...nux, + completed: true, + }) + + const prefs = await agent.getPreferences() + const nuxs = prefs.bskyAppState.nuxs + + expect(nuxs.length).toEqual(1) + expect(nuxs.find((n) => n.id === nux.id)?.completed).toEqual(true) + }) + + it('bskyAppRemoveNuxs', async () => { + await agent.bskyAppRemoveNuxs([nux.id]) + + const prefs = await agent.getPreferences() + const nuxs = prefs.bskyAppState.nuxs + + expect(nuxs.length).toEqual(0) + }) + + it('bskyAppUpsertNux validates nux', async () => { + // @ts-expect-error + expect(() => agent.bskyAppUpsertNux({ name: 'a' })).rejects.toThrow() + expect(() => + agent.bskyAppUpsertNux({ id: 'a', completed: false, foo: 'bar' }), + ).rejects.toThrow() + }) + }) + // end }) }) diff --git a/packages/api/tests/moderation-prefs.test.ts b/packages/api/tests/moderation-prefs.test.ts index a166df30db6..9ce6b58c6f7 100644 --- a/packages/api/tests/moderation-prefs.test.ts +++ b/packages/api/tests/moderation-prefs.test.ts @@ -84,6 +84,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) }) @@ -133,6 +134,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) expect(agent.labelers).toStrictEqual(['did:plc:other']) @@ -167,6 +169,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) expect(agent.labelers).toStrictEqual([]) @@ -223,6 +226,7 @@ describe('agent', () => { bskyAppState: { activeProgressGuide: undefined, queuedNudges: [], + nuxs: [], }, }) }) diff --git a/packages/aws/CHANGELOG.md b/packages/aws/CHANGELOG.md index bb3ff22da5b..eda48ba0314 100644 --- a/packages/aws/CHANGELOG.md +++ b/packages/aws/CHANGELOG.md @@ -1,5 +1,47 @@ # @atproto/aws +## 0.2.7 + +### Patch Changes + +- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13)]: + - @atproto/common@0.4.4 + - @atproto/crypto@0.4.1 + - @atproto/repo@0.5.3 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]: + - @atproto/common@0.4.3 + - @atproto/repo@0.5.2 + - @atproto/crypto@0.4.1 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies [[`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]: + - @atproto/common@0.4.2 + - @atproto/crypto@0.4.1 + - @atproto/repo@0.5.1 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [[`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442)]: + - @atproto/repo@0.5.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [[`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31)]: + - @atproto/crypto@0.4.1 + - @atproto/repo@0.4.3 + ## 0.2.2 ### Patch Changes diff --git a/packages/aws/package.json b/packages/aws/package.json index e46dd2d7cc6..678ce2ce35b 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/aws", - "version": "0.2.2", + "version": "0.2.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 3d275fa9dce..0ecccd9eb0c 100644 --- a/packages/bsky/CHANGELOG.md +++ b/packages/bsky/CHANGELOG.md @@ -1,5 +1,123 @@ # @atproto/bsky +## 0.0.87 + +### Patch Changes + +- Updated dependencies [[`a0531ce42`](https://github.com/bluesky-social/atproto/commit/a0531ce429f5139cb0e2cc19aa9b338599947e44)]: + - @atproto/api@0.13.11 + +## 0.0.86 + +### Patch Changes + +- Updated dependencies [[`df14df522`](https://github.com/bluesky-social/atproto/commit/df14df522bb7986e56ee1f6a0f5d862e1ea6f4d5)]: + - @atproto/api@0.13.10 + +## 0.0.85 + +### Patch Changes + +- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13), [`a2bad977a`](https://github.com/bluesky-social/atproto/commit/a2bad977a8d941b4075ea3ffee3d6f7a0c0f467c)]: + - @atproto/common@0.4.4 + - @atproto/api@0.13.9 + - @atproto/crypto@0.4.1 + - @atproto/repo@0.5.3 + - @atproto/sync@0.1.3 + - @atproto/xrpc-server@0.7.1 + +## 0.0.84 + +### Patch Changes + +- Updated dependencies [[`2676206e4`](https://github.com/bluesky-social/atproto/commit/2676206e422233fefbf2d9d182e8d462f0957c93), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]: + - @atproto/api@0.13.8 + - @atproto/xrpc-server@0.7.0 + - @atproto/lexicon@0.4.2 + - @atproto/common@0.4.3 + - @atproto/identity@0.4.2 + - @atproto/repo@0.5.2 + - @atproto/sync@0.1.2 + - @atproto/crypto@0.4.1 + +## 0.0.83 + +### Patch Changes + +- [#2810](https://github.com/bluesky-social/atproto/pull/2810) [`33aa0c722`](https://github.com/bluesky-social/atproto/commit/33aa0c722226a18215af0ae1833c7c552fc7aaa7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add NUX API + +- Updated dependencies [[`e6bd5aecc`](https://github.com/bluesky-social/atproto/commit/e6bd5aecce7954d60e5fb263297e697ab7aab98e), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`33aa0c722`](https://github.com/bluesky-social/atproto/commit/33aa0c722226a18215af0ae1833c7c552fc7aaa7)]: + - @atproto/api@0.13.7 + - @atproto/common@0.4.2 + - @atproto/xrpc-server@0.6.4 + - @atproto/crypto@0.4.1 + - @atproto/repo@0.5.1 + - @atproto/sync@0.1.1 + +## 0.0.82 + +### Patch Changes + +- Updated dependencies [[`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442), [`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442)]: + - @atproto/sync@0.1.0 + - @atproto/repo@0.5.0 + +## 0.0.81 + +### Patch Changes + +- Updated dependencies [[`e4d41d66f`](https://github.com/bluesky-social/atproto/commit/e4d41d66fa4757a696363f39903562458967b63d)]: + - @atproto/api@0.13.6 + +## 0.0.80 + +### Patch Changes + +- [#2751](https://github.com/bluesky-social/atproto/pull/2751) [`80ada8f47`](https://github.com/bluesky-social/atproto/commit/80ada8f47628f55f3074cd16a52857e98d117e14) Thanks [@devinivy](https://github.com/devinivy)! - Lexicons and support for video embeds within bsky posts. + +- Updated dependencies [[`80ada8f47`](https://github.com/bluesky-social/atproto/commit/80ada8f47628f55f3074cd16a52857e98d117e14)]: + - @atproto/api@0.13.5 + +## 0.0.79 + +### Patch Changes + +- [#2737](https://github.com/bluesky-social/atproto/pull/2737) [`a8e1f9000`](https://github.com/bluesky-social/atproto/commit/a8e1f9000d9617c4df9d9f0e74ae0e0b73fcfd66) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `threadgate: ThreadgateView` to response from `getPostThread` + +- Updated dependencies [[`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`bbca17bc5`](https://github.com/bluesky-social/atproto/commit/bbca17bc5388e0b2af26fb107347c8ab507ee42f), [`a8e1f9000`](https://github.com/bluesky-social/atproto/commit/a8e1f9000d9617c4df9d9f0e74ae0e0b73fcfd66), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]: + - @atproto/xrpc-server@0.6.3 + - @atproto/api@0.13.4 + - @atproto/crypto@0.4.1 + - @atproto/identity@0.4.1 + - @atproto/repo@0.4.3 + +## 0.0.78 + +### Patch Changes + +- [#2735](https://github.com/bluesky-social/atproto/pull/2735) [`4ab248354`](https://github.com/bluesky-social/atproto/commit/4ab2483547d5dabfba88ed4419a4f374bbd7cae7) Thanks [@haileyok](https://github.com/haileyok)! - add `quoteCount` to embed view + +- Updated dependencies [[`4ab248354`](https://github.com/bluesky-social/atproto/commit/4ab2483547d5dabfba88ed4419a4f374bbd7cae7)]: + - @atproto/api@0.13.3 + +## 0.0.77 + +### Patch Changes + +- [#2658](https://github.com/bluesky-social/atproto/pull/2658) [`2a0c088cc`](https://github.com/bluesky-social/atproto/commit/2a0c088cc5d502ca70da9612a261186aa2f2e1fb) Thanks [@haileyok](https://github.com/haileyok)! - Adds `app.bsky.feed.getQuotes` lexicon and handlers + +- [#2675](https://github.com/bluesky-social/atproto/pull/2675) [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds `postgate` records to power quote gating and detached quote posts, plus `hiddenReplies` to the `threadgate` record. + +- Updated dependencies [[`2a0c088cc`](https://github.com/bluesky-social/atproto/commit/2a0c088cc5d502ca70da9612a261186aa2f2e1fb), [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27)]: + - @atproto/api@0.13.2 + +## 0.0.76 + +### Patch Changes + +- Updated dependencies [[`acbacbbd5`](https://github.com/bluesky-social/atproto/commit/acbacbbd5621473b14ee7a6a3132f675806d23b1), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae)]: + - @atproto/xrpc-server@0.6.2 + ## 0.0.75 ### Patch Changes diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 09de8d04795..22c4b35e0fd 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.75", + "version": "0.0.87", "license": "MIT", "description": "Reference implementation of app.bsky App View (Bluesky API)", "keywords": [ @@ -33,6 +33,7 @@ "@atproto/identity": "workspace:^", "@atproto/lexicon": "workspace:^", "@atproto/repo": "workspace:^", + "@atproto/sync": "workspace:^", "@atproto/syntax": "workspace:^", "@atproto/xrpc-server": "workspace:^", "@bufbuild/protobuf": "^1.5.0", @@ -47,6 +48,7 @@ "http-terminator": "^3.2.0", "ioredis": "^5.3.2", "jose": "^5.0.1", + "key-encoder": "^2.0.3", "kysely": "^0.22.0", "multiformats": "^9.9.0", "murmurhash": "^2.0.1", @@ -76,6 +78,7 @@ "@types/qs": "^6.9.7", "axios": "^0.27.2", "jest": "^28.1.2", - "ts-node": "^10.8.2" + "ts-node": "^10.8.2", + "typescript": "^5.4.4" } } diff --git a/packages/bsky/proto/bsky.proto b/packages/bsky/proto/bsky.proto index e0ffb2bdfce..f1f5aa30afd 100644 --- a/packages/bsky/proto/bsky.proto +++ b/packages/bsky/proto/bsky.proto @@ -79,6 +79,9 @@ message PostRecordMeta { bool violates_thread_gate = 1; bool has_media = 2; bool is_reply = 3; + bool violates_embedding_rules = 4; + bool has_post_gate = 5; + bool has_thread_gate = 6; } message GetPostRecordsRequest { @@ -122,6 +125,14 @@ message GetThreadGateRecordsResponse { repeated Record records = 1; } +message GetPostgateRecordsRequest { + repeated string uris = 1; +} + +message GetPostgateRecordsResponse { + repeated Record records = 1; +} + message GetLabelerRecordsRequest { repeated string uris = 1; } @@ -220,6 +231,17 @@ message GetLikesBySubjectSortedResponse { string cursor = 2; } +message GetQuotesBySubjectSortedRequest { + RecordRef subject = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetQuotesBySubjectSortedResponse { + repeated string uris = 1; + string cursor = 2; +} + // - return like uris for user A on subject B, C, D... // - viewer state on posts message GetLikesByActorAndSubjectsRequest { @@ -260,6 +282,7 @@ message GetInteractionCountsResponse { repeated int32 likes = 1; repeated int32 reposts = 2; repeated int32 replies = 3; + repeated int32 quotes = 4; } message GetCountsForUsersRequest { @@ -293,6 +316,15 @@ message GetListCountsResponse { repeated int32 list_items = 1; } +message GetNewUserCountForRangeRequest { + google.protobuf.Timestamp start = 1; + google.protobuf.Timestamp end = 2; +} + +message GetNewUserCountForRangeResponse { + int32 count = 1; +} + // // Reposts // @@ -568,7 +600,6 @@ message GetThreadMutesOnSubjectsResponse { repeated bool muted = 1; } - // // Blocks // @@ -1030,15 +1061,19 @@ message GetRecordTakedownResponse { // Polo-backed Graph Endpoints -// GetFollowsFollowing gets the list of DIDs that the actor follows that also follow the target + + +// GetFollowsFollowing gets the list of DIDs that the actor follows that also follow the targets message GetFollowsFollowingRequest { string actor_did = 1; repeated string target_dids = 2; } + message FollowsFollowing { string target_did = 1; repeated string dids = 2; } + message GetFollowsFollowingResponse { repeated FollowsFollowing results = 1; } @@ -1068,6 +1103,7 @@ service Service { rpc GetActorChatDeclarationRecords(GetActorChatDeclarationRecordsRequest) returns (GetActorChatDeclarationRecordsResponse); rpc GetRepostRecords(GetRepostRecordsRequest) returns (GetRepostRecordsResponse); rpc GetThreadGateRecords(GetThreadGateRecordsRequest) returns (GetThreadGateRecordsResponse); + rpc GetPostgateRecords(GetPostgateRecordsRequest) returns (GetPostgateRecordsResponse); rpc GetLabelerRecords(GetLabelerRecordsRequest) returns (GetLabelerRecordsResponse); rpc GetStarterPackRecords(GetStarterPackRecordsRequest) returns (GetStarterPackRecordsResponse); @@ -1087,11 +1123,15 @@ service Service { rpc GetRepostsByActorAndSubjects(GetRepostsByActorAndSubjectsRequest) returns (GetRepostsByActorAndSubjectsResponse); rpc GetActorReposts(GetActorRepostsRequest) returns (GetActorRepostsResponse); + // Quotes + rpc GetQuotesBySubjectSorted(GetQuotesBySubjectSortedRequest) returns (GetQuotesBySubjectSortedResponse); + // Interaction Counts rpc GetInteractionCounts(GetInteractionCountsRequest) returns (GetInteractionCountsResponse); rpc GetCountsForUsers(GetCountsForUsersRequest) returns (GetCountsForUsersResponse); rpc GetStarterPackCounts(GetStarterPackCountsRequest) returns (GetStarterPackCountsResponse); rpc GetListCounts(GetListCountsRequest) returns (GetListCountsResponse); + rpc GetNewUserCountForRange(GetNewUserCountForRangeRequest) returns (GetNewUserCountForRangeResponse); // Profile rpc GetActors(GetActorsRequest) returns (GetActorsResponse); diff --git a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts index 81fb34e5ea5..22ace2f3a34 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts @@ -10,11 +10,20 @@ import { Hydrator, } from '../../../../hydration/hydrator' import { Views } from '../../../../views' +import { ids } from '../../../../lexicon/lexicons' export default function (server: Server, ctx: AppContext) { const getProfile = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.actor.getProfiles({ - auth: ctx.authVerifier.standardOptional, + auth: ctx.authVerifier.standardOptionalParameterized({ + lxmCheck: (method) => { + if (!method) return false + return ( + method === ids.AppBskyActorGetProfiles || + method.startsWith('chat.bsky.') + ) + }, + }), handler: async ({ auth, params, req }) => { const viewer = auth.credentials.iss const labelers = ctx.reqLabelers(req) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index d3903a57343..897a1c25149 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -13,7 +13,7 @@ import { import { Views } from '../../../../views' import { DataPlaneClient } from '../../../../data-plane' import { parseString } from '../../../../hydration/util' -import { creatorFromUri } from '../../../../views/util' +import { uriToDid as creatorFromUri } from '../../../../util/uris' import { FeedItem } from '../../../../hydration/feed' export default function (server: Server, ctx: AppContext) { diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 55075180ae3..ea353bb0625 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -14,6 +14,7 @@ import { import { Views } from '../../../../views' import { DataPlaneClient } from '../../../../data-plane' import { parseString } from '../../../../hydration/util' +import { uriToDid } from '../../../../util/uris' import { Actor } from '../../../../hydration/actor' import { FeedItem, Post } from '../../../../hydration/feed' import { FeedType } from '../../../../proto/bsky_pb' @@ -79,21 +80,45 @@ export const skeleton = async (inputs: { if (clearlyBadCursor(params.cursor)) { return { actor, filter: params.filter, items: [] } } + + const isFirstPageRequest = !params.cursor + const shouldInsertPinnedPost = + isFirstPageRequest && + params.includePins && + !!actor.profile?.pinnedPost && + uriToDid(actor.profile.pinnedPost.uri) === actor.did + const res = await ctx.dataplane.getAuthorFeed({ actorDid: did, limit: params.limit, cursor: params.cursor, feedType: FILTER_TO_FEED_TYPE[params.filter], }) + + let items: FeedItem[] = res.items.map((item) => ({ + post: { uri: item.uri, cid: item.cid || undefined }, + repost: item.repost + ? { uri: item.repost, cid: item.repostCid || undefined } + : undefined, + })) + + if (shouldInsertPinnedPost && actor.profile?.pinnedPost) { + const pinnedItem = { + post: { + uri: actor.profile.pinnedPost.uri, + cid: actor.profile.pinnedPost.cid, + }, + authorPinned: true, + } + + items = items.filter((item) => item.post.uri !== pinnedItem.post.uri) + items.unshift(pinnedItem) + } + return { actor, filter: params.filter, - items: res.items.map((item) => ({ - post: { uri: item.uri, cid: item.cid || undefined }, - repost: item.repost - ? { uri: item.repost, cid: item.repostCid || undefined } - : undefined, - })), + items, cursor: parseString(res.cursor), } } @@ -147,7 +172,7 @@ const noBlocksOrMutedReposts = (inputs: { skeleton.items = skeleton.items.filter((item) => { return ( checkBlocksAndMutes(item) && - (item.repost || selfThread.ok(item.post.uri)) + (item.repost || item.authorPinned || selfThread.ok(item.post.uri)) ) }) } else { diff --git a/packages/bsky/src/api/app/bsky/feed/getFeed.ts b/packages/bsky/src/api/app/bsky/feed/getFeed.ts index dfc363bee44..398b9ff1340 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -29,6 +29,7 @@ import { unpackIdentityServices, } from '../../../../data-plane' import { resHeaders } from '../../../util' +import { ids } from '../../../../lexicon/lexicons' export default function (server: Server, ctx: AppContext) { const getFeed = createPipeline( @@ -38,7 +39,17 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getFeed({ - auth: ctx.authVerifier.standardOptionalAnyAud, + auth: ctx.authVerifier.standardOptionalParameterized({ + lxmCheck: (method) => { + return ( + method !== undefined && + [ids.AppBskyFeedGetFeedSkeleton, ids.AppBskyFeedGetFeed].includes( + method, + ) + ) + }, + skipAudCheck: true, + }), handler: async ({ params, auth, req }) => { const viewer = auth.credentials.iss const labelers = ctx.reqLabelers(req) diff --git a/packages/bsky/src/api/app/bsky/feed/getLikes.ts b/packages/bsky/src/api/app/bsky/feed/getLikes.ts index bea4531198c..8da529a3ed5 100644 --- a/packages/bsky/src/api/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getLikes.ts @@ -11,7 +11,7 @@ import { } from '../../../../hydration/hydrator' import { Views } from '../../../../views' import { parseString } from '../../../../hydration/util' -import { creatorFromUri } from '../../../../views/util' +import { uriToDid as creatorFromUri } from '../../../../util/uris' import { clearlyBadCursor, resHeaders } from '../../../util' import { InvalidRequestError } from '@atproto/xrpc-server' diff --git a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts index cd7ea89aacf..7d30f3ac67c 100644 --- a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts @@ -7,12 +7,14 @@ import { HydrateCtx, HydrationState, Hydrator, + mergeStates, } 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' +import { uriToDid } from '../../../../util/uris' export default function (server: Server, ctx: AppContext) { const getListFeed = createPipeline( @@ -71,23 +73,34 @@ const hydration = async (inputs: { skeleton: Skeleton }): Promise => { const { ctx, params, skeleton } = inputs - return ctx.hydrator.hydrateFeedItems(skeleton.items, params.hydrateCtx) + const [feedItemsState, bidirectionalBlocks] = await Promise.all([ + ctx.hydrator.hydrateFeedItems(skeleton.items, params.hydrateCtx), + getBlocks({ ctx, params, skeleton }), + ]) + return mergeStates(feedItemsState, { + bidirectionalBlocks, + }) } const noBlocksOrMutes = (inputs: { ctx: Context + params: Params skeleton: Skeleton hydration: HydrationState }): Skeleton => { - const { ctx, skeleton, hydration } = inputs + const { ctx, params, skeleton, hydration } = inputs skeleton.items = skeleton.items.filter((item) => { const bam = ctx.views.feedItemBlocksAndMutes(item, hydration) + const creatorBlocks = hydration.bidirectionalBlocks?.get( + uriToDid(params.list), + ) return ( !bam.authorBlocked && !bam.authorMuted && !bam.originatorBlocked && !bam.originatorMuted && - !bam.ancestorAuthorBlocked + !bam.ancestorAuthorBlocked && + !creatorBlocks?.get(uriToDid(item.post.uri)) ) }) return skeleton @@ -105,6 +118,20 @@ const presentation = (inputs: { return { feed, cursor: skeleton.cursor } } +const getBlocks = async (input: { + ctx: Context + skeleton: Skeleton + params: Params +}) => { + const { ctx, skeleton, params } = input + const pairs: Map = new Map() + pairs.set( + uriToDid(params.list), + skeleton.items.map((item) => uriToDid(item.post.uri)), + ) + return await ctx.hydrator.hydrateBidirectionalBlocks(pairs) +} + type Context = { hydrator: Hydrator views: Views diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 5342cb0d33d..d9d83c1b3e5 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -1,6 +1,10 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { isNotFoundPost } from '../../../../lexicon/types/app/bsky/feed/defs' +import { + isNotFoundPost, + isThreadViewPost, +} from '../../../../lexicon/types/app/bsky/feed/defs' +import { isRecord as isPostRecord } from '../../../../lexicon/types/app/bsky/feed/post' import { QueryParams, OutputSchema, @@ -17,6 +21,7 @@ import { import { HydrateCtx, Hydrator } from '../../../../hydration/hydrator' import { Views } from '../../../../views' import { DataPlaneClient, isDataplaneError, Code } from '../../../../data-plane' +import { postUriToThreadgateUri } from '../../../../util/uris' export default function (server: Server, ctx: AppContext) { const getPostThread = createPipeline( @@ -108,9 +113,19 @@ const presentation = ( }) if (isNotFoundPost(thread)) { // @TODO technically this could be returned as a NotFoundPost based on lexicon - throw new InvalidRequestError(`Post not found: ${params.uri}`, 'NotFound') + throw new InvalidRequestError( + `Post not found: ${skeleton.anchor}`, + 'NotFound', + ) } - return { thread } + const rootUri = + hydration.posts?.get(skeleton.anchor)?.record.reply?.root.uri ?? + skeleton.anchor + const threadgate = ctx.views.threadgate( + postUriToThreadgateUri(rootUri), + hydration, + ) + return { thread, threadgate } } type Context = { diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index 283639a5606..86fa1cc3d72 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -9,13 +9,21 @@ import { Hydrator, } from '../../../../hydration/hydrator' import { Views } from '../../../../views' -import { creatorFromUri } from '../../../../views/util' +import { uriToDid as creatorFromUri } from '../../../../util/uris' import { resHeaders } from '../../../util' +import { ids } from '../../../../lexicon/lexicons' export default function (server: Server, ctx: AppContext) { const getPosts = createPipeline(skeleton, hydration, noBlocks, presentation) server.app.bsky.feed.getPosts({ - auth: ctx.authVerifier.standardOptional, + auth: ctx.authVerifier.standardOptionalParameterized({ + lxmCheck: (method) => { + if (!method) return false + return ( + method === ids.AppBskyFeedGetPosts || method.startsWith('chat.bsky.') + ) + }, + }), handler: async ({ params, auth, req }) => { const viewer = auth.credentials.iss const labelers = ctx.reqLabelers(req) diff --git a/packages/bsky/src/api/app/bsky/feed/getQuotes.ts b/packages/bsky/src/api/app/bsky/feed/getQuotes.ts new file mode 100644 index 00000000000..c38762d9383 --- /dev/null +++ b/packages/bsky/src/api/app/bsky/feed/getQuotes.ts @@ -0,0 +1,112 @@ +import { AtUri } from '@atproto/syntax' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { createPipeline } from '../../../../pipeline' +import { clearlyBadCursor, resHeaders } from '../../../util' +import { + HydrateCtx, + HydrationState, + Hydrator, +} from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { mapDefined } from '@atproto/common' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getQuotes' +import { parseString } from '../../../../hydration/util' +import { uriToDid } from '../../../../util/uris' + +export default function (server: Server, ctx: AppContext) { + const getQuotes = createPipeline(skeleton, hydration, noBlocks, presentation) + server.app.bsky.feed.getQuotes({ + auth: ctx.authVerifier.standardOptional, + handler: async ({ params, auth, req }) => { + const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth) + const labelers = ctx.reqLabelers(req) + const hydrateCtx = await ctx.hydrator.createContext({ + labelers, + viewer, + includeTakedowns, + }) + const result = await getQuotes({ ...params, hydrateCtx }, ctx) + return { + encoding: 'application/json', + body: result, + headers: resHeaders({ labelers: hydrateCtx.labelers }), + } + }, + }) +} + +const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + if (clearlyBadCursor(params.cursor)) { + return { uris: [] } + } + const quotesRes = await ctx.hydrator.dataplane.getQuotesBySubjectSorted({ + subject: { uri: params.uri, cid: params.cid }, + cursor: params.cursor, + limit: params.limit, + }) + return { + uris: quotesRes.uris, + cursor: parseString(quotesRes.cursor), + } +} + +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return await ctx.hydrator.hydratePosts( + skeleton.uris.map((uri) => ({ uri })), + params.hydrateCtx, + ) +} + +const noBlocks = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + skeleton.uris = skeleton.uris.filter((uri) => { + const embedBlock = hydration.postBlocks?.get(uri)?.embed + const authorDid = uriToDid(uri) + return !ctx.views.viewerBlockExists(authorDid, hydration) && !embedBlock + }) + return skeleton +} + +const presentation = (inputs: { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, params, skeleton, hydration } = inputs + const postViews = mapDefined(skeleton.uris, (uri) => { + return ctx.views.post(uri, hydration) + }) + return { + posts: postViews, + cursor: skeleton.cursor, + uri: params.uri, + cid: params.cid, + } +} + +type Context = { + hydrator: Hydrator + views: Views +} + +type Params = QueryParams & { hydrateCtx: HydrateCtx } + +type Skeleton = { + uris: string[] + cursor?: string +} diff --git a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts index a07a554a24b..6055c55798d 100644 --- a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts @@ -10,7 +10,7 @@ import { } from '../../../../hydration/hydrator' import { Views } from '../../../../views' import { parseString } from '../../../../hydration/util' -import { creatorFromUri } from '../../../../views/util' +import { uriToDid as creatorFromUri } from '../../../../util/uris' import { clearlyBadCursor, resHeaders } from '../../../util' export default function (server: Server, ctx: AppContext) { diff --git a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts index cdb796d1a60..853c5453032 100644 --- a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts @@ -14,7 +14,7 @@ import { HydrateCtx, Hydrator } from '../../../../hydration/hydrator' import { Views } from '../../../../views' import { DataPlaneClient } from '../../../../data-plane' import { parseString } from '../../../../hydration/util' -import { creatorFromUri } from '../../../../views/util' +import { uriToDid as creatorFromUri } from '../../../../util/uris' import { resHeaders } from '../../../util' export default function (server: Server, ctx: AppContext) { diff --git a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts index 47ff9ab5068..76a501a3c6e 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts @@ -10,7 +10,7 @@ import { SkeletonFnInput, createPipeline, } from '../../../../pipeline' -import { didFromUri } from '../../../../hydration/util' +import { uriToDid as didFromUri } from '../../../../util/uris' import { HydrateCtx, Hydrator, diff --git a/packages/bsky/src/api/app/bsky/graph/getList.ts b/packages/bsky/src/api/app/bsky/graph/getList.ts index e0b16c952e6..995bc294c6e 100644 --- a/packages/bsky/src/api/app/bsky/graph/getList.ts +++ b/packages/bsky/src/api/app/bsky/graph/getList.ts @@ -19,7 +19,7 @@ import { import { Views } from '../../../../views' import { clearlyBadCursor, resHeaders } from '../../../util' import { ListItemInfo } from '../../../../proto/bsky_pb' -import { didFromUri } from '../../../../hydration/util' +import { uriToDid as didFromUri } from '../../../../util/uris' export default function (server: Server, ctx: AppContext) { const getList = createPipeline(skeleton, hydration, noBlocks, presentation) @@ -70,7 +70,7 @@ const hydration = async ( params.hydrateCtx, ), ]) - const bidirectionalBlocks = await maybeGetBlocksForReferenceList({ + const bidirectionalBlocks = await maybeGetBlocksForReferenceAndCurateList({ ctx, params, skeleton, @@ -106,7 +106,7 @@ const presentation = ( return { list, items, cursor } } -const maybeGetBlocksForReferenceList = async (input: { +const maybeGetBlocksForReferenceAndCurateList = async (input: { ctx: Context listState: HydrationState skeleton: SkeletonState @@ -118,8 +118,8 @@ const maybeGetBlocksForReferenceList = async (input: { const listRecord = listState.lists?.get(list) const creator = didFromUri(list) if ( - listRecord?.record.purpose !== 'app.bsky.graph.defs#referencelist' || - params.hydrateCtx.viewer === creator + params.hydrateCtx.viewer === creator || + listRecord?.record.purpose === 'app.bsky.graph.defs#modlist' ) { return } diff --git a/packages/bsky/src/api/app/bsky/graph/getLists.ts b/packages/bsky/src/api/app/bsky/graph/getLists.ts index 081c64fbc19..31d241710c2 100644 --- a/packages/bsky/src/api/app/bsky/graph/getLists.ts +++ b/packages/bsky/src/api/app/bsky/graph/getLists.ts @@ -1,4 +1,5 @@ import { mapDefined } from '@atproto/common' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getLists' import { REFERENCELIST } from '../../../../lexicon/types/app/bsky/graph/defs' @@ -49,8 +50,12 @@ const skeleton = async ( if (clearlyBadCursor(params.cursor)) { return { listUris: [] } } + + const [did] = await ctx.hydrator.actor.getDids([params.actor]) + if (!did) throw new InvalidRequestError('Profile not found') + const { listUris, cursor } = await ctx.hydrator.dataplane.getActorLists({ - actorDid: params.actor, + actorDid: did, cursor: params.cursor, limit: params.limit, }) diff --git a/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index 61401d91359..ade08e15074 100644 --- a/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -56,19 +56,12 @@ export default function (server: Server, ctx: AppContext) { const skeleton = async (input: SkeletonFnInput) => { const { params, ctx } = input - const gates = ctx.featureGates const [relativeToDid] = await ctx.hydrator.actor.getDids([params.actor]) if (!relativeToDid) { throw new InvalidRequestError('Actor not found') } - if ( - ctx.suggestionsAgent && - gates.check( - await gates.user({ did: params.hydrateCtx.viewer }), - gates.ids.NewSuggestedFollowsByActor, - ) - ) { + if (ctx.suggestionsAgent) { const res = await ctx.suggestionsAgent.api.app.bsky.unspecced.getSuggestionsSkeleton( { @@ -78,6 +71,7 @@ const skeleton = async (input: SkeletonFnInput) => { { headers: params.headers }, ) return { + isFallback: !res.data.relativeToDid, suggestedDids: res.data.actors.map((a) => a.did), headers: res.headers, } @@ -87,6 +81,7 @@ const skeleton = async (input: SkeletonFnInput) => { relativeToDid, }) return { + isFallback: true, suggestedDids: dids, } } @@ -120,7 +115,7 @@ const presentation = ( const suggestions = mapDefined(suggestedDids, (did) => ctx.views.profileDetailed(did, hydration), ) - return { suggestions, headers } + return { isFallback: skeleton.isFallback, suggestions, headers } } type Context = { @@ -136,6 +131,7 @@ type Params = QueryParams & { } type SkeletonState = { + isFallback: boolean suggestedDids: string[] headers?: Record } diff --git a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts index d6d0653cc42..cf2c9674c7f 100644 --- a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts @@ -2,6 +2,7 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/notification/listNotifications' +import { isRecord as isPostRecord } from '../../../../lexicon/types/app/bsky/feed/post' import AppContext from '../../../../context' import { createPipeline, @@ -13,7 +14,7 @@ import { import { HydrateCtx, Hydrator } from '../../../../hydration/hydrator' import { Views } from '../../../../views' import { Notification } from '../../../../proto/bsky_pb' -import { didFromUri } from '../../../../hydration/util' +import { uriToDid as didFromUri } from '../../../../util/uris' import { clearlyBadCursor, resHeaders } from '../../../util' export default function (server: Server, ctx: AppContext) { @@ -90,13 +91,38 @@ const hydration = async ( const noBlockOrMutes = ( input: RulesFnInput, ) => { - const { skeleton, hydration, ctx } = input + const { skeleton, hydration, ctx, params } = input skeleton.notifs = skeleton.notifs.filter((item) => { const did = didFromUri(item.uri) - return ( - !ctx.views.viewerBlockExists(did, hydration) && - !ctx.views.viewerMuteExists(did, hydration) - ) + if ( + ctx.views.viewerBlockExists(did, hydration) || + ctx.views.viewerMuteExists(did, hydration) + ) { + return false + } + // Filter out hidden replies only if the viewer owns + // the threadgate and they hid the reply. + if (item.reason === 'reply') { + const post = hydration.posts?.get(item.uri) + if (post) { + const rootPostUri = isPostRecord(post.record) + ? post.record.reply?.root.uri + : undefined + const isRootPostByViewer = + rootPostUri && didFromUri(rootPostUri) === params.hydrateCtx?.viewer + const isHiddenReply = isRootPostByViewer + ? ctx.views.replyIsHiddenByThreadgate( + item.uri, + rootPostUri, + hydration, + ) + : false + if (isHiddenReply) { + return false + } + } + } + return true }) return skeleton } diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index 3beac0d413b..eb6db24ac60 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -13,6 +13,7 @@ import getPostThread from './app/bsky/feed/getPostThread' import getPosts from './app/bsky/feed/getPosts' import searchPosts from './app/bsky/feed/searchPosts' import getActorLikes from './app/bsky/feed/getActorLikes' +import getQuotes from './app/bsky/feed/getQuotes' import getProfile from './app/bsky/actor/getProfile' import getProfiles from './app/bsky/actor/getProfiles' import getRepostedBy from './app/bsky/feed/getRepostedBy' @@ -72,6 +73,7 @@ export default function (server: Server, ctx: AppContext) { getFeedGenerators(server, ctx) getLikes(server, ctx) getListFeed(server, ctx) + getQuotes(server, ctx) getPostThread(server, ctx) getPosts(server, ctx) searchPosts(server, ctx) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index a9ac7b408b1..ed5d7811cec 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -1,8 +1,12 @@ +import { KeyObject, createPublicKey } from 'node:crypto' import { AuthRequiredError, + parseReqNsid, verifyJwt as verifyServiceJwt, } from '@atproto/xrpc-server' +import KeyEncoder from 'key-encoder' import * as ui8 from 'uint8arrays' +import * as jose from 'jose' import express from 'express' import { Code, @@ -17,6 +21,11 @@ type ReqCtx = { req: express.Request } +type StandardAuthOpts = { + skipAudCheck?: boolean + lxmCheck?: (method?: string) => boolean +} + export enum RoleStatus { Valid, Invalid, @@ -53,11 +62,18 @@ type ModServiceOutput = { } } +const ALLOWED_AUTH_SCOPES = new Set([ + 'com.atproto.access', + 'com.atproto.appPass', + 'com.atproto.appPassPrivileged', +]) + export type AuthVerifierOpts = { ownDid: string alternateAudienceDids: string[] modServiceDid: string adminPasses: string[] + entrywayJwtPublicKey?: KeyObject } export class AuthVerifier { @@ -65,6 +81,7 @@ export class AuthVerifier { public standardAudienceDids: Set public modServiceDid: string private adminPasses: Set + private entrywayJwtPublicKey?: KeyObject constructor( public dataplane: DataPlaneClient, @@ -77,64 +94,70 @@ export class AuthVerifier { ]) this.modServiceDid = opts.modServiceDid this.adminPasses = new Set(opts.adminPasses) + this.entrywayJwtPublicKey = opts.entrywayJwtPublicKey } // verifiers (arrow fns to preserve scope) + standardOptionalParameterized = + (opts: StandardAuthOpts) => + 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 }, + } + } else if (isBearerToken(ctx.req)) { + // @NOTE temporarily accept entryway session tokens to shed load from PDS instances + const token = bearerTokenFromReq(ctx.req) + const header = token ? jose.decodeProtectedHeader(token) : undefined + if (header?.typ === 'at+jwt') { + // we should never use entryway session tokens in the case of flexible auth audiences (namely in the case of getFeed) + if (opts.skipAudCheck) { + throw new AuthRequiredError('Malformed token', 'InvalidToken') + } + return this.entrywaySession(ctx) + } - 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, { + lxmCheck: opts.lxmCheck, + iss: null, + aud: null, + }) + if (!opts.skipAudCheck && !this.standardAudienceDids.has(aud)) { + throw new AuthRequiredError( + 'jwt audience does not match service did', + 'BadJwtAudience', + ) + } + return { + credentials: { + type: 'standard', + iss, + aud, + }, + } + } else { + return this.nullCreds() } } - const { iss, aud } = await this.verifyServiceJwt(ctx, { - aud: null, - iss: null, - }) - if (!this.standardAudienceDids.has(aud)) { - throw new AuthRequiredError( - 'jwt audience does not match service did', - 'BadJwtAudience', - ) - } - return { - credentials: { - type: 'standard', - iss, - aud, - }, - } - } - standardOptional = async ( - ctx: ReqCtx, - ): Promise => { - if (isBearerToken(ctx.req) || isBasicToken(ctx.req)) { - return this.standard(ctx) - } - return this.nullCreds() - } + standardOptional: (ctx: ReqCtx) => Promise = + this.standardOptionalParameterized({}) - standardOptionalAnyAud = async ( - ctx: ReqCtx, - ): Promise => { - if (!isBearerToken(ctx.req)) { - return this.nullCreds() + standard = async (ctx: ReqCtx): Promise => { + const output = await this.standardOptional(ctx) + if (output.credentials.type === 'none') { + throw new AuthRequiredError(undefined, 'AuthMissing') } - const { iss, aud } = await this.verifyServiceJwt(ctx, { - aud: null, - iss: null, - }) - return { credentials: { type: 'standard', iss, aud } } + return output as StandardOutput } role = (ctx: ReqCtx): RoleOutput => { @@ -182,6 +205,54 @@ export class AuthVerifier { } } + // @NOTE this auth verifier method is not recommended to be implemented by most appviews + // this is a short term fix to remove proxy load from Bluesky's PDS and in line with possible + // future plans to have the client talk directly with the appview + entrywaySession = async (reqCtx: ReqCtx): Promise => { + const token = bearerTokenFromReq(reqCtx.req) + if (!token) { + throw new AuthRequiredError(undefined, 'AuthMissing') + } + + // if entryway jwt key not configured then do not parsed these tokens + if (!this.entrywayJwtPublicKey) { + throw new AuthRequiredError('Malformed token', 'InvalidToken') + } + + const res = await jose + .jwtVerify(token, this.entrywayJwtPublicKey) + .catch((err) => { + if (err?.['code'] === 'ERR_JWT_EXPIRED') { + throw new AuthRequiredError('Token has expired', 'ExpiredToken') + } + throw new AuthRequiredError( + 'Token could not be verified', + 'InvalidToken', + ) + }) + + const { sub, aud, scope } = res.payload + if (typeof sub !== 'string' || !sub.startsWith('did:')) { + throw new AuthRequiredError('Malformed token', 'InvalidToken') + } else if ( + typeof aud !== 'string' || + !aud.startsWith('did:web:') || + !aud.endsWith('.bsky.network') + ) { + throw new AuthRequiredError('Bad token aud', 'InvalidToken') + } else if (typeof scope !== 'string' || !ALLOWED_AUTH_SCOPES.has(scope)) { + throw new AuthRequiredError('Bad token scope', 'InvalidToken') + } + + return { + credentials: { + type: 'standard', + aud: this.ownDid, + iss: sub, + }, + } + } + modService = async (reqCtx: ReqCtx): Promise => { const { iss, aud } = await this.verifyServiceJwt(reqCtx, { aud: this.ownDid, @@ -215,7 +286,11 @@ export class AuthVerifier { async verifyServiceJwt( reqCtx: ReqCtx, - opts: { aud: string | null; iss: string[] | null }, + opts: { + iss: string[] | null + aud: string | null + lxmCheck?: (method?: string) => boolean + }, ) { const getSigningKey = async ( iss: string, @@ -243,17 +318,40 @@ export class AuthVerifier { } return didKey } + const assertLxmCheck = () => { + const lxm = parseReqNsid(reqCtx.req) + if ( + (opts.lxmCheck && !opts.lxmCheck(payload.lxm)) || + (!opts.lxmCheck && payload.lxm !== lxm) + ) { + throw new AuthRequiredError( + payload.lxm !== undefined + ? `bad jwt lexicon method ("lxm"). must match: ${lxm}` + : `missing jwt lexicon method ("lxm"). must match: ${lxm}`, + 'BadJwtLexiconMethod', + ) + } + } const jwtStr = bearerTokenFromReq(reqCtx.req) if (!jwtStr) { throw new AuthRequiredError('missing jwt', 'MissingJwt') } + // if validating additional scopes, skip scope check in initial validation & follow up afterwards const payload = await verifyServiceJwt( jwtStr, opts.aud, null, getSigningKey, ) + if ( + !payload.iss.endsWith('#atproto_labeler') || + payload.lxm !== undefined + ) { + // @TODO currently permissive of labelers who dont set lxm yet. + // we'll allow ozone self-hosters to upgrade before removing this condition. + assertLxmCheck() + } return { iss: payload.iss, aud: payload.aud } } @@ -338,3 +436,9 @@ export const buildBasicAuth = (username: string, password: string): string => { ui8.toString(ui8.fromString(`${username}:${password}`, 'utf8'), 'base64pad') ) } + +const keyEncoder = new KeyEncoder('secp256k1') +export const createPublicKeyObject = (publicKeyHex: string): KeyObject => { + const key = keyEncoder.encodePublic(publicKeyHex, 'raw', 'pem') + return createPublicKey({ format: 'pem', key }) +} diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index 3510e180121..3bc3c695657 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -8,6 +8,7 @@ export interface ServerConfigValues { publicUrl?: string serverDid: string alternateAudienceDids: string[] + entrywayJwtPublicKeyHex?: string // external services dataplaneUrls: string[] dataplaneHttpVersion?: '1.1' | '2' @@ -24,6 +25,8 @@ export interface ServerConfigValues { suggestionsUrl?: string suggestionsApiKey?: string cdnUrl?: string + videoPlaylistUrlPattern?: string + videoThumbnailUrlPattern?: string blobRateLimitBypassKey?: string blobRateLimitBypassHostname?: string // identity @@ -54,10 +57,17 @@ export class ServerConfig { const alternateAudienceDids = process.env.BSKY_ALT_AUDIENCE_DIDS ? process.env.BSKY_ALT_AUDIENCE_DIDS.split(',') : [] + const entrywayJwtPublicKeyHex = + process.env.BSKY_ENTRYWAY_JWT_PUBLIC_KEY_HEX || undefined 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 + // e.g. https://video.invalid/watch/%s/%s/playlist.m3u8 + const videoPlaylistUrlPattern = process.env.BSKY_VIDEO_PLAYLIST_URL_PATTERN + // e.g. https://video.invalid/watch/%s/%s/thumbnail.jpg + const videoThumbnailUrlPattern = + process.env.BSKY_VIDEO_THUMBNAIL_URL_PATTERN const blobCacheLocation = process.env.BSKY_BLOB_CACHE_LOC const searchUrl = process.env.BSKY_SEARCH_URL || @@ -119,6 +129,7 @@ export class ServerConfig { publicUrl, serverDid, alternateAudienceDids, + entrywayJwtPublicKeyHex, dataplaneUrls, dataplaneHttpVersion, dataplaneIgnoreBadTls, @@ -129,6 +140,8 @@ export class ServerConfig { labelsFromIssuerDids, handleResolveNameservers, cdnUrl, + videoPlaylistUrlPattern, + videoThumbnailUrlPattern, blobCacheLocation, bsyncUrl, bsyncApiKey, @@ -185,6 +198,10 @@ export class ServerConfig { return this.cfg.alternateAudienceDids } + get entrywayJwtPublicKeyHex() { + return this.cfg.entrywayJwtPublicKeyHex + } + get dataplaneUrls() { return this.cfg.dataplaneUrls } @@ -245,6 +262,14 @@ export class ServerConfig { return this.cfg.cdnUrl } + get videoPlaylistUrlPattern() { + return this.cfg.videoPlaylistUrlPattern + } + + get videoThumbnailUrlPattern() { + return this.cfg.videoThumbnailUrlPattern + } + get blobRateLimitBypassKey() { return this.cfg.blobRateLimitBypassKey } diff --git a/packages/bsky/src/data-plane/client.ts b/packages/bsky/src/data-plane/client.ts index dd525267fe3..11e68a6680c 100644 --- a/packages/bsky/src/data-plane/client.ts +++ b/packages/bsky/src/data-plane/client.ts @@ -34,7 +34,10 @@ export const createDataPlaneClient = ( try { return await client.lib[method.localName](...args) } catch (err) { - if (err instanceof ConnectError && err.code === Code.Unavailable) { + if ( + err instanceof ConnectError && + (err.code === Code.Unavailable || err.code === Code.Aborted) + ) { tries++ error = err remainingClients = getRemainingClients(remainingClients, client) diff --git a/packages/bsky/src/data-plane/server/db/database-schema.ts b/packages/bsky/src/data-plane/server/db/database-schema.ts index c9de76b4857..195b09483e0 100644 --- a/packages/bsky/src/data-plane/server/db/database-schema.ts +++ b/packages/bsky/src/data-plane/server/db/database-schema.ts @@ -6,7 +6,8 @@ import * as post from './tables/post' import * as postEmbed from './tables/post-embed' import * as postAgg from './tables/post-agg' import * as repost from './tables/repost' -import * as threadGate from './tables/thread-gate' +import * as threadgate from './tables/thread-gate' +import * as postgate from './tables/post-gate' import * as feedItem from './tables/feed-item' import * as follow from './tables/follow' import * as like from './tables/like' @@ -35,6 +36,7 @@ import * as taggedSuggestion from './tables/tagged-suggestion' import * as blobTakedown from './tables/blob-takedown' import * as labeler from './tables/labeler' import * as starterPack from './tables/starter-pack' +import * as quote from './tables/quote' export type DatabaseSchemaType = duplicateRecord.PartialDB & profile.PartialDB & @@ -43,7 +45,8 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB & postEmbed.PartialDB & postAgg.PartialDB & repost.PartialDB & - threadGate.PartialDB & + threadgate.PartialDB & + postgate.PartialDB & feedItem.PartialDB & follow.PartialDB & like.PartialDB & @@ -71,7 +74,8 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB & blobTakedown.PartialDB & labeler.PartialDB & starterPack.PartialDB & - taggedSuggestion.PartialDB + taggedSuggestion.PartialDB & + quote.PartialDB export type DatabaseSchema = Kysely diff --git a/packages/bsky/src/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.ts b/packages/bsky/src/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.ts new file mode 100644 index 00000000000..5111dc717f5 --- /dev/null +++ b/packages/bsky/src/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.ts @@ -0,0 +1,12 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('post_agg') + .addColumn('quoteCount', 'bigint', (col) => col.notNull().defaultTo(0)) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.alterTable('post_agg').dropColumn('quoteCount').execute() +} diff --git a/packages/bsky/src/data-plane/server/db/migrations/20240723T220703655Z-quotes.ts b/packages/bsky/src/data-plane/server/db/migrations/20240723T220703655Z-quotes.ts new file mode 100644 index 00000000000..84a734f98c7 --- /dev/null +++ b/packages/bsky/src/data-plane/server/db/migrations/20240723T220703655Z-quotes.ts @@ -0,0 +1,28 @@ +import { Kysely, sql } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('quote') + .addColumn('uri', 'varchar', (col) => col.primaryKey()) + .addColumn('cid', 'varchar', (col) => col.notNull()) + .addColumn('subject', 'varchar', (col) => col.notNull()) + .addColumn('subjectCid', 'varchar', (col) => col.notNull()) + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .addColumn('indexedAt', 'varchar', (col) => col.notNull()) + .addColumn('sortAt', 'varchar', (col) => + col + .generatedAlwaysAs(sql`least("createdAt", "indexedAt")`) + .stored() + .notNull(), + ) + .execute() + await db.schema + .createIndex('quote_subject_cursor_idx') + .on('quote') + .columns(['subject', 'sortAt', 'cid']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('quote').execute() +} diff --git a/packages/bsky/src/data-plane/server/db/migrations/20240801T193939827Z-post-gate.ts b/packages/bsky/src/data-plane/server/db/migrations/20240801T193939827Z-post-gate.ts new file mode 100644 index 00000000000..93dbcbc14ab --- /dev/null +++ b/packages/bsky/src/data-plane/server/db/migrations/20240801T193939827Z-post-gate.ts @@ -0,0 +1,17 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('post_gate') + .addColumn('uri', 'varchar', (col) => col.primaryKey()) + .addColumn('cid', 'varchar', (col) => col.notNull()) + .addColumn('creator', 'varchar', (col) => col.notNull()) + .addColumn('postUri', 'varchar', (col) => col.notNull().unique()) + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .addColumn('indexedAt', 'varchar', (col) => col.notNull()) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('post_gate').execute() +} diff --git a/packages/bsky/src/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.ts b/packages/bsky/src/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.ts new file mode 100644 index 00000000000..aa6c6296ad6 --- /dev/null +++ b/packages/bsky/src/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.ts @@ -0,0 +1,25 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('post') + .addColumn('violatesEmbeddingRules', 'boolean') + .execute() + await db.schema + .alterTable('post') + .addColumn('hasThreadGate', 'boolean') + .execute() + await db.schema + .alterTable('post') + .addColumn('hasPostGate', 'boolean') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema + .alterTable('post') + .dropColumn('violatesEmbeddingRules') + .execute() + await db.schema.alterTable('post').dropColumn('hasThreadGate').execute() + await db.schema.alterTable('post').dropColumn('hasPostGate').execute() +} diff --git a/packages/bsky/src/data-plane/server/db/migrations/20240829T211238293Z-simplify-actor-sync.ts b/packages/bsky/src/data-plane/server/db/migrations/20240829T211238293Z-simplify-actor-sync.ts new file mode 100644 index 00000000000..954656aee6a --- /dev/null +++ b/packages/bsky/src/data-plane/server/db/migrations/20240829T211238293Z-simplify-actor-sync.ts @@ -0,0 +1,23 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema.alterTable('actor_sync').dropColumn('commitDataCid').execute() + await db.schema.alterTable('actor_sync').dropColumn('rebaseCount').execute() + await db.schema.alterTable('actor_sync').dropColumn('tooBigCount').execute() + // Migration code +} + +export async function down(db: Kysely): Promise { + await db.schema + .alterTable('actor_sync') + .addColumn('commitDataCid', 'varchar', (col) => col.notNull()) + .execute() + await db.schema + .alterTable('actor_sync') + .addColumn('rebaseCount', 'integer', (col) => col.notNull()) + .execute() + await db.schema + .alterTable('actor_sync') + .addColumn('tooBigCount', 'integer', (col) => col.notNull()) + .execute() +} diff --git a/packages/bsky/src/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.ts b/packages/bsky/src/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.ts new file mode 100644 index 00000000000..97f3f699f54 --- /dev/null +++ b/packages/bsky/src/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.ts @@ -0,0 +1,17 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('profile') + .addColumn('pinnedPost', 'varchar') + .execute() + await db.schema + .alterTable('profile') + .addColumn('pinnedPostCid', 'varchar') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.alterTable('profile').dropColumn('pinnedPost').execute() + await db.schema.alterTable('profile').dropColumn('pinnedPostCid').execute() +} diff --git a/packages/bsky/src/data-plane/server/db/migrations/index.ts b/packages/bsky/src/data-plane/server/db/migrations/index.ts index e2f09a5a5f4..956f44491b6 100644 --- a/packages/bsky/src/data-plane/server/db/migrations/index.ts +++ b/packages/bsky/src/data-plane/server/db/migrations/index.ts @@ -38,3 +38,9 @@ export * as _20240530T170337073Z from './20240530T170337073Z-account-deactivatio export * as _20240606T171229898Z from './20240606T171229898Z-thread-mutes' export * as _20240606T222548219Z from './20240606T222548219Z-starter-packs' export * as _20240719T203853939Z from './20240719T203853939Z-priority-notifs' +export * as _20240723T220700077Z from './20240723T220700077Z-quotes-post-aggs' +export * as _20240723T220703655Z from './20240723T220703655Z-quotes' +export * as _20240801T193939827Z from './20240801T193939827Z-post-gate' +export * as _20240808T224251220Z from './20240808T224251220Z-post-gate-flags' +export * as _20240829T211238293Z from './20240829T211238293Z-simplify-actor-sync' +export * as _20240831T134810923Z from './20240831T134810923Z-pinned-posts' diff --git a/packages/bsky/src/data-plane/server/db/tables/actor-sync.ts b/packages/bsky/src/data-plane/server/db/tables/actor-sync.ts index a9e2372ccb2..bd5cc293d09 100644 --- a/packages/bsky/src/data-plane/server/db/tables/actor-sync.ts +++ b/packages/bsky/src/data-plane/server/db/tables/actor-sync.ts @@ -1,10 +1,7 @@ export interface ActorSync { did: string commitCid: string - commitDataCid: string repoRev: string | null - rebaseCount: number - tooBigCount: number } export const tableName = 'actor_sync' diff --git a/packages/bsky/src/data-plane/server/db/tables/post-agg.ts b/packages/bsky/src/data-plane/server/db/tables/post-agg.ts index 5341347403d..8bff49067de 100644 --- a/packages/bsky/src/data-plane/server/db/tables/post-agg.ts +++ b/packages/bsky/src/data-plane/server/db/tables/post-agg.ts @@ -7,6 +7,7 @@ export interface PostAgg { likeCount: Generated replyCount: Generated repostCount: Generated + quoteCount: Generated } export type PartialDB = { diff --git a/packages/bsky/src/data-plane/server/db/tables/post-gate.ts b/packages/bsky/src/data-plane/server/db/tables/post-gate.ts new file mode 100644 index 00000000000..b017405a556 --- /dev/null +++ b/packages/bsky/src/data-plane/server/db/tables/post-gate.ts @@ -0,0 +1,12 @@ +const tableName = 'post_gate' + +export interface Postgate { + uri: string + cid: string + creator: string + postUri: string + createdAt: string + indexedAt: string +} + +export type PartialDB = { [tableName]: Postgate } diff --git a/packages/bsky/src/data-plane/server/db/tables/post.ts b/packages/bsky/src/data-plane/server/db/tables/post.ts index 6c01b76c8e0..1238a087ba6 100644 --- a/packages/bsky/src/data-plane/server/db/tables/post.ts +++ b/packages/bsky/src/data-plane/server/db/tables/post.ts @@ -15,6 +15,9 @@ export interface Post { tags: string[] | null invalidReplyRoot: boolean | null violatesThreadGate: boolean | null + violatesEmbeddingRules: boolean | null + hasThreadGate: boolean | null + hasPostGate: boolean | null createdAt: string indexedAt: string sortAt: GeneratedAlways diff --git a/packages/bsky/src/data-plane/server/db/tables/profile.ts b/packages/bsky/src/data-plane/server/db/tables/profile.ts index ea46f934e9c..4693c94cdc8 100644 --- a/packages/bsky/src/data-plane/server/db/tables/profile.ts +++ b/packages/bsky/src/data-plane/server/db/tables/profile.ts @@ -9,6 +9,8 @@ export interface Profile { avatarCid: string | null bannerCid: string | null joinedViaStarterPackUri: string | null + pinnedPost: string | null + pinnedPostCid: string | null createdAt: string indexedAt: string } diff --git a/packages/bsky/src/data-plane/server/db/tables/quote.ts b/packages/bsky/src/data-plane/server/db/tables/quote.ts new file mode 100644 index 00000000000..5d15dbddabc --- /dev/null +++ b/packages/bsky/src/data-plane/server/db/tables/quote.ts @@ -0,0 +1,15 @@ +import { GeneratedAlways } from 'kysely' + +const tableName = 'quote' + +export interface Quote { + uri: string + cid: string + subject: string + subjectCid: string + createdAt: string + indexedAt: string + sortAt: GeneratedAlways +} + +export type PartialDB = { [tableName]: Quote } diff --git a/packages/bsky/src/data-plane/server/indexing/index.ts b/packages/bsky/src/data-plane/server/indexing/index.ts index 15cf0c35136..eb07b1d123d 100644 --- a/packages/bsky/src/data-plane/server/indexing/index.ts +++ b/packages/bsky/src/data-plane/server/indexing/index.ts @@ -5,7 +5,6 @@ import { readCarWithRoot, WriteOpAction, verifyRepo, - Commit, VerifiedRepo, getAndParseRecord, } from '@atproto/repo' @@ -17,6 +16,7 @@ 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 Postgate from './plugins/post-gate' import * as Like from './plugins/like' import * as Repost from './plugins/repost' import * as Follow from './plugins/follow' @@ -38,6 +38,7 @@ export class IndexingService { records: { post: Post.PluginType threadGate: Threadgate.PluginType + postGate: Postgate.PluginType like: Like.PluginType repost: Repost.PluginType follow: Follow.PluginType @@ -60,6 +61,7 @@ export class IndexingService { this.records = { post: Post.makePlugin(this.db, this.background), threadGate: Threadgate.makePlugin(this.db, this.background), + postGate: Postgate.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), @@ -224,45 +226,25 @@ export class IndexingService { ) } - async setCommitLastSeen( - commit: Commit, - details: { commit: CID; rebase: boolean; tooBig: boolean }, - ) { + async setCommitLastSeen(did: string, commit: CID, rev: string) { const { ref } = this.db.db.dynamic await this.db.db .insertInto('actor_sync') .values({ - did: commit.did, - commitCid: details.commit.toString(), - commitDataCid: commit.data.toString(), - repoRev: commit.rev ?? null, - rebaseCount: details.rebase ? 1 : 0, - tooBigCount: details.tooBig ? 1 : 0, + did, + commitCid: commit.toString(), + repoRev: rev ?? null, }) .onConflict((oc) => { - const sync = (col: string) => ref(`actor_sync.${col}`) const excluded = (col: string) => ref(`excluded.${col}`) return oc.column('did').doUpdateSet({ commitCid: sql`${excluded('commitCid')}`, - commitDataCid: sql`${excluded('commitDataCid')}`, repoRev: sql`${excluded('repoRev')}`, - rebaseCount: sql`${sync('rebaseCount')} + ${excluded('rebaseCount')}`, - tooBigCount: sql`${sync('tooBigCount')} + ${excluded('tooBigCount')}`, }) }) .execute() } - async checkCommitNeedsIndexing(commit: Commit) { - const sync = await this.db.db - .selectFrom('actor_sync') - .select('commitDataCid') - .where('did', '=', commit.did) - .executeTakeFirst() - if (!sync) return true - return sync.commitDataCid !== commit.data.toString() - } - findIndexerForCollection(collection: string) { const indexers = Object.values( this.records as Record>, @@ -365,6 +347,10 @@ export class IndexingService { .deleteFrom('thread_gate') .where('creator', '=', did) .execute() + await this.db.db + .deleteFrom('post_gate') + .where('creator', '=', did) + .execute() // notifications await this.db.db .deleteFrom('notification') diff --git a/packages/bsky/src/data-plane/server/indexing/plugins/post-gate.ts b/packages/bsky/src/data-plane/server/indexing/plugins/post-gate.ts new file mode 100644 index 00000000000..3b92d4a1312 --- /dev/null +++ b/packages/bsky/src/data-plane/server/indexing/plugins/post-gate.ts @@ -0,0 +1,104 @@ +import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { CID } from 'multiformats/cid' +import * as Postgate from '../../../../lexicon/types/app/bsky/feed/postgate' +import * as lex from '../../../../lexicon/lexicons' +import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema' +import { Database } from '../../db' +import RecordProcessor from '../processor' +import { BackgroundQueue } from '../../background' + +const lexId = lex.ids.AppBskyFeedPostgate +type IndexedGate = DatabaseSchemaType['post_gate'] + +const insertFn = async ( + db: DatabaseSchema, + uri: AtUri, + cid: CID, + obj: Postgate.Record, + timestamp: string, +): Promise => { + const postUri = new AtUri(obj.post) + if (postUri.host !== uri.host || postUri.rkey !== uri.rkey) { + throw new InvalidRequestError( + 'Creator and rkey of post gate does not match its post', + ) + } + const inserted = await db + .insertInto('post_gate') + .values({ + uri: uri.toString(), + cid: cid.toString(), + creator: uri.host, + postUri: obj.post, + createdAt: normalizeDatetimeAlways(obj.createdAt), + indexedAt: timestamp, + }) + .onConflict((oc) => oc.doNothing()) + .returningAll() + .executeTakeFirst() + await db + .updateTable('post') + .where('uri', '=', postUri.toString()) + .set({ hasPostGate: true }) + .executeTakeFirst() + return inserted || null +} + +const findDuplicate = async ( + db: DatabaseSchema, + _uri: AtUri, + obj: Postgate.Record, +): Promise => { + const found = await db + .selectFrom('post_gate') + .where('postUri', '=', obj.post) + .selectAll() + .executeTakeFirst() + return found ? new AtUri(found.uri) : null +} + +const notifsForInsert = () => { + return [] +} + +const deleteFn = async ( + db: DatabaseSchema, + uri: AtUri, +): Promise => { + const deleted = await db + .deleteFrom('post_gate') + .where('uri', '=', uri.toString()) + .returningAll() + .executeTakeFirst() + if (deleted) { + await db + .updateTable('post') + .where('uri', '=', deleted.postUri) + .set({ hasPostGate: false }) + .executeTakeFirst() + } + return deleted || null +} + +const notifsForDelete = () => { + return { notifs: [], toDelete: [] } +} + +export type PluginType = RecordProcessor + +export const makePlugin = ( + db: Database, + background: BackgroundQueue, +): PluginType => { + return new RecordProcessor(db, background, { + lexId, + insertFn, + findDuplicate, + deleteFn, + notifsForInsert, + notifsForDelete, + }) +} + +export default makePlugin diff --git a/packages/bsky/src/data-plane/server/indexing/plugins/post.ts b/packages/bsky/src/data-plane/server/indexing/plugins/post.ts index cc4121ab667..eb495fcb13f 100644 --- a/packages/bsky/src/data-plane/server/indexing/plugins/post.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/post.ts @@ -7,6 +7,7 @@ import { ReplyRef, } from '../../../../lexicon/types/app/bsky/feed/post' import { Record as GateRecord } from '../../../../lexicon/types/app/bsky/feed/threadgate' +import { Record as PostgateRecord } from '../../../../lexicon/types/app/bsky/feed/postgate' 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' @@ -26,9 +27,14 @@ import { getDescendentsQb, invalidReplyRoot as checkInvalidReplyRoot, violatesThreadGate as checkViolatesThreadGate, - postToThreadgateUri, } from '../../util' import { BackgroundQueue } from '../../background' +import { parsePostgate } from '../../../../views/util' +import { + postUriToThreadgateUri, + postUriToPostgateUri, + uriToDid, +} from '../../../../util/uris' type Notif = Insertable type Post = Selectable @@ -52,6 +58,7 @@ type IndexedPost = { embeds?: (PostEmbedImage[] | PostEmbedExternal | PostEmbedRecord)[] ancestors?: PostAncestor[] descendents?: PostDescendent[] + threadgate?: GateRecord } const lexId = lex.ids.AppBskyFeedPost @@ -168,6 +175,7 @@ const insertFn = async ( await db.insertInto('post_embed_external').values(externalEmbed).execute() } else if (isEmbedRecord(postEmbed)) { const { record } = postEmbed + const embedUri = new AtUri(record.uri) const recordEmbed = { postUri: uri.toString(), embedUri: record.uri, @@ -175,9 +183,59 @@ const insertFn = async ( } embeds.push(recordEmbed) await db.insertInto('post_embed_record').values(recordEmbed).execute() + + if (embedUri.collection === lex.ids.AppBskyFeedPost) { + const quote = { + uri: uri.toString(), + cid: cid.toString(), + subject: record.uri, + subjectCid: record.cid, + createdAt: normalizeDatetimeAlways(obj.createdAt), + indexedAt: timestamp, + } + await db + .insertInto('quote') + .values(quote) + .onConflict((oc) => oc.doNothing()) + .returningAll() + .executeTakeFirst() + + const quoteCountQb = db + .insertInto('post_agg') + .values({ + uri: record.uri.toString(), + quoteCount: db + .selectFrom('quote') + .where('quote.subjectCid', '=', record.cid.toString()) + .select(countAll.as('count')), + }) + .onConflict((oc) => + oc + .column('uri') + .doUpdateSet({ quoteCount: excluded(db, 'quoteCount') }), + ) + await quoteCountQb.execute() + + const { violatesEmbeddingRules } = await validatePostEmbed( + db, + embedUri.toString(), + uri.toString(), + ) + Object.assign(insertedPost, { + violatesEmbeddingRules: violatesEmbeddingRules, + }) + if (violatesEmbeddingRules) { + await db + .updateTable('post') + .where('uri', '=', insertedPost.uri) + .set({ violatesEmbeddingRules: violatesEmbeddingRules }) + .executeTakeFirst() + } + } } } + const threadgate = await getThreadgateRecord(db, post.replyRoot || post.uri) const ancestors = await getAncestorsAndSelfQb(db, { uri: post.uri, parentHeight: REPLY_NOTIF_DEPTH, @@ -200,6 +258,7 @@ const insertFn = async ( embeds, ancestors, descendents, + threadgate, } } @@ -228,19 +287,22 @@ const notifsForInsert = (obj: IndexedPost) => { }) } } - for (const embed of obj.embeds ?? []) { - if ('embedUri' in embed) { - const embedUri = new AtUri(embed.embedUri) - if (embedUri.collection === lex.ids.AppBskyFeedPost) { - maybeNotify({ - did: embedUri.host, - reason: 'quote', - reasonSubject: embedUri.toString(), - author: obj.post.creator, - recordUri: obj.post.uri, - recordCid: obj.post.cid, - sortAt: obj.post.sortAt, - }) + + if (!obj.post.violatesEmbeddingRules) { + for (const embed of obj.embeds ?? []) { + if ('embedUri' in embed) { + const embedUri = new AtUri(embed.embedUri) + if (embedUri.collection === lex.ids.AppBskyFeedPost) { + maybeNotify({ + did: embedUri.host, + reason: 'quote', + reasonSubject: embedUri.toString(), + author: obj.post.creator, + recordUri: obj.post.uri, + recordCid: obj.post.cid, + sortAt: obj.post.sortAt, + }) + } } } } @@ -250,6 +312,8 @@ const notifsForInsert = (obj: IndexedPost) => { return notifs } + const threadgateHiddenReplies = obj.threadgate?.hiddenReplies || [] + // reply notifications for (const ancestor of obj.ancestors ?? []) { @@ -265,6 +329,8 @@ const notifsForInsert = (obj: IndexedPost) => { recordCid: obj.post.cid, sortAt: obj.post.sortAt, }) + // found hidden reply, don't notify any higher ancestors + if (threadgateHiddenReplies.includes(ancestorUri.toString())) break } } @@ -304,6 +370,7 @@ const deleteFn = async ( .executeTakeFirst(), db.deleteFrom('feed_item').where('postUri', '=', uriStr).executeTakeFirst(), ]) + await db.deleteFrom('quote').where('subject', '=', uriStr).execute() const deletedEmbeds: ( | PostEmbedImage[] | PostEmbedExternal @@ -333,7 +400,27 @@ const deleteFn = async ( deletedEmbeds.push(deletedExternals) } if (deletedPosts) { + const embedUri = new AtUri(deletedPosts.embedUri) deletedEmbeds.push(deletedPosts) + + if (embedUri.collection === lex.ids.AppBskyFeedPost) { + await db.deleteFrom('quote').where('uri', '=', uriStr).execute() + await db + .insertInto('post_agg') + .values({ + uri: deletedPosts.embedUri, + quoteCount: db + .selectFrom('quote') + .where('quote.subjectCid', '=', deletedPosts.embedCid.toString()) + .select(countAll.as('count')), + }) + .onConflict((oc) => + oc + .column('uri') + .doUpdateSet({ quoteCount: excluded(db, 'quoteCount') }), + ) + .execute() + } } return deleted ? { @@ -434,7 +521,7 @@ async function validateReply( const violatesThreadGate = await checkViolatesThreadGate( db, creator, - new AtUri(reply.root.uri).hostname, + uriToDid(reply.root.uri), replyRefs.root?.record ?? null, replyRefs.gate?.record ?? null, ) @@ -444,10 +531,58 @@ async function validateReply( } } +async function getThreadgateRecord(db: DatabaseSchema, postUri: string) { + const threadgateRecordUri = postUriToThreadgateUri(postUri) + const results = await db + .selectFrom('record') + .where('record.uri', '=', threadgateRecordUri) + .selectAll() + .execute() + const threadgateRecord = results.find( + (ref) => ref.uri === threadgateRecordUri, + ) + if (threadgateRecord) { + return jsonStringToLex(threadgateRecord.json) as GateRecord + } +} + +async function validatePostEmbed( + db: DatabaseSchema, + embedUri: string, + parentUri: string, +) { + const postgateRecordUri = postUriToPostgateUri(embedUri) + const postgateRecord = await db + .selectFrom('record') + .where('record.uri', '=', postgateRecordUri) + .selectAll() + .executeTakeFirst() + if (!postgateRecord) { + return { + violatesEmbeddingRules: false, + } + } + const { + embeddingRules: { canEmbed }, + } = parsePostgate({ + gate: jsonStringToLex(postgateRecord.json) as PostgateRecord, + viewerDid: uriToDid(parentUri), + authorDid: uriToDid(embedUri), + }) + if (canEmbed) { + return { + violatesEmbeddingRules: false, + } + } + return { + violatesEmbeddingRules: true, + } +} + async function getReplyRefs(db: DatabaseSchema, reply: ReplyRef) { const replyRoot = reply.root.uri const replyParent = reply.parent.uri - const replyGate = postToThreadgateUri(replyRoot) + const replyGate = postUriToThreadgateUri(replyRoot) const results = await db .selectFrom('record') .where('record.uri', 'in', [replyRoot, replyGate, replyParent]) diff --git a/packages/bsky/src/data-plane/server/indexing/plugins/thread-gate.ts b/packages/bsky/src/data-plane/server/indexing/plugins/thread-gate.ts index 0402fe8289f..bbcd42190ed 100644 --- a/packages/bsky/src/data-plane/server/indexing/plugins/thread-gate.ts +++ b/packages/bsky/src/data-plane/server/indexing/plugins/thread-gate.ts @@ -37,6 +37,11 @@ const insertFn = async ( .onConflict((oc) => oc.doNothing()) .returningAll() .executeTakeFirst() + await db + .updateTable('post') + .where('uri', '=', postUri.toString()) + .set({ hasThreadGate: true }) + .executeTakeFirst() return inserted || null } @@ -66,6 +71,13 @@ const deleteFn = async ( .where('uri', '=', uri.toString()) .returningAll() .executeTakeFirst() + if (deleted) { + await db + .updateTable('post') + .where('uri', '=', deleted.postUri) + .set({ hasThreadGate: false }) + .executeTakeFirst() + } return deleted || null } diff --git a/packages/bsky/src/data-plane/server/routes/index.ts b/packages/bsky/src/data-plane/server/routes/index.ts index b2a6b4cb237..5504d38f812 100644 --- a/packages/bsky/src/data-plane/server/routes/index.ts +++ b/packages/bsky/src/data-plane/server/routes/index.ts @@ -15,6 +15,7 @@ import mutes from './mutes' import notifs from './notifs' import posts from './posts' import profile from './profile' +import quotes from './quotes' import records from './records' import relationships from './relationships' import reposts from './reposts' @@ -42,6 +43,7 @@ export default (db: Database, idResolver: IdResolver) => ...notifs(db), ...posts(db), ...profile(db), + ...quotes(db), ...records(db), ...relationships(db), ...reposts(db), diff --git a/packages/bsky/src/data-plane/server/routes/interactions.ts b/packages/bsky/src/data-plane/server/routes/interactions.ts index 94947beff50..a9ec11e0eed 100644 --- a/packages/bsky/src/data-plane/server/routes/interactions.ts +++ b/packages/bsky/src/data-plane/server/routes/interactions.ts @@ -8,7 +8,7 @@ export default (db: Database): Partial> => ({ async getInteractionCounts(req) { const uris = req.refs.map((ref) => ref.uri) if (uris.length === 0) { - return { likes: [], replies: [], reposts: [] } + return { likes: [], replies: [], reposts: [], quotes: [] } } const res = await db.db .selectFrom('post_agg') @@ -20,6 +20,7 @@ export default (db: Database): Partial> => ({ likes: uris.map((uri) => byUri[uri]?.likeCount ?? 0), replies: uris.map((uri) => byUri[uri]?.replyCount ?? 0), reposts: uris.map((uri) => byUri[uri]?.repostCount ?? 0), + quotes: uris.map((uri) => byUri[uri]?.quoteCount ?? 0), } }, async getCountsForUsers(req) { diff --git a/packages/bsky/src/data-plane/server/routes/quotes.ts b/packages/bsky/src/data-plane/server/routes/quotes.ts new file mode 100644 index 00000000000..70992101611 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/quotes.ts @@ -0,0 +1,32 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../../proto/bsky_connect' +import { Database } from '../db' +import { paginate, TimeCidKeyset } from '../db/pagination' + +export default (db: Database): Partial> => ({ + async getQuotesBySubjectSorted(req) { + const { subject, cursor, limit } = req + const { ref } = db.db.dynamic + + if (!subject?.uri) return { uris: [] } + + let builder = db.db + .selectFrom('quote') + .where('quote.subject', '=', subject.uri) + .select(['quote.uri', 'quote.cid', 'quote.sortAt']) + + const keyset = new TimeCidKeyset(ref('quote.sortAt'), ref('quote.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + + const quotes = await builder.execute() + + return { + uris: quotes.map((q) => q.uri), + cursor: keyset.packFromResult(quotes), + } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/records.ts b/packages/bsky/src/data-plane/server/routes/records.ts index 0135673983e..769208c26c1 100644 --- a/packages/bsky/src/data-plane/server/routes/records.ts +++ b/packages/bsky/src/data-plane/server/routes/records.ts @@ -20,6 +20,7 @@ export default (db: Database): Partial> => ({ getProfileRecords: getRecords(db, ids.AppBskyActorProfile), getRepostRecords: getRecords(db, ids.AppBskyFeedRepost), getThreadGateRecords: getRecords(db, ids.AppBskyFeedThreadgate), + getPostgateRecords: getRecords(db, ids.AppBskyFeedPostgate), getLabelerRecords: getRecords(db, ids.AppBskyLabelerService), getActorChatDeclarationRecords: getRecords(db, ids.ChatBskyActorDeclaration), getStarterPackRecords: getRecords(db, ids.AppBskyGraphStarterpack), @@ -74,7 +75,13 @@ export const getPostRecords = (db: Database) => { ? await db.db .selectFrom('post') .where('uri', 'in', req.uris) - .select(['uri', 'violatesThreadGate']) + .select([ + 'uri', + 'violatesThreadGate', + 'violatesEmbeddingRules', + 'hasThreadGate', + 'hasPostGate', + ]) .execute() : [], ]) @@ -82,6 +89,9 @@ export const getPostRecords = (db: Database) => { const meta = req.uris.map((uri) => { return new PostRecordMeta({ violatesThreadGate: !!byKey[uri]?.violatesThreadGate, + violatesEmbeddingRules: !!byKey[uri]?.violatesEmbeddingRules, + hasThreadGate: !!byKey[uri]?.hasThreadGate, + hasPostGate: !!byKey[uri]?.hasPostGate, }) }) return { records, meta } diff --git a/packages/bsky/src/data-plane/server/subscription.ts b/packages/bsky/src/data-plane/server/subscription.ts new file mode 100644 index 00000000000..c52e09f89c5 --- /dev/null +++ b/packages/bsky/src/data-plane/server/subscription.ts @@ -0,0 +1,104 @@ +import { Firehose, MemoryRunner } from '@atproto/sync' +import { IdResolver } from '@atproto/identity' +import { WriteOpAction } from '@atproto/repo' +import { subLogger as log } from '../../logger' +import { IndexingService } from './indexing' +import { Database } from './db' +import { BackgroundQueue } from './background' + +export class RepoSubscription { + firehose: Firehose + runner: MemoryRunner + background: BackgroundQueue + indexingSvc: IndexingService + + constructor( + public opts: { service: string; db: Database; idResolver: IdResolver }, + ) { + const { service, db, idResolver } = opts + this.background = new BackgroundQueue(db) + this.indexingSvc = new IndexingService(db, idResolver, this.background) + + const { runner, firehose } = createFirehose({ + idResolver, + service, + indexingSvc: this.indexingSvc, + }) + this.runner = runner + this.firehose = firehose + } + + start() { + this.firehose.start() + } + + async restart() { + await this.destroy() + const { runner, firehose } = createFirehose({ + idResolver: this.opts.idResolver, + service: this.opts.service, + indexingSvc: this.indexingSvc, + }) + this.runner = runner + this.firehose = firehose + this.start() + } + + async processAll() { + await this.runner.processAll() + await this.background.processAll() + } + + async destroy() { + await this.firehose.destroy() + await this.runner.destroy() + await this.background.processAll() + } +} + +const createFirehose = (opts: { + idResolver: IdResolver + service: string + indexingSvc: IndexingService +}) => { + const { idResolver, service, indexingSvc } = opts + const runner = new MemoryRunner({ startCursor: 0 }) + const firehose = new Firehose({ + idResolver, + runner, + service, + unauthenticatedHandles: true, // indexing service handles these + unauthenticatedCommits: true, // @TODO there seems to be a very rare issue where the authenticator thinks a block is missing in deletion ops + onError: (err) => log.error({ err }, 'error in subscription'), + handleEvent: async (evt) => { + if (evt.event === 'identity') { + await indexingSvc.indexHandle(evt.did, evt.time, true) + } else if (evt.event === 'account') { + if (evt.active === false && evt.status === 'deleted') { + await indexingSvc.deleteActor(evt.did) + } else { + await indexingSvc.updateActorStatus(evt.did, evt.active, evt.status) + } + } else { + const indexFn = + evt.event === 'delete' + ? indexingSvc.deleteRecord(evt.uri) + : indexingSvc.indexRecord( + evt.uri, + evt.cid, + evt.record, + evt.event === 'create' + ? WriteOpAction.Create + : WriteOpAction.Update, + evt.time, + ) + await Promise.all([ + indexFn, + indexingSvc.setCommitLastSeen(evt.did, evt.commit, evt.rev), + indexingSvc.indexHandle(evt.did, evt.time), + ]) + } + }, + }) + return { firehose, runner } +} diff --git a/packages/bsky/src/data-plane/server/subscription/index.ts b/packages/bsky/src/data-plane/server/subscription/index.ts deleted file mode 100644 index 8ae4ff4be7a..00000000000 --- a/packages/bsky/src/data-plane/server/subscription/index.ts +++ /dev/null @@ -1,352 +0,0 @@ -import assert from 'node:assert' -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/syntax' -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, - cborToLexRecord, - def, - Commit, -} from '@atproto/repo' -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, - PartitionedQueue, - ProcessableMessage, - loggableMessage, -} from './util' -import { BackgroundQueue } from '../background' - -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( - private opts: { - service: string - db: Database - idResolver: IdResolver - background: BackgroundQueue - }, - ) { - this.background = new BackgroundQueue(this.opts.db) - this.indexingSvc = new IndexingService( - this.opts.db, - this.opts.idResolver, - this.background, - ) - } - - 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 - } - }) - .finally(() => (this.running = undefined)) - } - - 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 - } - } - - private async handleMessage( - item: ConsecutiveItem, - envelope: Envelope, - ) { - 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.isAccount(msg)) { - await this.handleAccountEvt(msg) - } else if (message.isTombstone(msg)) { - // Ignore tombstones - } else if (message.isMigrate(msg)) { - // Ignore migrations - } else { - const exhaustiveCheck: never = msg - throw new Error(`Unhandled message type: ${exhaustiveCheck['$type']}`) - } - } catch (err) { - // We log messages we can't process and move on: - // otherwise the cursor would get stuck on a poison message. - log.error( - { err, message: loggableMessage(msg) }, - 'indexer message processing error', - ) - } finally { - const latest = item.complete().at(-1) - if (latest !== undefined) { - this.cursor = latest - } - } - } - - private async handleCommit(msg: message.Commit) { - const indexRecords = async () => { - const { root, rootCid, ops } = await getOps(msg) - if (msg.tooBig) { - await this.indexingSvc.indexRepo(msg.repo, rootCid.toString()) - await this.indexingSvc.setCommitLastSeen(root, msg) - return - } - if (msg.rebase) { - const needsReindex = - await this.indexingSvc.checkCommitNeedsIndexing(root) - if (needsReindex) { - await this.indexingSvc.indexRepo(msg.repo, rootCid.toString()) - } - await this.indexingSvc.setCommitLastSeen(root, msg) - return - } - for (const op of ops) { - if (op.action === WriteOpAction.Delete) { - await this.indexingSvc.deleteRecord(op.uri) - } else { - try { - await this.indexingSvc.indexRecord( - op.uri, - op.cid, - op.record, - op.action, // create or update - msg.time, - ) - } catch (err) { - if (err instanceof ValidationError) { - log.warn( - { - did: msg.repo, - commit: msg.commit.toString(), - uri: op.uri.toString(), - cid: op.cid.toString(), - }, - 'skipping indexing of invalid record', - ) - } else { - log.error( - { - err, - did: msg.repo, - commit: msg.commit.toString(), - uri: op.uri.toString(), - cid: op.cid.toString(), - }, - 'skipping indexing due to error processing record', - ) - } - } - } - } - await this.indexingSvc.setCommitLastSeen(root, msg) - } - const results = await Promise.allSettled([ - indexRecords(), - this.indexingSvc.indexHandle(msg.repo, msg.time), - ]) - handleAllSettledErrors(results) - } - - private async handleUpdateHandle(msg: message.Handle) { - await this.indexingSvc.indexHandle(msg.did, msg.time, true) - } - - private async handleIdentityEvt(msg: message.Identity) { - await this.indexingSvc.indexHandle(msg.did, msg.time, true) - } - - private async handleAccountEvt(msg: message.Account) { - if (msg.active === false && msg.status === 'deleted') { - await this.indexingSvc.deleteActor(msg.did) - } else { - await this.indexingSvc.updateActorStatus(msg.did, msg.active, msg.status) - } - } - - 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.isAccount(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( - msg: message.Commit, -): Promise<{ root: Commit; rootCid: CID; ops: PreparedWrite[] }> { - const car = await readCarWithRoot(msg.blocks as Uint8Array) - const rootBytes = car.blocks.get(car.root) - assert(rootBytes, 'Missing commit block in car slice') - - const root = def.commit.schema.parse(cborDecode(rootBytes)) - const ops: PreparedWrite[] = msg.ops.map((op) => { - const [collection, rkey] = op.path.split('/') - assert(collection && rkey) - if ( - op.action === WriteOpAction.Create || - op.action === WriteOpAction.Update - ) { - assert(op.cid) - const record = car.blocks.get(op.cid) - assert(record) - return { - action: - op.action === WriteOpAction.Create - ? WriteOpAction.Create - : WriteOpAction.Update, - cid: op.cid, - record: cborToLexRecord(record), - blobs: [], - uri: AtUri.make(msg.repo, collection, rkey), - } - } else if (op.action === WriteOpAction.Delete) { - return { - action: WriteOpAction.Delete, - uri: AtUri.make(msg.repo, collection, rkey), - } - } else { - throw new Error(`Unknown repo op action: ${op.action}`) - } - }) - - return { root, rootCid: car.root, ops } -} - -type PreparedCreate = { - action: WriteOpAction.Create - uri: AtUri - cid: CID - record: Record - blobs: CID[] // differs from similar type in pds -} - -type PreparedUpdate = { - action: WriteOpAction.Update - uri: AtUri - cid: CID - record: Record - blobs: CID[] // differs from similar type in pds -} - -type PreparedDelete = { - action: WriteOpAction.Delete - uri: AtUri -} - -type PreparedWrite = PreparedCreate | PreparedUpdate | PreparedDelete diff --git a/packages/bsky/src/data-plane/server/subscription/util.ts b/packages/bsky/src/data-plane/server/subscription/util.ts deleted file mode 100644 index 115f6aca718..00000000000 --- a/packages/bsky/src/data-plane/server/subscription/util.ts +++ /dev/null @@ -1,156 +0,0 @@ -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. -export class PartitionedQueue { - main: PQueue - partitions = new Map() - - constructor(opts: { concurrency: number }) { - this.main = new PQueue({ concurrency: opts.concurrency }) - } - - async add(partitionId: string, task: () => Promise) { - if (this.main.isPaused) return - return this.main.add(() => { - return this.getPartition(partitionId).add(task) - }) - } - - async destroy() { - this.main.pause() - this.main.clear() - this.partitions.forEach((p) => p.clear()) - await this.main.onIdle() // All in-flight work completes - } - - private getPartition(partitionId: string) { - let partition = this.partitions.get(partitionId) - if (!partition) { - partition = new PQueue({ concurrency: 1 }) - partition.once('idle', () => this.partitions.delete(partitionId)) - this.partitions.set(partitionId, partition) - } - return partition - } -} - -export class LatestQueue { - queue = new PQueue({ concurrency: 1 }) - - async add(task: () => Promise) { - if (this.queue.isPaused) return - this.queue.clear() // Only queue the latest task, invalidate any previous ones - return this.queue.add(task) - } - - async destroy() { - this.queue.pause() - this.queue.clear() - await this.queue.onIdle() // All in-flight work completes - } -} - -/** - * Add items to a list, and mark those items as - * completed. Upon item completion, get list of consecutive - * items completed at the head of the list. Example: - * - * const consecutive = new ConsecutiveList() - * const item1 = consecutive.push(1) - * const item2 = consecutive.push(2) - * const item3 = consecutive.push(3) - * item2.complete() // [] - * item1.complete() // [1, 2] - * item3.complete() // [3] - * - */ -export class ConsecutiveList { - list: ConsecutiveItem[] = [] - - push(value: T) { - const item = new ConsecutiveItem(this, value) - this.list.push(item) - return item - } - - complete(): T[] { - let i = 0 - while (this.list[i]?.isComplete) { - i += 1 - } - return this.list.splice(0, i).map((item) => item.value) - } -} - -export class ConsecutiveItem { - isComplete = false - constructor( - private consecutive: ConsecutiveList, - public value: T, - ) {} - - complete() { - this.isComplete = true - return this.consecutive.complete() - } -} - -export class PerfectMap extends Map { - get(key: K): V { - const val = super.get(key) - assert(val !== undefined, `Key not found in PerfectMap: ${key}`) - return val - } -} - -// These are the message types that have a sequence number and a repo -export type ProcessableMessage = - | message.Commit - | message.Handle - | message.Identity - | message.Migrate - | message.Tombstone - -export function loggableMessage(msg: RepoMessage) { - if (message.isCommit(msg)) { - const { seq, rebase, prev, repo, commit, time, tooBig, blobs } = msg - return { - $type: msg.$type, - seq, - rebase, - prev: prev?.toString(), - repo, - commit: commit.toString(), - time, - tooBig, - hasBlobs: blobs.length > 0, - } - } else if (message.isHandle(msg)) { - return msg - } else if (message.isIdentity(msg)) { - return msg - } else if (message.isAccount(msg)) { - return msg - } else if (message.isMigrate(msg)) { - return msg - } else if (message.isTombstone(msg)) { - return msg - } else if (message.isInfo(msg)) { - return msg - } - return msg -} - -export function jitter(maxMs) { - return Math.round((Math.random() - 0.5) * maxMs * 2) -} - -export function strToInt(str: string) { - const int = parseInt(str, 10) - assert(!isNaN(int), 'string could not be parsed to an integer') - return int -} diff --git a/packages/bsky/src/data-plane/server/util.ts b/packages/bsky/src/data-plane/server/util.ts index d15b7ffa518..e873fb1f5bc 100644 --- a/packages/bsky/src/data-plane/server/util.ts +++ b/packages/bsky/src/data-plane/server/util.ts @@ -1,6 +1,4 @@ import { sql } from 'kysely' -import { AtUri } from '@atproto/syntax' -import { ids } from '../../lexicon/lexicons' import { Record as PostRecord, ReplyRef, @@ -144,9 +142,3 @@ export const violatesThreadGate = async ( return true } - -export const postToThreadgateUri = (postUri: string) => { - const gateUri = new AtUri(postUri) - gateUri.collection = ids.AppBskyFeedThreadgate - return gateUri.toString() -} diff --git a/packages/bsky/src/feature-gates.ts b/packages/bsky/src/feature-gates.ts index 3e22afa141f..ccfa9280609 100644 --- a/packages/bsky/src/feature-gates.ts +++ b/packages/bsky/src/feature-gates.ts @@ -10,7 +10,11 @@ export type Config = { } export enum GateID { - NewSuggestedFollowsByActor = 'new_sugg_foll_by_actor', + /** + * Left here ensure this is interpreted as a string enum and therefore + * appease TS + */ + _ = '', } /** diff --git a/packages/bsky/src/hydration/feed.ts b/packages/bsky/src/hydration/feed.ts index 246b076bac3..61623ad4173 100644 --- a/packages/bsky/src/hydration/feed.ts +++ b/packages/bsky/src/hydration/feed.ts @@ -4,6 +4,7 @@ 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 { Record as PostgateRecord } from '../lexicon/types/app/bsky/feed/postgate' import { HydrationMap, ItemRef, @@ -12,11 +13,15 @@ import { parseString, split, } from './util' -import { AtUri } from '@atproto/syntax' -import { ids } from '../lexicon/lexicons' import { dedupeStrs } from '@atproto/common' +import { postUriToThreadgateUri, postUriToPostgateUri } from '../util/uris' -export type Post = RecordInfo & { violatesThreadGate: boolean } +export type Post = RecordInfo & { + violatesThreadGate: boolean + violatesEmbeddingRules: boolean + hasThreadGate: boolean + hasPostGate: boolean +} export type Posts = HydrationMap export type PostViewerState = { @@ -31,6 +36,7 @@ export type PostAgg = { likes: number replies: number reposts: number + quotes: number } export type PostAggs = HydrationMap @@ -58,12 +64,22 @@ export type FeedGenViewerStates = HydrationMap export type Threadgate = RecordInfo export type Threadgates = HydrationMap +export type Postgate = RecordInfo +export type Postgates = HydrationMap export type ThreadRef = ItemRef & { threadRoot: 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 type FeedItem = { + post: ItemRef + repost?: ItemRef + /** + * If true, overrides the `reason` with `app.bsky.feed.defs#reasonPin`. Used + * only in author feeds. + */ + authorPinned?: boolean +} export class FeedHydrator { constructor(public dataplane: DataPlaneClient) {} @@ -83,7 +99,21 @@ export class FeedHydrator { 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) + const violatesEmbeddingRules = res.meta[i].violatesEmbeddingRules + const hasThreadGate = res.meta[i].hasThreadGate + const hasPostGate = res.meta[i].hasPostGate + return acc.set( + uri, + record + ? { + ...record, + violatesThreadGate, + violatesEmbeddingRules, + hasThreadGate, + hasPostGate, + } + : null, + ) }, base) } @@ -135,6 +165,7 @@ export class FeedHydrator { likes: counts.likes[i] ?? 0, reposts: counts.reposts[i] ?? 0, replies: counts.replies[i] ?? 0, + quotes: counts.quotes[i] ?? 0, }) }, new HydrationMap()) } @@ -185,14 +216,14 @@ export class FeedHydrator { 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 uris = postUris.map(postUriToThreadgateUri) + return this.getThreadgateRecords(uris, includeTakedowns) + } + + async getThreadgateRecords( + uris: string[], + includeTakedowns = false, + ): Promise { const res = await this.dataplane.getThreadGateRecords({ uris }) return uris.reduce((acc, uri, i) => { const record = parseRecord( @@ -203,6 +234,29 @@ export class FeedHydrator { }, new HydrationMap()) } + async getPostgatesForPosts( + postUris: string[], + includeTakedowns = false, + ): Promise { + if (!postUris.length) return new HydrationMap() + const uris = postUris.map(postUriToPostgateUri) + return this.getPostgateRecords(uris, includeTakedowns) + } + + async getPostgateRecords( + uris: string[], + includeTakedowns = false, + ): Promise { + const res = await this.dataplane.getPostgateRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord( + res.records[i], + includeTakedowns, + ) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + async getLikes(uris: string[], includeTakedowns = false): Promise { if (!uris.length) return new HydrationMap() const res = await this.dataplane.getLikeRecords({ uris }) diff --git a/packages/bsky/src/hydration/hydrator.ts b/packages/bsky/src/hydration/hydrator.ts index d0daf3fc921..9c4919d5c95 100644 --- a/packages/bsky/src/hydration/hydrator.ts +++ b/packages/bsky/src/hydration/hydrator.ts @@ -6,7 +6,7 @@ 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 { isListRule as isThreadgateListRule } from '../lexicon/types/app/bsky/feed/threadgate' import { hydrationLogger } from '../logger' import { ActorHydrator, @@ -38,12 +38,12 @@ import { HydrationMap, RecordInfo, ItemRef, - didFromUri, urisByCollection, mergeMaps, mergeNestedMaps, mergeManyMaps, } from './util' +import { uriToDid as didFromUri } from '../util/uris' import { FeedGenAggs, FeedGens, @@ -56,6 +56,7 @@ import { PostAggs, PostViewerStates, Threadgates, + Postgates, FeedItem, } from './feed' import { ParsedLabelers } from '../util' @@ -91,6 +92,7 @@ export type HydrationState = { follows?: Follows followBlocks?: FollowBlocks threadgates?: Threadgates + postgates?: Postgates lists?: Lists listAggs?: ListAggs listViewers?: ListViewerStates @@ -109,9 +111,13 @@ export type HydrationState = { bidirectionalBlocks?: BidirectionalBlocks } -export type PostBlock = { embed: boolean; reply: boolean } +export type PostBlock = { embed: boolean; parent: boolean; root: boolean } export type PostBlocks = HydrationMap -type PostBlockPairs = { embed?: RelationshipPair; reply?: RelationshipPair } +type PostBlockPairs = { + embed?: RelationshipPair + parent?: RelationshipPair + root?: RelationshipPair +} export type FollowBlock = boolean export type FollowBlocks = HydrationMap @@ -337,27 +343,47 @@ export class Hydrator { const urisLayer1 = nestedRecordUrisFromPosts(postsLayer0) const additionalRootUris = rootUrisFromPosts(postsLayer0) // supports computing threadgates const urisLayer1ByCollection = urisByCollection(urisLayer1) - const postUrisLayer1 = urisLayer1ByCollection.get(ids.AppBskyFeedPost) ?? [] + const embedPostUrisLayer1 = + urisLayer1ByCollection.get(ids.AppBskyFeedPost) ?? [] const postsLayer1 = await this.feed.getPosts( - [...postUrisLayer1, ...additionalRootUris], + [...embedPostUrisLayer1, ...additionalRootUris], ctx.includeTakedowns, ) // second level embeds, ignoring any additional root uris we mixed-in to the previous layer - const urisLayer2 = nestedRecordUrisFromPosts(postsLayer1, postUrisLayer1) + const urisLayer2 = nestedRecordUrisFromPosts( + postsLayer1, + embedPostUrisLayer1, + ) const urisLayer2ByCollection = urisByCollection(urisLayer2) - const postUrisLayer2 = urisLayer2ByCollection.get(ids.AppBskyFeedPost) ?? [] + const embedPostUrisLayer2 = + urisLayer2ByCollection.get(ids.AppBskyFeedPost) ?? [] const threadRootUris = new Set() for (const [uri, post] of postsLayer0) { if (post) { threadRootUris.add(rootUriFromPost(post) ?? uri) } } + const postUrisWithThreadgates = new Set() + for (const uri of threadRootUris) { + const post = postsLayer0.get(uri) + /* + * Checking `post.hasThreadGate` is an optimization, which tells us that + * this post has a threadgate record associated with it. `hydratePosts` + * always hydrates root posts via `additionalRootUris`, so we try to + * check the optimization flag were possible. If the post is unavailable + * for whatever reason, we fall back to requesting threadgate records + * that may not exist. + */ + if (!post || post.hasThreadGate) { + postUrisWithThreadgates.add(uri) + } + } const [postsLayer2, threadgates] = await Promise.all([ - this.feed.getPosts(postUrisLayer2, ctx.includeTakedowns), - this.feed.getThreadgatesForPosts([...threadRootUris.values()]), + this.feed.getPosts(embedPostUrisLayer2, ctx.includeTakedowns), + this.feed.getThreadgatesForPosts([...postUrisWithThreadgates.values()]), ]) // collect list/feedgen embeds, lists in threadgates, post record hydration - const gateListUris = getListUrisFromGates(threadgates) + const threadgateListUris = getListUrisFromThreadgates(threadgates) const nestedListUris = [ ...(urisLayer1ByCollection.get(ids.AppBskyGraphList) ?? []), ...(urisLayer2ByCollection.get(ids.AppBskyGraphList) ?? []), @@ -369,7 +395,7 @@ export class Hydrator { const nestedLabelerDids = [ ...(urisLayer1ByCollection.get(ids.AppBskyLabelerService) ?? []), ...(urisLayer2ByCollection.get(ids.AppBskyLabelerService) ?? []), - ].map((uri) => new AtUri(uri).hostname) + ].map(didFromUri) const nestedStarterPackUris = [ ...(urisLayer1ByCollection.get(ids.AppBskyGraphStarterpack) ?? []), ...(urisLayer2ByCollection.get(ids.AppBskyGraphStarterpack) ?? []), @@ -379,13 +405,19 @@ export class Hydrator { const allPostUris = [...posts.keys()] const allRefs = [ ...refs, - ...postUrisLayer1.map(uriToRef), // supports aggregates on embed #viewRecords - ...postUrisLayer2.map(uriToRef), + ...embedPostUrisLayer1.map(uriToRef), // supports aggregates on embed #viewRecords + ...embedPostUrisLayer2.map(uriToRef), ] const threadRefs = allRefs.map((ref) => ({ ...ref, threadRoot: posts.get(ref.uri)?.record.reply?.root.uri ?? ref.uri, })) + const postUrisWithPostgates = new Set() + for (const [uri, post] of posts) { + if (post && post.hasPostGate) { + postUrisWithPostgates.add(uri) + } + } const [ postAggs, @@ -397,6 +429,7 @@ export class Hydrator { feedGenState, labelerState, starterPackState, + postgates, ] = await Promise.all([ this.feed.getPostAggregates(allRefs), ctx.viewer @@ -405,10 +438,11 @@ export class Hydrator { this.label.getLabelsForSubjects(allPostUris, ctx.labelers), this.hydratePostBlocks(posts), this.hydrateProfiles(allPostUris.map(didFromUri), ctx), - this.hydrateLists([...nestedListUris, ...gateListUris], ctx), + this.hydrateLists([...nestedListUris, ...threadgateListUris], ctx), this.hydrateFeedGens(nestedFeedGenUris, ctx), this.hydrateLabelers(nestedLabelerDids, ctx), this.hydrateStarterPacksBasic(nestedStarterPackUris, ctx), + this.feed.getPostgatesForPosts([...postUrisWithPostgates.values()]), ]) if (!ctx.includeTakedowns) { actionTakedownLabels(allPostUris, posts, labels) @@ -427,6 +461,7 @@ export class Hydrator { postBlocks, labels, threadgates, + postgates, ctx, }, ) @@ -445,10 +480,17 @@ export class Hydrator { // 3p block for replies const parentUri = post.reply?.parent.uri const parentDid = parentUri && didFromUri(parentUri) - if (parentDid) { + if (parentDid && parentDid !== creator) { const pair: RelationshipPair = [creator, parentDid] relationships.push(pair) - postBlockPairs.reply = pair + postBlockPairs.parent = pair + } + const rootUri = post.reply?.root.uri + const rootDid = rootUri && didFromUri(rootUri) + if (rootDid && rootDid !== creator) { + const pair: RelationshipPair = [creator, rootDid] + relationships.push(pair) + postBlockPairs.root = pair } // 3p block for record embeds for (const embedUri of nestedRecordUris(post)) { @@ -457,12 +499,13 @@ export class Hydrator { postBlockPairs.embed = pair } } - // replace embed/reply pairs with block state + // replace embed/parent/root pairs with block state const blocks = await this.graph.getBidirectionalBlocks(relationships) - for (const [uri, { embed, reply }] of postBlocksPairs) { + for (const [uri, { embed, parent, root }] of postBlocksPairs) { postBlocks.set(uri, { embed: !!embed && blocks.isBlocked(...embed), - reply: !!reply && blocks.isBlocked(...reply), + parent: !!parent && blocks.isBlocked(...parent), + root: !!root && blocks.isBlocked(...root), }) } return postBlocks @@ -743,6 +786,21 @@ export class Hydrator { this.label.getLabelsForSubjects(uris, ctx.labelers), this.hydrateProfiles(uris.map(didFromUri), ctx), ]) + const viewerRootPostUris = new Set() + for (const notif of notifs) { + if (notif.reason === 'reply') { + const post = posts.get(notif.uri) + if (post) { + const rootUri = post.record.reply?.root.uri + if (rootUri && didFromUri(rootUri) === ctx.viewer) { + viewerRootPostUris.add(rootUri) + } + } + } + } + const threadgates = await this.feed.getThreadgatesForPosts([ + ...viewerRootPostUris.values(), + ]) actionTakedownLabels(postUris, posts, labels) return mergeStates(profileState, { posts, @@ -750,6 +808,7 @@ export class Hydrator { reposts, follows, labels, + threadgates, ctx, }) } @@ -881,6 +940,18 @@ export class Hydrator { (await this.feed.getFeedGens([uri], includeTakedowns)).get(uri) ?? undefined ) + } else if (collection === ids.AppBskyFeedThreadgate) { + return ( + (await this.feed.getThreadgateRecords([uri], includeTakedowns)).get( + uri, + ) ?? undefined + ) + } else if (collection === ids.AppBskyFeedPostgate) { + return ( + (await this.feed.getPostgateRecords([uri], includeTakedowns)).get( + uri, + ) ?? undefined + ) } else if (collection === ids.AppBskyLabelerService) { if (parsed.rkey !== 'self') return const did = parsed.hostname @@ -1039,10 +1110,10 @@ const nestedRecordUris = (post: Post['record']): string[] => { return uris } -const getListUrisFromGates = (gates: Threadgates) => { +const getListUrisFromThreadgates = (gates: Threadgates) => { const uris: string[] = [] for (const gate of gates.values()) { - const listRules = gate?.record.allow?.filter(isListRule) ?? [] + const listRules = gate?.record.allow?.filter(isThreadgateListRule) ?? [] for (const rule of listRules) { uris.push(rule.list) } @@ -1073,6 +1144,7 @@ export const mergeStates = ( follows: mergeMaps(stateA.follows, stateB.follows), followBlocks: mergeMaps(stateA.followBlocks, stateB.followBlocks), threadgates: mergeMaps(stateA.threadgates, stateB.threadgates), + postgates: mergeMaps(stateA.postgates, stateB.postgates), lists: mergeMaps(stateA.lists, stateB.lists), listAggs: mergeMaps(stateA.listAggs, stateB.listAggs), listViewers: mergeMaps(stateA.listViewers, stateB.listViewers), diff --git a/packages/bsky/src/hydration/util.ts b/packages/bsky/src/hydration/util.ts index 88e5fa65d8e..e07a8af02ed 100644 --- a/packages/bsky/src/hydration/util.ts +++ b/packages/bsky/src/hydration/util.ts @@ -116,10 +116,6 @@ export const parseCid = (cidStr: string | undefined): CID | undefined => { } } -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) { diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 8dbfc9545a7..da79b7a9a8d 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -20,10 +20,11 @@ import { Keypair } from '@atproto/crypto' import { createDataPlaneClient } from './data-plane/client' import { Hydrator } from './hydration/hydrator' import { Views } from './views' -import { AuthVerifier } from './auth-verifier' +import { AuthVerifier, createPublicKeyObject } from './auth-verifier' import { authWithApiKey as bsyncAuth, createBsyncClient } from './bsync' import { authWithApiKey as courierAuth, createCourierClient } from './courier' import { FeatureGates } from './feature-gates' +import { VideoUriBuilder } from './views/util' export * from './data-plane' export type { ServerConfigValues } from './config' @@ -63,6 +64,14 @@ export class BskyAppView { const imgUriBuilder = new ImageUriBuilder( config.cdnUrl || `${config.publicUrl}/img`, ) + const videoUriBuilder = new VideoUriBuilder({ + playlistUrlPattern: + config.videoPlaylistUrlPattern || + `${config.publicUrl}/vid/%s/%s/playlist.m3u8`, + thumbnailUrlPattern: + config.videoThumbnailUrlPattern || + `${config.publicUrl}/vid/%s/%s/thumbnail.jpg`, + }) let imgProcessingServer: ImageProcessingServer | undefined if (!config.cdnUrl) { @@ -92,7 +101,7 @@ export class BskyAppView { rejectUnauthorized: !config.dataplaneIgnoreBadTls, }) const hydrator = new Hydrator(dataplane, config.labelsFromIssuerDids) - const views = new Views(imgUriBuilder) + const views = new Views(imgUriBuilder, videoUriBuilder) const bsyncClient = createBsyncClient({ baseUrl: config.bsyncUrl, @@ -110,11 +119,15 @@ export class BskyAppView { : [], }) + const entrywayJwtPublicKey = config.entrywayJwtPublicKeyHex + ? createPublicKeyObject(config.entrywayJwtPublicKeyHex) + : undefined const authVerifier = new AuthVerifier(dataplane, { ownDid: config.serverDid, alternateAudienceDids: config.alternateAudienceDids, modServiceDid: config.modServiceDid, adminPasses: config.adminPasswords, + entrywayJwtPublicKey, }) const featureGates = new FeatureGates({ diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 137c9f161bc..dd9602d85bc 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -102,6 +102,7 @@ import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' import * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed' import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' +import * as AppBskyFeedGetQuotes from './types/app/bsky/feed/getQuotes' import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' import * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds' import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' @@ -138,6 +139,9 @@ import * as AppBskyUnspeccedGetSuggestionsSkeleton from './types/app/bsky/unspec import * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions' import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton' import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton' +import * as AppBskyVideoGetJobStatus from './types/app/bsky/video/getJobStatus' +import * as AppBskyVideoGetUploadLimits from './types/app/bsky/video/getUploadLimits' +import * as AppBskyVideoUploadVideo from './types/app/bsky/video/uploadVideo' import * as ChatBskyActorDeleteAccount from './types/chat/bsky/actor/deleteAccount' import * as ChatBskyActorExportAccountData from './types/chat/bsky/actor/exportAccountData' import * as ChatBskyConvoDeleteMessageForSelf from './types/chat/bsky/convo/deleteMessageForSelf' @@ -1139,6 +1143,7 @@ export class AppBskyNS { notification: AppBskyNotificationNS richtext: AppBskyRichtextNS unspecced: AppBskyUnspeccedNS + video: AppBskyVideoNS constructor(server: Server) { this._server = server @@ -1150,6 +1155,7 @@ export class AppBskyNS { this.notification = new AppBskyNotificationNS(server) this.richtext = new AppBskyRichtextNS(server) this.unspecced = new AppBskyUnspeccedNS(server) + this.video = new AppBskyVideoNS(server) } } @@ -1385,6 +1391,17 @@ export class AppBskyFeedNS { return this._server.xrpc.method(nsid, cfg) } + getQuotes( + cfg: ConfigOf< + AV, + AppBskyFeedGetQuotes.Handler>, + AppBskyFeedGetQuotes.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getQuotes' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getRepostedBy( cfg: ConfigOf< AV, @@ -1822,6 +1839,47 @@ export class AppBskyUnspeccedNS { } } +export class AppBskyVideoNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + getJobStatus( + cfg: ConfigOf< + AV, + AppBskyVideoGetJobStatus.Handler>, + AppBskyVideoGetJobStatus.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.video.getJobStatus' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getUploadLimits( + cfg: ConfigOf< + AV, + AppBskyVideoGetUploadLimits.Handler>, + AppBskyVideoGetUploadLimits.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.video.getUploadLimits' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + uploadVideo( + cfg: ConfigOf< + AV, + AppBskyVideoUploadVideo.Handler>, + AppBskyVideoUploadVideo.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.video.uploadVideo' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + export class ChatNS { _server: Server bsky: ChatBskyNS diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 4d2d84ebd44..9e9d0375f2d 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -1254,9 +1254,8 @@ export const schemaDict = { }, validate: { type: 'boolean', - default: true, description: - "Can be set to 'false' to skip Lexicon schema validation of record data, for all operations.", + "Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons.", }, writes: { type: 'array', @@ -1279,6 +1278,31 @@ export const schemaDict = { }, }, }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: [], + properties: { + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + results: { + type: 'array', + items: { + type: 'union', + refs: [ + 'lex:com.atproto.repo.applyWrites#createResult', + 'lex:com.atproto.repo.applyWrites#updateResult', + 'lex:com.atproto.repo.applyWrites#deleteResult', + ], + closed: true, + }, + }, + }, + }, + }, errors: [ { name: 'InvalidSwap', @@ -1336,6 +1360,47 @@ export const schemaDict = { }, }, }, + createResult: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, + }, + }, + updateResult: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, + }, + }, + deleteResult: { + type: 'object', + required: [], + properties: {}, + }, }, }, ComAtprotoRepoCreateRecord: { @@ -1370,9 +1435,8 @@ export const schemaDict = { }, validate: { type: 'boolean', - default: true, description: - "Can be set to 'false' to skip Lexicon schema validation of record data.", + "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.", }, record: { type: 'unknown', @@ -1401,6 +1465,14 @@ export const schemaDict = { type: 'string', format: 'cid', }, + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, }, }, }, @@ -1414,6 +1486,25 @@ export const schemaDict = { }, }, }, + ComAtprotoRepoDefs: { + lexicon: 1, + id: 'com.atproto.repo.defs', + defs: { + commitMeta: { + type: 'object', + required: ['cid', 'rev'], + properties: { + cid: { + type: 'string', + format: 'cid', + }, + rev: { + type: 'string', + }, + }, + }, + }, + }, ComAtprotoRepoDeleteRecord: { lexicon: 1, id: 'com.atproto.repo.deleteRecord', @@ -1458,6 +1549,18 @@ export const schemaDict = { }, }, }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + }, + }, + }, errors: [ { name: 'InvalidSwap', @@ -1583,6 +1686,11 @@ export const schemaDict = { }, }, }, + errors: [ + { + name: 'RecordNotFound', + }, + ], }, }, }, @@ -1778,9 +1886,8 @@ export const schemaDict = { }, validate: { type: 'boolean', - default: true, description: - "Can be set to 'false' to skip Lexicon schema validation of record data.", + "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.", }, record: { type: 'unknown', @@ -1815,6 +1922,14 @@ export const schemaDict = { type: 'string', format: 'cid', }, + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, }, }, }, @@ -4080,6 +4195,10 @@ export const schemaDict = { ref: 'lex:com.atproto.label.defs#label', }, }, + pinnedPost: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, }, }, profileAssociated: { @@ -4462,6 +4581,15 @@ export const schemaDict = { maxLength: 100, }, }, + nuxs: { + description: 'Storage for NUXs the user has encountered.', + type: 'array', + maxLength: 100, + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#nux', + }, + }, }, }, bskyAppProgressGuide: { @@ -4476,6 +4604,34 @@ export const schemaDict = { }, }, }, + nux: { + type: 'object', + description: 'A new user experiences (NUX) storage object', + required: ['id', 'completed'], + properties: { + id: { + type: 'string', + maxLength: 100, + }, + completed: { + type: 'boolean', + default: false, + }, + data: { + description: + 'Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.', + type: 'string', + maxLength: 3000, + maxGraphemes: 300, + }, + expiresAt: { + type: 'string', + format: 'datetime', + description: + 'The date and time at which the NUX will expire and should be considered completed.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { @@ -4665,6 +4821,10 @@ export const schemaDict = { type: 'ref', ref: 'lex:com.atproto.repo.strongRef', }, + pinnedPost: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, createdAt: { type: 'string', format: 'datetime', @@ -4796,6 +4956,28 @@ export const schemaDict = { }, }, }, + AppBskyEmbedDefs: { + lexicon: 1, + id: 'app.bsky.embed.defs', + defs: { + aspectRatio: { + type: 'object', + description: + 'width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.', + required: ['width', 'height'], + properties: { + width: { + type: 'integer', + minimum: 1, + }, + height: { + type: 'integer', + minimum: 1, + }, + }, + }, + }, + }, AppBskyEmbedExternal: { lexicon: 1, id: 'app.bsky.embed.external', @@ -4900,23 +5082,7 @@ export const schemaDict = { }, aspectRatio: { type: 'ref', - ref: 'lex:app.bsky.embed.images#aspectRatio', - }, - }, - }, - aspectRatio: { - type: 'object', - description: - 'width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.', - required: ['width', 'height'], - properties: { - width: { - type: 'integer', - minimum: 1, - }, - height: { - type: 'integer', - minimum: 1, + ref: 'lex:app.bsky.embed.defs#aspectRatio', }, }, }, @@ -4957,7 +5123,7 @@ export const schemaDict = { }, aspectRatio: { type: 'ref', - ref: 'lex:app.bsky.embed.images#aspectRatio', + ref: 'lex:app.bsky.embed.defs#aspectRatio', }, }, }, @@ -4989,6 +5155,7 @@ export const schemaDict = { 'lex:app.bsky.embed.record#viewRecord', 'lex:app.bsky.embed.record#viewNotFound', 'lex:app.bsky.embed.record#viewBlocked', + 'lex:app.bsky.embed.record#viewDetached', 'lex:app.bsky.feed.defs#generatorView', 'lex:app.bsky.graph.defs#listView', 'lex:app.bsky.labeler.defs#labelerView', @@ -5033,12 +5200,16 @@ export const schemaDict = { likeCount: { type: 'integer', }, + quoteCount: { + type: 'integer', + }, embeds: { type: 'array', items: { type: 'union', refs: [ 'lex:app.bsky.embed.images#view', + 'lex:app.bsky.embed.video#view', 'lex:app.bsky.embed.external#view', 'lex:app.bsky.embed.record#view', 'lex:app.bsky.embed.recordWithMedia#view', @@ -5083,6 +5254,20 @@ export const schemaDict = { }, }, }, + viewDetached: { + type: 'object', + required: ['uri', 'detached'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + detached: { + type: 'boolean', + const: true, + }, + }, + }, }, }, AppBskyEmbedRecordWithMedia: { @@ -5101,7 +5286,11 @@ export const schemaDict = { }, media: { type: 'union', - refs: ['lex:app.bsky.embed.images', 'lex:app.bsky.embed.external'], + refs: [ + 'lex:app.bsky.embed.images', + 'lex:app.bsky.embed.video', + 'lex:app.bsky.embed.external', + ], }, }, }, @@ -5117,6 +5306,7 @@ export const schemaDict = { type: 'union', refs: [ 'lex:app.bsky.embed.images#view', + 'lex:app.bsky.embed.video#view', 'lex:app.bsky.embed.external#view', ], }, @@ -5124,6 +5314,85 @@ export const schemaDict = { }, }, }, + AppBskyEmbedVideo: { + lexicon: 1, + id: 'app.bsky.embed.video', + description: 'A video embedded in a Bluesky record (eg, a post).', + defs: { + main: { + type: 'object', + required: ['video'], + properties: { + video: { + type: 'blob', + accept: ['video/mp4'], + maxSize: 50000000, + }, + captions: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.embed.video#caption', + }, + maxLength: 20, + }, + alt: { + type: 'string', + description: + 'Alt text description of the video, for accessibility.', + maxGraphemes: 1000, + maxLength: 10000, + }, + aspectRatio: { + type: 'ref', + ref: 'lex:app.bsky.embed.defs#aspectRatio', + }, + }, + }, + caption: { + type: 'object', + required: ['lang', 'file'], + properties: { + lang: { + type: 'string', + format: 'language', + }, + file: { + type: 'blob', + accept: ['text/vtt'], + maxSize: 20000, + }, + }, + }, + view: { + type: 'object', + required: ['cid', 'playlist'], + properties: { + cid: { + type: 'string', + format: 'cid', + }, + playlist: { + type: 'string', + format: 'uri', + }, + thumbnail: { + type: 'string', + format: 'uri', + }, + alt: { + type: 'string', + maxGraphemes: 1000, + maxLength: 10000, + }, + aspectRatio: { + type: 'ref', + ref: 'lex:app.bsky.embed.defs#aspectRatio', + }, + }, + }, + }, + }, AppBskyFeedDefs: { lexicon: 1, id: 'app.bsky.feed.defs', @@ -5151,6 +5420,7 @@ export const schemaDict = { type: 'union', refs: [ 'lex:app.bsky.embed.images#view', + 'lex:app.bsky.embed.video#view', 'lex:app.bsky.embed.external#view', 'lex:app.bsky.embed.record#view', 'lex:app.bsky.embed.recordWithMedia#view', @@ -5165,6 +5435,9 @@ export const schemaDict = { likeCount: { type: 'integer', }, + quoteCount: { + type: 'integer', + }, indexedAt: { type: 'string', format: 'datetime', @@ -5205,6 +5478,12 @@ export const schemaDict = { replyDisabled: { type: 'boolean', }, + embeddingDisabled: { + type: 'boolean', + }, + pinned: { + type: 'boolean', + }, }, }, feedViewPost: { @@ -5221,7 +5500,10 @@ export const schemaDict = { }, reason: { type: 'union', - refs: ['lex:app.bsky.feed.defs#reasonRepost'], + refs: [ + 'lex:app.bsky.feed.defs#reasonRepost', + 'lex:app.bsky.feed.defs#reasonPin', + ], }, feedContext: { type: 'string', @@ -5273,6 +5555,10 @@ export const schemaDict = { }, }, }, + reasonPin: { + type: 'object', + properties: {}, + }, threadViewPost: { type: 'object', required: ['post'], @@ -5430,7 +5716,10 @@ export const schemaDict = { }, reason: { type: 'union', - refs: ['lex:app.bsky.feed.defs#skeletonReasonRepost'], + refs: [ + 'lex:app.bsky.feed.defs#skeletonReasonRepost', + 'lex:app.bsky.feed.defs#skeletonReasonPin', + ], }, feedContext: { type: 'string', @@ -5450,6 +5739,10 @@ export const schemaDict = { }, }, }, + skeletonReasonPin: { + type: 'object', + properties: {}, + }, threadgateView: { type: 'object', properties: { @@ -5728,7 +6021,7 @@ export const schemaDict = { main: { type: 'query', description: - 'Get a list of posts liked by an actor. Does not require auth.', + 'Get a list of posts liked by an actor. Requires auth, actor must be the requesting account.', parameters: { type: 'params', required: ['actor'], @@ -5815,6 +6108,10 @@ export const schemaDict = { ], default: 'posts_with_replies', }, + includePins: { + type: 'boolean', + default: false, + }, }, }, output: { @@ -6227,6 +6524,10 @@ export const schemaDict = { 'lex:app.bsky.feed.defs#blockedPost', ], }, + threadgate: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#threadgateView', + }, }, }, }, @@ -6280,6 +6581,69 @@ export const schemaDict = { }, }, }, + AppBskyFeedGetQuotes: { + lexicon: 1, + id: 'app.bsky.feed.getQuotes', + defs: { + main: { + type: 'query', + description: 'Get a list of quotes for a given post.', + parameters: { + type: 'params', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + description: 'Reference (AT-URI) of post record', + }, + cid: { + type: 'string', + format: 'cid', + description: + 'If supplied, filters to quotes of specific version (by CID) of the post record.', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['uri', 'posts'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + cursor: { + type: 'string', + }, + posts: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#postView', + }, + }, + }, + }, + }, + }, + }, + }, AppBskyFeedGetRepostedBy: { lexicon: 1, id: 'app.bsky.feed.getRepostedBy', @@ -6505,6 +6869,7 @@ export const schemaDict = { type: 'union', refs: [ 'lex:app.bsky.embed.images', + 'lex:app.bsky.embed.video', 'lex:app.bsky.embed.external', 'lex:app.bsky.embed.record', 'lex:app.bsky.embed.recordWithMedia', @@ -6596,6 +6961,56 @@ export const schemaDict = { }, }, }, + AppBskyFeedPostgate: { + lexicon: 1, + id: 'app.bsky.feed.postgate', + defs: { + main: { + type: 'record', + key: 'tid', + description: + 'Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository.', + record: { + type: 'object', + required: ['post', 'createdAt'], + properties: { + createdAt: { + type: 'string', + format: 'datetime', + }, + post: { + type: 'string', + format: 'at-uri', + description: 'Reference (AT-URI) to the post record.', + }, + detachedEmbeddingUris: { + type: 'array', + maxLength: 50, + items: { + type: 'string', + format: 'at-uri', + }, + description: + 'List of AT-URIs embedding this post that the author has detached from.', + }, + embeddingRules: { + type: 'array', + maxLength: 5, + items: { + type: 'union', + refs: ['lex:app.bsky.feed.postgate#disableRule'], + }, + }, + }, + }, + }, + disableRule: { + type: 'object', + description: 'Disables embedding of this post.', + properties: {}, + }, + }, + }, AppBskyFeedRepost: { lexicon: 1, id: 'app.bsky.feed.repost', @@ -6781,7 +7196,7 @@ export const schemaDict = { type: 'record', key: 'tid', 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 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'], @@ -6807,6 +7222,15 @@ export const schemaDict = { type: 'string', format: 'datetime', }, + hiddenReplies: { + type: 'array', + maxLength: 50, + items: { + type: 'string', + format: 'at-uri', + }, + description: 'List of hidden reply URIs.', + }, }, }, }, @@ -7846,6 +8270,12 @@ export const schemaDict = { ref: 'lex:app.bsky.actor.defs#profileView', }, }, + isFallback: { + type: 'boolean', + description: + 'If true, response has fallen-back to generic results, and is not scoped using relativeToDid', + default: false, + }, }, }, }, @@ -8802,6 +9232,12 @@ export const schemaDict = { ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor', }, }, + relativeToDid: { + type: 'string', + format: 'did', + description: + 'DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer.', + }, }, }, }, @@ -9049,6 +9485,138 @@ export const schemaDict = { }, }, }, + AppBskyVideoDefs: { + lexicon: 1, + id: 'app.bsky.video.defs', + defs: { + jobStatus: { + type: 'object', + required: ['jobId', 'did', 'state'], + properties: { + jobId: { + type: 'string', + }, + did: { + type: 'string', + format: 'did', + }, + state: { + type: 'string', + description: + 'The state of the video processing job. All values not listed as a known value indicate that the job is in process.', + knownValues: ['JOB_STATE_COMPLETED', 'JOB_STATE_FAILED'], + }, + progress: { + type: 'integer', + minimum: 0, + maximum: 100, + description: 'Progress within the current processing state.', + }, + blob: { + type: 'blob', + }, + error: { + type: 'string', + }, + message: { + type: 'string', + }, + }, + }, + }, + }, + AppBskyVideoGetJobStatus: { + lexicon: 1, + id: 'app.bsky.video.getJobStatus', + defs: { + main: { + type: 'query', + description: 'Get status details for a video processing job.', + parameters: { + type: 'params', + required: ['jobId'], + properties: { + jobId: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['jobStatus'], + properties: { + jobStatus: { + type: 'ref', + ref: 'lex:app.bsky.video.defs#jobStatus', + }, + }, + }, + }, + }, + }, + }, + AppBskyVideoGetUploadLimits: { + lexicon: 1, + id: 'app.bsky.video.getUploadLimits', + defs: { + main: { + type: 'query', + description: 'Get video upload limits for the authenticated user.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['canUpload'], + properties: { + canUpload: { + type: 'boolean', + }, + remainingDailyVideos: { + type: 'integer', + }, + remainingDailyBytes: { + type: 'integer', + }, + message: { + type: 'string', + }, + error: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + AppBskyVideoUploadVideo: { + lexicon: 1, + id: 'app.bsky.video.uploadVideo', + defs: { + main: { + type: 'procedure', + description: 'Upload a video to be processed then stored on the PDS.', + input: { + encoding: 'video/mp4', + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['jobStatus'], + properties: { + jobStatus: { + type: 'ref', + ref: 'lex:app.bsky.video.defs#jobStatus', + }, + }, + }, + }, + }, + }, + }, ChatBskyActorDeclaration: { lexicon: 1, id: 'chat.bsky.actor.declaration', @@ -9991,6 +10559,7 @@ export const ids = { ComAtprotoModerationDefs: 'com.atproto.moderation.defs', ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites', ComAtprotoRepoCreateRecord: 'com.atproto.repo.createRecord', + ComAtprotoRepoDefs: 'com.atproto.repo.defs', ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord', ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo', ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord', @@ -10056,10 +10625,12 @@ export const ids = { AppBskyActorPutPreferences: 'app.bsky.actor.putPreferences', AppBskyActorSearchActors: 'app.bsky.actor.searchActors', AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead', + AppBskyEmbedDefs: 'app.bsky.embed.defs', AppBskyEmbedExternal: 'app.bsky.embed.external', AppBskyEmbedImages: 'app.bsky.embed.images', AppBskyEmbedRecord: 'app.bsky.embed.record', AppBskyEmbedRecordWithMedia: 'app.bsky.embed.recordWithMedia', + AppBskyEmbedVideo: 'app.bsky.embed.video', AppBskyFeedDefs: 'app.bsky.feed.defs', AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator', AppBskyFeedGenerator: 'app.bsky.feed.generator', @@ -10074,11 +10645,13 @@ export const ids = { AppBskyFeedGetListFeed: 'app.bsky.feed.getListFeed', AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread', AppBskyFeedGetPosts: 'app.bsky.feed.getPosts', + AppBskyFeedGetQuotes: 'app.bsky.feed.getQuotes', AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy', AppBskyFeedGetSuggestedFeeds: 'app.bsky.feed.getSuggestedFeeds', AppBskyFeedGetTimeline: 'app.bsky.feed.getTimeline', AppBskyFeedLike: 'app.bsky.feed.like', AppBskyFeedPost: 'app.bsky.feed.post', + AppBskyFeedPostgate: 'app.bsky.feed.postgate', AppBskyFeedRepost: 'app.bsky.feed.repost', AppBskyFeedSearchPosts: 'app.bsky.feed.searchPosts', AppBskyFeedSendInteractions: 'app.bsky.feed.sendInteractions', @@ -10131,6 +10704,10 @@ export const ids = { AppBskyUnspeccedSearchActorsSkeleton: 'app.bsky.unspecced.searchActorsSkeleton', AppBskyUnspeccedSearchPostsSkeleton: 'app.bsky.unspecced.searchPostsSkeleton', + AppBskyVideoDefs: 'app.bsky.video.defs', + AppBskyVideoGetJobStatus: 'app.bsky.video.getJobStatus', + AppBskyVideoGetUploadLimits: 'app.bsky.video.getUploadLimits', + AppBskyVideoUploadVideo: 'app.bsky.video.uploadVideo', ChatBskyActorDeclaration: 'chat.bsky.actor.declaration', ChatBskyActorDefs: 'chat.bsky.actor.defs', ChatBskyActorDeleteAccount: 'chat.bsky.actor.deleteAccount', 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 c7eadff70d7..97b51e14312 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts @@ -7,6 +7,7 @@ import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' import * as AppBskyGraphDefs from '../graph/defs' +import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' export interface ProfileViewBasic { did: string @@ -74,6 +75,7 @@ export interface ProfileViewDetailed { createdAt?: string viewer?: ViewerState labels?: ComAtprotoLabelDefs.Label[] + pinnedPost?: ComAtprotoRepoStrongRef.Main [k: string]: unknown } @@ -469,6 +471,8 @@ export interface BskyAppStatePref { activeProgressGuide?: BskyAppProgressGuide /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */ queuedNudges?: string[] + /** Storage for NUXs the user has encountered. */ + nuxs?: Nux[] [k: string]: unknown } @@ -501,3 +505,24 @@ export function isBskyAppProgressGuide(v: unknown): v is BskyAppProgressGuide { export function validateBskyAppProgressGuide(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#bskyAppProgressGuide', v) } + +/** A new user experiences (NUX) storage object */ +export interface Nux { + id: string + completed: boolean + /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */ + data?: string + /** The date and time at which the NUX will expire and should be considered completed. */ + expiresAt?: string + [k: string]: unknown +} + +export function isNux(v: unknown): v is Nux { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.actor.defs#nux' + ) +} + +export function validateNux(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#nux', v) +} 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 9fd9b737b9c..617bdf2cd73 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/profile.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/profile.ts @@ -20,6 +20,7 @@ export interface Record { | ComAtprotoLabelDefs.SelfLabels | { $type: string; [k: string]: unknown } joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main + pinnedPost?: ComAtprotoRepoStrongRef.Main createdAt?: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/app/bsky/embed/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/embed/defs.ts new file mode 100644 index 00000000000..cc253f9e610 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/embed/defs.ts @@ -0,0 +1,26 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +/** width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit. */ +export interface AspectRatio { + width: number + height: number + [k: string]: unknown +} + +export function isAspectRatio(v: unknown): v is AspectRatio { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.defs#aspectRatio' + ) +} + +export function validateAspectRatio(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.defs#aspectRatio', v) +} 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 96399867a1a..1fddf42f8eb 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/embed/images.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/embed/images.ts @@ -5,6 +5,7 @@ import { ValidationResult, BlobRef } from '@atproto/lexicon' import { lexicons } from '../../../../lexicons' import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' +import * as AppBskyEmbedDefs from './defs' export interface Main { images: Image[] @@ -28,7 +29,7 @@ export interface Image { image: BlobRef /** Alt text description of the image, for accessibility. */ alt: string - aspectRatio?: AspectRatio + aspectRatio?: AppBskyEmbedDefs.AspectRatio [k: string]: unknown } @@ -42,25 +43,6 @@ export function validateImage(v: unknown): ValidationResult { return lexicons.validate('app.bsky.embed.images#image', v) } -/** width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit. */ -export interface AspectRatio { - width: number - height: number - [k: string]: unknown -} - -export function isAspectRatio(v: unknown): v is AspectRatio { - return ( - isObj(v) && - hasProp(v, '$type') && - v.$type === 'app.bsky.embed.images#aspectRatio' - ) -} - -export function validateAspectRatio(v: unknown): ValidationResult { - return lexicons.validate('app.bsky.embed.images#aspectRatio', v) -} - export interface View { images: ViewImage[] [k: string]: unknown @@ -83,7 +65,7 @@ export interface ViewImage { fullsize: string /** Alt text description of the image, for accessibility. */ alt: string - aspectRatio?: AspectRatio + aspectRatio?: AppBskyEmbedDefs.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 b455e49d6a3..452c8fb7f71 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/embed/record.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/embed/record.ts @@ -12,6 +12,7 @@ import * as AppBskyLabelerDefs from '../labeler/defs' import * as AppBskyActorDefs from '../actor/defs' import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' import * as AppBskyEmbedImages from './images' +import * as AppBskyEmbedVideo from './video' import * as AppBskyEmbedExternal from './external' import * as AppBskyEmbedRecordWithMedia from './recordWithMedia' @@ -38,6 +39,7 @@ export interface View { | ViewRecord | ViewNotFound | ViewBlocked + | ViewDetached | AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView | AppBskyLabelerDefs.LabelerView @@ -66,8 +68,10 @@ export interface ViewRecord { replyCount?: number repostCount?: number likeCount?: number + quoteCount?: number embeds?: ( | AppBskyEmbedImages.View + | AppBskyEmbedVideo.View | AppBskyEmbedExternal.View | View | AppBskyEmbedRecordWithMedia.View @@ -125,3 +129,21 @@ export function isViewBlocked(v: unknown): v is ViewBlocked { export function validateViewBlocked(v: unknown): ValidationResult { return lexicons.validate('app.bsky.embed.record#viewBlocked', v) } + +export interface ViewDetached { + uri: string + detached: true + [k: string]: unknown +} + +export function isViewDetached(v: unknown): v is ViewDetached { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.record#viewDetached' + ) +} + +export function validateViewDetached(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.record#viewDetached', v) +} diff --git a/packages/bsky/src/lexicon/types/app/bsky/embed/recordWithMedia.ts b/packages/bsky/src/lexicon/types/app/bsky/embed/recordWithMedia.ts index f8f1ae50b9e..4df0ee5c2d7 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/embed/recordWithMedia.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/embed/recordWithMedia.ts @@ -7,12 +7,14 @@ import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' import * as AppBskyEmbedRecord from './record' import * as AppBskyEmbedImages from './images' +import * as AppBskyEmbedVideo from './video' import * as AppBskyEmbedExternal from './external' export interface Main { record: AppBskyEmbedRecord.Main media: | AppBskyEmbedImages.Main + | AppBskyEmbedVideo.Main | AppBskyEmbedExternal.Main | { $type: string; [k: string]: unknown } [k: string]: unknown @@ -35,6 +37,7 @@ export interface View { record: AppBskyEmbedRecord.View media: | AppBskyEmbedImages.View + | AppBskyEmbedVideo.View | AppBskyEmbedExternal.View | { $type: string; [k: string]: unknown } [k: string]: unknown diff --git a/packages/bsky/src/lexicon/types/app/bsky/embed/video.ts b/packages/bsky/src/lexicon/types/app/bsky/embed/video.ts new file mode 100644 index 00000000000..50eb59aa038 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/embed/video.ts @@ -0,0 +1,67 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import * as AppBskyEmbedDefs from './defs' + +export interface Main { + video: BlobRef + captions?: Caption[] + /** Alt text description of the video, for accessibility. */ + alt?: string + aspectRatio?: AppBskyEmbedDefs.AspectRatio + [k: string]: unknown +} + +export function isMain(v: unknown): v is Main { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.embed.video#main' || + v.$type === 'app.bsky.embed.video') + ) +} + +export function validateMain(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.video#main', v) +} + +export interface Caption { + lang: string + file: BlobRef + [k: string]: unknown +} + +export function isCaption(v: unknown): v is Caption { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.video#caption' + ) +} + +export function validateCaption(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.video#caption', v) +} + +export interface View { + cid: string + playlist: string + thumbnail?: string + alt?: string + aspectRatio?: AppBskyEmbedDefs.AspectRatio + [k: string]: unknown +} + +export function isView(v: unknown): v is View { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.embed.video#view' + ) +} + +export function validateView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.video#view', v) +} 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 ac2a7ed3638..1c6f6ce78f6 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts @@ -7,6 +7,7 @@ import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' import * as AppBskyActorDefs from '../actor/defs' import * as AppBskyEmbedImages from '../embed/images' +import * as AppBskyEmbedVideo from '../embed/video' import * as AppBskyEmbedExternal from '../embed/external' import * as AppBskyEmbedRecord from '../embed/record' import * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia' @@ -21,6 +22,7 @@ export interface PostView { record: {} embed?: | AppBskyEmbedImages.View + | AppBskyEmbedVideo.View | AppBskyEmbedExternal.View | AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View @@ -28,6 +30,7 @@ export interface PostView { replyCount?: number repostCount?: number likeCount?: number + quoteCount?: number indexedAt: string viewer?: ViewerState labels?: ComAtprotoLabelDefs.Label[] @@ -51,6 +54,8 @@ export interface ViewerState { like?: string threadMuted?: boolean replyDisabled?: boolean + embeddingDisabled?: boolean + pinned?: boolean [k: string]: unknown } @@ -69,7 +74,7 @@ export function validateViewerState(v: unknown): ValidationResult { export interface FeedViewPost { post: PostView reply?: ReplyRef - reason?: ReasonRepost | { $type: string; [k: string]: unknown } + reason?: ReasonRepost | ReasonPin | { $type: string; [k: string]: unknown } /** Context provided by feed generator that may be passed back alongside interactions. */ feedContext?: string [k: string]: unknown @@ -130,6 +135,22 @@ export function validateReasonRepost(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#reasonRepost', v) } +export interface ReasonPin { + [k: string]: unknown +} + +export function isReasonPin(v: unknown): v is ReasonPin { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#reasonPin' + ) +} + +export function validateReasonPin(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#reasonPin', v) +} + export interface ThreadViewPost { post: PostView parent?: @@ -261,7 +282,10 @@ export function validateGeneratorViewerState(v: unknown): ValidationResult { export interface SkeletonFeedPost { post: string - reason?: SkeletonReasonRepost | { $type: string; [k: string]: unknown } + reason?: + | SkeletonReasonRepost + | SkeletonReasonPin + | { $type: string; [k: string]: unknown } /** Context that will be passed through to client and may be passed to feed generator back alongside interactions. */ feedContext?: string [k: string]: unknown @@ -296,6 +320,22 @@ export function validateSkeletonReasonRepost(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#skeletonReasonRepost', v) } +export interface SkeletonReasonPin { + [k: string]: unknown +} + +export function isSkeletonReasonPin(v: unknown): v is SkeletonReasonPin { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#skeletonReasonPin' + ) +} + +export function validateSkeletonReasonPin(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#skeletonReasonPin', v) +} + export interface ThreadgateView { uri?: string cid?: string 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 017c7a6a2d4..60fa2f74f92 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -20,6 +20,7 @@ export interface QueryParams { | 'posts_with_media' | 'posts_and_author_threads' | (string & {}) + includePins: boolean } export type InputSchema = undefined 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 ae232fd91a2..768977193ef 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getPostThread.ts @@ -26,6 +26,7 @@ export interface OutputSchema { | AppBskyFeedDefs.NotFoundPost | AppBskyFeedDefs.BlockedPost | { $type: string; [k: string]: unknown } + threadgate?: AppBskyFeedDefs.ThreadgateView [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getQuotes.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getQuotes.ts new file mode 100644 index 00000000000..38d04ad88c1 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getQuotes.ts @@ -0,0 +1,54 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + /** Reference (AT-URI) of post record */ + uri: string + /** If supplied, filters to quotes of specific version (by CID) of the post record. */ + cid?: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + uri: string + cid?: string + cursor?: string + posts: AppBskyFeedDefs.PostView[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | 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/app/bsky/feed/post.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts index 881e3d199aa..f9f2827c8d0 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts @@ -7,6 +7,7 @@ import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' import * as AppBskyRichtextFacet from '../richtext/facet' import * as AppBskyEmbedImages from '../embed/images' +import * as AppBskyEmbedVideo from '../embed/video' import * as AppBskyEmbedExternal from '../embed/external' import * as AppBskyEmbedRecord from '../embed/record' import * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia' @@ -23,6 +24,7 @@ export interface Record { reply?: ReplyRef embed?: | AppBskyEmbedImages.Main + | AppBskyEmbedVideo.Main | AppBskyEmbedExternal.Main | AppBskyEmbedRecord.Main | AppBskyEmbedRecordWithMedia.Main diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/postgate.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/postgate.ts new file mode 100644 index 00000000000..dcc4c22a425 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/postgate.ts @@ -0,0 +1,47 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +export interface Record { + createdAt: string + /** Reference (AT-URI) to the post record. */ + post: string + /** List of AT-URIs embedding this post that the author has detached from. */ + detachedEmbeddingUris?: string[] + embeddingRules?: (DisableRule | { $type: string; [k: string]: unknown })[] + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.feed.postgate#main' || + v.$type === 'app.bsky.feed.postgate') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.postgate#main', v) +} + +/** Disables embedding of this post. */ +export interface DisableRule { + [k: string]: unknown +} + +export function isDisableRule(v: unknown): v is DisableRule { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.postgate#disableRule' + ) +} + +export function validateDisableRule(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.postgate#disableRule', v) +} diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts index e14140d5609..c0e618c8f53 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts @@ -16,6 +16,8 @@ export interface Record { | { $type: string; [k: string]: unknown } )[] createdAt: string + /** List of hidden reply URIs. */ + hiddenReplies?: string[] [k: string]: unknown } 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 8f310334d0a..99b97e708f5 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -17,6 +17,8 @@ export type InputSchema = undefined export interface OutputSchema { suggestions: AppBskyActorDefs.ProfileView[] + /** If true, response has fallen-back to generic results, and is not scoped using relativeToDid */ + isFallback?: boolean [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts index af351ef9ecb..b801fa34d72 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts @@ -23,6 +23,8 @@ export type InputSchema = undefined export interface OutputSchema { cursor?: string actors: AppBskyUnspeccedDefs.SkeletonSearchActor[] + /** DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer. */ + relativeToDid?: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/app/bsky/video/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/video/defs.ts new file mode 100644 index 00000000000..12f4d674794 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/video/defs.ts @@ -0,0 +1,32 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +export interface JobStatus { + jobId: string + did: string + /** The state of the video processing job. All values not listed as a known value indicate that the job is in process. */ + state: 'JOB_STATE_COMPLETED' | 'JOB_STATE_FAILED' | (string & {}) + /** Progress within the current processing state. */ + progress?: number + blob?: BlobRef + error?: string + message?: string + [k: string]: unknown +} + +export function isJobStatus(v: unknown): v is JobStatus { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.video.defs#jobStatus' + ) +} + +export function validateJobStatus(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.video.defs#jobStatus', v) +} diff --git a/packages/bsky/src/lexicon/types/app/bsky/video/getJobStatus.ts b/packages/bsky/src/lexicon/types/app/bsky/video/getJobStatus.ts new file mode 100644 index 00000000000..3e075ba2195 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/video/getJobStatus.ts @@ -0,0 +1,46 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as AppBskyVideoDefs from './defs' + +export interface QueryParams { + jobId: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + jobStatus: AppBskyVideoDefs.JobStatus + [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/app/bsky/video/getUploadLimits.ts b/packages/bsky/src/lexicon/types/app/bsky/video/getUploadLimits.ts new file mode 100644 index 00000000000..f0f23003b0b --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/video/getUploadLimits.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 { + canUpload: boolean + remainingDailyVideos?: number + remainingDailyBytes?: number + message?: string + error?: string + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | 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/app/bsky/video/uploadVideo.ts b/packages/bsky/src/lexicon/types/app/bsky/video/uploadVideo.ts new file mode 100644 index 00000000000..c23ff677a4b --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/video/uploadVideo.ts @@ -0,0 +1,48 @@ +/** + * 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, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as AppBskyVideoDefs from './defs' + +export interface QueryParams {} + +export type InputSchema = string | Uint8Array | Blob + +export interface OutputSchema { + jobStatus: AppBskyVideoDefs.JobStatus + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'video/mp4' + body: stream.Readable +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess | 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/repo/applyWrites.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/applyWrites.ts index 3956d7c3048..4e3c8dcef3d 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/applyWrites.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/applyWrites.ts @@ -7,32 +7,45 @@ import { lexicons } from '../../../../lexicons' import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as ComAtprotoRepoDefs from './defs' export interface QueryParams {} export interface InputSchema { /** The handle or DID of the repo (aka, current account). */ repo: string - /** Can be set to 'false' to skip Lexicon schema validation of record data, for all operations. */ - validate: boolean + /** Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons. */ + 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 } +export interface OutputSchema { + commit?: ComAtprotoRepoDefs.CommitMeta + results?: (CreateResult | UpdateResult | DeleteResult)[] + [k: string]: unknown +} + export interface HandlerInput { encoding: 'application/json' body: InputSchema } +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + export interface HandlerError { status: number message?: string error?: 'InvalidSwap' } -export type HandlerOutput = HandlerError | void +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough export type HandlerReqCtx = { auth: HA params: QueryParams @@ -102,3 +115,57 @@ export function isDelete(v: unknown): v is Delete { export function validateDelete(v: unknown): ValidationResult { return lexicons.validate('com.atproto.repo.applyWrites#delete', v) } + +export interface CreateResult { + uri: string + cid: string + validationStatus?: 'valid' | 'unknown' | (string & {}) + [k: string]: unknown +} + +export function isCreateResult(v: unknown): v is CreateResult { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.applyWrites#createResult' + ) +} + +export function validateCreateResult(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.applyWrites#createResult', v) +} + +export interface UpdateResult { + uri: string + cid: string + validationStatus?: 'valid' | 'unknown' | (string & {}) + [k: string]: unknown +} + +export function isUpdateResult(v: unknown): v is UpdateResult { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.applyWrites#updateResult' + ) +} + +export function validateUpdateResult(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.applyWrites#updateResult', v) +} + +export interface DeleteResult { + [k: string]: unknown +} + +export function isDeleteResult(v: unknown): v is DeleteResult { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.applyWrites#deleteResult' + ) +} + +export function validateDeleteResult(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.applyWrites#deleteResult', v) +} 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 55cc95d0ad7..5cac0848bf1 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/createRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/createRecord.ts @@ -7,6 +7,7 @@ import { lexicons } from '../../../../lexicons' import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as ComAtprotoRepoDefs from './defs' export interface QueryParams {} @@ -17,8 +18,8 @@ export interface InputSchema { collection: string /** The Record Key. */ rkey?: string - /** Can be set to 'false' to skip Lexicon schema validation of record data. */ - validate: boolean + /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */ + validate?: boolean /** The record itself. Must contain a $type field. */ record: {} /** Compare and swap with the previous commit by CID. */ @@ -29,6 +30,8 @@ export interface InputSchema { export interface OutputSchema { uri: string cid: string + commit?: ComAtprotoRepoDefs.CommitMeta + validationStatus?: 'valid' | 'unknown' | (string & {}) [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/defs.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/defs.ts new file mode 100644 index 00000000000..8357fe2a611 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/defs.ts @@ -0,0 +1,25 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +export interface CommitMeta { + cid: string + rev: string + [k: string]: unknown +} + +export function isCommitMeta(v: unknown): v is CommitMeta { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.defs#commitMeta' + ) +} + +export function validateCommitMeta(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.defs#commitMeta', v) +} 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 3bb97be0aad..e594cd00adf 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/deleteRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/deleteRecord.ts @@ -7,6 +7,7 @@ import { lexicons } from '../../../../lexicons' import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as ComAtprotoRepoDefs from './defs' export interface QueryParams {} @@ -24,18 +25,29 @@ export interface InputSchema { [k: string]: unknown } +export interface OutputSchema { + commit?: ComAtprotoRepoDefs.CommitMeta + [k: string]: unknown +} + export interface HandlerInput { encoding: 'application/json' body: InputSchema } +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + export interface HandlerError { status: number message?: string error?: 'InvalidSwap' } -export type HandlerOutput = HandlerError | void +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 1a737a848be..9d03da19530 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/getRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/getRecord.ts @@ -39,6 +39,7 @@ export interface HandlerSuccess { export interface HandlerError { status: number message?: string + error?: 'RecordNotFound' } export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 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 193841a2294..5cde768d7c2 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/putRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/putRecord.ts @@ -7,6 +7,7 @@ import { lexicons } from '../../../../lexicons' import { isObj, hasProp } from '../../../../util' import { CID } from 'multiformats/cid' import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' +import * as ComAtprotoRepoDefs from './defs' export interface QueryParams {} @@ -17,8 +18,8 @@ export interface InputSchema { collection: string /** The Record Key. */ rkey: string - /** Can be set to 'false' to skip Lexicon schema validation of record data. */ - validate: boolean + /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */ + validate?: boolean /** The record to write. */ record: {} /** Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation */ @@ -31,6 +32,8 @@ export interface InputSchema { export interface OutputSchema { uri: string cid: string + commit?: ComAtprotoRepoDefs.CommitMeta + validationStatus?: 'valid' | 'unknown' | (string & {}) [k: string]: unknown } diff --git a/packages/bsky/src/proto/bsky_connect.ts b/packages/bsky/src/proto/bsky_connect.ts index 14df8872eec..42303cbe8e8 100644 --- a/packages/bsky/src/proto/bsky_connect.ts +++ b/packages/bsky/src/proto/bsky_connect.ts @@ -124,16 +124,22 @@ import { GetMutelistSubscriptionsResponse, GetMutesRequest, GetMutesResponse, + GetNewUserCountForRangeRequest, + GetNewUserCountForRangeResponse, GetNotificationSeenRequest, GetNotificationSeenResponse, GetNotificationsRequest, GetNotificationsResponse, + GetPostgateRecordsRequest, + GetPostgateRecordsResponse, GetPostRecordsRequest, GetPostRecordsResponse, GetPostReplyCountsRequest, GetPostReplyCountsResponse, GetProfileRecordsRequest, GetProfileRecordsResponse, + GetQuotesBySubjectSortedRequest, + GetQuotesBySubjectSortedResponse, GetRecordTakedownRequest, GetRecordTakedownResponse, GetRelationshipsRequest, @@ -309,6 +315,15 @@ export const Service = { O: GetThreadGateRecordsResponse, kind: MethodKind.Unary, }, + /** + * @generated from rpc bsky.Service.GetPostgateRecords + */ + getPostgateRecords: { + name: 'GetPostgateRecords', + I: GetPostgateRecordsRequest, + O: GetPostgateRecordsResponse, + kind: MethodKind.Unary, + }, /** * @generated from rpc bsky.Service.GetLabelerRecords */ @@ -423,6 +438,17 @@ export const Service = { O: GetActorRepostsResponse, kind: MethodKind.Unary, }, + /** + * Quotes + * + * @generated from rpc bsky.Service.GetQuotesBySubjectSorted + */ + getQuotesBySubjectSorted: { + name: 'GetQuotesBySubjectSorted', + I: GetQuotesBySubjectSortedRequest, + O: GetQuotesBySubjectSortedResponse, + kind: MethodKind.Unary, + }, /** * Interaction Counts * @@ -461,6 +487,15 @@ export const Service = { O: GetListCountsResponse, kind: MethodKind.Unary, }, + /** + * @generated from rpc bsky.Service.GetNewUserCountForRange + */ + getNewUserCountForRange: { + name: 'GetNewUserCountForRange', + I: GetNewUserCountForRangeRequest, + O: GetNewUserCountForRangeResponse, + kind: MethodKind.Unary, + }, /** * Profile * diff --git a/packages/bsky/src/proto/bsky_pb.ts b/packages/bsky/src/proto/bsky_pb.ts index b9774bdcb06..594d4eb03b4 100644 --- a/packages/bsky/src/proto/bsky_pb.ts +++ b/packages/bsky/src/proto/bsky_pb.ts @@ -960,6 +960,21 @@ export class PostRecordMeta extends Message { */ isReply = false + /** + * @generated from field: bool violates_embedding_rules = 4; + */ + violatesEmbeddingRules = false + + /** + * @generated from field: bool has_post_gate = 5; + */ + hasPostGate = false + + /** + * @generated from field: bool has_thread_gate = 6; + */ + hasThreadGate = false + constructor(data?: PartialMessage) { super() proto3.util.initPartial(data, this) @@ -976,6 +991,24 @@ export class PostRecordMeta extends Message { }, { no: 2, name: 'has_media', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, { no: 3, name: 'is_reply', kind: 'scalar', T: 8 /* ScalarType.BOOL */ }, + { + no: 4, + name: 'violates_embedding_rules', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + { + no: 5, + name: 'has_post_gate', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, + { + no: 6, + name: 'has_thread_gate', + kind: 'scalar', + T: 8 /* ScalarType.BOOL */, + }, ]) static fromBinary( @@ -1608,6 +1641,122 @@ export class GetThreadGateRecordsResponse 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.GetPostgateRecordsRequest' + 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, + ): GetPostgateRecordsRequest { + return new GetPostgateRecordsRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetPostgateRecordsRequest { + return new GetPostgateRecordsRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetPostgateRecordsRequest { + return new GetPostgateRecordsRequest().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetPostgateRecordsRequest + | PlainMessage + | undefined, + b: + | GetPostgateRecordsRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetPostgateRecordsRequest, a, b) + } +} + +/** + * @generated from message bsky.GetPostgateRecordsResponse + */ +export class GetPostgateRecordsResponse 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.GetPostgateRecordsResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'records', kind: 'message', T: Record, repeated: true }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetPostgateRecordsResponse { + return new GetPostgateRecordsResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetPostgateRecordsResponse { + return new GetPostgateRecordsResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetPostgateRecordsResponse { + return new GetPostgateRecordsResponse().fromJsonString(jsonString, options) + } + + static equals( + a: + | GetPostgateRecordsResponse + | PlainMessage + | undefined, + b: + | GetPostgateRecordsResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetPostgateRecordsResponse, a, b) + } +} + /** * @generated from message bsky.GetLabelerRecordsRequest */ @@ -2679,6 +2828,146 @@ export class GetLikesBySubjectSortedResponse 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.GetQuotesBySubjectSortedRequest' + 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, + ): GetQuotesBySubjectSortedRequest { + return new GetQuotesBySubjectSortedRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetQuotesBySubjectSortedRequest { + return new GetQuotesBySubjectSortedRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetQuotesBySubjectSortedRequest { + return new GetQuotesBySubjectSortedRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetQuotesBySubjectSortedRequest + | PlainMessage + | undefined, + b: + | GetQuotesBySubjectSortedRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetQuotesBySubjectSortedRequest, a, b) + } +} + +/** + * @generated from message bsky.GetQuotesBySubjectSortedResponse + */ +export class GetQuotesBySubjectSortedResponse 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.GetQuotesBySubjectSortedResponse' + 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, + ): GetQuotesBySubjectSortedResponse { + return new GetQuotesBySubjectSortedResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetQuotesBySubjectSortedResponse { + return new GetQuotesBySubjectSortedResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetQuotesBySubjectSortedResponse { + return new GetQuotesBySubjectSortedResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetQuotesBySubjectSortedResponse + | PlainMessage + | undefined, + b: + | GetQuotesBySubjectSortedResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetQuotesBySubjectSortedResponse, a, b) + } +} + /** * - return like uris for user A on subject B, C, D... * - viewer state on posts @@ -3062,6 +3351,11 @@ export class GetInteractionCountsResponse extends Message) { super() proto3.util.initPartial(data, this) @@ -3091,6 +3385,13 @@ export class GetInteractionCountsResponse extends Message { } } +/** + * @generated from message bsky.GetNewUserCountForRangeRequest + */ +export class GetNewUserCountForRangeRequest extends Message { + /** + * @generated from field: google.protobuf.Timestamp start = 1; + */ + start?: Timestamp + + /** + * @generated from field: google.protobuf.Timestamp end = 2; + */ + end?: Timestamp + + constructor(data?: PartialMessage) { + super() + proto3.util.initPartial(data, this) + } + + static readonly runtime: typeof proto3 = proto3 + static readonly typeName = 'bsky.GetNewUserCountForRangeRequest' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'start', kind: 'message', T: Timestamp }, + { no: 2, name: 'end', kind: 'message', T: Timestamp }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetNewUserCountForRangeRequest { + return new GetNewUserCountForRangeRequest().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetNewUserCountForRangeRequest { + return new GetNewUserCountForRangeRequest().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetNewUserCountForRangeRequest { + return new GetNewUserCountForRangeRequest().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetNewUserCountForRangeRequest + | PlainMessage + | undefined, + b: + | GetNewUserCountForRangeRequest + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetNewUserCountForRangeRequest, a, b) + } +} + +/** + * @generated from message bsky.GetNewUserCountForRangeResponse + */ +export class GetNewUserCountForRangeResponse 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.GetNewUserCountForRangeResponse' + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: 'count', kind: 'scalar', T: 5 /* ScalarType.INT32 */ }, + ]) + + static fromBinary( + bytes: Uint8Array, + options?: Partial, + ): GetNewUserCountForRangeResponse { + return new GetNewUserCountForRangeResponse().fromBinary(bytes, options) + } + + static fromJson( + jsonValue: JsonValue, + options?: Partial, + ): GetNewUserCountForRangeResponse { + return new GetNewUserCountForRangeResponse().fromJson(jsonValue, options) + } + + static fromJsonString( + jsonString: string, + options?: Partial, + ): GetNewUserCountForRangeResponse { + return new GetNewUserCountForRangeResponse().fromJsonString( + jsonString, + options, + ) + } + + static equals( + a: + | GetNewUserCountForRangeResponse + | PlainMessage + | undefined, + b: + | GetNewUserCountForRangeResponse + | PlainMessage + | undefined, + ): boolean { + return proto3.util.equals(GetNewUserCountForRangeResponse, a, b) + } +} + /** * - return repost uris where subject uri is subject A * - `getReposts` list for a post @@ -10496,7 +10919,7 @@ export class GetRecordTakedownResponse extends Message 1 ? undefined : postView.embed ? [postView.embed] : [], } @@ -944,9 +1008,19 @@ export class Views { return this.embedBlocked(uri, state) } + const post = state.posts?.get(postUri) + if (post?.violatesEmbeddingRules) { + return this.embedDetached(uri) + } + if (parsedUri.collection === ids.AppBskyFeedPost) { const view = this.embedPostView(uri, state, depth) if (!view) return this.embedNotFound(uri) + const postgateRecordUri = postUriToPostgateUri(parsedUri.toString()) + const postgate = state.postgates?.get(postgateRecordUri) + if (postgate?.record?.detachedEmbeddingUris?.includes(postUri)) { + return this.embedDetached(uri) + } return this.recordEmbedWrapper(view, withTypeTag) } else if (parsedUri.collection === ids.AppBskyFeedGenerator) { const view = this.feedGenerator(uri, state) @@ -989,9 +1063,11 @@ export class Views { depth: number, ): RecordWithMediaView | undefined { const creator = creatorFromUri(postUri) - let mediaEmbed: ImagesEmbedView | ExternalEmbedView + let mediaEmbed: ImagesEmbedView | VideoEmbedView | ExternalEmbedView if (isImagesEmbed(embed.media)) { mediaEmbed = this.imagesEmbed(creator, embed.media) + } else if (isVideoEmbed(embed.media)) { + mediaEmbed = this.videoEmbed(creator, embed.media) } else if (isExternalEmbed(embed.media)) { mediaEmbed = this.externalEmbed(creator, embed.media) } else { @@ -1010,13 +1086,15 @@ export class Views { return true } const rootUriStr: string = post?.record.reply?.root.uri ?? uri - const gate = state.threadgates?.get(postToGateUri(rootUriStr))?.record + const gate = state.threadgates?.get( + postUriToThreadgateUri(rootUriStr), + )?.record const viewer = state.ctx?.viewer if (!gate || !viewer) { return undefined } const rootPost = state.posts?.get(rootUriStr)?.record - const ownerDid = new AtUri(rootUriStr).hostname + const ownerDid = creatorFromUri(rootUriStr) const { canReply, allowFollowing, @@ -1037,6 +1115,39 @@ export class Views { return true } + userPostEmbeddingDisabled( + uri: string, + state: HydrationState, + ): boolean | undefined { + const post = state.posts?.get(uri) + if (!post) { + return true + } + const postgateRecordUri = postUriToPostgateUri(uri) + const gate = state.postgates?.get(postgateRecordUri)?.record + const viewerDid = state.ctx?.viewer ?? undefined + const { + embeddingRules: { canEmbed }, + } = parsePostgate({ + gate, + viewerDid, + authorDid: creatorFromUri(uri), + }) + if (canEmbed) { + return false + } + return true + } + + viewerPinned(uri: string, state: HydrationState, authorDid: string) { + if (!state.ctx?.viewer || state.ctx.viewer !== authorDid) return + const actor = state.actors?.get(authorDid) + if (!actor) return + const pinnedPost = actor.profile?.pinnedPost + if (!pinnedPost) return undefined + return pinnedPost.uri === uri + } + notification( notif: Notification, lastSeenAt: string | undefined, @@ -1093,14 +1204,6 @@ export class Views { } } -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 index 54f6fb40543..3470a857207 100644 --- a/packages/bsky/src/views/types.ts +++ b/packages/bsky/src/views/types.ts @@ -2,6 +2,10 @@ import { Main as ImagesEmbed, View as ImagesEmbedView, } from '../lexicon/types/app/bsky/embed/images' +import { + Main as VideoEmbed, + View as VideoEmbedView, +} from '../lexicon/types/app/bsky/embed/video' import { Main as ExternalEmbed, View as ExternalEmbedView, @@ -29,6 +33,11 @@ export type { 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 VideoEmbed, + View as VideoEmbedView, +} from '../lexicon/types/app/bsky/embed/video' +export { isMain as isVideoEmbed } from '../lexicon/types/app/bsky/embed/video' export type { Main as ExternalEmbed, View as ExternalEmbedView, @@ -39,6 +48,7 @@ export type { View as RecordEmbedView, ViewBlocked as EmbedBlocked, ViewNotFound as EmbedNotFound, + ViewDetached as EmbedDetached, ViewRecord as PostEmbedView, } from '../lexicon/types/app/bsky/embed/record' export { isMain as isRecordEmbed } from '../lexicon/types/app/bsky/embed/record' @@ -58,10 +68,16 @@ 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 Embed = + | ImagesEmbed + | VideoEmbed + | ExternalEmbed + | RecordEmbed + | RecordWithMedia export type EmbedView = | ImagesEmbedView + | VideoEmbedView | ExternalEmbedView | RecordEmbedView | RecordWithMediaView diff --git a/packages/bsky/src/views/util.ts b/packages/bsky/src/views/util.ts index 6f63945ef0a..f9e54f713cf 100644 --- a/packages/bsky/src/views/util.ts +++ b/packages/bsky/src/views/util.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/syntax' +import * as util from 'node:util' import { BlobRef } from '@atproto/lexicon' import { Record as PostRecord } from '../lexicon/types/app/bsky/feed/post' import { @@ -7,12 +7,12 @@ import { isListRule, isMentionRule, } from '../lexicon/types/app/bsky/feed/threadgate' +import { + Record as PostgateRecord, + isDisableRule as isPostgateDisableRule, +} from '../lexicon/types/app/bsky/feed/postgate' 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, @@ -27,8 +27,8 @@ export const parseThreadGate = ( return { canReply: true } } - const allowMentions = !!gate.allow.find(isMentionRule) - const allowFollowing = !!gate.allow.find(isFollowingRule) + const allowMentions = gate.allow.some(isMentionRule) + const allowFollowing = gate.allow.some(isFollowingRule) const allowListUris = gate.allow?.filter(isListRule).map((item) => item.list) // check mentions first since it's quick and synchronous @@ -62,3 +62,57 @@ export const cidFromBlobJson = (json: BlobRef) => { } return (json['cid'] ?? '') as string } + +export const parsePostgate = ({ + gate, + viewerDid, + authorDid, +}: { + gate: PostgateRecord | undefined + viewerDid: string | undefined + authorDid: string +}): ParsedPostgate => { + if (viewerDid === authorDid) { + return { embeddingRules: { canEmbed: true } } + } + // default state is unset, allow everyone + if (!gate || !gate.embeddingRules) { + return { embeddingRules: { canEmbed: true } } + } + + const disabled = gate.embeddingRules.some(isPostgateDisableRule) + if (disabled) { + return { embeddingRules: { canEmbed: false } } + } + + return { embeddingRules: { canEmbed: true } } +} + +type ParsedPostgate = { + embeddingRules: { + canEmbed: boolean + } +} + +export class VideoUriBuilder { + constructor( + private opts: { + playlistUrlPattern: string // e.g. https://hostname/vid/%s/%s/playlist.m3u8 + thumbnailUrlPattern: string // e.g. https://hostname/vid/%s/%s/thumbnail.jpg + }, + ) {} + playlist({ did, cid }: { did: string; cid: string }) { + return util.format( + this.opts.playlistUrlPattern, + encodeURIComponent(did), + encodeURIComponent(cid), + ) + } + thumbnail({ did, cid }: { did: string; cid: string }) { + return util.format( + this.opts.thumbnailUrlPattern, + encodeURIComponent(did), + encodeURIComponent(cid), + ) + } +} diff --git a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap index 94499551670..099dd259c0d 100644 --- a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap @@ -26,6 +26,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -42,6 +43,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, } @@ -73,6 +75,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -89,6 +92,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, } @@ -160,6 +164,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -176,6 +181,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, } @@ -249,6 +255,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -265,6 +272,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, } @@ -531,6 +539,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -548,6 +557,7 @@ Array [ "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -573,6 +583,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -586,6 +597,7 @@ Array [ "repostCount": 0, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -644,6 +656,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(2)", @@ -662,6 +675,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -707,6 +721,7 @@ Array [ "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "like": "record(8)", "threadMuted": false, }, @@ -730,6 +745,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -749,6 +765,7 @@ Array [ "repostCount": 0, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -786,6 +803,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -795,6 +813,7 @@ Array [ "repostCount": 1, "uri": "record(10)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -831,6 +850,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -840,6 +860,7 @@ Array [ "repostCount": 1, "uri": "record(10)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -919,6 +940,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(2)", @@ -938,6 +960,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(5)", @@ -987,6 +1010,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1017,6 +1041,7 @@ Array [ "repostCount": 1, "uri": "record(11)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1079,6 +1104,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1139,6 +1165,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(3)", @@ -1157,6 +1184,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1215,6 +1243,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1264,6 +1293,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1302,6 +1332,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1360,6 +1391,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1377,6 +1409,7 @@ Array [ "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1435,6 +1468,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(5)", @@ -1453,6 +1487,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1498,6 +1533,7 @@ Array [ "repostCount": 0, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "like": "record(8)", "threadMuted": false, }, @@ -1521,6 +1557,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1540,6 +1577,7 @@ Array [ "repostCount": 0, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1577,6 +1615,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1586,6 +1625,7 @@ Array [ "repostCount": 1, "uri": "record(10)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1622,6 +1662,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1631,6 +1672,7 @@ Array [ "repostCount": 1, "uri": "record(10)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, diff --git a/packages/bsky/tests/_util.ts b/packages/bsky/tests/_util.ts index 7f13d383aaf..75c83f1a6d7 100644 --- a/packages/bsky/tests/_util.ts +++ b/packages/bsky/tests/_util.ts @@ -80,6 +80,15 @@ export const forSnapshot = (obj: unknown) => { const [, did, cid] = match return str.replace(did, take(users, did)).replace(cid, take(cids, cid)) } + if (str.match(/\/vid\/did%3Aplc%3A[^/]+\/[^/]+\/[^/]+$/)) { + // Match video urls + const match = str.match(/\/vid\/(did%3Aplc%3A[^/]+)\/([^/]+)\/[^/]+$/) + if (!match) return str + const [, did, cid] = match + return str + .replace(did, take(users, decodeURIComponent(did))) + .replace(cid, take(cids, cid)) + } let isCid: boolean try { CID.parse(str) diff --git a/packages/bsky/tests/admin/admin-auth.test.ts b/packages/bsky/tests/admin/admin-auth.test.ts index d811bf55dc3..027ff49eb1e 100644 --- a/packages/bsky/tests/admin/admin-auth.test.ts +++ b/packages/bsky/tests/admin/admin-auth.test.ts @@ -3,6 +3,7 @@ import { AtpAgent } from '@atproto/api' import { Secp256k1Keypair } from '@atproto/crypto' import { createServiceAuthHeaders } from '@atproto/xrpc-server' import { RepoRef } from '../../src/lexicon/types/com/atproto/admin/defs' +import { ids } from '../../src/lexicon/lexicons' describe('admin auth', () => { let network: TestNetwork @@ -68,10 +69,10 @@ describe('admin auth', () => { }) it('allows service auth requests from the configured appview did', async () => { - const headers = await createServiceAuthHeaders({ + const updateHeaders = await createServiceAuthHeaders({ iss: modServiceDid, aud: bskyDid, - lxm: null, + lxm: ids.ComAtprotoAdminUpdateSubjectStatus, keypair: modServiceKey, }) await agent.api.com.atproto.admin.updateSubjectStatus( @@ -80,14 +81,20 @@ describe('admin auth', () => { takedown: { applied: true, ref: 'test-repo' }, }, { - ...headers, + ...updateHeaders, encoding: 'application/json', }, ) + const getHeaders = await createServiceAuthHeaders({ + iss: modServiceDid, + aud: bskyDid, + lxm: ids.ComAtprotoAdminGetSubjectStatus, + keypair: modServiceKey, + }) const res = await agent.api.com.atproto.admin.getSubjectStatus( { did: repoSubject.did }, - headers, + getHeaders, ) expect(res.data.subject.did).toBe(repoSubject.did) expect(res.data.takedown?.applied).toBe(true) @@ -97,7 +104,7 @@ describe('admin auth', () => { const headers = await createServiceAuthHeaders({ iss: altModDid, aud: bskyDid, - lxm: null, + lxm: ids.ComAtprotoAdminUpdateSubjectStatus, keypair: modServiceKey, }) const attempt = agent.api.com.atproto.admin.updateSubjectStatus( @@ -118,7 +125,7 @@ describe('admin auth', () => { const headers = await createServiceAuthHeaders({ iss: sc.dids.alice, aud: bskyDid, - lxm: null, + lxm: ids.ComAtprotoAdminUpdateSubjectStatus, keypair: aliceKey, }) const attempt = agent.api.com.atproto.admin.updateSubjectStatus( @@ -139,7 +146,7 @@ describe('admin auth', () => { const headers = await createServiceAuthHeaders({ iss: modServiceDid, aud: bskyDid, - lxm: null, + lxm: ids.ComAtprotoAdminUpdateSubjectStatus, keypair: badKey, }) const attempt = agent.api.com.atproto.admin.updateSubjectStatus( @@ -162,7 +169,7 @@ describe('admin auth', () => { const headers = await createServiceAuthHeaders({ iss: modServiceDid, aud: sc.dids.alice, - lxm: null, + lxm: ids.ComAtprotoAdminUpdateSubjectStatus, keypair: modServiceKey, }) const attempt = agent.api.com.atproto.admin.updateSubjectStatus( diff --git a/packages/bsky/tests/auth.test.ts b/packages/bsky/tests/auth.test.ts index da756723bb8..a6af4a84d48 100644 --- a/packages/bsky/tests/auth.test.ts +++ b/packages/bsky/tests/auth.test.ts @@ -2,6 +2,7 @@ import { AtpAgent } from '@atproto/api' import { SeedClient, TestNetwork, usersSeed } from '@atproto/dev-env' import { createServiceJwt } from '@atproto/xrpc-server' import { Keypair, Secp256k1Keypair } from '@atproto/crypto' +import { ids } from '../src/lexicon/lexicons' describe('auth', () => { let network: TestNetwork @@ -29,7 +30,7 @@ describe('auth', () => { const jwt = await createServiceJwt({ iss: issuer, aud: network.bsky.ctx.cfg.serverDid, - lxm: null, + lxm: ids.AppBskyActorGetProfile, keypair, }) return agent.api.app.bsky.actor.getProfile( diff --git a/packages/bsky/tests/data-plane/__snapshots__/indexing.test.ts.snap b/packages/bsky/tests/data-plane/__snapshots__/indexing.test.ts.snap index 66c19cf7e00..495786e1d43 100644 --- a/packages/bsky/tests/data-plane/__snapshots__/indexing.test.ts.snap +++ b/packages/bsky/tests/data-plane/__snapshots__/indexing.test.ts.snap @@ -59,6 +59,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -68,6 +69,7 @@ Array [ "repostCount": 0, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -87,6 +89,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -106,6 +109,7 @@ Array [ "repostCount": 1, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -164,6 +168,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -199,6 +204,7 @@ Array [ "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -217,6 +223,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -226,6 +233,7 @@ Array [ "repostCount": 1, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -282,6 +290,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(9)", @@ -332,6 +341,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 1, "uri": "record(7)", @@ -374,6 +384,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -390,6 +401,7 @@ Array [ "repostCount": 0, "uri": "record(6)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -409,6 +421,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -418,6 +431,7 @@ Array [ "repostCount": 1, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -445,6 +459,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -462,6 +477,7 @@ Array [ "repostCount": 0, "uri": "record(12)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -554,6 +570,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -577,6 +594,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -621,6 +639,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -644,6 +663,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, diff --git a/packages/bsky/tests/data-plane/handle-invalidation.test.ts b/packages/bsky/tests/data-plane/handle-invalidation.test.ts index 8469a8507ef..8c366e23d4b 100644 --- a/packages/bsky/tests/data-plane/handle-invalidation.test.ts +++ b/packages/bsky/tests/data-plane/handle-invalidation.test.ts @@ -1,6 +1,7 @@ import { DAY } from '@atproto/common' import { TestNetwork, SeedClient, usersSeed } from '@atproto/dev-env' import { AtpAgent } from '@atproto/api' +import { ids } from '../../src/lexicon/lexicons' describe('handle invalidation', () => { let network: TestNetwork @@ -62,7 +63,12 @@ describe('handle invalidation', () => { const res = await agent.api.app.bsky.actor.getProfile( { actor: eveAccnt.did }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyActorGetProfile, + ), + }, ) expect(res.data.handle).toEqual('handle.invalid') }) @@ -77,7 +83,12 @@ describe('handle invalidation', () => { await network.processAll() const res = await agent.api.app.bsky.actor.getProfile( { actor: alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyActorGetProfile, + ), + }, ) expect(res.data.handle).toEqual('handle.invalid') }) @@ -92,7 +103,12 @@ describe('handle invalidation', () => { await network.processAll() const res = await agent.api.app.bsky.actor.getProfile( { actor: alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyActorGetProfile, + ), + }, ) expect(res.data.handle).toEqual(sc.accounts[alice].handle) }) @@ -112,13 +128,23 @@ describe('handle invalidation', () => { const aliceRes = await agent.api.app.bsky.actor.getProfile( { actor: alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyActorGetProfile, + ), + }, ) expect(aliceRes.data.handle).toEqual('handle.invalid') const bobRes = await agent.api.app.bsky.actor.getProfile( { actor: bob }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyActorGetProfile, + ), + }, ) expect(bobRes.data.handle).toEqual(sc.accounts[alice].handle) }) diff --git a/packages/bsky/tests/data-plane/indexing.test.ts b/packages/bsky/tests/data-plane/indexing.test.ts index 406d56305f3..b3ed9680d29 100644 --- a/packages/bsky/tests/data-plane/indexing.test.ts +++ b/packages/bsky/tests/data-plane/indexing.test.ts @@ -97,7 +97,12 @@ describe('indexing', () => { const getAfterCreate = await agent.api.app.bsky.feed.getPostThread( { uri: uri.toString() }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(getAfterCreate.data)).toMatchSnapshot() const createNotifications = await getNotifications(db, uri) @@ -107,7 +112,12 @@ describe('indexing', () => { const getAfterUpdate = await agent.api.app.bsky.feed.getPostThread( { uri: uri.toString() }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(getAfterUpdate.data)).toMatchSnapshot() const updateNotifications = await getNotifications(db, uri) @@ -117,7 +127,12 @@ describe('indexing', () => { const getAfterDelete = agent.api.app.bsky.feed.getPostThread( { uri: uri.toString() }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) await expect(getAfterDelete).rejects.toThrow(/Post not found:/) const deleteNotifications = await getNotifications(db, uri) @@ -162,7 +177,12 @@ describe('indexing', () => { const getAfterCreate = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.dan }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyActorGetProfile, + ), + }, ) expect(forSnapshot(getAfterCreate.data)).toMatchSnapshot() @@ -171,7 +191,12 @@ describe('indexing', () => { const getAfterUpdate = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.dan }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyActorGetProfile, + ), + }, ) expect(forSnapshot(getAfterUpdate.data)).toMatchSnapshot() @@ -180,7 +205,12 @@ describe('indexing', () => { const getAfterDelete = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.dan }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyActorGetProfile, + ), + }, ) expect(forSnapshot(getAfterDelete.data)).toMatchSnapshot() }) @@ -247,6 +277,7 @@ describe('indexing', () => { replyCount: 1, repostCount: 1, likeCount: 1, + quoteCount: 0, }) // Cleanup const del = (uri: AtUri) => { @@ -331,7 +362,12 @@ describe('indexing', () => { data: { notifications }, } = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(notifications).toHaveLength(2) @@ -393,7 +429,7 @@ describe('indexing', () => { describe('indexRepo', () => { beforeAll(async () => { - network.bsky.sub.run() + await network.bsky.sub.restart() await basicSeed(sc, false) await network.processAll() await network.bsky.sub.destroy() @@ -404,15 +440,30 @@ describe('indexing', () => { // Mark originals const { data: origProfile } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyActorGetProfile, + ), + }, ) const { data: origFeed } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) const { data: origFollows } = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyGraphGetFollows, + ), + }, ) // Index const { data: commit } = @@ -424,15 +475,30 @@ describe('indexing', () => { // Check const { data: profile } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyActorGetProfile, + ), + }, ) const { data: feed } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) const { data: follows } = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(forSnapshot([origProfile, origFeed, origFollows])).toEqual( forSnapshot([profile, feed, follows]), @@ -468,15 +534,30 @@ describe('indexing', () => { // Check const { data: profile } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyActorGetProfile, + ), + }, ) const { data: feed } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) const { data: follows } = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(profile.description).toEqual('freshening things up') expect(feed.feed[0].post.uri).toEqual(newPost.ref.uriStr) @@ -525,12 +606,22 @@ describe('indexing', () => { // Check const getGoodPost = agent.api.app.bsky.feed.getPostThread( { uri: writes[0].uri.toString(), depth: 0 }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) await expect(getGoodPost).resolves.toBeDefined() const getBadPost = agent.api.app.bsky.feed.getPostThread( { uri: writes[1].uri.toString(), depth: 0 }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) await expect(getBadPost).rejects.toThrow('Post not found') }) @@ -540,7 +631,12 @@ describe('indexing', () => { const getIndexedHandle = async (did) => { const res = await agent.api.app.bsky.actor.getProfile( { actor: did }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyActorGetProfile, + ), + }, ) return res.data.handle } @@ -618,13 +714,23 @@ describe('indexing', () => { it('does not unindex actor when they are still being hosted by their pds', async () => { const { data: profileBefore } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyActorGetProfile, + ), + }, ) // Attempt indexing tombstone await network.bsky.sub.indexingSvc.deleteActor(sc.dids.alice) const { data: profileAfter } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyActorGetProfile, + ), + }, ) expect(profileAfter).toEqual(profileBefore) }) @@ -633,7 +739,12 @@ describe('indexing', () => { const { alice } = sc.dids const getProfileBefore = agent.api.app.bsky.actor.getProfile( { actor: alice }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyActorGetProfile, + ), + }, ) await expect(getProfileBefore).resolves.toBeDefined() // Delete account on pds @@ -651,7 +762,12 @@ describe('indexing', () => { await network.bsky.sub.indexingSvc.deleteActor(alice) const getProfileAfter = agent.api.app.bsky.actor.getProfile( { actor: alice }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyActorGetProfile, + ), + }, ) await expect(getProfileAfter).rejects.toThrow('Profile not found') }) diff --git a/packages/bsky/tests/data-plane/subscription/repo.test.ts b/packages/bsky/tests/data-plane/subscription.test.ts similarity index 91% rename from packages/bsky/tests/data-plane/subscription/repo.test.ts rename to packages/bsky/tests/data-plane/subscription.test.ts index 8faa5538ab6..6b52ccac650 100644 --- a/packages/bsky/tests/data-plane/subscription/repo.test.ts +++ b/packages/bsky/tests/data-plane/subscription.test.ts @@ -1,11 +1,11 @@ import { AtpAgent } from '@atproto/api' import { cborDecode, cborEncode } from '@atproto/common' -import { DatabaseSchemaType } from '../../../src/data-plane/server/db/database-schema' +import { DatabaseSchemaType } from '../../src/data-plane/server/db/database-schema' import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env' import { PreparedWrite, sequencer } from '@atproto/pds' import { CommitData } from '@atproto/repo' -import { ids } from '../../../src/lexicon/lexicons' -import { forSnapshot } from '../../_util' +import { ids } from '../../src/lexicon/lexicons' +import { forSnapshot } from '../_util' type Database = TestNetwork['bsky']['db'] @@ -60,12 +60,7 @@ describe('sync', () => { const originalTableDump = await getTableDump() // Reprocess repos via sync subscription, on top of existing indices - 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.sub.run() + await network.bsky.sub.restart() await network.processAll() // Permissive of indexedAt times changing diff --git a/packages/bsky/tests/data-plane/subscription/util.test.ts b/packages/bsky/tests/data-plane/subscription/util.test.ts deleted file mode 100644 index 9c2bbf92bfb..00000000000 --- a/packages/bsky/tests/data-plane/subscription/util.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { wait } from '@atproto/common' -import { randomStr } from '@atproto/crypto' -import { - ConsecutiveList, - LatestQueue, - PartitionedQueue, -} from '../../../src/data-plane/server/subscription/util' - -describe('subscription utils', () => { - describe('ConsecutiveList', () => { - it('tracks consecutive complete items.', () => { - const consecutive = new ConsecutiveList() - // add items - const item1 = consecutive.push(1) - const item2 = consecutive.push(2) - const item3 = consecutive.push(3) - expect(item1.isComplete).toEqual(false) - expect(item2.isComplete).toEqual(false) - expect(item3.isComplete).toEqual(false) - // complete items out of order - expect(consecutive.list.length).toBe(3) - expect(item2.complete()).toEqual([]) - expect(item2.isComplete).toEqual(true) - expect(consecutive.list.length).toBe(3) - expect(item1.complete()).toEqual([1, 2]) - expect(item1.isComplete).toEqual(true) - expect(consecutive.list.length).toBe(1) - expect(item3.complete()).toEqual([3]) - expect(consecutive.list.length).toBe(0) - expect(item3.isComplete).toEqual(true) - }) - }) - - describe('LatestQueue', () => { - it('only performs most recently queued item.', async () => { - const latest = new LatestQueue() - const complete: number[] = [] - latest.add(async () => { - await wait(1) - complete.push(1) - }) - latest.add(async () => { - await wait(1) - complete.push(2) - }) - latest.add(async () => { - await wait(1) - complete.push(3) - }) - latest.add(async () => { - await wait(1) - complete.push(4) - }) - await latest.queue.onIdle() - expect(complete).toEqual([1, 4]) // skip 2, 3 - latest.add(async () => { - await wait(1) - complete.push(5) - }) - latest.add(async () => { - await wait(1) - complete.push(6) - }) - await latest.queue.onIdle() - expect(complete).toEqual([1, 4, 5, 6]) - }) - - it('stops processing queued messages on destroy.', async () => { - const latest = new LatestQueue() - const complete: number[] = [] - latest.add(async () => { - await wait(1) - complete.push(1) - }) - latest.add(async () => { - await wait(1) - complete.push(2) - }) - const destroyed = latest.destroy() - latest.add(async () => { - await wait(1) - complete.push(3) - }) - await destroyed - expect(complete).toEqual([1]) // 2 was cleared, 3 was after destroy - // show that waiting on destroyed above was already enough to reflect all complete items - await latest.queue.onIdle() - expect(complete).toEqual([1]) - }) - }) - - describe('PartitionedQueue', () => { - it('performs work in parallel across partitions, serial within a partition.', async () => { - const partitioned = new PartitionedQueue({ concurrency: Infinity }) - const complete: number[] = [] - // partition 1 items start slow but get faster: slow should still complete first. - partitioned.add('1', async () => { - await wait(30) - complete.push(11) - }) - partitioned.add('1', async () => { - await wait(20) - complete.push(12) - }) - partitioned.add('1', async () => { - await wait(1) - complete.push(13) - }) - expect(partitioned.partitions.size).toEqual(1) - // partition 2 items complete quickly except the last, which is slowest of all events. - partitioned.add('2', async () => { - await wait(1) - complete.push(21) - }) - partitioned.add('2', async () => { - await wait(1) - complete.push(22) - }) - partitioned.add('2', async () => { - await wait(1) - complete.push(23) - }) - partitioned.add('2', async () => { - await wait(60) - complete.push(24) - }) - expect(partitioned.partitions.size).toEqual(2) - await partitioned.main.onIdle() - expect(complete).toEqual([21, 22, 23, 11, 12, 13, 24]) - expect(partitioned.partitions.size).toEqual(0) - }) - - it('limits overall concurrency.', async () => { - const partitioned = new PartitionedQueue({ concurrency: 1 }) - const complete: number[] = [] - // if concurrency were not constrained, partition 1 would complete all items - // before any items from partition 2. since it is constrained, the work is complete in the order added. - partitioned.add('1', async () => { - await wait(1) - complete.push(11) - }) - partitioned.add('2', async () => { - await wait(10) - complete.push(21) - }) - partitioned.add('1', async () => { - await wait(1) - complete.push(12) - }) - partitioned.add('2', async () => { - await wait(10) - complete.push(22) - }) - // only partition 1 exists so far due to the concurrency - expect(partitioned.partitions.size).toEqual(1) - await partitioned.main.onIdle() - expect(complete).toEqual([11, 21, 12, 22]) - expect(partitioned.partitions.size).toEqual(0) - }) - - it('settles with many items.', async () => { - const partitioned = new PartitionedQueue({ concurrency: 100 }) - const complete: { partition: string; id: number }[] = [] - const partitions = new Set() - for (let i = 0; i < 500; ++i) { - const partition = randomStr(1, 'base16').slice(0, 1) - partitions.add(partition) - partitioned.add(partition, async () => { - await wait((i % 2) * 2) - complete.push({ partition, id: i }) - }) - } - expect(partitioned.partitions.size).toEqual(partitions.size) - await partitioned.main.onIdle() - expect(complete.length).toEqual(500) - for (const partition of partitions) { - const ids = complete - .filter((item) => item.partition === partition) - .map((item) => item.id) - expect(ids).toEqual([...ids].sort((a, b) => a - b)) - } - expect(partitioned.partitions.size).toEqual(0) - }) - }) -}) diff --git a/packages/bsky/tests/data-plane/thread-mutes.test.ts b/packages/bsky/tests/data-plane/thread-mutes.test.ts index 8b03b0d973e..5e96432463f 100644 --- a/packages/bsky/tests/data-plane/thread-mutes.test.ts +++ b/packages/bsky/tests/data-plane/thread-mutes.test.ts @@ -1,5 +1,6 @@ import { RecordRef, SeedClient, TestNetwork, usersSeed } from '@atproto/dev-env' import { AtpAgent } from '@atproto/api' +import { ids } from '../../src/lexicon/lexicons' describe('thread mutes', () => { let network: TestNetwork @@ -35,7 +36,10 @@ describe('thread mutes', () => { { root: rootPost.uriStr }, { encoding: 'application/json', - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphMuteThread, + ), }, ) }) @@ -46,7 +50,7 @@ describe('thread mutes', () => { uris: [rootPost.uriStr, replyPost.uriStr], }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetPosts), }, ) expect(res.data.posts[0].viewer?.threadMuted).toBe(true) @@ -60,7 +64,12 @@ describe('thread mutes', () => { const notifsRes = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(notifsRes.data.notifications.length).toBe(0) }) @@ -72,7 +81,12 @@ describe('thread mutes', () => { const notifsRes = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(notifsRes.data.notifications.length).toBe(0) }) @@ -84,7 +98,12 @@ describe('thread mutes', () => { const notifsRes = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(notifsRes.data.notifications.length).toBe(0) }) @@ -96,7 +115,12 @@ describe('thread mutes', () => { const notifsRes = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(notifsRes.data.notifications.length).toBe(0) }) @@ -106,7 +130,10 @@ describe('thread mutes', () => { { root: rootPost.uriStr }, { encoding: 'application/json', - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphUnmuteThread, + ), }, ) }) @@ -117,7 +144,7 @@ describe('thread mutes', () => { uris: [rootPost.uriStr, replyPost.uriStr], }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetPosts), }, ) expect(res.data.posts[0].viewer?.threadMuted).toBe(false) @@ -133,7 +160,12 @@ describe('thread mutes', () => { const notifsRes = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(notifsRes.data.notifications.length).toBe(4) }) diff --git a/packages/bsky/tests/entryway-auth.test.ts b/packages/bsky/tests/entryway-auth.test.ts new file mode 100644 index 00000000000..75d54248f31 --- /dev/null +++ b/packages/bsky/tests/entryway-auth.test.ts @@ -0,0 +1,174 @@ +import * as nodeCrypto from 'node:crypto' +import KeyEncoder from 'key-encoder' +import * as ui8 from 'uint8arrays' +import * as jose from 'jose' +import * as crypto from '@atproto/crypto' +import { AtpAgent, AtUri } from '@atproto/api' +import { basicSeed, SeedClient, TestNetwork } from '@atproto/dev-env' +import assert from 'node:assert' +import { MINUTE } from '@atproto/common' + +const keyEncoder = new KeyEncoder('secp256k1') + +const derivePrivKey = async ( + keypair: crypto.ExportableKeypair, +): Promise => { + const privKeyRaw = await keypair.export() + const privKeyEncoded = keyEncoder.encodePrivate( + ui8.toString(privKeyRaw, 'hex'), + 'raw', + 'pem', + ) + return nodeCrypto.createPrivateKey(privKeyEncoded) +} + +// @NOTE temporary measure, see note on entrywaySession in bsky/src/auth-verifier.ts +describe('entryway auth', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + let alice: string + let jwtPrivKey: nodeCrypto.KeyObject + + beforeAll(async () => { + const keypair = await crypto.Secp256k1Keypair.create({ exportable: true }) + jwtPrivKey = await derivePrivKey(keypair) + const entrywayJwtPublicKeyHex = ui8.toString( + keypair.publicKeyBytes(), + 'hex', + ) + + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_entryway_auth', + bsky: { + entrywayJwtPublicKeyHex, + }, + }) + agent = network.bsky.getClient() + sc = network.getSeedClient() + await basicSeed(sc) + await network.processAll() + alice = sc.dids.alice + }) + + afterAll(async () => { + await network.close() + }) + + it('works', async () => { + const signer = new jose.SignJWT({ scope: 'com.atproto.access' }) + .setSubject(alice) + .setIssuedAt() + .setExpirationTime('60mins') + .setAudience('did:web:fake.server.bsky.network') + .setProtectedHeader({ + typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html + alg: 'ES256K', + }) + const token = await signer.sign(jwtPrivKey) + const res = await agent.app.bsky.actor.getProfile( + { actor: sc.dids.bob }, + { headers: { authorization: `Bearer ${token}` } }, + ) + expect(res.data.did).toEqual(sc.dids.bob) + // ensure this request is personalized for alice + const followingUri = res.data.viewer?.following + assert(followingUri) + const parsed = new AtUri(followingUri) + expect(parsed.hostname).toEqual(alice) + }) + + it('does not work on bad scopes', async () => { + const signer = new jose.SignJWT({ scope: 'com.atproto.refresh' }) + .setSubject(alice) + .setIssuedAt() + .setExpirationTime('60mins') + .setAudience('did:web:fake.server.bsky.network') + .setProtectedHeader({ + typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html + alg: 'ES256K', + }) + const token = await signer.sign(jwtPrivKey) + const attempt = agent.app.bsky.actor.getProfile( + { actor: sc.dids.bob }, + { headers: { authorization: `Bearer ${token}` } }, + ) + await expect(attempt).rejects.toThrow('Bad token scope') + }) + + it('does not work on expired tokens', async () => { + const time = Math.floor((Date.now() - 5 * MINUTE) / 1000) + const signer = new jose.SignJWT({ scope: 'com.atproto.access' }) + .setSubject(alice) + .setIssuedAt() + .setExpirationTime(time) + .setAudience('did:web:fake.server.bsky.network') + .setProtectedHeader({ + typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html + alg: 'ES256K', + }) + const token = await signer.sign(jwtPrivKey) + const attempt = agent.app.bsky.actor.getProfile( + { actor: sc.dids.bob }, + { headers: { authorization: `Bearer ${token}` } }, + ) + await expect(attempt).rejects.toThrow('Token has expired') + }) + + it('does not work on bad auds', async () => { + const signer = new jose.SignJWT({ scope: 'com.atproto.access' }) + .setSubject(alice) + .setIssuedAt() + .setExpirationTime('60mins') + .setAudience('did:web:my.personal.pds.com') + .setProtectedHeader({ + typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html + alg: 'ES256K', + }) + const token = await signer.sign(jwtPrivKey) + const attempt = agent.app.bsky.actor.getProfile( + { actor: sc.dids.bob }, + { headers: { authorization: `Bearer ${token}` } }, + ) + await expect(attempt).rejects.toThrow('Bad token aud') + }) + + it('does not work with bad signatures', async () => { + const fakeKey = await crypto.Secp256k1Keypair.create({ exportable: true }) + const fakeJwtKey = await derivePrivKey(fakeKey) + const signer = new jose.SignJWT({ scope: 'com.atproto.access' }) + .setSubject(alice) + .setIssuedAt() + .setExpirationTime('60mins') + .setAudience('did:web:my.personal.pds.com') + .setProtectedHeader({ + typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html + alg: 'ES256K', + }) + const token = await signer.sign(fakeJwtKey) + const attempt = agent.app.bsky.actor.getProfile( + { actor: sc.dids.bob }, + { headers: { authorization: `Bearer ${token}` } }, + ) + await expect(attempt).rejects.toThrow('Token could not be verified') + }) + + it('does not work on flexible aud routes', async () => { + const signer = new jose.SignJWT({ scope: 'com.atproto.access' }) + .setSubject(alice) + .setIssuedAt() + .setExpirationTime('60mins') + .setAudience('did:web:fake.server.bsky.network') + .setProtectedHeader({ + typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html + alg: 'ES256K', + }) + const token = await signer.sign(jwtPrivKey) + const feedUri = AtUri.make(alice, 'app.bsky.feed.generator', 'fake-feed') + const attempt = agent.app.bsky.feed.getFeed( + { feed: feedUri.toString() }, + { headers: { authorization: `Bearer ${token}` } }, + ) + await expect(attempt).rejects.toThrow('Malformed token') + }) +}) diff --git a/packages/bsky/tests/feed-generation.test.ts b/packages/bsky/tests/feed-generation.test.ts index 82629717f7d..e73505e1c65 100644 --- a/packages/bsky/tests/feed-generation.test.ts +++ b/packages/bsky/tests/feed-generation.test.ts @@ -191,7 +191,12 @@ describe('feed generation', () => { const paginator = async (cursor?: string) => { const res = await agent.api.app.bsky.feed.getActorFeeds( { actor: alice, cursor, limit: 2 }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetActorFeeds, + ), + }, ) return res.data } @@ -224,7 +229,12 @@ describe('feed generation', () => { await network.processAll() const view = await agent.api.app.bsky.feed.getPosts( { uris: [res.uri] }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetPosts, + ), + }, ) expect(view.data.posts.length).toBe(1) expect(forSnapshot(view.data.posts[0])).toMatchSnapshot() @@ -246,7 +256,12 @@ describe('feed generation', () => { await network.processAll() const view = await agent.api.app.bsky.feed.getPosts( { uris: [res.uri] }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetPosts, + ), + }, ) expect(view.data.posts.length).toBe(1) expect(forSnapshot(view.data.posts[0])).toMatchSnapshot() @@ -300,7 +315,12 @@ describe('feed generation', () => { await network.processAll() const view = await agent.api.app.bsky.feed.getPosts( { uris: [res.uri] }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetPosts, + ), + }, ) expect(view.data.posts.length).toBe(1) expect(forSnapshot(view.data.posts[0])).toMatchSnapshot() @@ -325,7 +345,12 @@ describe('feed generation', () => { await network.processAll() const view = await agent.api.app.bsky.feed.getPosts( { uris: [res.uri] }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetPosts, + ), + }, ) expect(view.data.posts.length).toBe(1) expect(forSnapshot(view.data.posts[0])).toMatchSnapshot() @@ -335,7 +360,12 @@ describe('feed generation', () => { it('describes a feed gen & returns online status', async () => { const resEven = await agent.api.app.bsky.feed.getFeedGenerator( { feed: feedUriAll }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetFeedGenerator, + ), + }, ) expect(forSnapshot(resEven.data)).toMatchSnapshot() expect(resEven.data.isOnline).toBe(true) @@ -345,7 +375,12 @@ describe('feed generation', () => { it('does not describe taken-down feed', async () => { const tryGetFeed = agent.api.app.bsky.feed.getFeedGenerator( { feed: feedUriPrime }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetFeedGenerator, + ), + }, ) await expect(tryGetFeed).rejects.toThrow('could not find feed') }) @@ -354,7 +389,12 @@ describe('feed generation', () => { it.skip('handles an unsupported algo', async () => { const resOdd = await agent.api.app.bsky.feed.getFeedGenerator( { feed: feedUriOdd }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetFeedGenerator, + ), + }, ) expect(resOdd.data.isOnline).toBe(true) expect(resOdd.data.isValid).toBe(false) @@ -391,7 +431,12 @@ describe('feed generation', () => { { feed: allUriBob.toString(), }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetFeedGenerator, + ), + }, ) expect(res.data.isOnline).toBe(false) expect(res.data.isValid).toBe(false) @@ -402,7 +447,12 @@ describe('feed generation', () => { it('describes multiple feed gens', async () => { const resEven = await agent.api.app.bsky.feed.getFeedGenerators( { feeds: [feedUriEven, feedUriAll, feedUriPrime] }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetFeedGenerators, + ), + }, ) expect(forSnapshot(resEven.data)).toMatchSnapshot() expect(resEven.data.feeds.map((fg) => fg.uri)).not.toContain(feedUriPrime) // taken-down @@ -413,7 +463,12 @@ describe('feed generation', () => { it('returns list of suggested feed generators', async () => { const resEven = await agent.api.app.bsky.feed.getSuggestedFeeds( {}, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetSuggestedFeeds, + ), + }, ) expect(forSnapshot(resEven.data)).toMatchSnapshot() expect(resEven.data.feeds.map((fg) => fg.uri)).not.toContain(feedUriPrime) // taken-down @@ -424,7 +479,12 @@ describe('feed generation', () => { it('gets popular feed generators', async () => { const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( {}, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyUnspeccedGetPopularFeedGenerators, + ), + }, ) expect(res.data.feeds.map((f) => f.uri)).not.toContain(feedUriPrime) // taken-down expect(res.data.feeds.map((f) => f.uri)).toEqual([ @@ -437,7 +497,12 @@ describe('feed generation', () => { it('searches feed generators', async () => { const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( { query: 'all' }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyUnspeccedGetPopularFeedGenerators, + ), + }, ) expect(res.data.feeds.map((f) => f.uri)).toEqual([feedUriAll]) }) @@ -446,17 +511,32 @@ describe('feed generation', () => { const resFull = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( {}, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyUnspeccedGetPopularFeedGenerators, + ), + }, ) const resOne = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( { limit: 2 }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyUnspeccedGetPopularFeedGenerators, + ), + }, ) const resTwo = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( { cursor: resOne.data.cursor }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyUnspeccedGetPopularFeedGenerators, + ), + }, ) expect([...resOne.data.feeds, ...resTwo.data.feeds]).toEqual( resFull.data.feeds, @@ -468,7 +548,13 @@ describe('feed generation', () => { it('resolves basic feed contents.', async () => { const feed = await agent.api.app.bsky.feed.getFeed( { feed: feedUriEven }, - { headers: await network.serviceHeaders(alice, gen.did) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetFeed, + gen.did, + ), + }, ) expect(feed.data.feed.map((item) => item.post.uri)).toEqual([ sc.posts[sc.dids.alice][0].ref.uriStr, @@ -493,7 +579,13 @@ describe('feed generation', () => { const paginator = async (cursor?: string) => { const res = await agent.api.app.bsky.feed.getFeed( { feed: feedUriAll, cursor, limit: 2 }, - { headers: await network.serviceHeaders(alice, gen.did) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetFeed, + gen.did, + ), + }, ) return res.data } @@ -514,7 +606,13 @@ describe('feed generation', () => { it('paginates, handling feed not respecting limit.', async () => { const res = await agent.api.app.bsky.feed.getFeed( { feed: feedUriBadPagination, limit: 3 }, - { headers: await network.serviceHeaders(alice, gen.did) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetFeed, + gen.did, + ), + }, ) // refused to respect pagination limit, so it got cut short by appview but the cursor remains. expect(res.data.feed.length).toBeLessThanOrEqual(3) @@ -529,7 +627,13 @@ describe('feed generation', () => { it('fails on unknown feed.', async () => { const tryGetFeed = agent.api.app.bsky.feed.getFeed( { feed: feedUriOdd }, - { headers: await network.serviceHeaders(alice, gen.did) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetFeed, + gen.did, + ), + }, ) await expect(tryGetFeed).rejects.toMatchObject({ error: 'UnknownFeed', @@ -539,7 +643,9 @@ describe('feed generation', () => { it('resolves contents of taken-down feed.', async () => { const tryGetFeed = agent.api.app.bsky.feed.getFeed( { feed: feedUriPrime }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetFeed), + }, ) await expect(tryGetFeed).resolves.toBeDefined() }) @@ -547,19 +653,19 @@ describe('feed generation', () => { it('receives proper auth details.', async () => { const feed = await agent.api.app.bsky.feed.getFeed( { feed: feedUriEven }, - { headers: await network.serviceHeaders(alice, gen.did) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetFeedSkeleton, + gen.did, + ), + }, ) expect(feed.data['$auth']?.['aud']).toEqual(gen.did) expect(feed.data['$auth']?.['iss']).toEqual(alice) - }) - - it('receives proper auth details.', async () => { - const feed = await agent.api.app.bsky.feed.getFeed( - { feed: feedUriEven }, - { headers: await network.serviceHeaders(alice, gen.did) }, + expect(feed.data['$auth']?.['lxm']).toEqual( + ids.AppBskyFeedGetFeedSkeleton, ) - expect(feed.data['$auth']?.['aud']).toEqual(gen.did) - expect(feed.data['$auth']?.['iss']).toEqual(alice) }) it('passes through auth error from feed.', async () => { @@ -575,7 +681,13 @@ describe('feed generation', () => { it('provides timing info in server-timing header.', async () => { const result = await agent.api.app.bsky.feed.getFeed( { feed: feedUriEven }, - { headers: await network.serviceHeaders(alice, gen.did) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetFeed, + gen.did, + ), + }, ) expect(result.headers['server-timing']).toMatch( /^skele;dur=\d+, hydr;dur=\d+$/, @@ -586,7 +698,13 @@ describe('feed generation', () => { await gen.close() // @NOTE must be last test const tryGetFeed = agent.api.app.bsky.feed.getFeed( { feed: feedUriEven }, - { headers: await network.serviceHeaders(alice, gen.did) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetFeed, + gen.did, + ), + }, ) await expect(tryGetFeed).rejects.toThrow('feed unavailable') }) diff --git a/packages/bsky/tests/postgates.test.ts b/packages/bsky/tests/postgates.test.ts new file mode 100644 index 00000000000..5234fd77655 --- /dev/null +++ b/packages/bsky/tests/postgates.test.ts @@ -0,0 +1,186 @@ +import { TestNetwork, SeedClient, RecordRef } from '@atproto/dev-env' +import AtpAgent, { AppBskyEmbedRecord } from '@atproto/api' + +import { ids } from '../src/lexicon/lexicons' +import { postgatesSeed, Users } from './seed/postgates' + +describe('postgates', () => { + let network: TestNetwork + let agent: AtpAgent + let pdsAgent: AtpAgent + let sc: SeedClient + let users: Users + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_tests_postgates', + }) + agent = network.bsky.getClient() + pdsAgent = network.pds.getClient() + sc = network.getSeedClient() + + const result = await postgatesSeed(sc) + users = result.users + + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + describe(`quotee <-> quoter`, () => { + it(`quotee detaches own post from quoter`, async () => { + const quoteePost = await sc.post(users.quotee.did, `post`) + const quoterPost = await sc.post( + users.quoter.did, + `quote post`, + undefined, + undefined, + quoteePost.ref, + ) + await pdsAgent.api.app.bsky.feed.postgate.create( + { + repo: users.quotee.did, + rkey: quoteePost.ref.uri.rkey, + }, + { + post: quoteePost.ref.uriStr, + createdAt: new Date().toISOString(), + detachedEmbeddingUris: [quoterPost.ref.uriStr], + }, + sc.getHeaders(users.quotee.did), + ) + await network.processAll() + + const root = await agent.api.app.bsky.feed.getPostThread( + { uri: quoterPost.ref.uriStr }, + { + headers: await network.serviceHeaders( + users.viewer.did, + ids.AppBskyFeedGetPostThread, + ), + }, + ) + + expect( + // @ts-ignore I know more than you + AppBskyEmbedRecord.isViewDetached(root.data.thread.post.embed.record), + ).toBe(true) + }) + + it(`postgate made by bystander has no effect`, async () => { + const quoteePost = await sc.post(users.quotee.did, `post`) + const quoterPost = await sc.post( + users.quoter.did, + `quote post`, + undefined, + undefined, + quoteePost.ref, + ) + await pdsAgent.api.app.bsky.feed.postgate.create( + { + repo: users.viewer.did, + rkey: quoteePost.ref.uri.rkey, + }, + { + post: quoteePost.ref.uriStr, + createdAt: new Date().toISOString(), + detachedEmbeddingUris: [quoterPost.ref.uriStr], + }, + sc.getHeaders(users.viewer.did), + ) + await network.processAll() + + const root = await agent.api.app.bsky.feed.getPostThread( + { uri: quoterPost.ref.uriStr }, + { + headers: await network.serviceHeaders( + users.viewer.did, + ids.AppBskyFeedGetPostThread, + ), + }, + ) + + expect( + // @ts-ignore I know more than you + AppBskyEmbedRecord.isViewDetached(root.data.thread.post.embed.record), + ).toBe(false) + }) + }) + + describe(`embeddingRules`, () => { + it(`disables quoteposts`, async () => { + const quoteePost = await sc.post(users.quotee.did, `post`) + await pdsAgent.api.app.bsky.feed.postgate.create( + { + repo: users.quotee.did, + rkey: quoteePost.ref.uri.rkey, + }, + { + post: quoteePost.ref.uriStr, + createdAt: new Date().toISOString(), + embeddingRules: [{ $type: 'app.bsky.feed.postgate#disableRule' }], + }, + sc.getHeaders(users.quotee.did), + ) + await network.processAll() + + const root = await agent.api.app.bsky.feed.getPostThread( + { uri: quoteePost.ref.uriStr }, + { + headers: await network.serviceHeaders( + users.viewer.did, + ids.AppBskyFeedGetPostThread, + ), + }, + ) + + expect( + // @ts-ignore I know more than you + root.data.thread.post.viewer.embeddingDisabled, + ).toBe(true) + }) + + it(`quotepost created after quotes disabled hides embed`, async () => { + const quoteePost = await sc.post(users.quotee.did, `post`) + await pdsAgent.api.app.bsky.feed.postgate.create( + { + repo: users.quotee.did, + rkey: quoteePost.ref.uri.rkey, + }, + { + post: quoteePost.ref.uriStr, + createdAt: new Date().toISOString(), + embeddingRules: [{ $type: 'app.bsky.feed.postgate#disableRule' }], + }, + sc.getHeaders(users.quotee.did), + ) + await network.processAll() + + const quoterPost = await sc.post( + users.quoter.did, + `quote post`, + undefined, + undefined, + quoteePost.ref, + ) + await network.processAll() + + const root = await agent.api.app.bsky.feed.getPostThread( + { uri: quoterPost.ref.uriStr }, + { + headers: await network.serviceHeaders( + users.viewer.did, + ids.AppBskyFeedGetPostThread, + ), + }, + ) + + expect( + // @ts-ignore I know more than you + AppBskyEmbedRecord.isViewDetached(root.data.thread.post.embed.record), + ).toBe(true) + }) + }) +}) diff --git a/packages/bsky/tests/seed/feed-hidden-replies.ts b/packages/bsky/tests/seed/feed-hidden-replies.ts new file mode 100644 index 00000000000..ff64b599558 --- /dev/null +++ b/packages/bsky/tests/seed/feed-hidden-replies.ts @@ -0,0 +1,62 @@ +import { TestNetwork, SeedClient, TestNetworkNoAppView } from '@atproto/dev-env' + +export type User = { + id: string + did: string + email: string + handle: string + password: string + displayName: string + description: string + selfLabels: undefined +} + +function createUser(name: string): User { + return { + id: name, + // @ts-ignore overwritten below + did: undefined, + email: `${name}@test.com`, + handle: `${name}.test`, + password: `${name}-pass`, + displayName: name, + description: `hi im ${name} label_me`, + selfLabels: undefined, + } +} + +const users = { + viewer: createUser('viewer'), + + poster: createUser('poster'), + replier: createUser('replier'), + reposter: createUser('reposter'), +} + +export type Users = typeof users + +export async function feedHiddenRepliesSeed( + sc: SeedClient, +) { + const u = structuredClone(users) + + await sc.createAccount('poster', u.poster) + await sc.createAccount('replier', u.replier) + await sc.createAccount('viewer', u.viewer) + await sc.createAccount('reposter', u.reposter) + + Object.values(u).forEach((user) => { + u[user.id].did = sc.dids[user.id] + }) + + await sc.follow(u.viewer.did, u.poster.did) + await sc.follow(u.viewer.did, u.replier.did) + await sc.follow(u.viewer.did, u.reposter.did) + + await sc.network.processAll() + + return { + users: u, + seedClient: sc, + } +} diff --git a/packages/bsky/tests/seed/postgates.ts b/packages/bsky/tests/seed/postgates.ts new file mode 100644 index 00000000000..165a1a3ea74 --- /dev/null +++ b/packages/bsky/tests/seed/postgates.ts @@ -0,0 +1,56 @@ +import { TestNetwork, SeedClient, TestNetworkNoAppView } from '@atproto/dev-env' + +export type User = { + id: string + did: string + email: string + handle: string + password: string + displayName: string + description: string + selfLabels: undefined +} + +function createUser(name: string): User { + return { + id: name, + // @ts-ignore overwritten below + did: undefined, + email: `${name}@test.com`, + handle: `${name}.test`, + password: `${name}-pass`, + displayName: name, + description: `hi im ${name} label_me`, + selfLabels: undefined, + } +} + +const users = { + viewer: createUser('viewer'), + + quotee: createUser('quotee'), + quoter: createUser('quoter'), +} + +export type Users = typeof users + +export async function postgatesSeed( + sc: SeedClient, +) { + const u = structuredClone(users) + + await sc.createAccount('quotee', u.quotee) + await sc.createAccount('quoter', u.quoter) + await sc.createAccount('viewer', u.viewer) + + Object.values(u).forEach((user) => { + u[user.id].did = sc.dids[user.id] + }) + + await sc.network.processAll() + + return { + users: u, + seedClient: sc, + } +} diff --git a/packages/bsky/tests/server.test.ts b/packages/bsky/tests/server.test.ts index 3cc0257a4eb..f1bdafeb16d 100644 --- a/packages/bsky/tests/server.test.ts +++ b/packages/bsky/tests/server.test.ts @@ -89,7 +89,7 @@ describe('server', () => { { decompress: false, headers: { - ...(await network.serviceHeaders(alice)), + ...(await network.serviceHeaders(alice, 'app.bsky.feed.getTimeline')), 'accept-encoding': 'gzip', }, }, 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 58268e657df..c19d557e4a6 100644 --- a/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap @@ -35,6 +35,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -54,6 +55,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -130,6 +132,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -165,6 +168,7 @@ Array [ "repostCount": 0, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -201,6 +205,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -210,6 +215,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -285,6 +291,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(9)", @@ -335,6 +342,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 1, "uri": "record(7)", @@ -377,6 +385,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -393,6 +402,7 @@ Array [ "repostCount": 0, "uri": "record(6)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -430,6 +440,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -439,6 +450,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -484,6 +496,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -501,6 +514,7 @@ Array [ "repostCount": 0, "uri": "record(13)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -553,6 +567,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -588,6 +603,7 @@ Array [ "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -627,6 +643,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -636,6 +653,7 @@ Array [ "repostCount": 1, "uri": "record(1)", "viewer": Object { + "embeddingDisabled": false, "like": "record(5)", "threadMuted": false, }, @@ -675,6 +693,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -684,6 +703,7 @@ Array [ "repostCount": 1, "uri": "record(1)", "viewer": Object { + "embeddingDisabled": false, "like": "record(5)", "threadMuted": false, }, @@ -708,6 +728,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000+00:00", @@ -717,6 +738,7 @@ Array [ "repostCount": 0, "uri": "record(6)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -739,6 +761,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -752,6 +775,7 @@ Array [ "repostCount": 0, "uri": "record(7)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -830,6 +854,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(2)", @@ -849,6 +874,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(1)", @@ -898,6 +924,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -928,6 +955,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "repost": "record(4)", "threadMuted": false, }, @@ -961,6 +989,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -980,6 +1009,7 @@ Array [ "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1019,6 +1049,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1028,6 +1059,7 @@ Array [ "repostCount": 1, "uri": "record(6)", "viewer": Object { + "embeddingDisabled": false, "like": "record(10)", "threadMuted": false, }, @@ -1067,6 +1099,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1076,6 +1109,7 @@ Array [ "repostCount": 1, "uri": "record(6)", "viewer": Object { + "embeddingDisabled": false, "like": "record(10)", "threadMuted": false, }, @@ -1132,6 +1166,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(2)", @@ -1150,6 +1185,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1195,6 +1231,7 @@ Array [ "repostCount": 0, "uri": "record(1)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1238,6 +1275,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1257,6 +1295,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "repost": "record(5)", "threadMuted": false, }, @@ -1352,6 +1391,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1387,6 +1427,7 @@ Array [ "repostCount": 0, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1424,6 +1465,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1433,6 +1475,7 @@ Array [ "repostCount": 1, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "like": "record(7)", "repost": "record(6)", "threadMuted": false, @@ -1474,6 +1517,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1483,6 +1527,7 @@ Array [ "repostCount": 1, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "like": "record(7)", "repost": "record(6)", "threadMuted": false, @@ -1576,6 +1621,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(11)", @@ -1595,6 +1641,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(10)", @@ -1644,6 +1691,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1674,6 +1722,7 @@ Array [ "repostCount": 1, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1698,6 +1747,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1707,6 +1757,2469 @@ Array [ "repostCount": 0, "uri": "record(12)", "viewer": Object { + "embeddingDisabled": false, + "threadMuted": false, + }, + }, + }, +] +`; + +exports[`pds author feed views pins cannot pin someone else's post 1`] = ` +Array [ + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(1)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(2)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(3)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(4)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(4)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(5)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(5)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(6)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(6)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(7)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(7)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(8)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(10)", + "uri": "record(10)", + }, + "root": Object { + "cid": "cids(9)", + "uri": "record(9)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(8)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + "reply": Object { + "grandparentAuthor": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(11)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "did": "user(1)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(12)", + "following": "record(11)", + "muted": false, + }, + }, + "cid": "cids(10)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(2)/cids(12)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(2)/cids(12)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(10)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(10)", + "val": "test-label", + }, + Object { + "cid": "cids(10)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(10)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(12)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(9)", + "uri": "record(9)", + }, + "root": Object { + "cid": "cids(9)", + "uri": "record(9)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(10)", + "viewer": Object { + "embeddingDisabled": false, + "threadMuted": false, + }, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(9)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 3, + "repostCount": 1, + "uri": "record(9)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(13)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "associated": Object { + "chat": Object { + "allowIncoming": "none", + }, + }, + "did": "user(3)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(15)", + "muted": false, + }, + }, + "cid": "cids(14)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(4)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(18)", + "following": "record(17)", + "muted": false, + }, + }, + "cid": "cids(15)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 2, + "quoteCount": 1, + "replyCount": 0, + "repostCount": 0, + "uri": "record(16)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(12)", + }, + "size": 4114, + }, + }, + Object { + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(16)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(17)", + "uri": "record(19)", + }, + }, + }, + "text": "hi im carol", + }, + }, + }, + ], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 1, + "replyCount": 0, + "repostCount": 1, + "uri": "record(14)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(15)", + "uri": "record(16)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(13)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(13)", + "val": "test-label", + }, + ], + "likeCount": 2, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(14)", + "uri": "record(14)", + }, + }, + "text": "yoohoo label_me", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(13)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(9)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 3, + "repostCount": 1, + "uri": "record(9)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(18)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(18)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(0)", + "uri": "record(20)", + "val": "self-label", + }, + ], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(20)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, +] +`; + +exports[`pds author feed views pins params.includePins = false 1`] = ` +Array [ + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": true, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(1)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(2)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(3)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(4)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(4)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(5)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(5)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(6)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(6)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(7)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(9)", + "uri": "record(9)", + }, + "root": Object { + "cid": "cids(8)", + "uri": "record(8)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(7)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + "reply": Object { + "grandparentAuthor": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(10)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "did": "user(1)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(11)", + "following": "record(10)", + "muted": false, + }, + }, + "cid": "cids(9)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(2)/cids(11)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(2)/cids(11)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(9)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(9)", + "val": "test-label", + }, + Object { + "cid": "cids(9)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(9)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(11)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(8)", + "uri": "record(8)", + }, + "root": Object { + "cid": "cids(8)", + "uri": "record(8)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(9)", + "viewer": Object { + "embeddingDisabled": false, + "threadMuted": false, + }, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(8)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 3, + "repostCount": 1, + "uri": "record(8)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(12)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "associated": Object { + "chat": Object { + "allowIncoming": "none", + }, + }, + "did": "user(3)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(14)", + "muted": false, + }, + }, + "cid": "cids(13)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(4)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(17)", + "following": "record(16)", + "muted": false, + }, + }, + "cid": "cids(14)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 2, + "quoteCount": 1, + "replyCount": 0, + "repostCount": 0, + "uri": "record(15)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(11)", + }, + "size": 4114, + }, + }, + Object { + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(15)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(16)", + "uri": "record(18)", + }, + }, + }, + "text": "hi im carol", + }, + }, + }, + ], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 1, + "replyCount": 0, + "repostCount": 1, + "uri": "record(13)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(14)", + "uri": "record(15)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(12)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(12)", + "val": "test-label", + }, + ], + "likeCount": 2, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(13)", + "uri": "record(13)", + }, + }, + "text": "yoohoo label_me", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(12)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(8)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 3, + "repostCount": 1, + "uri": "record(8)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(17)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(17)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(0)", + "uri": "record(19)", + "val": "self-label", + }, + ], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(19)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, +] +`; + +exports[`pds author feed views pins params.includePins = true, pin is NOT in first page of results 1`] = ` +Array [ + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": true, + "threadMuted": false, + }, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonPin", + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(1)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(2)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, +] +`; + +exports[`pds author feed views pins params.includePins = true, pin is NOT in first page of results 2`] = ` +Array [ + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": true, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(1)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(2)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(3)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(4)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(6)", + "uri": "record(6)", + }, + "root": Object { + "cid": "cids(5)", + "uri": "record(5)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(4)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + "reply": Object { + "grandparentAuthor": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(7)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "did": "user(1)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(8)", + "following": "record(7)", + "muted": false, + }, + }, + "cid": "cids(6)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(2)/cids(8)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(2)/cids(8)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(6)", + "val": "test-label", + }, + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(6)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(8)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(5)", + "uri": "record(5)", + }, + "root": Object { + "cid": "cids(5)", + "uri": "record(5)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(6)", + "viewer": Object { + "embeddingDisabled": false, + "threadMuted": false, + }, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(5)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 3, + "repostCount": 1, + "uri": "record(5)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(9)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "associated": Object { + "chat": Object { + "allowIncoming": "none", + }, + }, + "did": "user(3)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(11)", + "muted": false, + }, + }, + "cid": "cids(10)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(4)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(14)", + "following": "record(13)", + "muted": false, + }, + }, + "cid": "cids(11)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 2, + "quoteCount": 1, + "replyCount": 0, + "repostCount": 0, + "uri": "record(12)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(8)", + }, + "size": 4114, + }, + }, + Object { + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(12)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(13)", + "uri": "record(15)", + }, + }, + }, + "text": "hi im carol", + }, + }, + }, + ], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 1, + "replyCount": 0, + "repostCount": 1, + "uri": "record(10)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(11)", + "uri": "record(12)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(9)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(9)", + "val": "test-label", + }, + ], + "likeCount": 2, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(10)", + "uri": "record(10)", + }, + }, + "text": "yoohoo label_me", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(9)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(5)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 3, + "repostCount": 1, + "uri": "record(5)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(14)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(14)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(0)", + "uri": "record(16)", + "val": "self-label", + }, + ], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(16)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, +] +`; + +exports[`pds author feed views pins params.includePins = true, pin is in first page of results 1`] = ` +Array [ + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": true, + "threadMuted": false, + }, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonPin", + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(1)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "not pinned post", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(2)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(5)", + "uri": "record(5)", + }, + "root": Object { + "cid": "cids(4)", + "uri": "record(4)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(3)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + "reply": Object { + "grandparentAuthor": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(6)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "did": "user(1)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(7)", + "following": "record(6)", + "muted": false, + }, + }, + "cid": "cids(5)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(2)/cids(7)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(2)/cids(7)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(5)", + "val": "test-label", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(5)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(7)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(4)", + "uri": "record(4)", + }, + "root": Object { + "cid": "cids(4)", + "uri": "record(4)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(5)", + "viewer": Object { + "embeddingDisabled": false, + "threadMuted": false, + }, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(4)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 3, + "repostCount": 1, + "uri": "record(4)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(8)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "associated": Object { + "chat": Object { + "allowIncoming": "none", + }, + }, + "did": "user(3)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(10)", + "muted": false, + }, + }, + "cid": "cids(9)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(4)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(13)", + "following": "record(12)", + "muted": false, + }, + }, + "cid": "cids(10)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 2, + "quoteCount": 1, + "replyCount": 0, + "repostCount": 0, + "uri": "record(11)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(7)", + }, + "size": 4114, + }, + }, + Object { + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(11)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(12)", + "uri": "record(14)", + }, + }, + }, + "text": "hi im carol", + }, + }, + }, + ], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 1, + "replyCount": 0, + "repostCount": 1, + "uri": "record(9)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(10)", + "uri": "record(11)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(8)", + "val": "test-label", + }, + ], + "likeCount": 2, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(9)", + "uri": "record(9)", + }, + }, + "text": "yoohoo label_me", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(8)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(4)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 3, + "repostCount": 1, + "uri": "record(4)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, + "threadMuted": false, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(0)", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(13)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(13)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(0)", + "uri": "record(15)", + "val": "self-label", + }, + ], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(15)", + "viewer": Object { + "embeddingDisabled": false, + "pinned": false, "threadMuted": false, }, }, @@ -1751,6 +4264,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1770,6 +4284,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1847,6 +4362,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1882,6 +4398,7 @@ Array [ "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1920,6 +4437,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1929,6 +4447,7 @@ Array [ "repostCount": 1, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "like": "record(6)", "threadMuted": false, }, @@ -2004,6 +4523,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(10)", @@ -2054,6 +4574,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 1, "uri": "record(9)", @@ -2096,6 +4617,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2112,6 +4634,7 @@ Array [ "repostCount": 0, "uri": "record(8)", "viewer": Object { + "embeddingDisabled": false, "like": "record(12)", "threadMuted": false, }, @@ -2152,6 +4675,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -2161,6 +4685,7 @@ Array [ "repostCount": 1, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "like": "record(6)", "threadMuted": false, }, @@ -2209,6 +4734,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2226,6 +4752,7 @@ Array [ "repostCount": 0, "uri": "record(13)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, 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 3b1c020e085..5270c1cb612 100644 --- a/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap @@ -73,6 +73,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 1, "uri": "record(3)", @@ -115,6 +116,7 @@ Object { }, ], "likeCount": 2, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -131,6 +133,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -187,6 +190,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -206,6 +210,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -251,6 +256,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -260,6 +266,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "like": "record(4)", "repost": "record(3)", "threadMuted": false, diff --git a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap index afdf3c0d34e..a0d7e67cce8 100644 --- a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap @@ -73,6 +73,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 1, "uri": "record(3)", @@ -115,6 +116,7 @@ Object { }, ], "likeCount": 2, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -131,6 +133,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -187,6 +190,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -206,6 +210,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -226,6 +231,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -245,6 +251,7 @@ Object { "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -291,6 +298,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -300,6 +308,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "like": "record(4)", "repost": "record(3)", "threadMuted": false, @@ -363,6 +372,7 @@ Object { }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -398,6 +408,7 @@ Object { "repostCount": 0, "uri": "record(7)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, 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 8fa7b79194a..31505cd7f9f 100644 --- a/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap @@ -37,6 +37,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -56,6 +57,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -133,6 +135,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -168,6 +171,7 @@ Array [ "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -206,6 +210,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -215,6 +220,7 @@ Array [ "repostCount": 1, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "like": "record(6)", "threadMuted": false, }, @@ -265,6 +271,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -300,6 +307,7 @@ Array [ "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -339,6 +347,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -348,6 +357,7 @@ Array [ "repostCount": 1, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "like": "record(6)", "threadMuted": false, }, @@ -387,6 +397,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -396,6 +407,7 @@ Array [ "repostCount": 1, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "like": "record(6)", "threadMuted": false, }, @@ -471,6 +483,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(10)", @@ -521,6 +534,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 1, "uri": "record(9)", @@ -563,6 +577,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -579,6 +594,7 @@ Array [ "repostCount": 0, "uri": "record(8)", "viewer": Object { + "embeddingDisabled": false, "like": "record(12)", "threadMuted": false, }, @@ -603,6 +619,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000+00:00", @@ -612,6 +629,7 @@ Array [ "repostCount": 0, "uri": "record(13)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -651,6 +669,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -660,6 +679,7 @@ Array [ "repostCount": 1, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "like": "record(6)", "threadMuted": false, }, @@ -684,6 +704,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -697,6 +718,7 @@ Array [ "repostCount": 0, "uri": "record(11)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -744,6 +766,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -761,6 +784,7 @@ Array [ "repostCount": 0, "uri": "record(14)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, diff --git a/packages/bsky/tests/views/__snapshots__/lists.test.ts.snap b/packages/bsky/tests/views/__snapshots__/lists.test.ts.snap index c700bd4ea3c..82842d664ab 100644 --- a/packages/bsky/tests/views/__snapshots__/lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/lists.test.ts.snap @@ -46,6 +46,52 @@ Array [ ] `; +exports[`bsky actor likes feed views does include users with creator block relationship in reference lists for creator 2`] = ` +Array [ + Object { + "subject": Object { + "did": "user(0)", + "handle": "frankie.test", + "labels": Array [], + "viewer": Object { + "blockedBy": true, + "muted": false, + }, + }, + "uri": "record(0)", + }, + Object { + "subject": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(0)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "description": "hi im bob label_me", + "did": "user(1)", + "displayName": "bobby", + "handle": "bob.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "uri": "record(1)", + }, + Object { + "subject": Object { + "did": "user(3)", + "handle": "eve.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "uri": "record(2)", + }, +] +`; + exports[`bsky actor likes feed views does not include reference lists in getActorLists 1`] = ` Array [ Object { @@ -62,6 +108,72 @@ Array [ "purpose": "app.bsky.graph.defs#curatelist", "uri": "record(0)", }, + Object { + "cid": "cids(1)", + "creator": Object { + "did": "user(0)", + "handle": "eve.test", + "labels": Array [], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "listItemCount": 3, + "name": "blah curate list!", + "purpose": "app.bsky.graph.defs#curatelist", + "uri": "record(1)", + }, +] +`; + +exports[`bsky actor likes feed views does not include users with creator block relationship in reference and curate lists for signed-out viewers 1`] = ` +Array [ + Object { + "subject": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "description": "hi im bob label_me", + "did": "user(0)", + "displayName": "bobby", + "handle": "bob.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + }, + "uri": "record(0)", + }, + Object { + "subject": Object { + "did": "user(2)", + "handle": "eve.test", + "labels": Array [], + }, + "uri": "record(1)", + }, +] +`; + +exports[`bsky actor likes feed views does not include users with creator block relationship in reference and curate lists for signed-out viewers 2`] = ` +Array [ + Object { + "subject": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "description": "hi im bob label_me", + "did": "user(0)", + "displayName": "bobby", + "handle": "bob.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + }, + "uri": "record(0)", + }, + Object { + "subject": Object { + "did": "user(2)", + "handle": "eve.test", + "labels": Array [], + }, + "uri": "record(1)", + }, ] `; @@ -100,6 +212,41 @@ Array [ ] `; +exports[`bsky actor likes feed views does not include users with creator block relationship in reference lists for non-creator, in-list viewers 2`] = ` +Array [ + Object { + "subject": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "description": "hi im bob label_me", + "did": "user(0)", + "displayName": "bobby", + "handle": "bob.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "uri": "record(0)", + }, + Object { + "subject": Object { + "did": "user(2)", + "handle": "eve.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "blocking": "record(2)", + "muted": false, + }, + }, + "uri": "record(1)", + }, +] +`; + exports[`bsky actor likes feed views does not include users with creator block relationship in reference lists for non-creator, not-in-list viewers 1`] = ` Array [ Object { @@ -134,7 +281,7 @@ Array [ ] `; -exports[`bsky actor likes feed views does not include users with creator block relationship in reference lists for signed-out viewers 1`] = ` +exports[`bsky actor likes feed views does not include users with creator block relationship in reference lists for non-creator, not-in-list viewers 2`] = ` Array [ Object { "subject": Object { @@ -146,6 +293,10 @@ Array [ "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, }, "uri": "record(0)", }, @@ -154,7 +305,73 @@ Array [ "did": "user(2)", "handle": "eve.test", "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "uri": "record(1)", + }, +] +`; + +exports[`bsky actor likes feed views does return all users regardless of creator block relationship in moderation lists 1`] = ` +Array [ + Object { + "subject": Object { + "did": "user(0)", + "handle": "greta.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "uri": "record(0)", + }, + Object { + "subject": Object { + "did": "user(1)", + "handle": "frankie.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "uri": "record(1)", + }, +] +`; + +exports[`bsky actor likes feed views supports using a handle as getList actor param 1`] = ` +Array [ + Object { + "cid": "cids(0)", + "creator": Object { + "did": "user(0)", + "handle": "eve.test", + "labels": Array [], }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "listItemCount": 0, + "name": "cool curate list", + "purpose": "app.bsky.graph.defs#curatelist", + "uri": "record(0)", + }, + Object { + "cid": "cids(1)", + "creator": Object { + "did": "user(0)", + "handle": "eve.test", + "labels": Array [], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "listItemCount": 3, + "name": "blah curate list!", + "purpose": "app.bsky.graph.defs#curatelist", "uri": "record(1)", }, ] 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 bbbbc74ff5b..6c784df7c1d 100644 --- a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap @@ -79,6 +79,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -95,6 +96,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, } @@ -136,6 +138,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -145,6 +148,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "like": "record(4)", "repost": "record(3)", "threadMuted": false, @@ -182,6 +186,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -201,6 +206,7 @@ Object { "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -263,6 +269,7 @@ Object { }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -298,6 +305,7 @@ Object { "repostCount": 0, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, diff --git a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap index 78198bbe5dc..aaeca6c58af 100644 --- a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap @@ -139,6 +139,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -148,6 +149,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -170,6 +172,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -189,6 +192,7 @@ Object { "repostCount": 0, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -239,6 +243,7 @@ Object { }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -274,6 +279,7 @@ Object { "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, diff --git a/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap b/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap index a923c6d4a73..bf7fee5ae73 100644 --- a/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap @@ -1,5 +1,195 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`pds posts views embeds video with record. 1`] = ` +Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + }, + "cid": "cids(0)", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia#view", + "media": Object { + "$type": "app.bsky.embed.video#view", + "alt": "alt text", + "aspectRatio": Object { + "height": 3, + "width": 4, + }, + "cid": "cids(3)", + "playlist": "https://bsky.public.url/vid/user(1)/cids(3)/playlist.m3u8", + "thumbnail": "https://bsky.public.url/vid/user(1)/cids(3)/thumbnail.jpg", + }, + "record": Object { + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + }, + "cid": "cids(4)", + "embeds": Array [], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 1, + "replyCount": 0, + "repostCount": 0, + "uri": "record(2)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "embedded", + }, + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.video", + "alt": "alt text", + "aspectRatio": Object { + "height": 3, + "width": 4, + }, + "video": Object { + "$type": "blob", + "mimeType": "image/mp4", + "ref": Object { + "$link": "cids(3)", + }, + "size": 13, + }, + }, + "record": Object { + "record": Object { + "cid": "cids(4)", + "uri": "record(2)", + }, + }, + }, + "text": "video", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", +} +`; + +exports[`pds posts views embeds video. 1`] = ` +Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + }, + "cid": "cids(0)", + "embed": Object { + "$type": "app.bsky.embed.video#view", + "alt": "alt text", + "aspectRatio": Object { + "height": 3, + "width": 4, + }, + "cid": "cids(3)", + "playlist": "https://bsky.public.url/vid/user(1)/cids(3)/playlist.m3u8", + "thumbnail": "https://bsky.public.url/vid/user(1)/cids(3)/thumbnail.jpg", + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.video", + "alt": "alt text", + "aspectRatio": Object { + "height": 3, + "width": 4, + }, + "video": Object { + "$type": "blob", + "mimeType": "image/mp4", + "ref": Object { + "$link": "cids(3)", + }, + "size": 13, + }, + }, + "text": "video", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", +} +`; + exports[`pds posts views fetches posts 1`] = ` Array [ Object { @@ -42,6 +232,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -59,6 +250,7 @@ Array [ "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -94,6 +286,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -103,6 +296,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -125,6 +319,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -138,6 +333,7 @@ Array [ "repostCount": 0, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -193,6 +389,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(3)", @@ -211,6 +408,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -256,6 +454,7 @@ Array [ "repostCount": 0, "uri": "record(6)", "viewer": Object { + "embeddingDisabled": false, "like": "record(9)", "threadMuted": false, }, @@ -332,6 +531,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(3)", @@ -351,6 +551,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(6)", @@ -400,6 +601,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -430,6 +632,7 @@ Array [ "repostCount": 1, "uri": "record(10)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -465,6 +668,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -484,6 +688,7 @@ Array [ "repostCount": 1, "uri": "record(12)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, diff --git a/packages/bsky/tests/views/__snapshots__/quotes.test.ts.snap b/packages/bsky/tests/views/__snapshots__/quotes.test.ts.snap new file mode 100644 index 00000000000..c1f47cd9dad --- /dev/null +++ b/packages/bsky/tests/views/__snapshots__/quotes.test.ts.snap @@ -0,0 +1,402 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`pds quote views decrements quote count when a quote is deleted 1`] = ` +Object { + "posts": Array [ + Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "did": "user(0)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(2)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(0)", + "val": "test-label", + }, + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "did:example:labeler", + "uri": "record(0)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(2)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(1)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(1)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object { + "embeddingDisabled": false, + "threadMuted": false, + }, + }, + ], +} +`; + +exports[`pds quote views does not return post when quote is deleted 1`] = ` +Object { + "cursor": "0000000000000__bafycid", + "posts": Array [ + Object { + "author": Object { + "did": "user(0)", + "handle": "eve.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(2)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "did": "user(1)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(1)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(1)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "embeds": Array [], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(1)", + "uri": "record(1)", + "val": "self-label", + }, + ], + "likeCount": 0, + "quoteCount": 1, + "replyCount": 0, + "repostCount": 0, + "uri": "record(1)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(1)", + "uri": "record(1)", + }, + }, + "text": "qUoTe 2", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object { + "embeddingDisabled": false, + "threadMuted": false, + }, + }, + ], + "uri": "record(1)", +} +`; + +exports[`pds quote views fetches post quotes 1`] = ` +Object { + "cursor": "0000000000000__bafycid", + "posts": Array [ + Object { + "author": Object { + "did": "user(0)", + "handle": "eve.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(2)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "did": "user(1)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(1)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(1)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "embeds": Array [], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(1)", + "uri": "record(1)", + "val": "self-label", + }, + ], + "likeCount": 0, + "quoteCount": 2, + "replyCount": 0, + "repostCount": 0, + "uri": "record(1)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(1)", + "uri": "record(1)", + }, + }, + "text": "qUoTe 2", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object { + "embeddingDisabled": false, + "threadMuted": false, + }, + }, + Object { + "author": Object { + "did": "user(0)", + "handle": "eve.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(4)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(2)@jpeg", + "createdAt": "1970-01-01T00:00:00.000Z", + "did": "user(1)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(1)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(1)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "embeds": Array [], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "src": "user(1)", + "uri": "record(1)", + "val": "self-label", + }, + ], + "likeCount": 0, + "quoteCount": 2, + "replyCount": 0, + "repostCount": 0, + "uri": "record(1)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "quoteCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(1)", + "uri": "record(1)", + }, + }, + "text": "qUoTe 1", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(3)", + "viewer": Object { + "embeddingDisabled": false, + "threadMuted": false, + }, + }, + ], + "uri": "record(1)", +} +`; diff --git a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap index 99dd6763232..220682610de 100644 --- a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap @@ -41,6 +41,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -50,6 +51,7 @@ Object { "repostCount": 1, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "like": "record(7)", "threadMuted": false, }, @@ -97,6 +99,7 @@ Object { }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -132,6 +135,7 @@ Object { "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -170,6 +174,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -189,6 +194,7 @@ Object { "repostCount": 2, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "repost": "record(6)", "threadMuted": false, }, @@ -234,6 +240,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -243,6 +250,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "like": "record(4)", "threadMuted": false, }, @@ -265,6 +273,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -284,6 +293,7 @@ Object { "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -333,6 +343,7 @@ Object { }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -368,6 +379,7 @@ Object { "repostCount": 0, "uri": "record(7)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -408,6 +420,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -427,6 +440,7 @@ Object { "repostCount": 2, "uri": "record(8)", "viewer": Object { + "embeddingDisabled": false, "repost": "record(9)", "threadMuted": false, }, @@ -476,6 +490,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -485,6 +500,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "like": "record(4)", "threadMuted": false, }, @@ -507,6 +523,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -526,6 +543,7 @@ Object { "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -574,6 +592,7 @@ Object { }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -609,6 +628,7 @@ Object { "repostCount": 0, "uri": "record(7)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -654,6 +674,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -663,6 +684,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "like": "record(4)", "threadMuted": false, }, @@ -685,6 +707,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -704,6 +727,7 @@ Object { "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -752,6 +776,7 @@ Object { }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -787,6 +812,7 @@ Object { "repostCount": 0, "uri": "record(7)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -832,6 +858,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -841,6 +868,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -864,6 +892,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -883,6 +912,7 @@ Object { "repostCount": 0, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -923,6 +953,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -942,6 +973,7 @@ Object { "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -990,6 +1022,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -999,6 +1032,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1048,6 +1082,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1067,6 +1102,7 @@ Object { "repostCount": 0, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1116,6 +1152,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1135,6 +1172,7 @@ Object { "repostCount": 2, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "repost": "record(6)", "threadMuted": false, }, @@ -1185,6 +1223,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1204,6 +1243,7 @@ Object { "repostCount": 2, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "repost": "record(6)", "threadMuted": false, }, @@ -1249,6 +1289,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1258,6 +1299,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "like": "record(4)", "threadMuted": false, }, @@ -1307,6 +1349,7 @@ Object { }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1342,6 +1385,7 @@ Object { "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1382,6 +1426,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1401,6 +1446,7 @@ Object { "repostCount": 2, "uri": "record(6)", "viewer": Object { + "embeddingDisabled": false, "repost": "record(7)", "threadMuted": false, }, @@ -1450,6 +1496,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1459,6 +1506,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "like": "record(4)", "threadMuted": false, }, @@ -1508,6 +1556,7 @@ Object { }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1543,6 +1592,7 @@ Object { "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, diff --git a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap index 66eed03bbd6..98372650cab 100644 --- a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap @@ -35,6 +35,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -54,6 +55,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -115,6 +117,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -124,6 +127,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -162,6 +166,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -171,6 +176,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -227,6 +233,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -246,6 +253,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -288,6 +296,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -297,6 +306,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -365,6 +375,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 1, "uri": "record(6)", @@ -407,6 +418,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -423,6 +435,7 @@ Array [ "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -460,6 +473,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -469,6 +483,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -502,6 +517,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -532,6 +548,7 @@ Array [ "repostCount": 1, "uri": "record(6)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -557,6 +574,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -566,6 +584,7 @@ Array [ "repostCount": 0, "uri": "record(8)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -611,6 +630,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -628,6 +648,7 @@ Array [ "repostCount": 0, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -670,6 +691,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -689,6 +711,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -750,6 +773,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -759,6 +783,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -797,6 +822,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -806,6 +832,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -862,6 +889,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -881,6 +909,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -923,6 +952,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -932,6 +962,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -954,6 +985,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -973,6 +1005,7 @@ Array [ "repostCount": 0, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1010,6 +1043,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1019,6 +1053,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1055,6 +1090,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1064,6 +1100,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1118,6 +1155,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1134,6 +1172,7 @@ Array [ "repostCount": 0, "uri": "record(8)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1158,6 +1197,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000+00:00", @@ -1167,6 +1207,7 @@ Array [ "repostCount": 0, "uri": "record(10)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1204,6 +1245,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1213,6 +1255,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1238,6 +1281,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1247,6 +1291,7 @@ Array [ "repostCount": 0, "uri": "record(13)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1312,6 +1357,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(15)", @@ -1338,6 +1384,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1383,6 +1430,7 @@ Array [ "repostCount": 0, "uri": "record(14)", "viewer": Object { + "embeddingDisabled": false, "like": "record(16)", "threadMuted": false, }, @@ -1416,6 +1464,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1429,6 +1478,7 @@ Array [ "repostCount": 0, "uri": "record(15)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1474,6 +1524,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1491,6 +1542,7 @@ Array [ "repostCount": 0, "uri": "record(17)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1533,6 +1585,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1552,6 +1605,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1647,6 +1701,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1682,6 +1737,7 @@ Array [ "repostCount": 0, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1718,6 +1774,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1727,6 +1784,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1765,6 +1823,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -1774,6 +1833,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -1878,6 +1938,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(11)", @@ -1905,6 +1966,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(8)", @@ -1954,6 +2016,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1984,6 +2047,7 @@ Array [ "repostCount": 1, "uri": "record(7)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2036,6 +2100,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2055,6 +2120,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2131,6 +2197,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2166,6 +2233,7 @@ Array [ "repostCount": 0, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2202,6 +2270,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -2211,6 +2280,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2233,6 +2303,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2252,6 +2323,7 @@ Array [ "repostCount": 0, "uri": "record(12)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2289,6 +2361,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -2298,6 +2371,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2334,6 +2408,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -2343,6 +2418,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2393,6 +2469,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2428,6 +2505,7 @@ Array [ "repostCount": 0, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2465,6 +2543,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -2474,6 +2553,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2510,6 +2590,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -2519,6 +2600,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2602,6 +2684,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(8)", @@ -2652,6 +2735,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 1, "uri": "record(7)", @@ -2694,6 +2778,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2710,6 +2795,7 @@ Array [ "repostCount": 0, "uri": "record(13)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2734,6 +2820,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000+00:00", @@ -2743,6 +2830,7 @@ Array [ "repostCount": 0, "uri": "record(14)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2780,6 +2868,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -2789,6 +2878,7 @@ Array [ "repostCount": 1, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -2874,6 +2964,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(11)", @@ -2901,6 +2992,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(8)", @@ -2950,6 +3042,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2980,6 +3073,7 @@ Array [ "repostCount": 1, "uri": "record(7)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -3005,6 +3099,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -3014,6 +3109,7 @@ Array [ "repostCount": 0, "uri": "record(15)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -3079,6 +3175,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(11)", @@ -3105,6 +3202,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -3150,6 +3248,7 @@ Array [ "repostCount": 0, "uri": "record(8)", "viewer": Object { + "embeddingDisabled": false, "like": "record(16)", "threadMuted": false, }, @@ -3183,6 +3282,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -3196,6 +3296,7 @@ Array [ "repostCount": 0, "uri": "record(11)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -3241,6 +3342,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -3258,6 +3360,7 @@ Array [ "repostCount": 0, "uri": "record(17)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -3345,6 +3448,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(4)", @@ -3372,6 +3476,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(2)", @@ -3421,6 +3526,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -3451,6 +3557,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -3504,6 +3611,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -3523,6 +3631,7 @@ Array [ "repostCount": 1, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -3599,6 +3708,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -3634,6 +3744,7 @@ Array [ "repostCount": 0, "uri": "record(10)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -3672,6 +3783,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -3681,6 +3793,7 @@ Array [ "repostCount": 1, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "like": "record(11)", "threadMuted": false, }, @@ -3703,6 +3816,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -3722,6 +3836,7 @@ Array [ "repostCount": 0, "uri": "record(12)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -3761,6 +3876,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -3770,6 +3886,7 @@ Array [ "repostCount": 1, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "like": "record(11)", "threadMuted": false, }, @@ -3809,6 +3926,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -3818,6 +3936,7 @@ Array [ "repostCount": 1, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "like": "record(11)", "threadMuted": false, }, @@ -3867,6 +3986,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -3902,6 +4022,7 @@ Array [ "repostCount": 0, "uri": "record(10)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -3941,6 +4062,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -3950,6 +4072,7 @@ Array [ "repostCount": 1, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "like": "record(11)", "threadMuted": false, }, @@ -3989,6 +4112,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -3998,6 +4122,7 @@ Array [ "repostCount": 1, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "like": "record(11)", "threadMuted": false, }, @@ -4083,6 +4208,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(2)", @@ -4133,6 +4259,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 1, "uri": "record(0)", @@ -4175,6 +4302,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4191,6 +4319,7 @@ Array [ "repostCount": 0, "uri": "record(13)", "viewer": Object { + "embeddingDisabled": false, "like": "record(14)", "threadMuted": false, }, @@ -4214,6 +4343,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000+00:00", @@ -4223,6 +4353,7 @@ Array [ "repostCount": 0, "uri": "record(15)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -4262,6 +4393,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -4271,6 +4403,7 @@ Array [ "repostCount": 1, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "like": "record(11)", "threadMuted": false, }, @@ -4334,6 +4467,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(4)", @@ -4360,6 +4494,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4405,6 +4540,7 @@ Array [ "repostCount": 0, "uri": "record(2)", "viewer": Object { + "embeddingDisabled": false, "like": "record(16)", "threadMuted": false, }, @@ -4436,6 +4572,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4449,6 +4586,7 @@ Array [ "repostCount": 0, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -4496,6 +4634,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4513,6 +4652,7 @@ Array [ "repostCount": 0, "uri": "record(17)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -4599,6 +4739,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(2)", @@ -4626,6 +4767,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(1)", @@ -4675,6 +4817,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4705,6 +4848,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "repost": "record(4)", "threadMuted": false, }, @@ -4758,6 +4902,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4777,6 +4922,7 @@ Array [ "repostCount": 1, "uri": "record(5)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -4854,6 +5000,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4889,6 +5036,7 @@ Array [ "repostCount": 0, "uri": "record(10)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -4927,6 +5075,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -4936,6 +5085,7 @@ Array [ "repostCount": 1, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "like": "record(11)", "threadMuted": false, }, @@ -4957,6 +5107,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4976,6 +5127,7 @@ Array [ "repostCount": 0, "uri": "record(12)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -5015,6 +5167,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -5024,6 +5177,7 @@ Array [ "repostCount": 1, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "like": "record(11)", "threadMuted": false, }, @@ -5063,6 +5217,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -5072,6 +5227,7 @@ Array [ "repostCount": 1, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "like": "record(11)", "threadMuted": false, }, @@ -5155,6 +5311,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(1)", @@ -5205,6 +5362,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 1, "uri": "record(0)", @@ -5247,6 +5405,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5263,6 +5422,7 @@ Array [ "repostCount": 0, "uri": "record(13)", "viewer": Object { + "embeddingDisabled": false, "like": "record(14)", "threadMuted": false, }, @@ -5303,6 +5463,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -5312,6 +5473,7 @@ Array [ "repostCount": 1, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "like": "record(11)", "threadMuted": false, }, @@ -5375,6 +5537,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(2)", @@ -5401,6 +5564,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5446,6 +5610,7 @@ Array [ "repostCount": 0, "uri": "record(1)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -5493,6 +5658,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5510,6 +5676,7 @@ Array [ "repostCount": 0, "uri": "record(15)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -5553,6 +5720,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5572,6 +5740,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { + "embeddingDisabled": false, "repost": "record(5)", "threadMuted": false, }, @@ -5667,6 +5836,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5702,6 +5872,7 @@ Array [ "repostCount": 0, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -5739,6 +5910,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -5748,6 +5920,7 @@ Array [ "repostCount": 1, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "like": "record(7)", "repost": "record(6)", "threadMuted": false, @@ -5789,6 +5962,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -5798,6 +5972,7 @@ Array [ "repostCount": 1, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "like": "record(7)", "repost": "record(6)", "threadMuted": false, @@ -5866,6 +6041,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5901,6 +6077,7 @@ Array [ "repostCount": 0, "uri": "record(4)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -5939,6 +6116,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -5948,6 +6126,7 @@ Array [ "repostCount": 1, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "like": "record(7)", "repost": "record(6)", "threadMuted": false, @@ -5987,6 +6166,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000000Z", @@ -5996,6 +6176,7 @@ Array [ "repostCount": 1, "uri": "record(3)", "viewer": Object { + "embeddingDisabled": false, "like": "record(7)", "repost": "record(6)", "threadMuted": false, @@ -6022,6 +6203,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000+00:00", @@ -6031,6 +6213,7 @@ Array [ "repostCount": 0, "uri": "record(9)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -6112,6 +6295,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(12)", @@ -6139,6 +6323,7 @@ Array [ }, ], "likeCount": 2, + "quoteCount": 1, "replyCount": 0, "repostCount": 0, "uri": "record(11)", @@ -6188,6 +6373,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -6218,6 +6404,7 @@ Array [ "repostCount": 1, "uri": "record(10)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -6242,6 +6429,7 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, + "quoteCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -6251,6 +6439,7 @@ Array [ "repostCount": 0, "uri": "record(13)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, @@ -6282,6 +6471,7 @@ Array [ }, ], "likeCount": 0, + "quoteCount": 1, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -6295,6 +6485,7 @@ Array [ "repostCount": 0, "uri": "record(12)", "viewer": Object { + "embeddingDisabled": false, "threadMuted": false, }, }, diff --git a/packages/bsky/tests/views/account-deactivation.test.ts b/packages/bsky/tests/views/account-deactivation.test.ts index 31b07c01c5c..1efebfdf12e 100644 --- a/packages/bsky/tests/views/account-deactivation.test.ts +++ b/packages/bsky/tests/views/account-deactivation.test.ts @@ -1,5 +1,6 @@ import { AtpAgent } from '@atproto/api' -import { basicSeed, SeedClient, TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' +import { ids } from '../../src/lexicon/lexicons' describe('bsky account deactivation', () => { let network: TestNetwork @@ -69,7 +70,12 @@ describe('bsky account deactivation', () => { it('does not return posts from deactivated in timelines', async () => { const res = await agent.api.app.bsky.feed.getTimeline( {}, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetTimeline, + ), + }, ) expect(res.data.feed.some((p) => p.post.author.did === alice)).toBe(false) }) diff --git a/packages/bsky/tests/views/actor-likes.test.ts b/packages/bsky/tests/views/actor-likes.test.ts index b65aa8ccc1b..6879bc58240 100644 --- a/packages/bsky/tests/views/actor-likes.test.ts +++ b/packages/bsky/tests/views/actor-likes.test.ts @@ -1,5 +1,6 @@ -import { AtpAgent, AtUri } from '@atproto/api' -import { basicSeed, SeedClient, TestNetwork } from '@atproto/dev-env' +import { AtUri, AtpAgent } from '@atproto/api' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' +import { ids } from '../../src/lexicon/lexicons' describe('bsky actor likes feed views', () => { let network: TestNetwork @@ -35,7 +36,12 @@ describe('bsky actor likes feed views', () => { data: { feed: bobLikes }, } = await agent.api.app.bsky.feed.getActorLikes( { actor: sc.accounts[bob].handle }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetActorLikes, + ), + }, ) expect(bobLikes).toHaveLength(3) @@ -43,7 +49,12 @@ describe('bsky actor likes feed views', () => { await expect( agent.api.app.bsky.feed.getActorLikes( { actor: sc.accounts[bob].handle }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetActorLikes, + ), + }, ), ).rejects.toThrow('Profile not found') }) @@ -66,7 +77,12 @@ describe('bsky actor likes feed views', () => { data: { feed }, } = await agent.api.app.bsky.feed.getActorLikes( { actor: sc.accounts[bob].handle }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetActorLikes, + ), + }, ) expect( @@ -100,7 +116,12 @@ describe('bsky actor likes feed views', () => { data: { feed }, } = await agent.api.app.bsky.feed.getActorLikes( { actor: sc.accounts[bob].handle }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetActorLikes, + ), + }, ) expect( diff --git a/packages/bsky/tests/views/actor-search.test.ts b/packages/bsky/tests/views/actor-search.test.ts index 7bbf7fa0af5..c82a87d8e33 100644 --- a/packages/bsky/tests/views/actor-search.test.ts +++ b/packages/bsky/tests/views/actor-search.test.ts @@ -2,6 +2,7 @@ import { AtpAgent } from '@atproto/api' import { wait } from '@atproto/common' import { TestNetwork, SeedClient, usersBulkSeed } from '@atproto/dev-env' import { forSnapshot, paginateAll, stripViewer } from '../_util' +import { ids } from '../../src/lexicon/lexicons' // @NOTE skipped to help with CI failures // The search code is not used in production & we should switch it out for tests on the search proxy interface @@ -38,9 +39,12 @@ describe.skip('pds actor search views', () => { .execute() // Process remaining profiles - network.bsky.sub.run() + await network.bsky.sub.restart() await network.processAll(50000) - headers = await network.serviceHeaders(Object.values(sc.dids)[0]) + headers = await network.serviceHeaders( + Object.values(sc.dids)[0], + ids.AppBskyActorSearchActorsTypeahead, + ) }) afterAll(async () => { diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 7cf6bf3c778..23f4f0072fe 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -1,4 +1,4 @@ -import { AtpAgent, AtUri } from '@atproto/api' +import { AtpAgent, AppBskyActorProfile, AppBskyFeedDefs } from '@atproto/api' import { TestNetwork, SeedClient, authorFeedSeed } from '@atproto/dev-env' import { forSnapshot, @@ -10,10 +10,13 @@ import { ReplyRef, isRecord } from '../../src/lexicon/types/app/bsky/feed/post' import { isView as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia' import { isView as isImageEmbed } from '../../src/lexicon/types/app/bsky/embed/images' import { isPostView } from '../../src/lexicon/types/app/bsky/feed/defs' +import { uriToDid } from '../../src/util/uris' +import { ids } from '../../src/lexicon/lexicons' describe('pds author feed views', () => { let network: TestNetwork let agent: AtpAgent + let pdsAgent: AtpAgent let sc: SeedClient // account dids, for convenience @@ -28,6 +31,7 @@ describe('pds author feed views', () => { dbPostgresSchema: 'bsky_views_author_feed', }) agent = network.bsky.getClient() + pdsAgent = network.pds.getClient() sc = network.getSeedClient() await authorFeedSeed(sc) await network.processAll() @@ -48,28 +52,48 @@ describe('pds author feed views', () => { it('fetches full author feeds for self (sorted, minimal viewer state).', async () => { const aliceForAlice = await agent.api.app.bsky.feed.getAuthorFeed( { actor: sc.accounts[alice].handle }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) expect(forSnapshot(aliceForAlice.data.feed)).toMatchSnapshot() const bobForBob = await agent.api.app.bsky.feed.getAuthorFeed( { actor: sc.accounts[bob].handle }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) expect(forSnapshot(bobForBob.data.feed)).toMatchSnapshot() const carolForCarol = await agent.api.app.bsky.feed.getAuthorFeed( { actor: sc.accounts[carol].handle }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) expect(forSnapshot(carolForCarol.data.feed)).toMatchSnapshot() const danForDan = await agent.api.app.bsky.feed.getAuthorFeed( { actor: sc.accounts[dan].handle }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) expect(forSnapshot(danForDan.data.feed)).toMatchSnapshot() @@ -78,7 +102,12 @@ describe('pds author feed views', () => { it("reflects fetching user's state in the feed.", async () => { const aliceForCarol = await agent.api.app.bsky.feed.getAuthorFeed( { actor: sc.accounts[alice].handle }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) aliceForCarol.data.feed.forEach((postView) => { @@ -99,7 +128,12 @@ describe('pds author feed views', () => { cursor, limit: 2, }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) return res.data } @@ -111,7 +145,12 @@ describe('pds author feed views', () => { const full = await agent.api.app.bsky.feed.getAuthorFeed( { actor: sc.accounts[alice].handle }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) expect(full.data.feed.length).toEqual(4) @@ -121,7 +160,12 @@ describe('pds author feed views', () => { it('fetches results unauthed.', async () => { const { data: authed } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: sc.accounts[alice].handle }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) const { data: unauthed } = await agent.api.app.bsky.feed.getAuthorFeed({ actor: sc.accounts[alice].handle, @@ -150,7 +194,12 @@ describe('pds author feed views', () => { 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) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) expect(preBlock.feed.length).toBeGreaterThan(0) @@ -161,7 +210,12 @@ describe('pds author feed views', () => { const attemptAsUser = agent.api.app.bsky.feed.getAuthorFeed( { actor: alice }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) await expect(attemptAsUser).rejects.toThrow('Profile not found') @@ -180,7 +234,12 @@ describe('pds author feed views', () => { it('blocked by record takedown.', async () => { const { data: preBlock } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: alice }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) expect(preBlock.feed.length).toBeGreaterThan(0) @@ -195,7 +254,12 @@ describe('pds author feed views', () => { await Promise.all([ agent.api.app.bsky.feed.getAuthorFeed( { actor: alice }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ), agent.api.app.bsky.feed.getAuthorFeed( { actor: alice }, @@ -324,15 +388,181 @@ describe('pds author feed views', () => { }), ).toBeTruthy() }) + + describe('pins', () => { + async function createAndPinPost() { + const post = await sc.post(alice, 'pinned post') + await network.processAll() + + const profile = await pdsAgent.com.atproto.repo.getRecord({ + repo: alice, + collection: 'app.bsky.actor.profile', + rkey: 'self', + }) + + if (!AppBskyActorProfile.isRecord(profile.data.value)) { + throw new Error('') + } + + const newProfile: AppBskyActorProfile.Record = { + ...profile, + pinnedPost: { + uri: post.ref.uriStr, + cid: post.ref.cid.toString(), + }, + } + + await sc.updateProfile(alice, newProfile) + + await network.processAll() + + return post + } + + it('params.includePins = true, pin is in first page of results', async () => { + await sc.post(alice, 'not pinned post') + const post = await createAndPinPost() + await sc.post(alice, 'not pinned post') + + const { data } = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: sc.accounts[alice].handle, includePins: true }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, + ) + + const pinnedPosts = data.feed.filter( + (item) => item.post.uri === post.ref.uriStr, + ) + expect(pinnedPosts.length).toEqual(1) + + const pinnedPost = data.feed.at(0) + expect(pinnedPost?.post?.uri).toEqual(post.ref.uriStr) + expect(pinnedPost?.post?.viewer?.pinned).toBeTruthy() + expect(AppBskyFeedDefs.isReasonPin(pinnedPost?.reason)).toBeTruthy() + + const notPinnedPost = data.feed.at(1) + expect(notPinnedPost?.post?.viewer?.pinned).toBeFalsy() + expect(forSnapshot(data.feed)).toMatchSnapshot() + }) + + it('params.includePins = true, pin is NOT in first page of results', async () => { + const post = await createAndPinPost() + await sc.post(alice, 'not pinned post') + await sc.post(alice, 'not pinned post') + await network.processAll() + const { data: page1 } = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: sc.accounts[alice].handle, includePins: true, limit: 2 }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, + ) + + // exists with `reason` + const pinnedPost = page1.feed.find( + (item) => item.post.uri === post.ref.uriStr, + ) + expect(pinnedPost?.post?.uri).toEqual(post.ref.uriStr) + expect(pinnedPost?.post?.viewer?.pinned).toBeTruthy() + expect(AppBskyFeedDefs.isReasonPin(pinnedPost?.reason)).toBeTruthy() + expect(forSnapshot(page1.feed)).toMatchSnapshot() + + const { data: page2 } = await agent.api.app.bsky.feed.getAuthorFeed( + { + actor: sc.accounts[alice].handle, + includePins: true, + cursor: page1.cursor, + }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, + ) + + // exists without `reason` + const laterPinnedPost = page2.feed.find( + (item) => item.post.uri === post.ref.uriStr, + ) + expect(laterPinnedPost?.post?.uri).toEqual(post.ref.uriStr) + expect(laterPinnedPost?.post?.viewer?.pinned).toBeTruthy() + expect(AppBskyFeedDefs.isReasonPin(laterPinnedPost?.reason)).toBeFalsy() + expect(forSnapshot(page2.feed)).toMatchSnapshot() + }) + + it('params.includePins = false', async () => { + const post = await createAndPinPost() + const { data } = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: sc.accounts[alice].handle }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, + ) + + // exists without `reason` + const pinnedPost = data.feed.find( + (item) => item.post.uri === post.ref.uriStr, + ) + expect(AppBskyFeedDefs.isReasonPin(pinnedPost?.reason)).toBeFalsy() + expect(forSnapshot(data.feed)).toMatchSnapshot() + }) + + it("cannot pin someone else's post", async () => { + const bobPost = await sc.post(bob, 'pinned post') + await sc.post(alice, 'not pinned post') + await network.processAll() + + const profile = await pdsAgent.com.atproto.repo.getRecord({ + repo: alice, + collection: 'app.bsky.actor.profile', + rkey: 'self', + }) + + if (!AppBskyActorProfile.isRecord(profile.data.value)) { + throw new Error('') + } + + const newProfile: AppBskyActorProfile.Record = { + ...profile, + pinnedPost: { + uri: bobPost.ref.uriStr, + cid: bobPost.ref.cid.toString(), + }, + } + + await sc.updateProfile(alice, newProfile) + + await network.processAll() + + const { data } = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: sc.accounts[alice].handle }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, + ) + + const pinnedPost = data.feed.find( + (item) => item.post.uri === bobPost.ref.uriStr, + ) + expect(pinnedPost).toBeUndefined() + expect(forSnapshot(data.feed)).toMatchSnapshot() + }) + }) }) function isReplyTo(reply: ReplyRef, did: string) { - return ( - getDidFromUri(reply.root.uri) === did && - getDidFromUri(reply.parent.uri) === did - ) -} - -function getDidFromUri(uri: string) { - return new AtUri(uri).hostname + return uriToDid(reply.root.uri) === did && uriToDid(reply.parent.uri) === did } diff --git a/packages/bsky/tests/views/block-lists.test.ts b/packages/bsky/tests/views/block-lists.test.ts index fbdc51bae82..5bd3989e9b5 100644 --- a/packages/bsky/tests/views/block-lists.test.ts +++ b/packages/bsky/tests/views/block-lists.test.ts @@ -1,6 +1,7 @@ import { AtpAgent, AtUri } from '@atproto/api' import { TestNetwork, SeedClient, RecordRef, basicSeed } from '@atproto/dev-env' import { forSnapshot } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('pds views with blocking from block lists', () => { let network: TestNetwork @@ -109,7 +110,12 @@ describe('pds views with blocking from block lists', () => { const { carol, dan } = sc.dids const { data: threadAlice } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[carol][0].ref.uriStr }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(threadAlice.thread).toEqual( expect.objectContaining({ @@ -120,7 +126,12 @@ describe('pds views with blocking from block lists', () => { ) const { data: threadCarol } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(threadCarol.thread).toEqual( expect.objectContaining({ @@ -135,7 +146,12 @@ describe('pds views with blocking from block lists', () => { // Contains reply by carol const { data: thread } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread)).toMatchSnapshot() }) @@ -144,7 +160,12 @@ describe('pds views with blocking from block lists', () => { // Parent is a post by dan const { data: thread } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: aliceReplyToDan.ref.uriStr }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread)).toMatchSnapshot() }) @@ -153,7 +174,12 @@ describe('pds views with blocking from block lists', () => { // Contains a deep embed of carol's post, blocked by dan const { data: thread } = await agent.api.app.bsky.feed.getPostThread( { depth: 0, uri: sc.posts[alice][2].ref.uriStr }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread)).toMatchSnapshot() }) @@ -161,7 +187,12 @@ describe('pds views with blocking from block lists', () => { it('errors on getting author feed', async () => { const attempt1 = agent.api.app.bsky.feed.getAuthorFeed( { actor: carol }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) await expect(attempt1).rejects.toMatchObject({ error: 'BlockedActor', @@ -169,7 +200,12 @@ describe('pds views with blocking from block lists', () => { const attempt2 = agent.api.app.bsky.feed.getAuthorFeed( { actor: dan }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) await expect(attempt2).rejects.toMatchObject({ error: 'BlockedByActor', @@ -179,7 +215,12 @@ describe('pds views with blocking from block lists', () => { it('strips blocked users out of getTimeline', async () => { const resCarol = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, ) expect( resCarol.data.feed.some((post) => post.post.author.did === dan), @@ -187,7 +228,9 @@ describe('pds views with blocking from block lists', () => { const resDan = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline), + }, ) expect( resDan.data.feed.some((post) => @@ -199,7 +242,12 @@ describe('pds views with blocking from block lists', () => { it('returns block status on getProfile', async () => { const resCarol = await agent.api.app.bsky.actor.getProfile( { actor: dan }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorGetProfile, + ), + }, ) expect(resCarol.data.viewer?.blocking).toBeUndefined() expect(resCarol.data.viewer?.blockingByList).toBeUndefined() @@ -207,7 +255,9 @@ describe('pds views with blocking from block lists', () => { const resDan = await agent.api.app.bsky.actor.getProfile( { actor: carol }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile), + }, ) expect(resDan.data.viewer?.blocking).toBeDefined() expect(resDan.data.viewer?.blockingByList?.uri).toEqual( @@ -219,7 +269,12 @@ describe('pds views with blocking from block lists', () => { it('returns block status on getProfiles', async () => { const resCarol = await agent.api.app.bsky.actor.getProfiles( { actors: [alice, dan] }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorGetProfiles, + ), + }, ) expect(resCarol.data.profiles[0].viewer?.blocking).toBeUndefined() expect(resCarol.data.profiles[0].viewer?.blockingByList).toBeUndefined() @@ -230,7 +285,9 @@ describe('pds views with blocking from block lists', () => { const resDan = await agent.api.app.bsky.actor.getProfiles( { actors: [alice, carol] }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfiles), + }, ) expect(resDan.data.profiles[0].viewer?.blocking).toBeUndefined() expect(resDan.data.profiles[0].viewer?.blockingByList).toBeUndefined() @@ -245,7 +302,9 @@ describe('pds views with blocking from block lists', () => { it('ignores self-blocks', async () => { const res = await agent.api.app.bsky.actor.getProfile( { actor: dan }, // dan subscribes to list that contains himself - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile), + }, ) expect(res.data.viewer?.blocking).toBeUndefined() expect(res.data.viewer?.blockingByList).toBeUndefined() @@ -257,7 +316,12 @@ describe('pds views with blocking from block lists', () => { { limit: 100, }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect( resCarol.data.notifications.some((notif) => notif.author.did === dan), @@ -267,7 +331,12 @@ describe('pds views with blocking from block lists', () => { { limit: 100, }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect( resDan.data.notifications.some((notif) => notif.author.did === carol), @@ -279,7 +348,12 @@ describe('pds views with blocking from block lists', () => { { term: 'dan.test', }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorSearchActors, + ), + }, ) expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() @@ -287,7 +361,12 @@ describe('pds views with blocking from block lists', () => { { term: 'carol.test', }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyActorSearchActors, + ), + }, ) expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() }) @@ -297,7 +376,12 @@ describe('pds views with blocking from block lists', () => { { term: 'dan.tes', }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorSearchActorsTypeahead, + ), + }, ) expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() @@ -305,7 +389,12 @@ describe('pds views with blocking from block lists', () => { { term: 'carol.tes', }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyActorSearchActorsTypeahead, + ), + }, ) expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() }) @@ -315,7 +404,12 @@ describe('pds views with blocking from block lists', () => { { term: 'dan.test', }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorSearchActorsTypeahead, + ), + }, ) expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeTruthy() @@ -323,7 +417,12 @@ describe('pds views with blocking from block lists', () => { { term: 'carol.test', }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyActorSearchActorsTypeahead, + ), + }, ) expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeTruthy() }) @@ -338,7 +437,12 @@ describe('pds views with blocking from block lists', () => { { limit: 100, }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorGetSuggestions, + ), + }, ) expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() @@ -346,7 +450,12 @@ describe('pds views with blocking from block lists', () => { { limit: 100, }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyActorGetSuggestions, + ), + }, ) expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() }) @@ -354,7 +463,7 @@ describe('pds views with blocking from block lists', () => { it('returns the contents of a list', async () => { const res = await agent.api.app.bsky.graph.getList( { list: listUri }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) }, ) expect(forSnapshot(res.data)).toMatchSnapshot() }) @@ -362,15 +471,15 @@ describe('pds views with blocking from block lists', () => { it('paginates getList', async () => { const full = await agent.api.app.bsky.graph.getList( { list: listUri }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) }, ) const first = await agent.api.app.bsky.graph.getList( { list: listUri, limit: 1 }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) }, ) const second = await agent.api.app.bsky.graph.getList( { list: listUri, cursor: first.data.cursor }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) }, ) const combined = [...first.data.items, ...second.data.items] expect(combined).toEqual(full.data.items) @@ -394,7 +503,7 @@ describe('pds views with blocking from block lists', () => { const res = await agent.api.app.bsky.graph.getLists( { actor: alice }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) }, ) expect(forSnapshot(res.data)).toMatchSnapshot() }) @@ -402,15 +511,15 @@ describe('pds views with blocking from block lists', () => { it('paginates getLists', async () => { const full = await agent.api.app.bsky.graph.getLists( { actor: alice }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) }, ) const first = await agent.api.app.bsky.graph.getLists( { actor: alice, limit: 1 }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) }, ) const second = await agent.api.app.bsky.graph.getLists( { actor: alice, cursor: first.data.cursor }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) }, ) const combined = [...first.data.lists, ...second.data.lists] expect(combined).toEqual(full.data.lists) @@ -429,7 +538,12 @@ describe('pds views with blocking from block lists', () => { const res = await agent.api.app.bsky.graph.getListBlocks( {}, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphGetListBlocks, + ), + }, ) expect(forSnapshot(res.data)).toMatchSnapshot() }) @@ -437,15 +551,30 @@ describe('pds views with blocking from block lists', () => { it('paginates getListBlocks', async () => { const full = await agent.api.app.bsky.graph.getListBlocks( {}, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphGetListBlocks, + ), + }, ) const first = await agent.api.app.bsky.graph.getListBlocks( { limit: 1 }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphGetListBlocks, + ), + }, ) const second = await agent.api.app.bsky.graph.getListBlocks( { cursor: first.data.cursor }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphGetListBlocks, + ), + }, ) const combined = [...first.data.lists, ...second.data.lists] expect(combined).toEqual(full.data.lists) @@ -470,7 +599,12 @@ describe('pds views with blocking from block lists', () => { const resCarol = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, ) expect( resCarol.data.feed.some((post) => post.post.author.did === dan), @@ -478,7 +612,9 @@ describe('pds views with blocking from block lists', () => { const resDan = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline), + }, ) expect( resDan.data.feed.some((post) => @@ -500,7 +636,12 @@ describe('pds views with blocking from block lists', () => { const resCarol = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, ) expect( resCarol.data.feed.some((post) => post.post.author.did === dan), @@ -508,7 +649,9 @@ describe('pds views with blocking from block lists', () => { const resDan = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline), + }, ) expect( resDan.data.feed.some((post) => diff --git a/packages/bsky/tests/views/blocks.test.ts b/packages/bsky/tests/views/blocks.test.ts index 50c1649a509..848b8f89c20 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -2,6 +2,7 @@ import assert from 'assert' import { TestNetwork, RecordRef, SeedClient, basicSeed } from '@atproto/dev-env' import { AtpAgent, AtUri } from '@atproto/api' import { assertIsThreadViewPost, forSnapshot } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('pds views with blocking', () => { let network: TestNetwork @@ -68,7 +69,12 @@ describe('pds views with blocking', () => { it('blocks thread post', async () => { const { data: threadAlice } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[carol][0].ref.uriStr }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(threadAlice).toEqual({ thread: { @@ -86,7 +92,12 @@ describe('pds views with blocking', () => { }) const { data: threadCarol } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(threadCarol).toEqual({ thread: { @@ -107,7 +118,12 @@ describe('pds views with blocking', () => { // Contains reply by carol const { data: thread } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread)).toMatchSnapshot() }) @@ -115,7 +131,12 @@ describe('pds views with blocking', () => { 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) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assertIsThreadViewPost(thread.thread) @@ -131,7 +152,12 @@ describe('pds views with blocking', () => { // Parent is a post by dan const { data: thread } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: aliceReplyToDan.ref.uriStr }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread)).toMatchSnapshot() }) @@ -140,7 +166,12 @@ describe('pds views with blocking', () => { // Contains a deep embed of carol's post, blocked by dan const { data: thread } = await agent.api.app.bsky.feed.getPostThread( { depth: 0, uri: sc.posts[alice][2].ref.uriStr }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread)).toMatchSnapshot() }) @@ -148,7 +179,12 @@ describe('pds views with blocking', () => { it('errors on getting author feed', async () => { const attempt1 = agent.api.app.bsky.feed.getAuthorFeed( { actor: carol }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) await expect(attempt1).rejects.toMatchObject({ error: 'BlockedActor', @@ -156,7 +192,12 @@ describe('pds views with blocking', () => { const attempt2 = agent.api.app.bsky.feed.getAuthorFeed( { actor: dan }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) await expect(attempt2).rejects.toMatchObject({ error: 'BlockedByActor', @@ -166,7 +207,12 @@ describe('pds views with blocking', () => { it('strips blocked users out of getTimeline', async () => { const resCarol = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, ) // dan's posts don't appear, nor alice's reply to dan, nor carol's reply to alice (which was a reply to dan) @@ -181,7 +227,9 @@ describe('pds views with blocking', () => { const resDan = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline), + }, ) expect( resDan.data.feed.some( @@ -201,7 +249,12 @@ describe('pds views with blocking', () => { const resCarol = await agent.api.app.bsky.feed.getListFeed( { list: listRef.uriStr, limit: 100 }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetListFeed, + ), + }, ) expect( resCarol.data.feed.some((post) => post.post.author.did === dan), @@ -209,7 +262,9 @@ describe('pds views with blocking', () => { const resDan = await agent.api.app.bsky.feed.getListFeed( { list: listRef.uriStr, limit: 100 }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetListFeed), + }, ) expect( resDan.data.feed.some((post) => post.post.author.did === carol), @@ -219,14 +274,21 @@ describe('pds views with blocking', () => { it('returns block status on getProfile', async () => { const resCarol = await agent.api.app.bsky.actor.getProfile( { actor: dan }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorGetProfile, + ), + }, ) expect(resCarol.data.viewer?.blocking).toBeUndefined() expect(resCarol.data.viewer?.blockedBy).toBe(true) const resDan = await agent.api.app.bsky.actor.getProfile( { actor: carol }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile), + }, ) expect(resDan.data.viewer?.blocking).toBeDefined() expect(resDan.data.viewer?.blockedBy).toBe(false) @@ -236,13 +298,15 @@ describe('pds views with blocking', () => { // there are follows between carol and dan const { data: profile } = await agent.api.app.bsky.actor.getProfile( { actor: carol }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile), + }, ) expect(profile.viewer?.following).toBeUndefined() expect(profile.viewer?.followedBy).toBeUndefined() const { data: result } = await agent.api.app.bsky.graph.getBlocks( {}, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetBlocks) }, ) const blocked = result.blocks.find((block) => block.did === carol) expect(blocked).toBeDefined() @@ -253,7 +317,12 @@ describe('pds views with blocking', () => { it('returns block status on getProfiles', async () => { const resCarol = await agent.api.app.bsky.actor.getProfiles( { actors: [alice, dan] }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorGetProfiles, + ), + }, ) expect(resCarol.data.profiles[0].viewer?.blocking).toBeUndefined() expect(resCarol.data.profiles[0].viewer?.blockingByList).toBeUndefined() @@ -264,7 +333,9 @@ describe('pds views with blocking', () => { const resDan = await agent.api.app.bsky.actor.getProfiles( { actors: [alice, carol] }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfiles), + }, ) expect(resDan.data.profiles[0].viewer?.blocking).toBeUndefined() expect(resDan.data.profiles[0].viewer?.blockingByList).toBeUndefined() @@ -277,13 +348,23 @@ describe('pds views with blocking', () => { it('does not return block violating follows', async () => { const resCarol = await agent.api.app.bsky.graph.getFollows( { actor: carol }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(resCarol.data.follows.some((f) => f.did === dan)).toBe(false) const resDan = await agent.api.app.bsky.graph.getFollows( { actor: dan }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(resDan.data.follows.some((f) => f.did === carol)).toBe(false) }) @@ -291,13 +372,23 @@ describe('pds views with blocking', () => { it('does not return block violating followers', async () => { const resCarol = await agent.api.app.bsky.graph.getFollowers( { actor: carol }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) expect(resCarol.data.followers.some((f) => f.did === dan)).toBe(false) const resDan = await agent.api.app.bsky.graph.getFollowers( { actor: dan }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) expect(resDan.data.followers.some((f) => f.did === carol)).toBe(false) }) @@ -309,7 +400,7 @@ describe('pds views with blocking', () => { const resCarol = await agent.api.app.bsky.feed.getPosts( { uris: [alicePost, carolPost, danPost] }, - { headers: await network.serviceHeaders(carol) }, + { headers: await network.serviceHeaders(carol, ids.AppBskyFeedGetPosts) }, ) expect(resCarol.data.posts.some((p) => p.uri === alicePost)).toBe(true) expect(resCarol.data.posts.some((p) => p.uri === carolPost)).toBe(true) @@ -317,7 +408,7 @@ describe('pds views with blocking', () => { const resDan = await agent.api.app.bsky.feed.getPosts( { uris: [alicePost, carolPost, danPost] }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetPosts) }, ) expect(resDan.data.posts.some((p) => p.uri === alicePost)).toBe(true) expect(resDan.data.posts.some((p) => p.uri === carolPost)).toBe(false) @@ -329,7 +420,12 @@ describe('pds views with blocking', () => { { limit: 100, }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect( resCarol.data.notifications.some((notif) => notif.author.did === dan), @@ -339,7 +435,12 @@ describe('pds views with blocking', () => { { limit: 100, }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect( resDan.data.notifications.some((notif) => notif.author.did === carol), @@ -351,7 +452,12 @@ describe('pds views with blocking', () => { { term: 'dan.test', }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorSearchActors, + ), + }, ) expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() @@ -359,7 +465,12 @@ describe('pds views with blocking', () => { { term: 'carol.test', }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyActorSearchActors, + ), + }, ) expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() }) @@ -369,7 +480,12 @@ describe('pds views with blocking', () => { { term: 'dan.tes', }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorSearchActorsTypeahead, + ), + }, ) expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() @@ -377,7 +493,12 @@ describe('pds views with blocking', () => { { term: 'carol.tes', }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyActorSearchActorsTypeahead, + ), + }, ) expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() }) @@ -387,7 +508,12 @@ describe('pds views with blocking', () => { { term: 'dan.test', }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorSearchActorsTypeahead, + ), + }, ) expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeTruthy() @@ -395,7 +521,12 @@ describe('pds views with blocking', () => { { term: 'carol.test', }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyActorSearchActorsTypeahead, + ), + }, ) expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeTruthy() }) @@ -410,7 +541,12 @@ describe('pds views with blocking', () => { { limit: 100, }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyActorGetSuggestions, + ), + }, ) expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() @@ -418,7 +554,12 @@ describe('pds views with blocking', () => { { limit: 100, }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyActorGetSuggestions, + ), + }, ) expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() }) @@ -429,7 +570,12 @@ describe('pds views with blocking', () => { const { data: replyThenBlock } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assertIsThreadViewPost(replyThenBlock.thread) @@ -445,7 +591,12 @@ describe('pds views with blocking', () => { await network.processAll() const { data: unblock } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assertIsThreadViewPost(unblock.thread) @@ -470,7 +621,12 @@ describe('pds views with blocking', () => { const { data: blockThenReply } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assertIsThreadViewPost(blockThenReply.thread) @@ -492,7 +648,12 @@ describe('pds views with blocking', () => { const { data: embedThenBlock } = await agent.api.app.bsky.feed.getPostThread( { depth: 0, uri: sc.posts[dan][1].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assertIsThreadViewPost(embedThenBlock.thread) @@ -509,7 +670,12 @@ describe('pds views with blocking', () => { await network.processAll() const { data: unblock } = await agent.api.app.bsky.feed.getPostThread( { depth: 0, uri: sc.posts[dan][1].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assertIsThreadViewPost(unblock.thread) @@ -535,7 +701,12 @@ describe('pds views with blocking', () => { const { data: blockThenEmbed } = await agent.api.app.bsky.feed.getPostThread( { depth: 0, uri: carolEmbedsDan.ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assertIsThreadViewPost(blockThenEmbed.thread) @@ -558,7 +729,12 @@ describe('pds views with blocking', () => { const embedBlockedUri = sc.posts[dan][1].ref.uriStr const { data: timeline } = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetTimeline, + ), + }, ) const replyBlockedPost = timeline.feed.find( (item) => item.post.uri === replyBlockedUri, @@ -593,7 +769,7 @@ describe('pds views with blocking', () => { const res = await agent.api.app.bsky.graph.getBlocks( {}, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetBlocks) }, ) const dids = res.data.blocks.map((block) => block.did).sort() expect(dids).toEqual([alice, carol].sort()) @@ -602,15 +778,15 @@ describe('pds views with blocking', () => { it('paginates getBlocks', async () => { const full = await agent.api.app.bsky.graph.getBlocks( {}, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetBlocks) }, ) const first = await agent.api.app.bsky.graph.getBlocks( { limit: 1 }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetBlocks) }, ) const second = await agent.api.app.bsky.graph.getBlocks( { cursor: first.data.cursor }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetBlocks) }, ) const combined = [...first.data.blocks, ...second.data.blocks] expect(combined).toEqual(full.data.blocks) @@ -619,7 +795,12 @@ describe('pds views with blocking', () => { it('returns knownFollowers with blocks filtered', async () => { const carolForAlice = await agent.api.app.bsky.actor.getProfile( { actor: bob }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyActorGetProfile, + ), + }, ) const knownFollowers = carolForAlice.data.viewer?.knownFollowers diff --git a/packages/bsky/tests/views/feed-hidden-replies.test.ts b/packages/bsky/tests/views/feed-hidden-replies.test.ts new file mode 100644 index 00000000000..a75dde85400 --- /dev/null +++ b/packages/bsky/tests/views/feed-hidden-replies.test.ts @@ -0,0 +1,246 @@ +import { TestNetwork, SeedClient } from '@atproto/dev-env' +import AtpAgent from '@atproto/api' + +import { ids } from '../../src/lexicon/lexicons' +import { feedHiddenRepliesSeed, Users } from '../seed/feed-hidden-replies' + +describe('feed hidden replies', () => { + let network: TestNetwork + let agent: AtpAgent + let pdsAgent: AtpAgent + let sc: SeedClient + let users: Users + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_tests_feed_hidden_replies', + }) + agent = network.bsky.getClient() + pdsAgent = network.pds.getClient() + sc = network.getSeedClient() + + const result = await feedHiddenRepliesSeed(sc) + users = result.users + + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + describe(`notifications`, () => { + it(`[A] -> [B] : B is hidden`, async () => { + const A = await sc.post(users.poster.did, `A`) + + await network.processAll() + + const B = await sc.reply(users.replier.did, A.ref, A.ref, `B`) + const C = await sc.reply(users.replier.did, A.ref, A.ref, `C`) + + await pdsAgent.api.app.bsky.feed.threadgate.create( + { + repo: A.ref.uri.host, + rkey: A.ref.uri.rkey, + }, + { + post: A.ref.uriStr, + createdAt: new Date().toISOString(), + hiddenReplies: [B.ref.uriStr], + }, + sc.getHeaders(A.ref.uri.host), + ) + + await network.processAll() + + const { + data: { notifications }, + } = await agent.api.app.bsky.notification.listNotifications( + {}, + { + headers: await network.serviceHeaders( + users.poster.did, + ids.AppBskyNotificationListNotifications, + ), + }, + ) + + const notificationB = notifications.find((item) => { + return item.uri === B.ref.uriStr + }) + const notificationC = notifications.find((item) => { + return item.uri === C.ref.uriStr + }) + + expect(notificationB).toBeUndefined() + expect(notificationC).toBeDefined() + }) + + it(`[A] -> [B] -> [C] : B is hidden, C results in no notification for A, notification for B`, async () => { + const A = await sc.post(users.poster.did, `A`) + await network.processAll() + const B = await sc.reply(users.replier.did, A.ref, A.ref, `B`) + + await pdsAgent.api.app.bsky.feed.threadgate.create( + { + repo: A.ref.uri.host, + rkey: A.ref.uri.rkey, + }, + { + post: A.ref.uriStr, + createdAt: new Date().toISOString(), + hiddenReplies: [B.ref.uriStr], + }, + sc.getHeaders(A.ref.uri.host), + ) + + await network.processAll() + + const C = await sc.reply(users.viewer.did, A.ref, B.ref, `C`) + + await network.processAll() + + const { + data: { notifications: posterNotifications }, + } = await agent.api.app.bsky.notification.listNotifications( + {}, + { + headers: await network.serviceHeaders( + users.poster.did, + ids.AppBskyNotificationListNotifications, + ), + }, + ) + + const posterNotificationB = posterNotifications.find((item) => { + return item.uri === B.ref.uriStr + }) + const posterNotificationC = posterNotifications.find((item) => { + return item.uri === C.ref.uriStr + }) + + expect(posterNotificationB).toBeUndefined() + expect(posterNotificationC).toBeUndefined() + + const { + data: { notifications: replierNotifications }, + } = await agent.api.app.bsky.notification.listNotifications( + {}, + { + headers: await network.serviceHeaders( + users.replier.did, + ids.AppBskyNotificationListNotifications, + ), + }, + ) + + const replierNotificationC = replierNotifications.find((item) => { + return item.uri === C.ref.uriStr + }) + + expect(replierNotificationC).toBeDefined() + }) + + it(`[A] -> [B] -> [C] -> [D] : C is hidden, D results in no notification for A or B, notification for C, C exists in B's notifications`, async () => { + const A = await sc.post(users.poster.did, `A`) + await network.processAll() + const B = await sc.reply(users.replier.did, A.ref, A.ref, `B`) + await network.processAll() + const C = await sc.reply(users.viewer.did, A.ref, B.ref, `C`) + await network.processAll() + + const { + data: { notifications: posterNotificationsBefore }, + } = await agent.api.app.bsky.notification.listNotifications( + {}, + { + headers: await network.serviceHeaders( + users.poster.did, + ids.AppBskyNotificationListNotifications, + ), + }, + ) + + const posterNotificationCBefore = posterNotificationsBefore.find( + (item) => { + return item.uri === C.ref.uriStr + }, + ) + + expect(posterNotificationCBefore).toBeDefined() + + await pdsAgent.api.app.bsky.feed.threadgate.create( + { + repo: A.ref.uri.host, + rkey: A.ref.uri.rkey, + }, + { + post: A.ref.uriStr, + createdAt: new Date().toISOString(), + hiddenReplies: [C.ref.uriStr], + }, + sc.getHeaders(A.ref.uri.host), + ) + await network.processAll() + const D = await sc.reply(users.viewer.did, A.ref, C.ref, `D`) + await network.processAll() + + const { + data: { notifications: posterNotifications }, + } = await agent.api.app.bsky.notification.listNotifications( + {}, + { + headers: await network.serviceHeaders( + users.poster.did, + ids.AppBskyNotificationListNotifications, + ), + }, + ) + + const posterNotificationB = posterNotifications.find((item) => { + return item.uri === B.ref.uriStr + }) + const posterNotificationC = posterNotifications.find((item) => { + return item.uri === C.ref.uriStr + }) + const posterNotificationD = posterNotifications.find((item) => { + return item.uri === D.ref.uriStr + }) + + expect(posterNotificationB).toBeDefined() + expect(posterNotificationC).toBeUndefined() // hidden bc OP + expect(posterNotificationD).toBeUndefined() // hidden bc no propogation + + const { + data: { notifications: replierNotifications }, + } = await agent.api.app.bsky.notification.listNotifications( + {}, + { + headers: await network.serviceHeaders( + users.replier.did, + ids.AppBskyNotificationListNotifications, + ), + }, + ) + + const replierNotificationC = replierNotifications.find((item) => { + return item.uri === C.ref.uriStr + }) + const replierNotificationD = replierNotifications.find((item) => { + return item.uri === D.ref.uriStr + }) + + expect(replierNotificationC).toBeDefined() // not hidden bc not OP + expect(replierNotificationD).toBeUndefined() // hidden bc no propogation + + await pdsAgent.api.app.bsky.feed.threadgate.delete( + { + repo: A.ref.uri.host, + rkey: A.ref.uri.rkey, + }, + sc.getHeaders(A.ref.uri.host), + ) + await network.processAll() + }) + }) +}) diff --git a/packages/bsky/tests/views/feed-view-post.test.ts b/packages/bsky/tests/views/feed-view-post.test.ts new file mode 100644 index 00000000000..532a2c9074d --- /dev/null +++ b/packages/bsky/tests/views/feed-view-post.test.ts @@ -0,0 +1,501 @@ +import { AtpAgent, AppBskyFeedDefs, AtUri } from '@atproto/api' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' +import { ids } from '../../src/lexicon/lexicons' + +/** + * The frontend computes feed slices for display using at-most one + * `FeedViewPost` slice. If that slice results in an "orphaned" thread e.g. + * parent or root is blocked, that slice is tossed out and the next + * `FeedViewPost` slice is considered. That process continues until a + * contiguous `root -> child` slice can be found. + * + * For the tests below, we test with up to 4 slices: one root and up to 3 + * replies. Some tests focus on the first slice, and others ensure that at + * least one contiguous slice is returned. + */ +const LIMIT = 4 + +describe('pds thread views', () => { + let network: TestNetwork + let agent: AtpAgent + let pdsAgent: AtpAgent + let sc: SeedClient + + // account dids, for convenience + let alice: string + let bob: string + let carol: string + let dan: string + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_views_feed_view_post', + }) + agent = network.bsky.getClient() + pdsAgent = network.pds.getClient() + sc = network.getSeedClient() + await basicSeed(sc) + alice = sc.dids.alice + bob = sc.dids.bob + carol = sc.dids.carol + dan = sc.dids.dan + + await sc.follow(carol, alice) + await sc.follow(carol, bob) + await sc.follow(carol, dan) + await sc.follow(dan, alice) + await sc.follow(dan, bob) + await sc.follow(dan, carol) + + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + it(`[A] -> [B], A blocks B, viewed as C`, async () => { + const A = await sc.post(alice, `A`) + await network.processAll() + const B = await sc.reply(bob, A.ref, A.ref, `B`) + await network.processAll() + const block = await pdsAgent.api.app.bsky.graph.block.create( + { repo: alice }, + { createdAt: new Date().toISOString(), subject: bob }, + sc.getHeaders(alice), + ) + + await network.processAll() + + const timeline = await agent.api.app.bsky.feed.getTimeline( + { limit: LIMIT }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, + ) + + const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr) + const sliceB = timeline.data.feed.find((f) => f.post.uri === B.ref.uriStr) + + expect(sliceA).toBeDefined() + expect(sliceB).toBeDefined() + + if (!sliceA || !sliceB) { + throw new Error('sliceA or sliceB is undefined') + } + + expect(AppBskyFeedDefs.isBlockedPost(sliceB.reply?.parent)).toBe(true) + expect(AppBskyFeedDefs.isBlockedPost(sliceB.reply?.root)).toBe(true) + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: new AtUri(block.uri).rkey }, + sc.getHeaders(alice), + ) + }) + + it(`[A] -> [B] -> [C], A blocks B, viewed as C`, async () => { + const A = await sc.post(alice, `A`) + await network.processAll() + const B = await sc.reply(bob, A.ref, A.ref, `B`) + await network.processAll() + const C = await sc.reply(carol, A.ref, B.ref, `C`) + const block = await pdsAgent.api.app.bsky.graph.block.create( + { repo: alice }, + { createdAt: new Date().toISOString(), subject: bob }, + sc.getHeaders(alice), + ) + + await network.processAll() + + const timeline = await agent.api.app.bsky.feed.getTimeline( + { limit: LIMIT }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, + ) + + const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr) + + expect(sliceC).toBeDefined() + expect(sliceC?.reply).toBeDefined() + + if (!sliceC || !sliceC.reply) { + throw new Error('sliceC is undefined') + } + + expect(sliceC.reply.parent.uri).toEqual(B.ref.uriStr) + expect(sliceC.reply.root.uri).toEqual(A.ref.uriStr) + expect(AppBskyFeedDefs.isPostView(sliceC.reply.parent)).toBe(true) + expect(AppBskyFeedDefs.isBlockedPost(sliceC.reply.root)).toBe(true) + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: new AtUri(block.uri).rkey }, + sc.getHeaders(alice), + ) + }) + + it(`[A] -> [B] -> [C], C blocks A, viewed as C`, async () => { + const A = await sc.post(alice, `A`) + await network.processAll() + const B = await sc.reply(bob, A.ref, A.ref, `B`) + await network.processAll() + const C = await sc.reply(carol, A.ref, B.ref, `C`) + const block = await pdsAgent.api.app.bsky.graph.block.create( + { repo: carol }, + { createdAt: new Date().toISOString(), subject: alice }, + sc.getHeaders(carol), + ) + + await network.processAll() + + const timeline = await agent.api.app.bsky.feed.getTimeline( + // make sure we process all slices in this test + { limit: LIMIT }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, + ) + + const sliceB = timeline.data.feed.find((f) => f.post.uri === B.ref.uriStr) + const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr) + + expect(sliceB).toBeUndefined() + expect(sliceC).toBeUndefined() + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: carol, rkey: new AtUri(block.uri).rkey }, + sc.getHeaders(carol), + ) + }) + + it(`[A] -> [B] -> [C], C blocks B, viewed as C`, async () => { + const A = await sc.post(alice, `A`) + await network.processAll() + const B = await sc.reply(bob, A.ref, A.ref, `B`) + await network.processAll() + const C = await sc.reply(carol, A.ref, B.ref, `C`) + const block = await pdsAgent.api.app.bsky.graph.block.create( + { repo: carol }, + { createdAt: new Date().toISOString(), subject: bob }, + sc.getHeaders(carol), + ) + + await network.processAll() + + const timeline = await agent.api.app.bsky.feed.getTimeline( + // make sure we process all slices in this test + { limit: LIMIT }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, + ) + + const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr) + const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr) + + expect(sliceA).toBeDefined() + expect(sliceC).toBeUndefined() + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: carol, rkey: new AtUri(block.uri).rkey }, + sc.getHeaders(carol), + ) + }) + + it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as C`, async () => { + const A = await sc.post(alice, `A`) + await network.processAll() + const B = await sc.reply(bob, A.ref, A.ref, `B`) + await network.processAll() + const C = await sc.reply(carol, A.ref, B.ref, `C`) + await network.processAll() + const D = await sc.reply(dan, A.ref, C.ref, `D`) + const block = await pdsAgent.api.app.bsky.graph.block.create( + { repo: alice }, + { createdAt: new Date().toISOString(), subject: carol }, + sc.getHeaders(alice), + ) + + await network.processAll() + + const timeline = await agent.api.app.bsky.feed.getTimeline( + { limit: LIMIT }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, + ) + + const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr) + + expect(sliceD).toBeDefined() + expect(sliceD?.reply).toBeDefined() + + if (!sliceD || !sliceD.reply) { + throw new Error('sliceD is undefined') + } + + expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr) + expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr) + expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true) + expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root)).toBe(true) + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: new AtUri(block.uri).rkey }, + sc.getHeaders(alice), + ) + }) + + it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as D`, async () => { + const A = await sc.post(alice, `A`) + await network.processAll() + const B = await sc.reply(bob, A.ref, A.ref, `B`) + await network.processAll() + const C = await sc.reply(carol, A.ref, B.ref, `C`) + await network.processAll() + const D = await sc.reply(dan, A.ref, C.ref, `D`) + const block = await pdsAgent.api.app.bsky.graph.block.create( + { repo: alice }, + { createdAt: new Date().toISOString(), subject: carol }, + sc.getHeaders(alice), + ) + + await network.processAll() + + const timeline = await agent.api.app.bsky.feed.getTimeline( + { limit: LIMIT }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline), + }, + ) + + const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr) + + expect(sliceD).toBeDefined() + expect(sliceD?.reply).toBeDefined() + + if (!sliceD || !sliceD.reply) { + throw new Error('sliceD is undefined') + } + + expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr) + expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr) + expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true) + expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root)).toBe(true) + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: new AtUri(block.uri).rkey }, + sc.getHeaders(alice), + ) + }) + + it(`[A] -> [B] -> [C] -> [D], A blocks B, viewed as C`, async () => { + const A = await sc.post(alice, `A`) + await network.processAll() + const B = await sc.reply(bob, A.ref, A.ref, `B`) + await network.processAll() + const C = await sc.reply(carol, A.ref, B.ref, `C`) + await network.processAll() + const D = await sc.reply(dan, A.ref, C.ref, `D`) + const block = await pdsAgent.api.app.bsky.graph.block.create( + { repo: alice }, + { createdAt: new Date().toISOString(), subject: bob }, + sc.getHeaders(alice), + ) + + await network.processAll() + + const timeline = await agent.api.app.bsky.feed.getTimeline( + { limit: LIMIT }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, + ) + + const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr) + + expect(sliceD).toBeDefined() + expect(sliceD?.reply).toBeDefined() + + if (!sliceD || !sliceD.reply) { + throw new Error('sliceD is undefined') + } + + expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr) + expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr) + expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true) + /* + * We don't walk the reply ancestors past whats available in the ReplyRef + */ + expect(AppBskyFeedDefs.isPostView(sliceD.reply.root)).toBe(true) + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: new AtUri(block.uri).rkey }, + sc.getHeaders(alice), + ) + }) + + it(`[A] -> [B] -> [C] -> [D], B blocks C, viewed as D`, async () => { + const A = await sc.post(alice, `A`) + await network.processAll() + const B = await sc.reply(bob, A.ref, A.ref, `B`) + await network.processAll() + const C = await sc.reply(carol, A.ref, B.ref, `C`) + await network.processAll() + const D = await sc.reply(dan, A.ref, C.ref, `D`) + const block = await pdsAgent.api.app.bsky.graph.block.create( + { repo: bob }, + { createdAt: new Date().toISOString(), subject: carol }, + sc.getHeaders(bob), + ) + + await network.processAll() + + const timeline = await agent.api.app.bsky.feed.getTimeline( + { limit: LIMIT }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline), + }, + ) + + const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr) + + expect(sliceD).toBeDefined() + expect(sliceD?.reply).toBeDefined() + + if (!sliceD || !sliceD.reply) { + throw new Error('sliceD is undefined') + } + + expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr) + expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr) + expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true) + /* + * We don't walk the reply ancestors past whats available in the ReplyRef + */ + expect(AppBskyFeedDefs.isPostView(sliceD.reply.root)).toBe(true) + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: bob, rkey: new AtUri(block.uri).rkey }, + sc.getHeaders(bob), + ) + }) + + it(`[A] -> [B] -> [C] -> [D], A blocks D, viewed as C`, async () => { + const A = await sc.post(alice, `A`) + await network.processAll() + const B = await sc.reply(bob, A.ref, A.ref, `B`) + await network.processAll() + const C = await sc.reply(carol, A.ref, B.ref, `C`) + await network.processAll() + const D = await sc.reply(dan, A.ref, C.ref, `D`) + const block = await pdsAgent.api.app.bsky.graph.block.create( + { repo: alice }, + { createdAt: new Date().toISOString(), subject: dan }, + sc.getHeaders(alice), + ) + + await network.processAll() + + const timeline = await agent.api.app.bsky.feed.getTimeline( + { limit: LIMIT }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, + ) + + const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr) + const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr) + + expect(sliceD).toBeDefined() + expect(sliceC).toBeDefined() + + if (!sliceD || !sliceC) { + throw new Error('sliceD or sliceC is undefined') + } + + expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply?.root)).toBe(true) + expect(AppBskyFeedDefs.isPostView(sliceC.reply?.parent)).toBe(true) + expect(AppBskyFeedDefs.isPostView(sliceC.reply?.root)).toBe(true) + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: new AtUri(block.uri).rkey }, + sc.getHeaders(alice), + ) + }) + + it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as D, A/B/C are outside first page`, async () => { + const A = await sc.post(alice, `A`) + await network.processAll() + const B = await sc.reply(bob, A.ref, A.ref, `B`) + await network.processAll() + const C = await sc.reply(carol, A.ref, B.ref, `C`) + await network.processAll() + + // push A/B/C to send page of results + await sc.post(alice, `Aa`) + await sc.post(alice, `Ab`) + await sc.post(alice, `Ac`) + await sc.post(alice, `Ad`) + + await network.processAll() + + const D = await sc.reply(dan, A.ref, C.ref, `D`) + const block = await pdsAgent.api.app.bsky.graph.block.create( + { repo: alice }, + { createdAt: new Date().toISOString(), subject: carol }, + sc.getHeaders(alice), + ) + + await network.processAll() + + const timeline = await agent.api.app.bsky.feed.getTimeline( + { limit: LIMIT }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline), + }, + ) + + const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr) + const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr) + + expect(sliceD).toBeDefined() + expect(sliceD?.reply).toBeDefined() + // not in first page of results + expect(sliceA).toBeUndefined() + + if (!sliceD || !sliceD.reply) { + throw new Error('sliceD is undefined') + } + + expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr) + expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr) + expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true) + expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root)).toBe(true) + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: new AtUri(block.uri).rkey }, + sc.getHeaders(alice), + ) + }) +}) diff --git a/packages/bsky/tests/views/follows.test.ts b/packages/bsky/tests/views/follows.test.ts index 2300a1b1ed8..83aa3a40dd6 100644 --- a/packages/bsky/tests/views/follows.test.ts +++ b/packages/bsky/tests/views/follows.test.ts @@ -1,6 +1,7 @@ import { AtpAgent } from '@atproto/api' import { TestNetwork, SeedClient, followsSeed } from '@atproto/dev-env' import { forSnapshot, paginateAll, stripViewer } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('pds follow views', () => { let agent: AtpAgent @@ -31,35 +32,60 @@ describe('pds follow views', () => { it('fetches followers', async () => { const aliceFollowers = await agent.api.app.bsky.graph.getFollowers( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) expect(forSnapshot(aliceFollowers.data)).toMatchSnapshot() const bobFollowers = await agent.api.app.bsky.graph.getFollowers( { actor: sc.dids.bob }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) expect(forSnapshot(bobFollowers.data)).toMatchSnapshot() const carolFollowers = await agent.api.app.bsky.graph.getFollowers( { actor: sc.dids.carol }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) expect(forSnapshot(carolFollowers.data)).toMatchSnapshot() const danFollowers = await agent.api.app.bsky.graph.getFollowers( { actor: sc.dids.dan }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) expect(forSnapshot(danFollowers.data)).toMatchSnapshot() const eveFollowers = await agent.api.app.bsky.graph.getFollowers( { actor: sc.dids.eve }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) expect(forSnapshot(eveFollowers.data)).toMatchSnapshot() @@ -68,11 +94,21 @@ describe('pds follow views', () => { it('fetches followers by handle', async () => { const byDid = await agent.api.app.bsky.graph.getFollowers( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) const byHandle = await agent.api.app.bsky.graph.getFollowers( { actor: sc.accounts[alice].handle }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) expect(byHandle.data).toEqual(byDid.data) }) @@ -86,7 +122,12 @@ describe('pds follow views', () => { cursor, limit: 2, }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) return res.data } @@ -98,7 +139,12 @@ describe('pds follow views', () => { const full = await agent.api.app.bsky.graph.getFollowers( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) expect(full.data.followers.length).toEqual(4) @@ -108,7 +154,12 @@ describe('pds follow views', () => { it('fetches followers unauthed', async () => { const { data: authed } = await agent.api.app.bsky.graph.getFollowers( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) const { data: unauthed } = await agent.api.app.bsky.graph.getFollowers({ actor: sc.dids.alice, @@ -124,7 +175,12 @@ describe('pds follow views', () => { const aliceFollowers = await agent.api.app.bsky.graph.getFollowers( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollowers, + ), + }, ) expect(aliceFollowers.data.followers.map((f) => f.did)).not.toContain( @@ -139,35 +195,60 @@ describe('pds follow views', () => { it('fetches follows', async () => { const aliceFollowers = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(forSnapshot(aliceFollowers.data)).toMatchSnapshot() const bobFollowers = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.bob }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(forSnapshot(bobFollowers.data)).toMatchSnapshot() const carolFollowers = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.carol }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(forSnapshot(carolFollowers.data)).toMatchSnapshot() const danFollowers = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.dan }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(forSnapshot(danFollowers.data)).toMatchSnapshot() const eveFollowers = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.eve }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(forSnapshot(eveFollowers.data)).toMatchSnapshot() @@ -176,11 +257,21 @@ describe('pds follow views', () => { it('fetches follows by handle', async () => { const byDid = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) const byHandle = await agent.api.app.bsky.graph.getFollows( { actor: sc.accounts[alice].handle }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(byHandle.data).toEqual(byDid.data) }) @@ -194,7 +285,12 @@ describe('pds follow views', () => { cursor, limit: 2, }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) return res.data } @@ -206,7 +302,12 @@ describe('pds follow views', () => { const full = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(full.data.follows.length).toEqual(4) @@ -216,7 +317,12 @@ describe('pds follow views', () => { it('fetches follows unauthed', async () => { const { data: authed } = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) const { data: unauthed } = await agent.api.app.bsky.graph.getFollows({ actor: sc.dids.alice, @@ -232,7 +338,12 @@ describe('pds follow views', () => { const aliceFollows = await agent.api.app.bsky.graph.getFollows( { actor: sc.dids.alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetFollows, + ), + }, ) expect(aliceFollows.data.follows.map((f) => f.did)).not.toContain( diff --git a/packages/bsky/tests/views/known-followers.test.ts b/packages/bsky/tests/views/known-followers.test.ts index 0959d36ecca..c6e4297c05c 100644 --- a/packages/bsky/tests/views/known-followers.test.ts +++ b/packages/bsky/tests/views/known-followers.test.ts @@ -2,6 +2,7 @@ import { TestNetwork, SeedClient } from '@atproto/dev-env' import { AtpAgent } from '@atproto/api' import { knownFollowersSeed } from '../seed/known-followers' +import { ids } from '../../src/lexicon/lexicons' describe('known followers (social proof)', () => { let network: TestNetwork @@ -70,7 +71,12 @@ describe('known followers (social proof)', () => { it('basic profile views do not return knownFollowers', async () => { const { data } = await agent.api.app.bsky.graph.getFollows( { actor: dids.base_res_1 }, - { headers: await network.serviceHeaders(dids.base_view) }, + { + headers: await network.serviceHeaders( + dids.base_view, + ids.AppBskyGraphGetFollows, + ), + }, ) const follow = data.follows[0] @@ -80,7 +86,12 @@ describe('known followers (social proof)', () => { it('getKnownFollowers: returns data', async () => { const { data } = await agent.api.app.bsky.graph.getKnownFollowers( { actor: dids.base_sub }, - { headers: await network.serviceHeaders(dids.base_view) }, + { + headers: await network.serviceHeaders( + dids.base_view, + ids.AppBskyGraphGetKnownFollowers, + ), + }, ) expect(data.subject.did).toBe(dids.base_sub) @@ -91,7 +102,12 @@ describe('known followers (social proof)', () => { it('getProfile: returns knownFollowers', async () => { const { data } = await agent.api.app.bsky.actor.getProfile( { actor: dids.base_sub }, - { headers: await network.serviceHeaders(dids.base_view) }, + { + headers: await network.serviceHeaders( + dids.base_view, + ids.AppBskyActorGetProfile, + ), + }, ) const knownFollowers = data.viewer?.knownFollowers @@ -103,7 +119,12 @@ describe('known followers (social proof)', () => { it('getProfile: filters 1st-party blocks', async () => { const { data } = await agent.api.app.bsky.actor.getProfile( { actor: dids.fp_block_sub }, - { headers: await network.serviceHeaders(dids.fp_block_view) }, + { + headers: await network.serviceHeaders( + dids.fp_block_view, + ids.AppBskyActorGetProfile, + ), + }, ) const knownFollowers = data.viewer?.knownFollowers @@ -114,7 +135,12 @@ describe('known followers (social proof)', () => { it('getProfile: filters second-party blocks', async () => { const result = await agent.api.app.bsky.actor.getProfile( { actor: dids.sp_block_sub }, - { headers: await network.serviceHeaders(dids.sp_block_view) }, + { + headers: await network.serviceHeaders( + dids.sp_block_view, + ids.AppBskyActorGetProfile, + ), + }, ) const knownFollowers = result.data.viewer?.knownFollowers @@ -125,7 +151,12 @@ describe('known followers (social proof)', () => { it('getProfiles: filters second-party blocks', async () => { const result = await agent.api.app.bsky.actor.getProfiles( { actors: [dids.sp_block_sub] }, - { headers: await network.serviceHeaders(dids.sp_block_view) }, + { + headers: await network.serviceHeaders( + dids.sp_block_view, + ids.AppBskyActorGetProfiles, + ), + }, ) expect(result.data.profiles).toHaveLength(1) @@ -138,7 +169,12 @@ describe('known followers (social proof)', () => { it('getProfiles: mix of results', async () => { const result = await agent.api.app.bsky.actor.getProfiles( { actors: [dids.mix_sub_1, dids.mix_sub_2, dids.mix_sub_3] }, - { headers: await network.serviceHeaders(dids.mix_view) }, + { + headers: await network.serviceHeaders( + dids.mix_view, + ids.AppBskyActorGetProfiles, + ), + }, ) expect(result.data.profiles).toHaveLength(3) diff --git a/packages/bsky/tests/views/labeler-service.test.ts b/packages/bsky/tests/views/labeler-service.test.ts index 96deab9d9e9..a979667aa6d 100644 --- a/packages/bsky/tests/views/labeler-service.test.ts +++ b/packages/bsky/tests/views/labeler-service.test.ts @@ -68,7 +68,12 @@ describe('labeler service views', () => { it('fetches labelers', async () => { const view = await agent.api.app.bsky.labeler.getServices( { dids: [alice, bob, 'did:example:missing'] }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyLabelerGetServices, + ), + }, ) expect(forSnapshot(view.data)).toMatchSnapshot() @@ -77,7 +82,12 @@ describe('labeler service views', () => { it('fetches labelers detailed', async () => { const view = await agent.api.app.bsky.labeler.getServices( { dids: [alice, bob, 'did:example:missing'], detailed: true }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyLabelerGetServices, + ), + }, ) expect(forSnapshot(view.data)).toMatchSnapshot() @@ -86,7 +96,12 @@ describe('labeler service views', () => { it('fetches labelers unauthed', async () => { const { data: authed } = await agent.api.app.bsky.labeler.getServices( { dids: [alice] }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyLabelerGetServices, + ), + }, ) const { data: unauthed } = await agent.api.app.bsky.labeler.getServices({ dids: [alice], @@ -99,7 +114,12 @@ describe('labeler service views', () => { { dids: [alice, bob, 'did:example:missing'], }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyLabelerGetServices, + ), + }, ) const { data: unauthed } = await agent.api.app.bsky.labeler.getServices({ dids: [alice, bob, 'did:example:missing'], @@ -138,7 +158,12 @@ describe('labeler service views', () => { it('renders profile as labeler in non-detailed profile views', async () => { const { data: res } = await agent.api.app.bsky.actor.searchActors( { q: sc.accounts[alice].handle }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyActorSearchActors, + ), + }, ) expect(res.actors.length).toBe(1) expect(res.actors[0].associated?.labeler).toBe(true) @@ -148,7 +173,12 @@ describe('labeler service views', () => { await network.bsky.ctx.dataplane.takedownActor({ did: alice }) const res = await agent.api.app.bsky.labeler.getServices( { dids: [alice, bob] }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyLabelerGetServices, + ), + }, ) expect(res.data.views.length).toBe(1) // @ts-ignore diff --git a/packages/bsky/tests/views/likes.test.ts b/packages/bsky/tests/views/likes.test.ts index 3f32afc56e9..cb26d6efb63 100644 --- a/packages/bsky/tests/views/likes.test.ts +++ b/packages/bsky/tests/views/likes.test.ts @@ -1,6 +1,7 @@ import { AtpAgent } from '@atproto/api' import { TestNetwork, SeedClient, likesSeed } from '@atproto/dev-env' import { constantDate, forSnapshot, paginateAll, stripViewer } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('pds like views', () => { let network: TestNetwork @@ -38,7 +39,7 @@ describe('pds like views', () => { it('fetches post likes', async () => { const alicePost = await agent.api.app.bsky.feed.getLikes( { uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) }, ) expect(forSnapshot(alicePost.data)).toMatchSnapshot() @@ -50,7 +51,7 @@ describe('pds like views', () => { it('fetches reply likes', async () => { const bobReply = await agent.api.app.bsky.feed.getLikes( { uri: sc.replies[bob][0].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) }, ) expect(forSnapshot(bobReply.data)).toMatchSnapshot() @@ -68,7 +69,9 @@ describe('pds like views', () => { cursor, limit: 2, }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes), + }, ) return res.data } @@ -80,7 +83,7 @@ describe('pds like views', () => { const full = await agent.api.app.bsky.feed.getLikes( { uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) }, ) expect(full.data.likes.length).toEqual(4) @@ -90,7 +93,7 @@ describe('pds like views', () => { it('fetches post likes unauthed', async () => { const { data: authed } = await agent.api.app.bsky.feed.getLikes( { uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) }, ) const { data: unauthed } = await agent.api.app.bsky.feed.getLikes({ uri: sc.posts[alice][1].ref.uriStr, diff --git a/packages/bsky/tests/views/list-feed.test.ts b/packages/bsky/tests/views/list-feed.test.ts index 593123f6acd..d0b0302d69d 100644 --- a/packages/bsky/tests/views/list-feed.test.ts +++ b/packages/bsky/tests/views/list-feed.test.ts @@ -6,6 +6,7 @@ import { stripViewer, stripViewerFromPost, } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('list feed views', () => { let network: TestNetwork @@ -42,7 +43,12 @@ describe('list feed views', () => { it('fetches list feed', async () => { const res = await agent.api.app.bsky.feed.getListFeed( { list: listRef.uriStr }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetListFeed, + ), + }, ) expect(forSnapshot(res.data.feed)).toMatchSnapshot() @@ -61,7 +67,12 @@ describe('list feed views', () => { cursor, limit: 2, }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetListFeed, + ), + }, ) return res.data } @@ -73,7 +84,12 @@ describe('list feed views', () => { const full = await agent.api.app.bsky.feed.getListFeed( { list: listRef.uriStr }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetListFeed, + ), + }, ) expect(full.data.feed.length).toEqual(7) @@ -83,7 +99,12 @@ describe('list feed views', () => { it('fetches results unauthed', async () => { const { data: authed } = await agent.api.app.bsky.feed.getListFeed( { list: listRef.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetListFeed, + ), + }, ) const { data: unauthed } = await agent.api.app.bsky.feed.getListFeed({ list: listRef.uriStr, @@ -154,4 +175,16 @@ describe('list feed views', () => { recordUri: postRef.uriStr, }) }) + + it('does not return posts with creator blocks', async () => { + await sc.block(bob, alice) + await network.processAll() + + const res = await agent.api.app.bsky.feed.getListFeed({ + list: listRef.uriStr, + }) + + const hasBob = res.data.feed.some((item) => item.post.author.did === bob) + expect(hasBob).toBe(false) + }) }) diff --git a/packages/bsky/tests/views/lists.test.ts b/packages/bsky/tests/views/lists.test.ts index 3f786996fd1..15266746d2b 100644 --- a/packages/bsky/tests/views/lists.test.ts +++ b/packages/bsky/tests/views/lists.test.ts @@ -1,13 +1,16 @@ import { AtpAgent } from '@atproto/api' import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { forSnapshot } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('bsky actor likes feed views', () => { let network: TestNetwork let agent: AtpAgent let sc: SeedClient + let curateList: string let referenceList: string + let alice: string let eve: string let frankie: string let greta: string @@ -34,17 +37,32 @@ describe('bsky actor likes feed views', () => { email: 'greta@greta.com', password: 'hunter4real', }) - const newList = await sc.createList( + + const newRefList = await sc.createList( sc.dids.eve, 'blah starter pack list!', 'reference', ) - await sc.addToList(sc.dids.eve, sc.dids.eve, newList) - await sc.addToList(sc.dids.eve, sc.dids.bob, newList) - await sc.addToList(sc.dids.eve, sc.dids.frankie, newList) + const newCurrList = await sc.createList( + sc.dids.eve, + 'blah curate list!', + 'curate', + ) + + await sc.addToList(sc.dids.eve, sc.dids.eve, newRefList) + await sc.addToList(sc.dids.eve, sc.dids.bob, newRefList) + await sc.addToList(sc.dids.eve, sc.dids.frankie, newRefList) + + await sc.addToList(sc.dids.eve, sc.dids.eve, newCurrList) + await sc.addToList(sc.dids.eve, sc.dids.bob, newCurrList) + await sc.addToList(sc.dids.eve, sc.dids.frankie, newCurrList) + await sc.block(sc.dids.frankie, sc.dids.eve) + await network.processAll() - referenceList = newList.uriStr + curateList = newCurrList.uriStr + referenceList = newRefList.uriStr + alice = sc.dids.alice eve = sc.dids.eve frankie = sc.dids.frankie greta = sc.dids.greta @@ -60,48 +78,100 @@ describe('bsky actor likes feed views', () => { const view = await agent.api.app.bsky.graph.getLists({ actor: eve, }) - expect(view.data.lists.length).toBe(1) + expect(view.data.lists.length).toBe(2) + expect(forSnapshot(view.data.lists)).toMatchSnapshot() + }) + + it('supports using a handle as getList actor param', async () => { + const view = await agent.app.bsky.graph.getLists({ + actor: 'eve.test', + }) + expect(view.data.lists.length).toBe(2) expect(forSnapshot(view.data.lists)).toMatchSnapshot() }) it('does not include users with creator block relationship in reference lists for non-creator, in-list viewers', async () => { - const view = await agent.api.app.bsky.graph.getList( + const curView = await agent.api.app.bsky.graph.getList( { - list: referenceList, + list: curateList, + }, + { + headers: await network.serviceHeaders(frankie, ids.AppBskyGraphGetList), }, - { headers: await network.serviceHeaders(frankie) }, ) - expect(view.data.items.length).toBe(2) - expect(forSnapshot(view.data.items)).toMatchSnapshot() + expect(curView.data.items.length).toBe(2) + expect(forSnapshot(curView.data.items)).toMatchSnapshot() + + const refView = await agent.api.app.bsky.graph.getList( + { list: referenceList }, + { + headers: await network.serviceHeaders(frankie, ids.AppBskyGraphGetList), + }, + ) + expect(refView.data.items.length).toBe(2) + expect(forSnapshot(refView.data.items)).toMatchSnapshot() }) it('does not include users with creator block relationship in reference lists for non-creator, not-in-list viewers', async () => { - const view = await agent.api.app.bsky.graph.getList( + const curView = await agent.api.app.bsky.graph.getList( { - list: referenceList, + list: curateList, }, - { headers: await network.serviceHeaders(greta) }, + { headers: await network.serviceHeaders(greta, ids.AppBskyGraphGetList) }, ) - expect(view.data.items.length).toBe(2) - expect(forSnapshot(view.data.items)).toMatchSnapshot() + expect(curView.data.items.length).toBe(2) + expect(forSnapshot(curView.data.items)).toMatchSnapshot() + + const refView = await agent.api.app.bsky.graph.getList( + { list: referenceList }, + { headers: await network.serviceHeaders(greta, ids.AppBskyGraphGetList) }, + ) + expect(refView.data.items.length).toBe(2) + expect(forSnapshot(refView.data.items)).toMatchSnapshot() }) - it('does not include users with creator block relationship in reference lists for signed-out viewers', async () => { - const view = await agent.api.app.bsky.graph.getList({ + it('does not include users with creator block relationship in reference and curate lists for signed-out viewers', async () => { + const curView = await agent.api.app.bsky.graph.getList({ + list: curateList, + }) + expect(curView.data.items.length).toBe(2) + expect(forSnapshot(curView.data.items)).toMatchSnapshot() + + const refView = await agent.api.app.bsky.graph.getList({ list: referenceList, }) - expect(view.data.items.length).toBe(2) - expect(forSnapshot(view.data.items)).toMatchSnapshot() + expect(refView.data.items.length).toBe(2) + expect(forSnapshot(refView.data.items)).toMatchSnapshot() }) it('does include users with creator block relationship in reference lists for creator', async () => { + const curView = await agent.api.app.bsky.graph.getList( + { list: curateList }, + { headers: await network.serviceHeaders(eve, ids.AppBskyGraphGetList) }, + ) + expect(curView.data.items.length).toBe(3) + expect(forSnapshot(curView.data.items)).toMatchSnapshot() + + const refView = await agent.api.app.bsky.graph.getList( + { list: referenceList }, + { headers: await network.serviceHeaders(eve, ids.AppBskyGraphGetList) }, + ) + expect(refView.data.items.length).toBe(3) + expect(forSnapshot(refView.data.items)).toMatchSnapshot() + }) + + it('does return all users regardless of creator block relationship in moderation lists', async () => { + const blockList = await sc.createList(eve, 'block list', 'mod') + await sc.addToList(eve, frankie, blockList) + await sc.addToList(eve, greta, blockList) + await sc.block(frankie, greta) + await network.processAll() + const view = await agent.api.app.bsky.graph.getList( - { - list: referenceList, - }, - { headers: await network.serviceHeaders(eve) }, + { list: blockList.uriStr }, + { headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetList) }, ) - expect(view.data.items.length).toBe(3) + expect(view.data.items.length).toBe(2) expect(forSnapshot(view.data.items)).toMatchSnapshot() }) }) diff --git a/packages/bsky/tests/views/mute-lists.test.ts b/packages/bsky/tests/views/mute-lists.test.ts index ee052e49f00..c06712de4d7 100644 --- a/packages/bsky/tests/views/mute-lists.test.ts +++ b/packages/bsky/tests/views/mute-lists.test.ts @@ -1,6 +1,7 @@ import { AtpAgent, AtUri } from '@atproto/api' import { TestNetwork, SeedClient, RecordRef, basicSeed } from '@atproto/dev-env' import { forSnapshot } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('bsky views with mutes from mute lists', () => { let network: TestNetwork @@ -98,7 +99,10 @@ describe('bsky views with mutes from mute lists', () => { }, { encoding: 'application/json', - headers: await network.serviceHeaders(dan), + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphMuteActorList, + ), }, ) }) @@ -106,7 +110,12 @@ describe('bsky views with mutes from mute lists', () => { it('flags mutes in threads', async () => { const res = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(res.data.thread)).toMatchSnapshot() }) @@ -117,7 +126,12 @@ describe('bsky views with mutes from mute lists', () => { const res = await agent.api.app.bsky.feed.getAuthorFeed( { actor: alice }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) expect( res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), @@ -127,7 +141,9 @@ describe('bsky views with mutes from mute lists', () => { it('removes content from muted users on getTimeline', async () => { const res = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline), + }, ) expect( res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), @@ -141,7 +157,12 @@ describe('bsky views with mutes from mute lists', () => { await sc.addToList(alice, dan, listRef) const res = await agent.api.app.bsky.feed.getListFeed( { list: listRef.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetListFeed, + ), + }, ) expect( res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), @@ -151,7 +172,9 @@ describe('bsky views with mutes from mute lists', () => { it('returns mute status on getProfile', async () => { const res = await agent.api.app.bsky.actor.getProfile( { actor: carol }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile), + }, ) expect(res.data.viewer?.muted).toBe(true) expect(res.data.viewer?.mutedByList?.uri).toBe(listUri) @@ -160,7 +183,9 @@ describe('bsky views with mutes from mute lists', () => { it('returns mute status on getProfiles', async () => { const res = await agent.api.app.bsky.actor.getProfiles( { actors: [alice, carol] }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfiles), + }, ) expect(res.data.profiles[0].viewer?.muted).toBe(false) expect(res.data.profiles[0].viewer?.mutedByList).toBeUndefined() @@ -171,7 +196,9 @@ describe('bsky views with mutes from mute lists', () => { it('ignores self-mutes', async () => { const res = await agent.api.app.bsky.actor.getProfile( { actor: dan }, // dan subscribes to list that contains himself - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile), + }, ) expect(res.data.viewer?.muted).toBe(false) expect(res.data.viewer?.mutedByList).toBeUndefined() @@ -182,7 +209,12 @@ describe('bsky views with mutes from mute lists', () => { { limit: 100, }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect( res.data.notifications.some((notif) => @@ -200,7 +232,12 @@ describe('bsky views with mutes from mute lists', () => { { limit: 100, }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyActorGetSuggestions, + ), + }, ) for (const actor of res.data.actors) { if ([bob, carol].includes(actor.did)) { @@ -216,7 +253,7 @@ describe('bsky views with mutes from mute lists', () => { it('returns the contents of a list', async () => { const res = await agent.api.app.bsky.graph.getList( { list: listUri }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) }, ) expect(forSnapshot(res.data)).toMatchSnapshot() }) @@ -224,15 +261,15 @@ describe('bsky views with mutes from mute lists', () => { it('paginates getList', async () => { const full = await agent.api.app.bsky.graph.getList( { list: listUri }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) }, ) const first = await agent.api.app.bsky.graph.getList( { list: listUri, limit: 1 }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) }, ) const second = await agent.api.app.bsky.graph.getList( { list: listUri, cursor: first.data.cursor }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) }, ) const combined = [...first.data.items, ...second.data.items] expect(combined).toEqual(full.data.items) @@ -257,7 +294,7 @@ describe('bsky views with mutes from mute lists', () => { const res = await agent.api.app.bsky.graph.getLists( { actor: alice }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) }, ) expect(forSnapshot(res.data)).toMatchSnapshot() }) @@ -265,15 +302,15 @@ describe('bsky views with mutes from mute lists', () => { it('paginates getLists', async () => { const full = await agent.api.app.bsky.graph.getLists( { actor: alice }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) }, ) const first = await agent.api.app.bsky.graph.getLists( { actor: alice, limit: 1 }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) }, ) const second = await agent.api.app.bsky.graph.getLists( { actor: alice, cursor: first.data.cursor }, - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) }, ) const combined = [...first.data.lists, ...second.data.lists] expect(combined).toEqual(full.data.lists) @@ -286,13 +323,21 @@ describe('bsky views with mutes from mute lists', () => { }, { encoding: 'application/json', - headers: await network.serviceHeaders(dan), + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphMuteActorList, + ), }, ) const res = await agent.api.app.bsky.graph.getListMutes( {}, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphGetListMutes, + ), + }, ) expect(forSnapshot(res.data)).toMatchSnapshot() }) @@ -300,15 +345,30 @@ describe('bsky views with mutes from mute lists', () => { it('paginates getListMutes', async () => { const full = await agent.api.app.bsky.graph.getListMutes( {}, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphGetListMutes, + ), + }, ) const first = await agent.api.app.bsky.graph.getListMutes( { limit: 1 }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphGetListMutes, + ), + }, ) const second = await agent.api.app.bsky.graph.getListMutes( { cursor: first.data.cursor }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphGetListMutes, + ), + }, ) const combined = [...first.data.lists, ...second.data.lists] expect(combined).toEqual(full.data.lists) @@ -321,13 +381,21 @@ describe('bsky views with mutes from mute lists', () => { }, { encoding: 'application/json', - headers: await network.serviceHeaders(dan), + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphUnmuteActorList, + ), }, ) const res = await agent.api.app.bsky.graph.getListMutes( {}, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders( + dan, + ids.AppBskyGraphGetListMutes, + ), + }, ) expect(res.data.lists.length).toBe(1) }) @@ -353,7 +421,7 @@ describe('bsky views with mutes from mute lists', () => { const got = await agent.api.app.bsky.graph.getList( { list: listUri }, - { headers: await network.serviceHeaders(alice) }, + { headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetList) }, ) expect(got.data.list.name).toBe('updated alice mutes') expect(got.data.list.description).toBe('new descript') @@ -372,7 +440,7 @@ describe('bsky views with mutes from mute lists', () => { await network.processAll() const res = await agent.api.app.bsky.feed.getPosts( { uris: [postRef.ref.uriStr] }, - { headers: await network.serviceHeaders(alice) }, + { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetPosts) }, ) expect(res.data.posts.length).toBe(1) expect(forSnapshot(res.data.posts[0])).toMatchSnapshot() @@ -397,7 +465,9 @@ describe('bsky views with mutes from mute lists', () => { const res = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline), + }, ) expect( res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), @@ -417,7 +487,9 @@ describe('bsky views with mutes from mute lists', () => { const res = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(dan) }, + { + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline), + }, ) expect( res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), diff --git a/packages/bsky/tests/views/mutes.test.ts b/packages/bsky/tests/views/mutes.test.ts index 49219dc1a46..5536a115d68 100644 --- a/packages/bsky/tests/views/mutes.test.ts +++ b/packages/bsky/tests/views/mutes.test.ts @@ -6,6 +6,7 @@ import { usersBulkSeed, } from '@atproto/dev-env' import { forSnapshot, paginateAll } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('mute views', () => { let network: TestNetwork @@ -45,7 +46,10 @@ describe('mute views', () => { await agent.api.app.bsky.graph.muteActor( { actor: did }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphMuteActor, + ), encoding: 'application/json', }, ) @@ -59,7 +63,12 @@ describe('mute views', () => { it('flags mutes in threads', async () => { const res = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(res.data.thread)).toMatchSnapshot() }) @@ -71,7 +80,12 @@ describe('mute views', () => { const res = await agent.api.app.bsky.feed.getAuthorFeed( { actor: dan }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) expect( res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), @@ -81,7 +95,12 @@ describe('mute views', () => { it('removes content from muted users on getTimeline', async () => { const res = await agent.api.app.bsky.feed.getTimeline( { limit: 100 }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetTimeline, + ), + }, ) expect( res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), @@ -95,7 +114,12 @@ describe('mute views', () => { await sc.addToList(alice, dan, listRef) const res = await agent.api.app.bsky.feed.getListFeed( { list: listRef.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetListFeed, + ), + }, ) expect( res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), @@ -105,7 +129,12 @@ describe('mute views', () => { it('returns mute status on getProfile', async () => { const res = await agent.api.app.bsky.actor.getProfile( { actor: bob }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyActorGetProfile, + ), + }, ) expect(res.data.viewer?.muted).toBe(true) }) @@ -113,7 +142,12 @@ describe('mute views', () => { it('returns mute status on getProfiles', async () => { const res = await agent.api.app.bsky.actor.getProfiles( { actors: [bob, carol, dan] }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyActorGetProfiles, + ), + }, ) expect(res.data.profiles[0].viewer?.muted).toBe(true) expect(res.data.profiles[1].viewer?.muted).toBe(true) @@ -125,7 +159,12 @@ describe('mute views', () => { { limit: 100, }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect( res.data.notifications.some((notif) => @@ -145,7 +184,12 @@ describe('mute views', () => { { limit: 100, }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyActorGetSuggestions, + ), + }, ) for (const actor of res.data.actors) { if (mutes.includes(actor.did) || mutes.includes(actor.handle)) { @@ -159,7 +203,9 @@ describe('mute views', () => { it('fetches mutes for the logged-in user.', async () => { const { data: view } = await agent.api.app.bsky.graph.getMutes( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetMutes), + }, ) expect(forSnapshot(view.mutes)).toMatchSnapshot() }) @@ -169,7 +215,12 @@ describe('mute views', () => { const paginator = async (cursor?: string) => { const { data: view } = await agent.api.app.bsky.graph.getMutes( { cursor, limit: 2 }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphGetMutes, + ), + }, ) return view } @@ -181,7 +232,9 @@ describe('mute views', () => { const full = await agent.api.app.bsky.graph.getMutes( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetMutes), + }, ) expect(full.data.mutes.length).toEqual(8) @@ -191,7 +244,9 @@ describe('mute views', () => { it('removes mute.', async () => { const { data: initial } = await agent.api.app.bsky.graph.getMutes( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetMutes), + }, ) expect(initial.mutes.length).toEqual(8) expect(initial.mutes.map((m) => m.handle)).toContain('elta48.test') @@ -199,14 +254,19 @@ describe('mute views', () => { await agent.api.app.bsky.graph.unmuteActor( { actor: sc.dids['elta48.test'] }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders( + alice, + ids.AppBskyGraphUnmuteActor, + ), encoding: 'application/json', }, ) const { data: final } = await agent.api.app.bsky.graph.getMutes( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetMutes), + }, ) expect(final.mutes.length).toEqual(7) expect(final.mutes.map((m) => m.handle)).not.toContain('elta48.test') @@ -214,7 +274,7 @@ describe('mute views', () => { await agent.api.app.bsky.graph.muteActor( { actor: sc.dids['elta48.test'] }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders(alice, ids.AppBskyGraphMuteActor), encoding: 'application/json', }, ) @@ -224,7 +284,7 @@ describe('mute views', () => { const promise = agent.api.app.bsky.graph.muteActor( { actor: alice }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders(alice, ids.AppBskyGraphMuteActor), encoding: 'application/json', }, ) diff --git a/packages/bsky/tests/views/notifications.test.ts b/packages/bsky/tests/views/notifications.test.ts index 511941eb14a..560916fd687 100644 --- a/packages/bsky/tests/views/notifications.test.ts +++ b/packages/bsky/tests/views/notifications.test.ts @@ -2,6 +2,7 @@ import { AtpAgent } from '@atproto/api' import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { forSnapshot, paginateAll } from '../_util' import { Notification } from '../../src/lexicon/types/app/bsky/notification/listNotifications' +import { ids } from '../../src/lexicon/lexicons' describe('notification views', () => { let network: TestNetwork @@ -48,14 +49,24 @@ describe('notification views', () => { const notifCountAlice = await agent.api.app.bsky.notification.getUnreadCount( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationGetUnreadCount, + ), + }, ) expect(notifCountAlice.data.count).toBe(12) const notifCountBob = await agent.api.app.bsky.notification.getUnreadCount( {}, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyNotificationGetUnreadCount, + ), + }, ) expect(notifCountBob.data.count).toBeGreaterThanOrEqual(3) @@ -75,14 +86,24 @@ describe('notification views', () => { const notifCountAlice = await agent.api.app.bsky.notification.getUnreadCount( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationGetUnreadCount, + ), + }, ) expect(notifCountAlice.data.count).toBe(13) const notifCountBob = await agent.api.app.bsky.notification.getUnreadCount( {}, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyNotificationGetUnreadCount, + ), + }, ) expect(notifCountBob.data.count).toBeGreaterThanOrEqual(4) @@ -97,7 +118,12 @@ describe('notification views', () => { const notifsAlice = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) const hasNotif = notifsAlice.data.notifications.some( (notif) => notif.uri === second.ref.uriStr, @@ -114,7 +140,12 @@ describe('notification views', () => { // Dan was quoted by alice const notifsDan = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(sc.dids.dan) }, + { + headers: await network.serviceHeaders( + sc.dids.dan, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(forSnapshot(sort(notifsDan.data.notifications))).toMatchSnapshot() }) @@ -122,7 +153,12 @@ describe('notification views', () => { it('fetches notifications without a last-seen', async () => { const notifRes = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) const notifs = notifRes.data.notifications @@ -140,7 +176,12 @@ describe('notification views', () => { const paginator = async (cursor?: string) => { const res = await agent.api.app.bsky.notification.listNotifications( { cursor, limit: 6 }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) return res.data } @@ -152,7 +193,12 @@ describe('notification views', () => { const full = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(full.data.notifications.length).toEqual(13) @@ -162,26 +208,44 @@ describe('notification views', () => { it('fetches notification count with a last-seen', async () => { const full = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) const seenAt = full.data.notifications[3].indexedAt await agent.api.app.bsky.notification.updateSeen( { seenAt }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationUpdateSeen, + ), encoding: 'application/json', }, ) const full2 = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(full2.data.notifications.length).toBe(full.data.notifications.length) expect(full2.data.seenAt).toEqual(seenAt) const notifCount = await agent.api.app.bsky.notification.getUnreadCount( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationGetUnreadCount, + ), + }, ) expect(notifCount.data.count).toBe( @@ -193,7 +257,10 @@ describe('notification views', () => { await agent.api.app.bsky.notification.updateSeen( { seenAt: new Date(0).toISOString() }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationUpdateSeen, + ), encoding: 'application/json', }, ) @@ -202,19 +269,32 @@ describe('notification views', () => { it('fetches notifications with a last-seen', async () => { const full = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) const seenAt = full.data.notifications[3].indexedAt await agent.api.app.bsky.notification.updateSeen( { seenAt }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationUpdateSeen, + ), encoding: 'application/json', }, ) const notifRes = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) const notifs = notifRes.data.notifications @@ -226,7 +306,10 @@ describe('notification views', () => { await agent.api.app.bsky.notification.updateSeen( { seenAt: new Date(0).toISOString() }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationUpdateSeen, + ), encoding: 'application/json', }, ) @@ -245,11 +328,21 @@ describe('notification views', () => { const notifRes = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) const notifCount = await agent.api.app.bsky.notification.getUnreadCount( {}, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationGetUnreadCount, + ), + }, ) const notifs = sort(notifRes.data.notifications) @@ -270,7 +363,12 @@ describe('notification views', () => { it('fetches notifications with explicit priority', async () => { const priority = await agent.api.app.bsky.notification.listNotifications( { priority: true }, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyNotificationListNotifications, + ), + }, ) // only notifs from follow (alice) expect( @@ -281,7 +379,12 @@ describe('notification views', () => { expect(forSnapshot(priority.data)).toMatchSnapshot() const noPriority = await agent.api.app.bsky.notification.listNotifications( { priority: false }, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(forSnapshot(noPriority.data)).toMatchSnapshot() }) @@ -291,13 +394,21 @@ describe('notification views', () => { { priority: true }, { encoding: 'application/json', - headers: await network.serviceHeaders(sc.dids.carol), + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyNotificationPutPreferences, + ), }, ) await network.processAll() const notifs = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyNotificationListNotifications, + ), + }, ) // only notifs from follow (alice) expect( @@ -312,7 +423,12 @@ describe('notification views', () => { const { data: notifs } = await agent.api.app.bsky.notification.listNotifications( { cursor: '90210::bafycid' }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(notifs).toMatchObject({ notifications: [] }) }) diff --git a/packages/bsky/tests/views/posts.test.ts b/packages/bsky/tests/views/posts.test.ts index 8f0fb7c4103..8fbaab77019 100644 --- a/packages/bsky/tests/views/posts.test.ts +++ b/packages/bsky/tests/views/posts.test.ts @@ -1,6 +1,9 @@ import { AppBskyFeedPost, AtpAgent } from '@atproto/api' import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { forSnapshot, stripViewerFromPost } from '../_util' +import { RecordEmbed, VideoEmbed } from '../../src/views/types' +import { RecordWithMedia } from '../../dist/views/types' +import { ids } from '../../src/lexicon/lexicons' describe('pds posts views', () => { let network: TestNetwork @@ -34,7 +37,12 @@ describe('pds posts views', () => { ] const posts = await agent.api.app.bsky.feed.getPosts( { uris }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPosts, + ), + }, ) expect(posts.data.posts.length).toBe(uris.length) @@ -53,7 +61,12 @@ describe('pds posts views', () => { const authed = await agent.api.app.bsky.feed.getPosts( { uris }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPosts, + ), + }, ) const unauthed = await agent.api.app.bsky.feed.getPosts({ uris, @@ -103,4 +116,77 @@ describe('pds posts views', () => { // @ts-ignore we know it's a post record expect(data.posts[0].record.tags).toEqual(['javascript', 'hehe']) }) + + it('embeds video.', async () => { + const { data: video } = await pdsAgent.api.com.atproto.repo.uploadBlob( + Buffer.from('notarealvideo'), + { + headers: sc.getHeaders(sc.dids.alice), + encoding: 'image/mp4', + }, + ) + const { uri } = await pdsAgent.api.app.bsky.feed.post.create( + { repo: sc.dids.alice }, + { + text: 'video', + createdAt: new Date().toISOString(), + embed: { + $type: 'app.bsky.embed.video', + video: video.blob, + alt: 'alt text', + aspectRatio: { height: 3, width: 4 }, + } satisfies VideoEmbed, + }, + sc.getHeaders(sc.dids.alice), + ) + await network.processAll() + const { data } = await agent.app.bsky.feed.getPosts({ uris: [uri] }) + expect(data.posts.length).toBe(1) + expect(forSnapshot(data.posts[0])).toMatchSnapshot() + }) + + it('embeds video with record.', async () => { + const { data: video } = await pdsAgent.api.com.atproto.repo.uploadBlob( + Buffer.from('notarealvideo'), + { + headers: sc.getHeaders(sc.dids.alice), + encoding: 'image/mp4', + }, + ) + const embedRecord = await pdsAgent.api.app.bsky.feed.post.create( + { repo: sc.dids.alice }, + { + text: 'embedded', + createdAt: new Date().toISOString(), + }, + sc.getHeaders(sc.dids.alice), + ) + const { uri } = await pdsAgent.api.app.bsky.feed.post.create( + { repo: sc.dids.alice }, + { + text: 'video', + createdAt: new Date().toISOString(), + embed: { + $type: 'app.bsky.embed.recordWithMedia', + record: { + record: { + uri: embedRecord.uri, + cid: embedRecord.cid, + }, + } satisfies RecordEmbed, + media: { + $type: 'app.bsky.embed.video', + video: video.blob, + alt: 'alt text', + aspectRatio: { height: 3, width: 4 }, + } satisfies VideoEmbed, + } satisfies RecordWithMedia, + }, + sc.getHeaders(sc.dids.alice), + ) + await network.processAll() + const { data } = await agent.app.bsky.feed.getPosts({ uris: [uri] }) + expect(data.posts.length).toBe(1) + expect(forSnapshot(data.posts[0])).toMatchSnapshot() + }) }) diff --git a/packages/bsky/tests/views/profile.test.ts b/packages/bsky/tests/views/profile.test.ts index b65cda53d75..fb0923e9143 100644 --- a/packages/bsky/tests/views/profile.test.ts +++ b/packages/bsky/tests/views/profile.test.ts @@ -38,7 +38,12 @@ describe('pds profile views', () => { it('fetches own profile', async () => { const aliceForAlice = await agent.api.app.bsky.actor.getProfile( { actor: alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyActorGetProfile, + ), + }, ) expect(forSnapshot(aliceForAlice.data)).toMatchSnapshot() @@ -47,7 +52,9 @@ describe('pds profile views', () => { it('reflects self-labels', async () => { const aliceForBob = await agent.api.app.bsky.actor.getProfile( { actor: alice }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile), + }, ) const labels = aliceForBob.data.labels @@ -61,7 +68,9 @@ describe('pds profile views', () => { it("fetches other's profile, with a follow", async () => { const aliceForBob = await agent.api.app.bsky.actor.getProfile( { actor: alice }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile), + }, ) expect(forSnapshot(aliceForBob.data)).toMatchSnapshot() @@ -70,7 +79,9 @@ describe('pds profile views', () => { it("fetches other's profile, without a follow", async () => { const danForBob = await agent.api.app.bsky.actor.getProfile( { actor: dan }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile), + }, ) expect(forSnapshot(danForBob.data)).toMatchSnapshot() @@ -90,7 +101,9 @@ describe('pds profile views', () => { 'missing.test', ], }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfiles), + }, ) expect(profiles.map((p) => p.handle)).toEqual([ @@ -135,7 +148,12 @@ describe('pds profile views', () => { const aliceForAlice = await agent.api.app.bsky.actor.getProfile( { actor: alice }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyActorGetProfile, + ), + }, ) expect(forSnapshot(aliceForAlice.data)).toMatchSnapshot() @@ -145,13 +163,15 @@ describe('pds profile views', () => { const byDid = await agent.api.app.bsky.actor.getProfile( { actor: alice }, { - headers: await network.serviceHeaders(bob), + headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile), }, ) const byHandle = await agent.api.app.bsky.actor.getProfile( { actor: sc.accounts[alice].handle }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile), + }, ) expect(byHandle.data).toEqual(byDid.data) @@ -160,7 +180,9 @@ describe('pds profile views', () => { it('fetches profile unauthed', async () => { const { data: authed } = await agent.api.app.bsky.actor.getProfile( { actor: alice }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile), + }, ) const { data: unauthed } = await agent.api.app.bsky.actor.getProfile({ actor: alice, @@ -173,7 +195,9 @@ describe('pds profile views', () => { { actors: [alice, 'bob.test', 'missing.test'], }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfiles), + }, ) const { data: unauthed } = await agent.api.app.bsky.actor.getProfiles({ actors: [alice, 'bob.test', 'missing.test'], @@ -188,7 +212,9 @@ describe('pds profile views', () => { }) const promise = agent.api.app.bsky.actor.getProfile( { actor: alice }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile), + }, ) await expect(promise).rejects.toThrow('Account has been suspended') diff --git a/packages/bsky/tests/views/quotes.test.ts b/packages/bsky/tests/views/quotes.test.ts new file mode 100644 index 00000000000..7efe4ad242a --- /dev/null +++ b/packages/bsky/tests/views/quotes.test.ts @@ -0,0 +1,134 @@ +import { quotesSeed, SeedClient, TestNetwork } from '@atproto/dev-env' +import AtpAgent from '@atproto/api' +import { forSnapshot } from '../_util' +import { ids } from '../../src/lexicon/lexicons' + +describe('pds quote views', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + + // account dids, for convenience + let alice: string + let bob: string + let carol: string + let eve: string + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_views_quotes', + }) + agent = network.bsky.getClient() + sc = network.getSeedClient() + await quotesSeed(sc) + await network.processAll() + alice = sc.dids.alice + bob = sc.dids.bob + carol = sc.dids.carol + eve = sc.dids.eve + }) + + afterAll(async () => { + await network.close() + }) + + it('fetches post quotes', async () => { + const alicePostQuotes = await agent.api.app.bsky.feed.getQuotes( + { uri: sc.posts[alice][0].ref.uriStr, limit: 30 }, + { headers: await network.serviceHeaders(eve, ids.AppBskyFeedGetQuotes) }, + ) + + expect(alicePostQuotes.data.posts.length).toBe(2) + expect(forSnapshot(alicePostQuotes.data)).toMatchSnapshot() + }) + + it('does not return post in list when the quote author has a block', async () => { + await sc.block(eve, carol) + await network.processAll() + + const quotes = await agent.api.app.bsky.feed.getQuotes( + { uri: sc.posts[alice][0].ref.uriStr, limit: 30 }, + { + headers: await network.serviceHeaders(carol, ids.AppBskyFeedGetQuotes), + }, + ) + + expect(quotes.data.posts.length).toBe(0) + await sc.unblock(eve, carol) + }) + + it('utilizes limit parameter and cursor', async () => { + const alicePostQuotes1 = await agent.api.app.bsky.feed.getQuotes( + { uri: sc.posts[alice][1].ref.uriStr, limit: 3 }, + { headers: await network.serviceHeaders(eve, ids.AppBskyFeedGetQuotes) }, + ) + + expect(alicePostQuotes1.data.posts.length).toBe(3) + expect(alicePostQuotes1.data.cursor).toBeDefined() + + const alicePostQuotes2 = await agent.api.app.bsky.feed.getQuotes( + { + uri: sc.posts[alice][1].ref.uriStr, + limit: 3, + cursor: alicePostQuotes1.data.cursor, + }, + { headers: await network.serviceHeaders(eve, ids.AppBskyFeedGetQuotes) }, + ) + + expect(alicePostQuotes2.data.posts.length).toBe(2) + }) + + it('does not return post when quote is deleted', async () => { + await sc.deletePost(eve, sc.posts[eve][0].ref.uri) + await network.processAll() + + const alicePostQuotes = await agent.api.app.bsky.feed.getQuotes( + { uri: sc.posts[alice][0].ref.uriStr, limit: 30 }, + { + headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetQuotes), + }, + ) + + expect(alicePostQuotes.data.posts.length).toBe(1) + expect(forSnapshot(alicePostQuotes.data)).toMatchSnapshot() + }) + + it('does not return any quotes when the quoted post is deleted', async () => { + await sc.deletePost(alice, sc.posts[alice][0].ref.uri) + await network.processAll() + + const alicePostQuotesAfter = await agent.api.app.bsky.feed.getQuotes( + { uri: sc.posts[alice][0].ref.uriStr, limit: 30 }, + { + headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetQuotes), + }, + ) + + expect(alicePostQuotesAfter.data.posts.length).toBe(0) + }) + + it('decrements quote count when a quote is deleted', async () => { + await sc.deletePost(eve, sc.posts[eve][2].ref.uri) + await network.processAll() + + const bobPost = await agent.api.app.bsky.feed.getPosts( + { uris: [sc.replies[bob][0].ref.uriStr] }, + { headers: await network.serviceHeaders(bob, ids.AppBskyFeedGetPosts) }, + ) + + expect(bobPost.data.posts[0].quoteCount).toEqual(0) + expect(forSnapshot(bobPost.data)).toMatchSnapshot() + }) + + it('does not return post in list when the embed is blocked', async () => { + await sc.block(carol, eve) + await network.processAll() + + const quotes = await agent.api.app.bsky.feed.getQuotes( + { uri: sc.posts[carol][1].ref.uriStr }, + { headers: await network.serviceHeaders(bob, ids.AppBskyFeedGetQuotes) }, + ) + + expect(quotes.data.posts.length).toBe(0) + }) +}) diff --git a/packages/bsky/tests/views/reposts.test.ts b/packages/bsky/tests/views/reposts.test.ts index 0ae2eea8773..ad03455e78c 100644 --- a/packages/bsky/tests/views/reposts.test.ts +++ b/packages/bsky/tests/views/reposts.test.ts @@ -1,6 +1,7 @@ import { AtpAgent } from '@atproto/api' import { TestNetwork, SeedClient, repostsSeed } from '@atproto/dev-env' import { forSnapshot, paginateAll, stripViewer } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('pds repost views', () => { let network: TestNetwork @@ -30,7 +31,12 @@ describe('pds repost views', () => { it('fetches reposted-by for a post', async () => { const view = await agent.api.app.bsky.feed.getRepostedBy( { uri: sc.posts[alice][2].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetRepostedBy, + ), + }, ) expect(view.data.uri).toEqual(sc.posts[sc.dids.alice][2].ref.uriStr) expect(forSnapshot(view.data.repostedBy)).toMatchSnapshot() @@ -39,7 +45,12 @@ describe('pds repost views', () => { it('fetches reposted-by for a reply', async () => { const view = await agent.api.app.bsky.feed.getRepostedBy( { uri: sc.replies[bob][0].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetRepostedBy, + ), + }, ) expect(view.data.uri).toEqual(sc.replies[sc.dids.bob][0].ref.uriStr) expect(forSnapshot(view.data.repostedBy)).toMatchSnapshot() @@ -54,7 +65,12 @@ describe('pds repost views', () => { cursor, limit: 2, }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetRepostedBy, + ), + }, ) return res.data } @@ -66,7 +82,12 @@ describe('pds repost views', () => { const full = await agent.api.app.bsky.feed.getRepostedBy( { uri: sc.posts[alice][2].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetRepostedBy, + ), + }, ) expect(full.data.repostedBy.length).toEqual(4) @@ -76,7 +97,12 @@ describe('pds repost views', () => { it('fetches reposted-by unauthed', async () => { const { data: authed } = await agent.api.app.bsky.feed.getRepostedBy( { uri: sc.posts[alice][2].ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetRepostedBy, + ), + }, ) const { data: unauthed } = await agent.api.app.bsky.feed.getRepostedBy({ uri: sc.posts[alice][2].ref.uriStr, diff --git a/packages/bsky/tests/views/starter-packs.test.ts b/packages/bsky/tests/views/starter-packs.test.ts index 7a78a2d6c55..9e844b7d155 100644 --- a/packages/bsky/tests/views/starter-packs.test.ts +++ b/packages/bsky/tests/views/starter-packs.test.ts @@ -3,6 +3,7 @@ import { TestNetwork, SeedClient, RecordRef, basicSeed } from '@atproto/dev-env' import { isRecord as isProfile } from '../../src/lexicon/types/app/bsky/actor/profile' import { forSnapshot } from '../_util' import assert from 'assert' +import { ids } from '../../src/lexicon/lexicons' describe('starter packs', () => { let network: TestNetwork @@ -126,7 +127,12 @@ describe('starter packs', () => { data: { notifications }, } = await agent.api.app.bsky.notification.listNotifications( { limit: 3 }, // three most recent - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyNotificationListNotifications, + ), + }, ) expect(notifications).toHaveLength(3) notifications.forEach((notif) => { @@ -144,7 +150,12 @@ describe('starter packs', () => { { starterPack: sp3.uriStr, }, - { headers: await network.serviceHeaders(sc.dids.frankie) }, + { + headers: await network.serviceHeaders( + sc.dids.frankie, + ids.AppBskyGraphGetStarterPack, + ), + }, ) expect(view.data.starterPack.listItemsSample?.length).toBe(2) expect(forSnapshot(view.data.starterPack.listItemsSample)).toMatchSnapshot() @@ -155,7 +166,12 @@ describe('starter packs', () => { { starterPack: sp3.uriStr, }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyGraphGetStarterPack, + ), + }, ) expect(view.data.starterPack.listItemsSample?.length).toBe(2) expect(forSnapshot(view.data.starterPack.listItemsSample)).toMatchSnapshot() @@ -171,10 +187,13 @@ describe('starter packs', () => { it('does include users with creator block relationship in list sample for creator', async () => { const view = await agent.api.app.bsky.graph.getStarterPack( + { starterPack: sp3.uriStr }, { - starterPack: sp3.uriStr, + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyGraphGetStarterPack, + ), }, - { headers: await network.serviceHeaders(sc.dids.alice) }, ) expect(view.data.starterPack.listItemsSample?.length).toBe(3) expect(forSnapshot(view.data.starterPack.listItemsSample)).toMatchSnapshot() diff --git a/packages/bsky/tests/views/suggested-follows.test.ts b/packages/bsky/tests/views/suggested-follows.test.ts index 27e6c09864e..eb647bd2343 100644 --- a/packages/bsky/tests/views/suggested-follows.test.ts +++ b/packages/bsky/tests/views/suggested-follows.test.ts @@ -1,5 +1,6 @@ import { AtpAgent, AtUri } from '@atproto/api' import { TestNetwork, SeedClient, likesSeed } from '@atproto/dev-env' +import { ids } from '../../src/lexicon/lexicons' describe('suggested follows', () => { let network: TestNetwork @@ -40,7 +41,12 @@ describe('suggested follows', () => { { actor: sc.dids.alice, }, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyGraphGetSuggestedFollowsByActor, + ), + }, ) expect(result.data.suggestions.length).toBe(4) // backfilled with 2 NPCs @@ -56,7 +62,12 @@ describe('suggested follows', () => { { actor: sc.dids.alice, }, - { headers: await network.serviceHeaders(sc.dids.fred) }, + { + headers: await network.serviceHeaders( + sc.dids.fred, + ids.AppBskyGraphGetSuggestedFollowsByActor, + ), + }, ) expect(result.data.suggestions.length).toBe(4) // backfilled with 2 NPCs @@ -76,7 +87,12 @@ describe('suggested follows', () => { { actor: sc.dids.alice, }, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyGraphGetSuggestedFollowsByActor, + ), + }, ) expect( @@ -101,7 +117,12 @@ describe('suggested follows', () => { { actor: sc.dids.alice, }, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyGraphGetSuggestedFollowsByActor, + ), + }, ) expect( @@ -126,7 +147,12 @@ describe('suggested follows', () => { { actor: sc.dids.alice, }, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyGraphGetSuggestedFollowsByActor, + ), + }, ) expect( diff --git a/packages/bsky/tests/views/suggestions.test.ts b/packages/bsky/tests/views/suggestions.test.ts index 050c2e01f67..c7628fda04d 100644 --- a/packages/bsky/tests/views/suggestions.test.ts +++ b/packages/bsky/tests/views/suggestions.test.ts @@ -1,6 +1,7 @@ import { AtpAgent } from '@atproto/api' import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { stripViewer } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('pds user search views', () => { let network: TestNetwork @@ -36,7 +37,12 @@ describe('pds user search views', () => { it('actor suggestion gives users', async () => { const result = await agent.api.app.bsky.actor.getSuggestions( {}, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyActorGetSuggestions, + ), + }, ) // does not include carol, because she is requesting @@ -50,7 +56,12 @@ describe('pds user search views', () => { it('does not suggest followed users', async () => { const result = await agent.api.app.bsky.actor.getSuggestions( {}, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyActorGetSuggestions, + ), + }, ) // alice follows everyone @@ -60,21 +71,36 @@ describe('pds user search views', () => { it('paginates', async () => { const result1 = await agent.api.app.bsky.actor.getSuggestions( { limit: 2 }, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyActorGetSuggestions, + ), + }, ) 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: 2, cursor: result1.data.cursor }, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyActorGetSuggestions, + ), + }, ) 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: 2, cursor: result2.data.cursor }, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyActorGetSuggestions, + ), + }, ) expect(result3.data.actors.length).toBe(0) expect(result3.data.cursor).toBeUndefined() @@ -83,7 +109,12 @@ describe('pds user search views', () => { it('fetches suggestions unauthed', async () => { const { data: authed } = await agent.api.app.bsky.actor.getSuggestions( {}, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyActorGetSuggestions, + ), + }, ) const { data: unauthed } = await agent.api.app.bsky.actor.getSuggestions({}) const omitViewerFollows = ({ did }) => { diff --git a/packages/bsky/tests/views/thread.test.ts b/packages/bsky/tests/views/thread.test.ts index f68898ea5bc..6cf095d795d 100644 --- a/packages/bsky/tests/views/thread.test.ts +++ b/packages/bsky/tests/views/thread.test.ts @@ -5,6 +5,7 @@ import { forSnapshot, stripViewerFromThread, } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('pds thread views', () => { let network: TestNetwork @@ -41,7 +42,12 @@ describe('pds thread views', () => { it('fetches deep post thread', async () => { const thread = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread.data.thread)).toMatchSnapshot() @@ -50,7 +56,12 @@ describe('pds thread views', () => { it('fetches shallow post thread', async () => { const thread = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread.data.thread)).toMatchSnapshot() @@ -65,7 +76,12 @@ describe('pds thread views', () => { `at://${sc.accounts[alice].handle}`, ), }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread.data.thread)).toMatchSnapshot() @@ -74,7 +90,12 @@ describe('pds thread views', () => { it('fetches ancestors', async () => { const thread = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.replies[alice][0].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread.data.thread)).toMatchSnapshot() @@ -83,7 +104,12 @@ describe('pds thread views', () => { it('fails for an unknown post', async () => { const promise = agent.api.app.bsky.feed.getPostThread( { uri: 'at://did:example:fake/does.not.exist/self' }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) await expect(promise).rejects.toThrow( @@ -94,7 +120,12 @@ describe('pds thread views', () => { it('fetches post thread unauthed', async () => { const { data: authed } = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) const { data: unauthed } = await agent.api.app.bsky.feed.getPostThread({ uri: sc.posts[alice][1].ref.uriStr, @@ -133,7 +164,12 @@ describe('pds thread views', () => { const thread1 = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread1.data.thread)).toMatchSnapshot() @@ -142,13 +178,23 @@ describe('pds thread views', () => { const thread2 = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread2.data.thread)).toMatchSnapshot() const thread3 = await agent.api.app.bsky.feed.getPostThread( { uri: sc.replies[alice][indexes.aliceReplyReply].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread3.data.thread)).toMatchSnapshot() }) @@ -179,7 +225,12 @@ describe('pds thread views', () => { const { data: goodReply1Thread } = await agent.api.app.bsky.feed.getPostThread( { uri: goodReply1.ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assertIsThreadViewPost(goodReply1Thread.thread) assertIsThreadViewPost(goodReply1Thread.thread.parent) @@ -197,7 +248,12 @@ describe('pds thread views', () => { const { data: badReplyThread } = await agent.api.app.bsky.feed.getPostThread( { uri: badReply.ref.uriStr }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assertIsThreadViewPost(badReplyThread.thread) expect(badReplyThread.thread.parent).toBeUndefined() // is not goodReply1 @@ -206,7 +262,12 @@ describe('pds thread views', () => { it('reflects self-labels', async () => { const { data: thread } = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][0].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) assertIsThreadViewPost(thread.thread) @@ -236,7 +297,12 @@ describe('pds thread views', () => { // Same as shallow post thread test, minus alice const promise = agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) await expect(promise).rejects.toThrow( @@ -257,7 +323,12 @@ describe('pds thread views', () => { // Same as deep post thread test, minus carol const thread = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread.data.thread)).toMatchSnapshot() @@ -276,7 +347,12 @@ describe('pds thread views', () => { // Same as ancestor post thread test, minus bob const thread = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.replies[alice][0].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread.data.thread)).toMatchSnapshot() @@ -295,7 +371,12 @@ describe('pds thread views', () => { const promise = agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: postRef.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) await expect(promise).rejects.toThrow( @@ -311,7 +392,12 @@ describe('pds thread views', () => { it('blocks ancestors by record', async () => { const threadPreTakedown = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.replies[alice][0].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) const parent = threadPreTakedown.data.thread.parent?.['post'] @@ -323,7 +409,12 @@ describe('pds thread views', () => { // Same as ancestor post thread test, minus parent post const thread = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.replies[alice][0].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread.data.thread)).toMatchSnapshot() @@ -337,7 +428,12 @@ describe('pds thread views', () => { it('blocks replies by record', async () => { const threadPreTakedown = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) const post1 = threadPreTakedown.data.thread.replies?.[0].post const post2 = threadPreTakedown.data.thread.replies?.[1].replies[0].post @@ -353,7 +449,12 @@ describe('pds thread views', () => { // Same as deep post thread test, minus some replies const thread = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][1].ref.uriStr }, - { headers: await network.serviceHeaders(bob) }, + { + headers: await network.serviceHeaders( + bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) expect(forSnapshot(thread.data.thread)).toMatchSnapshot() diff --git a/packages/bsky/tests/views/threadgating.test.ts b/packages/bsky/tests/views/threadgating.test.ts index 4d0027c5e35..2a459e24f5c 100644 --- a/packages/bsky/tests/views/threadgating.test.ts +++ b/packages/bsky/tests/views/threadgating.test.ts @@ -6,6 +6,7 @@ import { isThreadViewPost, } from '../../src/lexicon/types/app/bsky/feed/defs' import { forSnapshot } from '../_util' +import { ids } from '../../src/lexicon/lexicons' describe('views with thread gating', () => { let network: TestNetwork @@ -36,31 +37,38 @@ describe('views with thread gating', () => { ) => { const res = await agent.api.app.bsky.feed.getPosts( { uris: [uri] }, - { headers: await network.serviceHeaders(user) }, + { headers: await network.serviceHeaders(user, ids.AppBskyFeedGetPosts) }, ) expect(res.data.posts[0].viewer?.replyDisabled).toBe(blocked) } it('applies gate for empty rules.', async () => { const post = await sc.post(sc.dids.carol, 'empty rules') - await pdsAgent.api.app.bsky.feed.threadgate.create( - { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, - { post: post.ref.uriStr, createdAt: iso(), allow: [] }, - sc.getHeaders(sc.dids.carol), - ) + const { uri: threadgateUri } = + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + { post: post.ref.uriStr, createdAt: iso(), allow: [] }, + sc.getHeaders(sc.dids.carol), + ) await network.processAll() await sc.reply(sc.dids.alice, post.ref, post.ref, 'empty rules reply') await network.processAll() const { - data: { thread }, + data: { thread, threadgate }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(thread)) expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot() expect(thread.post.viewer?.replyDisabled).toBe(true) expect(thread.replies?.length).toEqual(0) + expect(threadgate?.uri).toEqual(threadgateUri) await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, true) }) @@ -83,7 +91,12 @@ describe('views with thread gating', () => { data: { notifications }, } = await agent.api.app.bsky.notification.listNotifications( {}, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyNotificationListNotifications, + ), + }, ) const notificationFromReply = notifications.find( (notif) => notif.uri === reply.ref.uriStr, @@ -137,7 +150,12 @@ describe('views with thread gating', () => { data: { thread: aliceThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(aliceThread)) expect(aliceThread.post.viewer?.replyDisabled).toBe(true) @@ -146,7 +164,12 @@ describe('views with thread gating', () => { data: { thread: danThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.dan) }, + { + headers: await network.serviceHeaders( + sc.dids.dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(danThread)) expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot() @@ -188,7 +211,12 @@ describe('views with thread gating', () => { data: { thread: danThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.dan) }, + { + headers: await network.serviceHeaders( + sc.dids.dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(danThread)) expect(danThread.post.viewer?.replyDisabled).toBe(true) @@ -197,7 +225,12 @@ describe('views with thread gating', () => { data: { thread: aliceThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(aliceThread)) expect(forSnapshot(aliceThread.post.threadgate)).toMatchSnapshot() @@ -280,7 +313,12 @@ describe('views with thread gating', () => { data: { thread: bobThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(bobThread)) expect(bobThread.post.viewer?.replyDisabled).toBe(true) @@ -289,7 +327,12 @@ describe('views with thread gating', () => { data: { thread: aliceThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(aliceThread)) expect(aliceThread.post.viewer?.replyDisabled).toBe(false) @@ -298,7 +341,12 @@ describe('views with thread gating', () => { data: { thread: danThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.dan) }, + { + headers: await network.serviceHeaders( + sc.dids.dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(danThread)) expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot() @@ -341,7 +389,12 @@ describe('views with thread gating', () => { data: { thread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(thread)) expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot() @@ -391,7 +444,12 @@ describe('views with thread gating', () => { data: { thread: bobThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.bob) }, + { + headers: await network.serviceHeaders( + sc.dids.bob, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(bobThread)) expect(bobThread.post.viewer?.replyDisabled).toBe(true) @@ -400,7 +458,12 @@ describe('views with thread gating', () => { data: { thread: aliceThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(aliceThread)) expect(aliceThread.post.viewer?.replyDisabled).toBe(false) @@ -409,7 +472,12 @@ describe('views with thread gating', () => { data: { thread: danThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.dan) }, + { + headers: await network.serviceHeaders( + sc.dids.dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(danThread)) expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot() @@ -443,7 +511,12 @@ describe('views with thread gating', () => { data: { thread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(thread)) expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot() @@ -497,7 +570,12 @@ describe('views with thread gating', () => { data: { thread: danThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: orphanedReply.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.dan) }, + { + headers: await network.serviceHeaders( + sc.dids.dan, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(danThread)) expect(danThread.post.viewer?.replyDisabled).toBe(true) @@ -506,7 +584,12 @@ describe('views with thread gating', () => { data: { thread: aliceThread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: orphanedReply.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(aliceThread)) assert( @@ -541,7 +624,12 @@ describe('views with thread gating', () => { data: { thread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.carol) }, + { + headers: await network.serviceHeaders( + sc.dids.carol, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(thread)) expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot() @@ -580,7 +668,12 @@ describe('views with thread gating', () => { data: { thread }, } = await agent.api.app.bsky.feed.getPostThread( { uri: badReply.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(thread)) expect(thread.post.viewer?.replyDisabled).toBe(true) // nobody can reply to this, not even alice. @@ -593,7 +686,12 @@ describe('views with thread gating', () => { data: { feed }, } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: sc.dids.dan }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetAuthorFeed, + ), + }, ) const [feedItem] = feed expect(feedItem.post.uri).toEqual(badReply.ref.uriStr) @@ -617,7 +715,12 @@ describe('views with thread gating', () => { data: { thread: threadA }, } = await agent.api.app.bsky.feed.getPostThread( { uri: postA.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(threadA)) expect(threadA.post.threadgate).toBeUndefined() @@ -628,7 +731,12 @@ describe('views with thread gating', () => { data: { thread: threadB }, } = await agent.api.app.bsky.feed.getPostThread( { uri: postB.ref.uriStr }, - { headers: await network.serviceHeaders(sc.dids.alice) }, + { + headers: await network.serviceHeaders( + sc.dids.alice, + ids.AppBskyFeedGetPostThread, + ), + }, ) assert(isThreadViewPost(threadB)) expect(threadB.post.threadgate).toBeUndefined() diff --git a/packages/bsky/tests/views/timeline.test.ts b/packages/bsky/tests/views/timeline.test.ts index 0be6162f029..53e6d2d2250 100644 --- a/packages/bsky/tests/views/timeline.test.ts +++ b/packages/bsky/tests/views/timeline.test.ts @@ -9,6 +9,7 @@ import { import { forSnapshot, getOriginator, paginateAll } from '../_util' import { FeedViewPost } from '../../src/lexicon/types/app/bsky/feed/defs' import { Database } from '../../src' +import { ids } from '../../src/lexicon/lexicons' const REVERSE_CHRON = 'reverse-chronological' @@ -68,7 +69,10 @@ describe('timeline views', () => { const aliceTL = await agent.api.app.bsky.feed.getTimeline( { algorithm: REVERSE_CHRON }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetTimeline, + ), }, ) @@ -78,7 +82,7 @@ describe('timeline views', () => { const bobTL = await agent.api.app.bsky.feed.getTimeline( { algorithm: REVERSE_CHRON }, { - headers: await network.serviceHeaders(bob), + headers: await network.serviceHeaders(bob, ids.AppBskyFeedGetTimeline), }, ) @@ -88,7 +92,10 @@ describe('timeline views', () => { const carolTL = await agent.api.app.bsky.feed.getTimeline( { algorithm: REVERSE_CHRON }, { - headers: await network.serviceHeaders(carol), + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), }, ) @@ -98,7 +105,7 @@ describe('timeline views', () => { const danTL = await agent.api.app.bsky.feed.getTimeline( { algorithm: REVERSE_CHRON }, { - headers: await network.serviceHeaders(dan), + headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline), }, ) @@ -110,13 +117,19 @@ describe('timeline views', () => { const defaultTL = await agent.api.app.bsky.feed.getTimeline( {}, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetTimeline, + ), }, ) const reverseChronologicalTL = await agent.api.app.bsky.feed.getTimeline( { algorithm: REVERSE_CHRON }, { - headers: await network.serviceHeaders(alice), + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetTimeline, + ), }, ) expect(defaultTL.data.feed).toEqual(reverseChronologicalTL.data.feed) @@ -131,7 +144,12 @@ describe('timeline views', () => { cursor, limit: 4, }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, ) return res.data } @@ -145,7 +163,12 @@ describe('timeline views', () => { { algorithm: REVERSE_CHRON, }, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, ) expect(full.data.feed.length).toEqual(7) @@ -155,11 +178,21 @@ describe('timeline views', () => { it('agrees what the first item is for limit=1 and other limits', async () => { const { data: timeline } = await agent.api.app.bsky.feed.getTimeline( { limit: 10 }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetTimeline, + ), + }, ) const { data: timelineLimit1 } = await agent.api.app.bsky.feed.getTimeline( { limit: 1 }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetTimeline, + ), + }, ) expect(timeline.feed.length).toBeGreaterThan(1) expect(timelineLimit1.feed.length).toEqual(1) @@ -169,7 +202,12 @@ describe('timeline views', () => { it('reflects self-labels', async () => { const carolTL = await agent.api.app.bsky.feed.getTimeline( {}, - { headers: await network.serviceHeaders(carol) }, + { + headers: await network.serviceHeaders( + carol, + ids.AppBskyFeedGetTimeline, + ), + }, ) const alicePost = carolTL.data.feed.find( @@ -201,7 +239,12 @@ describe('timeline views', () => { const aliceTL = await agent.api.app.bsky.feed.getTimeline( { algorithm: REVERSE_CHRON }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetTimeline, + ), + }, ) expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot() @@ -227,7 +270,12 @@ describe('timeline views', () => { const aliceTL = await agent.api.app.bsky.feed.getTimeline( { algorithm: REVERSE_CHRON }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetTimeline, + ), + }, ) expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot() @@ -245,7 +293,12 @@ describe('timeline views', () => { it('fails open on clearly bad cursor.', async () => { const { data: timeline } = await agent.api.app.bsky.feed.getTimeline( { cursor: '90210::bafycid' }, - { headers: await network.serviceHeaders(alice) }, + { + headers: await network.serviceHeaders( + alice, + ids.AppBskyFeedGetTimeline, + ), + }, ) expect(timeline).toEqual({ feed: [] }) }) diff --git a/packages/bsync/CHANGELOG.md b/packages/bsync/CHANGELOG.md index b1e02a15bde..ebb380b0642 100644 --- a/packages/bsync/CHANGELOG.md +++ b/packages/bsync/CHANGELOG.md @@ -1,5 +1,26 @@ # @atproto/bsync +## 0.0.8 + +### Patch Changes + +- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13)]: + - @atproto/common@0.4.4 + +## 0.0.7 + +### Patch Changes + +- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]: + - @atproto/common@0.4.3 + +## 0.0.6 + +### Patch Changes + +- Updated dependencies [[`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]: + - @atproto/common@0.4.2 + ## 0.0.5 ### Patch Changes diff --git a/packages/bsync/package.json b/packages/bsync/package.json index 7e40fe5d88c..d609243cf1d 100644 --- a/packages/bsync/package.json +++ b/packages/bsync/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsync", - "version": "0.0.5", + "version": "0.0.8", "license": "MIT", "description": "Sychronizing service for app.bsky App View (Bluesky API)", "keywords": [ diff --git a/packages/common-web/CHANGELOG.md b/packages/common-web/CHANGELOG.md index 4e96c06ed7c..6fd46db4957 100644 --- a/packages/common-web/CHANGELOG.md +++ b/packages/common-web/CHANGELOG.md @@ -1,5 +1,15 @@ # @atproto/common-web +## 0.3.1 + +### Patch Changes + +- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add omit() utility + +- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - DID document parsing optimization + +- [#2835](https://github.com/bluesky-social/atproto/pull/2835) [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - ponyfill URL.canParse + ## 0.3.0 ### Minor Changes diff --git a/packages/common-web/package.json b/packages/common-web/package.json index fa60f5c2b86..4f766cd1587 100644 --- a/packages/common-web/package.json +++ b/packages/common-web/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/common-web", - "version": "0.3.0", + "version": "0.3.1", "license": "MIT", "description": "Shared web-platform-friendly code for atproto libraries", "keywords": [ diff --git a/packages/common-web/src/check.ts b/packages/common-web/src/check.ts index f3ae32d5ea3..64451be4b2f 100644 --- a/packages/common-web/src/check.ts +++ b/packages/common-web/src/check.ts @@ -17,6 +17,11 @@ export const is = (obj: unknown, def: Checkable): obj is T => { return def.safeParse(obj).success } +export const create = + (def: Checkable) => + (v: unknown): v is T => + def.safeParse(v).success + export const assure = (def: Checkable, obj: unknown): T => { return def.parse(obj) } diff --git a/packages/common-web/src/did-doc.ts b/packages/common-web/src/did-doc.ts index b6821e74f5c..d96eb8acf52 100644 --- a/packages/common-web/src/did-doc.ts +++ b/packages/common-web/src/did-doc.ts @@ -17,11 +17,16 @@ export const getDid = (doc: DidDocument): string => { export const getHandle = (doc: DidDocument): string | undefined => { const aka = doc.alsoKnownAs - if (!aka) return undefined - const found = aka.find((name) => name.startsWith('at://')) - if (!found) return undefined - // strip off at:// prefix - return found.slice(5) + if (aka) { + for (let i = 0; i < aka.length; i++) { + const alias = aka[i] + if (alias.startsWith('at://')) { + // strip off "at://" prefix + return alias.slice(5) + } + } + } + return undefined } // @NOTE we parse to type/publicKeyMultibase to avoid the dependency on @atproto/crypto @@ -35,20 +40,20 @@ export const getVerificationMaterial = ( doc: DidDocument, keyId: string, ): { type: string; publicKeyMultibase: string } | undefined => { - const did = getDid(doc) - let keys = doc.verificationMethod - if (!keys) return undefined - if (typeof keys !== 'object') return undefined - if (!Array.isArray(keys)) { - keys = [keys] + // /!\ Hot path + + const key = findItemById(doc, 'verificationMethod', `#${keyId}`) + if (!key) { + return undefined + } + + if (!key.publicKeyMultibase) { + return undefined } - const found = keys.find( - (key) => key.id === `#${keyId}` || key.id === `${did}#${keyId}`, - ) - if (!found?.publicKeyMultibase) return undefined + return { - type: found.type, - publicKeyMultibase: found.publicKeyMultibase, + type: key.type, + publicKeyMultibase: key.publicKeyMultibase, } } @@ -83,43 +88,82 @@ export const getServiceEndpoint = ( doc: DidDocument, opts: { id: string; type?: string }, ) => { - const did = getDid(doc) - let services = doc.service - if (!services) return undefined - if (typeof services !== 'object') return undefined - if (!Array.isArray(services)) { - services = [services] + // /!\ Hot path + + const service = findItemById(doc, 'service', opts.id) + if (!service) { + return undefined } - const found = services.find( - (service) => service.id === opts.id || service.id === `${did}${opts.id}`, - ) - if (!found) return undefined - if (opts.type && found.type !== opts.type) { + + if (opts.type && service.type !== opts.type) { return undefined } - if (typeof found.serviceEndpoint !== 'string') { + + if (typeof service.serviceEndpoint !== 'string') { return undefined } - return validateUrl(found.serviceEndpoint) + + return validateUrl(service.serviceEndpoint) +} + +function findItemById< + D extends DidDocument, + T extends 'verificationMethod' | 'service', +>(doc: D, type: T, id: string): NonNullable[number] | undefined +function findItemById( + doc: DidDocument, + type: 'verificationMethod' | 'service', + id: string, +) { + // /!\ Hot path + + const items = doc[type] + if (items) { + for (let i = 0; i < items.length; i++) { + const item = items[i] + const itemId = item.id + + if ( + itemId[0] === '#' + ? itemId === id + : // Optimized version of: itemId === `${doc.id}${id}` + itemId.length === doc.id.length + id.length && + itemId[doc.id.length] === '#' && + itemId.endsWith(id) && + itemId.startsWith(doc.id) // <== We could probably skip this check + ) { + return item + } + } + } + return undefined } // Check protocol and hostname to prevent potential SSRF const validateUrl = (urlStr: string): string | undefined => { - let url - try { - url = new URL(urlStr) - } catch { + if (!urlStr.startsWith('http://') && !urlStr.startsWith('https://')) { return undefined } - if (!['http:', 'https:'].includes(url.protocol)) { - return undefined - } else if (!url.hostname) { + + if (!canParseUrl(urlStr)) { return undefined - } else { - return urlStr } + + return urlStr } +const canParseUrl = + URL.canParse ?? + // URL.canParse is not available in Node.js < 18.17.0 + ((urlStr: string): boolean => { + try { + new URL(urlStr) + return true + } catch { + return false + } + }) + // Types // -------- diff --git a/packages/common-web/src/util.ts b/packages/common-web/src/util.ts index 928be1c5169..3082b045b78 100644 --- a/packages/common-web/src/util.ts +++ b/packages/common-web/src/util.ts @@ -9,6 +9,36 @@ export const noUndefinedVals = ( return obj as Record } +/** + * Returns a shallow copy of the object without the specified keys. If the input + * is nullish, it returns the input. + */ +export function omit< + T extends undefined | null | Record, + K extends keyof NonNullable, +>( + object: T, + rejectedKeys: readonly K[], +): T extends undefined ? undefined : T extends null ? null : Omit +export function omit( + src: undefined | null | Record, + rejectedKeys: readonly string[], +): undefined | null | Record { + // Hot path + + if (!src) return src + + const dst = {} + const srcKeys = Object.keys(src) + for (let i = 0; i < srcKeys.length; i++) { + const key = srcKeys[i] + if (!rejectedKeys.includes(key)) { + dst[key] = src[key] + } + } + return dst +} + export const jitter = (maxMs: number) => { return Math.round((Math.random() - 0.5) * maxMs * 2) } diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index ee14a03aa32..7a145a91639 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -1,5 +1,26 @@ # @atproto/common +## 0.4.4 + +### Patch Changes + +- [#2834](https://github.com/bluesky-social/atproto/pull/2834) [`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow contentEncoding to be an array for consistency with typing of headers + +## 0.4.3 + +### Patch Changes + +- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - add streamToNodeBuffer utility to convert Uint8Array (async) iterables to Buffer + +- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6)]: + - @atproto/common-web@0.3.1 + +## 0.4.2 + +### Patch Changes + +- [#2464](https://github.com/bluesky-social/atproto/pull/2464) [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor optimization + ## 0.4.1 ### Patch Changes diff --git a/packages/common/package.json b/packages/common/package.json index 2e90f8ba9de..ae25f13582f 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/common", - "version": "0.4.1", + "version": "0.4.4", "license": "MIT", "description": "Shared web-platform-friendly code for atproto libraries", "keywords": [ diff --git a/packages/common/src/streams.ts b/packages/common/src/streams.ts index 5514e8a7ca6..c5400f4a2ba 100644 --- a/packages/common/src/streams.ts +++ b/packages/common/src/streams.ts @@ -1,18 +1,20 @@ import { - Stream, - Readable, + Duplex, PassThrough, + pipeline, + Readable, + Stream, Transform, TransformCallback, -} from 'stream' +} from 'node:stream' +import { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib' export const forwardStreamErrors = (...streams: Stream[]) => { - for (let i = 0; i < streams.length; ++i) { - const stream = streams[i] - const next = streams[i + 1] - if (next) { - stream.once('error', (err) => next.emit('error', err)) - } + for (let i = 1; i < streams.length; ++i) { + const prev = streams[i - 1] + const next = streams[i] + + prev.once('error', (err) => next.emit('error', err)) } } @@ -25,17 +27,32 @@ export const cloneStream = (stream: Readable): Readable => { export const streamSize = async (stream: Readable): Promise => { let size = 0 for await (const chunk of stream) { - size += chunk.length + size += Buffer.byteLength(chunk) } return size } -export const streamToBytes = async (stream: Readable): Promise => { - const bufs: Buffer[] = [] - for await (const bytes of stream) { - bufs.push(bytes) +export const streamToBytes = async (stream: AsyncIterable) => + // @NOTE Though Buffer is a sub-class of Uint8Array, we have observed + // inconsistencies when using a Buffer in place of Uint8Array. For this + // reason, we convert the Buffer to a Uint8Array. + new Uint8Array(await streamToNodeBuffer(stream)) + +// streamToBuffer identifier name already taken by @atproto/common-web +export const streamToNodeBuffer = async ( + stream: Iterable | AsyncIterable, +): Promise => { + const chunks: Uint8Array[] = [] + let totalLength = 0 // keep track of total length for Buffer.concat + for await (const chunk of stream) { + if (chunk instanceof Uint8Array) { + chunks.push(chunk) + totalLength += Buffer.byteLength(chunk) + } else { + throw new TypeError('expected Uint8Array') + } } - return new Uint8Array(Buffer.concat(bufs)) + return Buffer.concat(chunks, totalLength) } export const byteIterableToStream = ( @@ -68,3 +85,78 @@ export class MaxSizeChecker extends Transform { } } } + +export function decodeStream( + stream: Readable, + contentEncoding?: string | string[], +): Readable +export function decodeStream( + stream: AsyncIterable, + contentEncoding?: string | string[], +): AsyncIterable | Readable +export function decodeStream( + stream: Readable | AsyncIterable, + contentEncoding?: string | string[], +): Readable | AsyncIterable { + const decoders = createDecoders(contentEncoding) + if (decoders.length === 0) return stream + return pipeline([stream as Readable, ...decoders], () => {}) as Duplex +} + +/** + * Create a series of decoding streams based on the content-encoding header. The + * resulting streams should be piped together to decode the content. + * + * @see {@link https://datatracker.ietf.org/doc/html/rfc9110#section-8.4.1} + */ +export function createDecoders(contentEncoding?: string | string[]): Duplex[] { + const decoders: Duplex[] = [] + + if (contentEncoding?.length) { + const encodings: string[] = Array.isArray(contentEncoding) + ? contentEncoding.flatMap(commaSplit) + : contentEncoding.split(',') + for (const encoding of encodings) { + const normalizedEncoding = normalizeEncoding(encoding) + + // @NOTE + // > The default (identity) encoding [...] is used only in the + // > Accept-Encoding header, and SHOULD NOT be used in the + // > Content-Encoding header. + if (normalizedEncoding === 'identity') continue + + decoders.push(createDecoder(normalizedEncoding)) + } + } + + return decoders.reverse() +} + +function commaSplit(header: string): string[] { + return header.split(',') +} + +function normalizeEncoding(encoding: string) { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // > All content-coding values are case-insensitive... + return encoding.trim().toLowerCase() +} + +function createDecoder(normalizedEncoding: string): Duplex { + switch (normalizedEncoding) { + // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2 + case 'gzip': + case 'x-gzip': + return createGunzip() + case 'deflate': + return createInflate() + case 'br': + return createBrotliDecompress() + case 'identity': + return new PassThrough() + default: + throw new TypeError( + `Unsupported content-encoding: "${normalizedEncoding}"`, + ) + } +} diff --git a/packages/common/tests/streams.test.ts b/packages/common/tests/streams.test.ts index 735b19a8341..8c38078440f 100644 --- a/packages/common/tests/streams.test.ts +++ b/packages/common/tests/streams.test.ts @@ -61,14 +61,41 @@ describe('streams', () => { }) }) - describe('streamToBytes', () => { + describe('streamToNodeBuffer', () => { it('converts stream to byte array', async () => { const stream = Readable.from(Buffer.from('foo')) - const bytes = await streams.streamToBytes(stream) + const bytes = await streams.streamToNodeBuffer(stream) expect(bytes[0]).toBe('f'.charCodeAt(0)) expect(bytes[1]).toBe('o'.charCodeAt(0)) expect(bytes[2]).toBe('o'.charCodeAt(0)) + expect(bytes.length).toBe(3) + }) + + it('converts async iterable to byte array', async () => { + const iterable = (async function* () { + yield Buffer.from('b') + yield Buffer.from('a') + yield new Uint8Array(['r'.charCodeAt(0)]) + })() + const bytes = await streams.streamToNodeBuffer(iterable) + + expect(bytes[0]).toBe('b'.charCodeAt(0)) + expect(bytes[1]).toBe('a'.charCodeAt(0)) + expect(bytes[2]).toBe('r'.charCodeAt(0)) + expect(bytes.length).toBe(3) + }) + + it('throws error for non Uint8Array chunks', async () => { + const iterable: AsyncIterable = (async function* () { + yield Buffer.from('b') + yield Buffer.from('a') + yield 'r' + })() + + await expect(streams.streamToNodeBuffer(iterable)).rejects.toThrow( + 'expected Uint8Array', + ) }) }) diff --git a/packages/crypto/CHANGELOG.md b/packages/crypto/CHANGELOG.md index e194c82dd70..04ce19e9bcc 100644 --- a/packages/crypto/CHANGELOG.md +++ b/packages/crypto/CHANGELOG.md @@ -1,5 +1,11 @@ # @atproto/crypto +## 0.4.1 + +### Patch Changes + +- [#2743](https://github.com/bluesky-social/atproto/pull/2743) [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add "`jwtAlg`" option to `verifySignature()` function + ## 0.4.0 ### Minor Changes diff --git a/packages/crypto/package.json b/packages/crypto/package.json index b951e72b4bf..4bf6983637a 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/crypto", - "version": "0.4.0", + "version": "0.4.1", "license": "MIT", "description": "Library for cryptographic keys and signing in atproto", "keywords": [ diff --git a/packages/crypto/src/verify.ts b/packages/crypto/src/verify.ts index 50ba87aba2e..b0fbccbc1ff 100644 --- a/packages/crypto/src/verify.ts +++ b/packages/crypto/src/verify.ts @@ -7,9 +7,14 @@ export const verifySignature = ( didKey: string, data: Uint8Array, sig: Uint8Array, - opts?: VerifyOptions, + opts?: VerifyOptions & { + jwtAlg?: string + }, ): Promise => { const parsed = parseDidKey(didKey) + if (opts?.jwtAlg && opts.jwtAlg !== parsed.jwtAlg) { + throw new Error(`Expected key alg ${opts.jwtAlg}, got ${parsed.jwtAlg}`) + } const plugin = plugins.find((p) => p.jwtAlg === parsed.jwtAlg) if (!plugin) { throw new Error(`Unsupported signature alg: ${parsed.jwtAlg}`) diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md index d249381447f..2dd20a30be5 100644 --- a/packages/dev-env/CHANGELOG.md +++ b/packages/dev-env/CHANGELOG.md @@ -1,5 +1,161 @@ # @atproto/dev-env +## 0.3.54 + +### Patch Changes + +- Updated dependencies [[`a0531ce42`](https://github.com/bluesky-social/atproto/commit/a0531ce429f5139cb0e2cc19aa9b338599947e44)]: + - @atproto/api@0.13.11 + - @atproto/bsky@0.0.87 + - @atproto/ozone@0.1.49 + - @atproto/pds@0.4.63 + +## 0.3.53 + +### Patch Changes + +- Updated dependencies [[`df14df522`](https://github.com/bluesky-social/atproto/commit/df14df522bb7986e56ee1f6a0f5d862e1ea6f4d5)]: + - @atproto/ozone@0.1.48 + - @atproto/api@0.13.10 + - @atproto/bsky@0.0.86 + - @atproto/pds@0.4.62 + +## 0.3.52 + +### Patch Changes + +- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13), [`a2bad977a`](https://github.com/bluesky-social/atproto/commit/a2bad977a8d941b4075ea3ffee3d6f7a0c0f467c)]: + - @atproto/pds@0.4.61 + - @atproto/ozone@0.1.47 + - @atproto/api@0.13.9 + - @atproto/bsky@0.0.85 + - @atproto/bsync@0.0.8 + - @atproto/crypto@0.4.1 + - @atproto/sync@0.1.3 + - @atproto/xrpc-server@0.7.1 + +## 0.3.51 + +### Patch Changes + +- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`2676206e4`](https://github.com/bluesky-social/atproto/commit/2676206e422233fefbf2d9d182e8d462f0957c93), [`922b94ce3`](https://github.com/bluesky-social/atproto/commit/922b94ce379d861faaa5cf8448b7a44f04e474d3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a06634ae5`](https://github.com/bluesky-social/atproto/commit/a06634ae576217d53ef7ea7f8cbfa9faa8662634), [`b298bfd28`](https://github.com/bluesky-social/atproto/commit/b298bfd280c5de8b38b843fd852e6d2739a776d8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]: + - @atproto/common-web@0.3.1 + - @atproto/api@0.13.8 + - @atproto/pds@0.4.60 + - @atproto/xrpc-server@0.7.0 + - @atproto/lexicon@0.4.2 + - @atproto/bsky@0.0.84 + - @atproto/ozone@0.1.46 + - @atproto/identity@0.4.2 + - @atproto/sync@0.1.2 + - @atproto/bsync@0.0.7 + - @atproto/crypto@0.4.1 + +## 0.3.50 + +### Patch Changes + +- Updated dependencies [[`e6bd5aecc`](https://github.com/bluesky-social/atproto/commit/e6bd5aecce7954d60e5fb263297e697ab7aab98e), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`33aa0c722`](https://github.com/bluesky-social/atproto/commit/33aa0c722226a18215af0ae1833c7c552fc7aaa7)]: + - @atproto/api@0.13.7 + - @atproto/ozone@0.1.45 + - @atproto/xrpc-server@0.6.4 + - @atproto/bsky@0.0.83 + - @atproto/pds@0.4.59 + - @atproto/bsync@0.0.6 + - @atproto/crypto@0.4.1 + - @atproto/sync@0.1.1 + +## 0.3.49 + +### Patch Changes + +- Updated dependencies [[`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442), [`642c7ae96`](https://github.com/bluesky-social/atproto/commit/642c7ae968b0dd2bfb448aa6eba0c1fd9312d909)]: + - @atproto/sync@0.1.0 + - @atproto/ozone@0.1.44 + - @atproto/bsky@0.0.82 + - @atproto/pds@0.4.58 + +## 0.3.48 + +### Patch Changes + +- Updated dependencies [[`325859b8b`](https://github.com/bluesky-social/atproto/commit/325859b8bff8dcfdd1eb8cabd51bffedb03aad87), [`e4d41d66f`](https://github.com/bluesky-social/atproto/commit/e4d41d66fa4757a696363f39903562458967b63d)]: + - @atproto/ozone@0.1.43 + - @atproto/api@0.13.6 + - @atproto/bsky@0.0.81 + - @atproto/pds@0.4.57 + +## 0.3.47 + +### Patch Changes + +- Updated dependencies [[`80ada8f47`](https://github.com/bluesky-social/atproto/commit/80ada8f47628f55f3074cd16a52857e98d117e14)]: + - @atproto/bsky@0.0.80 + - @atproto/api@0.13.5 + - @atproto/pds@0.4.56 + - @atproto/ozone@0.1.42 + +## 0.3.46 + +### Patch Changes + +- Updated dependencies []: + - @atproto/pds@0.4.55 + - @atproto/bsky@0.0.79 + - @atproto/ozone@0.1.41 + +## 0.3.45 + +### Patch Changes + +- Updated dependencies [[`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`bbca17bc5`](https://github.com/bluesky-social/atproto/commit/bbca17bc5388e0b2af26fb107347c8ab507ee42f), [`a8e1f9000`](https://github.com/bluesky-social/atproto/commit/a8e1f9000d9617c4df9d9f0e74ae0e0b73fcfd66), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7)]: + - @atproto/xrpc-server@0.6.3 + - @atproto/pds@0.4.54 + - @atproto/api@0.13.4 + - @atproto/bsky@0.0.79 + - @atproto/crypto@0.4.1 + - @atproto/ozone@0.1.41 + - @atproto/identity@0.4.1 + +## 0.3.44 + +### Patch Changes + +- Updated dependencies [[`4ab248354`](https://github.com/bluesky-social/atproto/commit/4ab2483547d5dabfba88ed4419a4f374bbd7cae7)]: + - @atproto/bsky@0.0.78 + - @atproto/api@0.13.3 + - @atproto/pds@0.4.53 + - @atproto/ozone@0.1.40 + +## 0.3.43 + +### Patch Changes + +- Updated dependencies [[`2a0c088cc`](https://github.com/bluesky-social/atproto/commit/2a0c088cc5d502ca70da9612a261186aa2f2e1fb), [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27)]: + - @atproto/bsky@0.0.77 + - @atproto/api@0.13.2 + - @atproto/pds@0.4.52 + - @atproto/ozone@0.1.39 + +## 0.3.42 + +### Patch Changes + +- Updated dependencies [[`f9a2f3ed1`](https://github.com/bluesky-social/atproto/commit/f9a2f3ed172ae1a8dc1cca0e893e13eac2e4955d)]: + - @atproto/pds@0.4.51 + - @atproto/bsky@0.0.76 + - @atproto/ozone@0.1.38 + +## 0.3.41 + +### Patch Changes + +- Updated dependencies [[`acbacbbd5`](https://github.com/bluesky-social/atproto/commit/acbacbbd5621473b14ee7a6a3132f675806d23b1), [`acbacbbd5`](https://github.com/bluesky-social/atproto/commit/acbacbbd5621473b14ee7a6a3132f675806d23b1), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae)]: + - @atproto/xrpc-server@0.6.2 + - @atproto/pds@0.4.50 + - @atproto/ozone@0.1.38 + - @atproto/bsky@0.0.76 + ## 0.3.40 ### Patch Changes diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 7541a36511c..f593f66fdb7 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.3.40", + "version": "0.3.54", "license": "MIT", "description": "Local development environment helper for atproto development", "keywords": [ @@ -30,6 +30,7 @@ "@atproto/lexicon": "workspace:^", "@atproto/ozone": "workspace:^", "@atproto/pds": "workspace:^", + "@atproto/sync": "workspace:^", "@atproto/syntax": "workspace:^", "@atproto/xrpc-server": "workspace:^", "@did-plc/lib": "^0.0.1", diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 68165fae663..46981be0a18 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -3,7 +3,6 @@ import * as ui8 from 'uint8arrays' import * as bsky from '@atproto/bsky' import { AtpAgent } from '@atproto/api' import { Secp256k1Keypair } from '@atproto/crypto' -import { BackgroundQueue } from '@atproto/bsky' import { Client as PlcClient } from '@did-plc/lib' import { BskyConfig } from './types' import { ADMIN_PASSWORD, EXAMPLE_LABELER } from './const' @@ -92,11 +91,11 @@ export class TestBsky { service: cfg.repoProvider, db, idResolver: dataplane.idResolver, - background: new BackgroundQueue(db), }) await server.start() - sub.run() + + sub.start() return new TestBsky(url, port, db, server, dataplane, bsync, sub) } diff --git a/packages/dev-env/src/moderator-client.ts b/packages/dev-env/src/moderator-client.ts index 81a024b4ae2..6327e15147e 100644 --- a/packages/dev-env/src/moderator-client.ts +++ b/packages/dev-env/src/moderator-client.ts @@ -21,7 +21,10 @@ export class ModeratorClient { const result = await this.agent.tools.ozone.moderation.getEvent( { id }, { - headers: await this.ozone.modHeaders(role), + headers: await this.ozone.modHeaders( + 'tools.ozone.moderation.getEvent', + role, + ), }, ) return result.data @@ -31,7 +34,10 @@ export class ModeratorClient { const result = await this.agent.tools.ozone.moderation.queryStatuses( input, { - headers: await this.ozone.modHeaders(role), + headers: await this.ozone.modHeaders( + 'tools.ozone.moderation.queryStatuses', + role, + ), }, ) return result.data @@ -39,7 +45,10 @@ export class ModeratorClient { async queryEvents(input: QueryEventsParams, role?: ModLevel) { const result = await this.agent.tools.ozone.moderation.queryEvents(input, { - headers: await this.ozone.modHeaders(role), + headers: await this.ozone.modHeaders( + 'tools.ozone.moderation.queryEvents', + role, + ), }) return result.data } @@ -66,7 +75,10 @@ export class ModeratorClient { { event, subject, subjectBlobCids, createdBy, reason }, { encoding: 'application/json', - headers: await this.ozone.modHeaders(role), + headers: await this.ozone.modHeaders( + 'tools.ozone.moderation.emitEvent', + role, + ), }, ) return result.data @@ -93,7 +105,10 @@ export class ModeratorClient { }, { encoding: 'application/json', - headers: await this.ozone.modHeaders(role), + headers: await this.ozone.modHeaders( + 'tools.ozone.moderation.emitEvent', + role, + ), }, ) return result.data @@ -104,15 +119,17 @@ export class ModeratorClient { subject: TakeActionInput['subject'] subjectBlobCids?: TakeActionInput['subjectBlobCids'] durationInHours?: number + acknowledgeAccountSubjects?: boolean reason?: string }, role?: ModLevel, ) { - const { durationInHours, ...rest } = opts + const { durationInHours, acknowledgeAccountSubjects, ...rest } = opts return this.emitEvent( { event: { $type: 'tools.ozone.moderation.defs#modEventTakedown', + acknowledgeAccountSubjects, durationInHours, }, ...rest, diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index 4477c5d270b..1ce07a19138 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -117,7 +117,7 @@ export class TestNetwork extends TestNetworkNoAppView { mockNetworkUtilities(pds, bsky) await pds.processAll() - await bsky.sub.background.processAll() + await bsky.sub.processAll() await thirdPartyPds.close() let introspect: IntrospectServer | undefined = undefined @@ -140,9 +140,11 @@ export class TestNetwork extends TestNetworkNoAppView { const lastSeq = await this.pds.ctx.sequencer.curr() if (!lastSeq) return while (Date.now() - start < timeout) { - 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() + await sub.processAll() + const runnerCursor = await sub.runner.getCursor() + // if subscription claims to be done, ensure we are at the most recent cursor from PDS, else wait to process again + // (the subscription may claim to be finished before the PDS has even emitted it's event) + if (runnerCursor && runnerCursor >= lastSeq) { return } await wait(5) @@ -154,15 +156,14 @@ export class TestNetwork extends TestNetworkNoAppView { await this.pds.processAll() await this.ozone.processAll() await this.processFullSubscription(timeout) - await this.bsky.sub.background.processAll() } - async serviceHeaders(did: string, aud?: string) { + async serviceHeaders(did: string, lxm: string, aud?: string) { const keypair = await this.pds.ctx.actorStore.keypair(did) const jwt = await createServiceJwt({ iss: did, aud: aud ?? this.bsky.ctx.cfg.serverDid, - lxm: null, + lxm, keypair, }) return { authorization: `Bearer ${jwt}` } diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 56a56775be2..fabe73f5a51 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -143,7 +143,10 @@ export class TestOzone { this.ctx.cfg.access.triage.push(did) } - async modHeaders(role: 'admin' | 'moderator' | 'triage' = 'moderator') { + async modHeaders( + lxm: string, + role: 'admin' | 'moderator' | 'triage' = 'moderator', + ) { const account = role === 'admin' ? this.adminAccnt @@ -153,7 +156,7 @@ export class TestOzone { const jwt = await createServiceJwt({ iss: account.did, aud: this.ctx.cfg.service.did, - lxm: null, + lxm, keypair: account.key, }) return { authorization: `Bearer ${jwt}` } diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 87750227c2b..fefc5643add 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -45,7 +45,7 @@ export class TestPds { modServiceDid: 'did:example:invalid', plcRotationKeyK256PrivateKeyHex: plcRotationPriv, inviteRequired: false, - fetchDisableSsrfProtection: true, + disableSsrfProtection: true, serviceName: 'Development PDS', brandColor: '#ffcb1e', errorColor: undefined, diff --git a/packages/dev-env/src/seed/index.ts b/packages/dev-env/src/seed/index.ts index dcf8996c5fe..336da8995a3 100644 --- a/packages/dev-env/src/seed/index.ts +++ b/packages/dev-env/src/seed/index.ts @@ -7,3 +7,4 @@ export { default as likesSeed } from './likes' export { default as repostsSeed } from './reposts' export { default as usersBulkSeed } from './users-bulk' export { default as usersSeed } from './users' +export { default as quotesSeed } from './quotes' diff --git a/packages/dev-env/src/seed/quotes.ts b/packages/dev-env/src/seed/quotes.ts new file mode 100644 index 00000000000..444960f7ba7 --- /dev/null +++ b/packages/dev-env/src/seed/quotes.ts @@ -0,0 +1,53 @@ +import { SeedClient } from './client' +import { basicSeed } from './index' + +export default async (sc: SeedClient) => { + await basicSeed(sc) + await sc.createAccount('eve', { + email: 'eve@test.com', + handle: 'eve.test', + password: 'eve-pass', + }) + + await sc.post( + sc.dids.eve, + 'qUoTe 1', + undefined, + undefined, + sc.posts[sc.dids.alice][0].ref, + ) + await sc.post( + sc.dids.eve, + 'qUoTe 2', + undefined, + undefined, + sc.posts[sc.dids.alice][0].ref, + ) + + await sc.post( + sc.dids.eve, + 'qUoTe 3', + undefined, + undefined, + sc.replies[sc.dids.bob][0].ref, + ) + + const carolPost = await sc.post(sc.dids.carol, 'post') + await sc.post(sc.dids.eve, 'qUoTe 4', undefined, undefined, carolPost.ref) + + const spamPosts: Promise[] = [] + for (let i = 0; i < 5; i++) { + spamPosts.push( + sc.post( + sc.dids.eve, + `MASSIVE QUOTE SPAM ${i + 1}`, + undefined, + undefined, + sc.posts[sc.dids.alice][1].ref, + ), + ) + } + await Promise.all(spamPosts) + + return sc +} diff --git a/packages/did/CHANGELOG.md b/packages/did/CHANGELOG.md index 7acdf5bc6d3..3c183aabd90 100644 --- a/packages/did/CHANGELOG.md +++ b/packages/did/CHANGELOG.md @@ -1,5 +1,15 @@ # @atproto/did +## 0.1.2 + +### Patch Changes + +- [#2776](https://github.com/bluesky-social/atproto/pull/2776) [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Disallow path component in Web DID's (as per spec) + +- [#2776](https://github.com/bluesky-social/atproto/pull/2776) [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly parse localhost did:web + +- [#2776](https://github.com/bluesky-social/atproto/pull/2776) [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Code optimizations and documentation. Rename `check*` utility function to `assert*`. + ## 0.1.1 ### Patch Changes diff --git a/packages/did/package.json b/packages/did/package.json index 94a8449ca33..d1f5d57f64b 100644 --- a/packages/did/package.json +++ b/packages/did/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/did", - "version": "0.1.1", + "version": "0.1.2", "license": "MIT", "description": "DID resolution and verification library", "keywords": [ diff --git a/packages/did/src/atproto.ts b/packages/did/src/atproto.ts index cdd69291d4a..5c8d33f3839 100644 --- a/packages/did/src/atproto.ts +++ b/packages/did/src/atproto.ts @@ -1,12 +1,11 @@ import { InvalidDidError } from './did-error.js' import { Did } from './did.js' import { - checkDidPlc, - checkDidWeb, + assertDidPlc, + assertDidWeb, DID_PLC_PREFIX, DID_WEB_PREFIX, isDidPlc, - isDidWeb, } from './methods.js' // This file contains atproto-specific DID validation utilities. @@ -30,17 +29,17 @@ export function isAtprotoDid(input: unknown): input is AtprotoDid { } export function asAtprotoDid(input: unknown): AtprotoDid { - checkAtprotoDid(input) + assertAtprotoDid(input) return input } -export function checkAtprotoDid(input: unknown): asserts input is AtprotoDid { +export function assertAtprotoDid(input: unknown): asserts input is AtprotoDid { if (typeof input !== 'string') { throw new InvalidDidError(typeof input, `DID must be a string`) } else if (input.startsWith(DID_PLC_PREFIX)) { - checkDidPlc(input) + assertDidPlc(input) } else if (input.startsWith(DID_WEB_PREFIX)) { - checkDidWeb(input) + assertAtprotoDidWeb(input) } else { throw new InvalidDidError( input, @@ -49,27 +48,37 @@ export function checkAtprotoDid(input: unknown): asserts input is AtprotoDid { } } -/** - * @see {@link https://atproto.com/specs/did#blessed-did-methods} - */ -export function isAtprotoDidWeb(input: unknown): input is Did<'web'> { - // Optimization: make cheap checks first - if (typeof input !== 'string') { - return false - } +export function assertAtprotoDidWeb( + input: unknown, +): asserts input is Did<'web'> { + assertDidWeb(input) - // Path are not allowed if (input.includes(':', DID_WEB_PREFIX.length)) { - return false + throw new InvalidDidError( + input, + `Atproto does not allow path components in Web DIDs`, + ) } - // Port numbers are not allowed, except for localhost if ( input.includes('%3A', DID_WEB_PREFIX.length) && !input.startsWith('did:web:localhost%3A') ) { - return false + throw new InvalidDidError( + input, + `Atproto does not allow port numbers in Web DIDs, except for localhost`, + ) } +} - return isDidWeb(input) +/** + * @see {@link https://atproto.com/specs/did#blessed-did-methods} + */ +export function isAtprotoDidWeb(input: unknown): input is Did<'web'> { + try { + assertAtprotoDidWeb(input) + return true + } catch { + return false + } } diff --git a/packages/did/src/did-document.ts b/packages/did/src/did-document.ts index c22af71e25c..fcd4f7b50cd 100644 --- a/packages/did/src/did-document.ts +++ b/packages/did/src/did-document.ts @@ -117,35 +117,27 @@ export type DidDocument = z.infer< // @TODO: add other refinements ? export const didDocumentValidator = didDocumentSchema - .superRefine((data, ctx) => { - if (data.service) { - for (let i = 0; i < data.service.length; i++) { - if (data.service[i].id === data.id) { + // Ensure that every service id is unique + .superRefine(({ id: did, service }, ctx) => { + if (service) { + const visited = new Set() + + for (let i = 0; i < service.length; i++) { + const current = service[i] + + const serviceId = current.id.startsWith('#') + ? `${did}${current.id}` + : current.id + + if (!visited.has(serviceId)) { + visited.add(serviceId) + } else { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Service id must be different from the document id`, + message: `Duplicate service id (${current.id}) found in the document`, path: ['service', i, 'id'], }) } } } }) - .superRefine((data, ctx) => { - if (data.service) { - const normalizedIds = data.service.map((s) => - s.id?.startsWith('#') ? `${data.id}${s.id}` : s.id, - ) - - for (let i = 0; i < normalizedIds.length; i++) { - for (let j = i + 1; j < normalizedIds.length; j++) { - if (normalizedIds[i] === normalizedIds[j]) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Duplicate service id (${normalizedIds[j]}) found in the document`, - path: ['service', j, 'id'], - }) - } - } - } - } - }) diff --git a/packages/did/src/did.ts b/packages/did/src/did.ts index 55a4f1696d3..3a44f86937d 100644 --- a/packages/did/src/did.ts +++ b/packages/did/src/did.ts @@ -83,7 +83,7 @@ type AsDidMethodInternal< * Check if the input is a valid DID method name, at the position between * `start` (inclusive) and `end` (exclusive). */ -export function checkDidMethod( +export function assertDidMethod( input: string, start = 0, end = input.length, @@ -130,7 +130,7 @@ export function extractDidMethod(did: D) { * Check if the input is a valid DID method-specific identifier, at the position * between `start` (inclusive) and `end` (exclusive). */ -export function checkDidMsid( +export function assertDidMsid( input: string, start = 0, end = input.length, @@ -207,7 +207,7 @@ export function checkDidMsid( } } -export function checkDid(input: unknown): asserts input is Did { +export function assertDid(input: unknown): asserts input is Did { if (typeof input !== 'string') { throw new InvalidDidError(typeof input, `DID must be a string`) } @@ -226,13 +226,13 @@ export function checkDid(input: unknown): asserts input is Did { throw new InvalidDidError(input, `Missing colon after method name`) } - checkDidMethod(input, DID_PREFIX_LENGTH, idSep) - checkDidMsid(input, idSep + 1, length) + assertDidMethod(input, DID_PREFIX_LENGTH, idSep) + assertDidMsid(input, idSep + 1, length) } export function isDid(input: unknown): input is Did { try { - checkDid(input) + assertDid(input) return true } catch (err) { if (err instanceof DidError) { @@ -245,7 +245,7 @@ export function isDid(input: unknown): input is Did { } export function asDid(input: unknown): Did { - checkDid(input) + assertDid(input) return input } @@ -253,7 +253,7 @@ export const didSchema = z .string() .superRefine((value: string, ctx: z.RefinementCtx): value is Did => { try { - checkDid(value) + assertDid(value) return true } catch (err) { ctx.addIssue({ diff --git a/packages/did/src/methods/plc.ts b/packages/did/src/methods/plc.ts index bb851befecc..69f6dc632e9 100644 --- a/packages/did/src/methods/plc.ts +++ b/packages/did/src/methods/plc.ts @@ -8,23 +8,22 @@ const DID_PLC_LENGTH = 32 export { DID_PLC_PREFIX } export function isDidPlc(input: unknown): input is Did<'plc'> { - // Optimization: make cheap checks first + // Optimization: equivalent to try/catch around "assertDidPlc" if (typeof input !== 'string') return false - - try { - checkDidPlc(input) - return true - } catch { - return false + if (input.length !== DID_PLC_LENGTH) return false + if (!input.startsWith(DID_PLC_PREFIX)) return false + for (let i = DID_PLC_PREFIX_LENGTH; i < DID_PLC_LENGTH; i++) { + if (!isBase32Char(input.charCodeAt(i))) return false } + return true } export function asDidPlc(input: unknown): Did<'plc'> { - checkDidPlc(input) + assertDidPlc(input) return input } -export function checkDidPlc(input: unknown): asserts input is Did<'plc'> { +export function assertDidPlc(input: unknown): asserts input is Did<'plc'> { if (typeof input !== 'string') { throw new InvalidDidError(typeof input, `DID must be a string`) } @@ -40,12 +39,16 @@ export function checkDidPlc(input: unknown): asserts input is Did<'plc'> { throw new InvalidDidError(input, `Invalid did:plc prefix`) } - let c: number + // The following check is not necessary, as the check below is more strict: + + // assertDidMsid(input, DID_PLC_PREFIX.length) + for (let i = DID_PLC_PREFIX_LENGTH; i < DID_PLC_LENGTH; i++) { - c = input.charCodeAt(i) - // Base32 encoding ([a-z2-7]) - if ((c < 0x61 || c > 0x7a) && (c < 0x32 || c > 0x37)) { + if (!isBase32Char(input.charCodeAt(i))) { throw new InvalidDidError(input, `Invalid character at position ${i}`) } } } + +const isBase32Char = (c: number): boolean => + (c >= 0x61 && c <= 0x7a) || (c >= 0x32 && c <= 0x37) // [a-z2-7] diff --git a/packages/did/src/methods/web.ts b/packages/did/src/methods/web.ts index 4dcaf50c307..31405ccb023 100644 --- a/packages/did/src/methods/web.ts +++ b/packages/did/src/methods/web.ts @@ -1,5 +1,5 @@ import { InvalidDidError } from '../did-error.js' -import { Did, checkDidMsid } from '../did.js' +import { Did, assertDidMsid } from '../did.js' export const DID_WEB_PREFIX = `did:web:` satisfies Did<'web'> @@ -11,7 +11,7 @@ export function isDidWeb(input: unknown): input is Did<'web'> { if (typeof input !== 'string') return false try { - checkDidWeb(input) + assertDidWeb(input) return true } catch { return false @@ -19,11 +19,11 @@ export function isDidWeb(input: unknown): input is Did<'web'> { } export function asDidWeb(input: unknown): Did<'web'> { - checkDidWeb(input) + assertDidWeb(input) return input } -export function checkDidWeb(input: unknown): asserts input is Did<'web'> { +export function assertDidWeb(input: unknown): asserts input is Did<'web'> { if (typeof input !== 'string') { throw new InvalidDidError(typeof input, `DID must be a string`) } @@ -41,12 +41,16 @@ export function didWebToUrl(did: string): URL { } // Make sure every char is valid (per DID spec) - checkDidMsid(did, DID_WEB_PREFIX.length) + assertDidMsid(did, DID_WEB_PREFIX.length) try { const msid = did.slice(DID_WEB_PREFIX.length) const parts = msid.split(':').map(decodeURIComponent) - return new URL(`https://${parts.join('/')}`) + const url = new URL(`https://${parts.join('/')}`) + if (url.hostname === 'localhost') { + url.protocol = 'http:' + } + return url } catch (cause) { throw new InvalidDidError(did, 'Invalid Web DID', cause) } diff --git a/packages/identity/CHANGELOG.md b/packages/identity/CHANGELOG.md index 9789a2fa1c3..5cb39ffecb0 100644 --- a/packages/identity/CHANGELOG.md +++ b/packages/identity/CHANGELOG.md @@ -1,5 +1,20 @@ # @atproto/identity +## 0.4.2 + +### Patch Changes + +- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6)]: + - @atproto/common-web@0.3.1 + - @atproto/crypto@0.4.1 + +## 0.4.1 + +### Patch Changes + +- Updated dependencies [[`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31)]: + - @atproto/crypto@0.4.1 + ## 0.4.0 ### Minor Changes diff --git a/packages/identity/package.json b/packages/identity/package.json index 867ed489579..1d0047d20fe 100644 --- a/packages/identity/package.json +++ b/packages/identity/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/identity", - "version": "0.4.0", + "version": "0.4.2", "license": "MIT", "description": "Library for decentralized identities in atproto using DIDs and handles", "keywords": [ diff --git a/packages/internal/did-resolver/CHANGELOG.md b/packages/internal/did-resolver/CHANGELOG.md index 54a415d2341..eb4e497f904 100644 --- a/packages/internal/did-resolver/CHANGELOG.md +++ b/packages/internal/did-resolver/CHANGELOG.md @@ -1,5 +1,19 @@ # @atproto-labs/did-resolver +## 0.1.4 + +### Patch Changes + +- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]: + - @atproto-labs/fetch@0.1.1 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [[`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c)]: + - @atproto/did@0.1.2 + ## 0.1.2 ### Patch Changes diff --git a/packages/internal/did-resolver/package.json b/packages/internal/did-resolver/package.json index bb74cee42ae..6d464034cfc 100644 --- a/packages/internal/did-resolver/package.json +++ b/packages/internal/did-resolver/package.json @@ -1,6 +1,6 @@ { "name": "@atproto-labs/did-resolver", - "version": "0.1.2", + "version": "0.1.4", "license": "MIT", "description": "DID resolution and verification library", "keywords": [ diff --git a/packages/internal/did-resolver/src/methods/plc.ts b/packages/internal/did-resolver/src/methods/plc.ts index c110c9db20c..22a22de842a 100644 --- a/packages/internal/did-resolver/src/methods/plc.ts +++ b/packages/internal/did-resolver/src/methods/plc.ts @@ -6,7 +6,7 @@ import { fetchOkProcessor, } from '@atproto-labs/fetch' import { pipe } from '@atproto-labs/pipe' -import { Did, checkDidPlc, didDocumentValidator } from '@atproto/did' +import { Did, assertDidPlc, didDocumentValidator } from '@atproto/did' import { DidMethod, ResolveDidOptions } from '../did-method.js' @@ -43,7 +43,7 @@ export class DidPlcMethod implements DidMethod<'plc'> { async resolve(did: Did<'plc'>, options?: ResolveDidOptions) { // Although the did should start with `did:plc:` (thanks to typings), we // should still check if the msid is valid. - checkDidPlc(did) + assertDidPlc(did) const url = new URL(`/${did}`, this.plcDirectoryUrl) diff --git a/packages/internal/did-resolver/src/methods/web.ts b/packages/internal/did-resolver/src/methods/web.ts index 54150bf6f96..5302c22513d 100644 --- a/packages/internal/did-resolver/src/methods/web.ts +++ b/packages/internal/did-resolver/src/methods/web.ts @@ -30,6 +30,11 @@ export class DidWebMethod implements DidMethod<'web'> { async resolve(did: Did<'web'>, options?: ResolveDidOptions) { const didDocumentUrl = buildDidWebDocumentUrl(did) + // Note we do not explicitly check for "localhost" here. Instead, we rely on + // the injected 'fetch' function to handle the URL. If the URL is + // "localhost", or resolves to a private IP address, the fetch function is + // responsible for handling it. + return this.fetch(didDocumentUrl, { redirect: 'error', headers: { accept: 'application/did+ld+json,application/json' }, diff --git a/packages/internal/fetch-node/CHANGELOG.md b/packages/internal/fetch-node/CHANGELOG.md index 4752d081c8c..b8fcd062618 100644 --- a/packages/internal/fetch-node/CHANGELOG.md +++ b/packages/internal/fetch-node/CHANGELOG.md @@ -1,5 +1,22 @@ # @atproto-labs/fetch-node +## 0.1.2 + +### Patch Changes + +- [#2854](https://github.com/bluesky-social/atproto/pull/2854) [`8943c1008`](https://github.com/bluesky-social/atproto/commit/8943c10082702bbc0fc150237c6cc421251afd51) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Disable use of HTTP2 when checking SSRF IP + +## 0.1.1 + +### Patch Changes + +- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Prevent bypass of ssrf ip verification + +- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose IP filtering utilities + +- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]: + - @atproto-labs/fetch@0.1.1 + ## 0.1.0 ### Minor Changes diff --git a/packages/internal/fetch-node/package.json b/packages/internal/fetch-node/package.json index b10bb80d4b5..00c5106e684 100644 --- a/packages/internal/fetch-node/package.json +++ b/packages/internal/fetch-node/package.json @@ -1,6 +1,6 @@ { "name": "@atproto-labs/fetch-node", - "version": "0.1.0", + "version": "0.1.2", "license": "MIT", "description": "SSRF protection for fetch() in Node.js", "keywords": [ diff --git a/packages/internal/fetch-node/src/index.ts b/packages/internal/fetch-node/src/index.ts index b7022c681b9..479b2dc0abb 100644 --- a/packages/internal/fetch-node/src/index.ts +++ b/packages/internal/fetch-node/src/index.ts @@ -1,4 +1,5 @@ export * from '@atproto-labs/fetch' export * from './safe.js' -export * from './ssrf.js' +export * from './unicast.js' +export * from './util.js' diff --git a/packages/internal/fetch-node/src/safe.ts b/packages/internal/fetch-node/src/safe.ts index b64915f2e55..c5eea6335f4 100644 --- a/packages/internal/fetch-node/src/safe.ts +++ b/packages/internal/fetch-node/src/safe.ts @@ -1,16 +1,18 @@ import { + asRequest, DEFAULT_FORBIDDEN_DOMAIN_NAMES, Fetch, fetchMaxSizeProcessor, forbiddenDomainNameRequestTransform, protocolCheckRequestTransform, - requireHostHeaderTranform, + redirectCheckRequestTransform, + requireHostHeaderTransform, timedFetch, toRequestTransformer, } from '@atproto-labs/fetch' import { pipe } from '@atproto-labs/pipe' -import { ssrfFetchWrap } from './ssrf.js' +import { unicastFetchWrap } from './unicast.js' export type SafeFetchWrapOptions = NonNullable< Parameters[0] @@ -19,31 +21,43 @@ export type SafeFetchWrapOptions = NonNullable< /** * Wrap a fetch function with safety checks so that it can be safely used * with user provided input (URL). + * + * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html} */ export function safeFetchWrap({ fetch = globalThis.fetch as Fetch, responseMaxSize = 512 * 1024, // 512kB - allowHttp = false, - allowData = false, ssrfProtection = true, + allowCustomPort = !ssrfProtection, + allowData = false, + allowHttp = !ssrfProtection, + allowIpHost = true, + allowPrivateIps = !ssrfProtection, timeout = 10e3, forbiddenDomainNames = DEFAULT_FORBIDDEN_DOMAIN_NAMES as Iterable, } = {}): Fetch { return toRequestTransformer( pipe( /** - * Prevent using http:, file: or data: protocols. + * Disable HTTP redirects */ - protocolCheckRequestTransform( - ['https:'] - .concat(allowHttp ? ['http:'] : []) - .concat(allowData ? ['data:'] : []), - ), + redirectCheckRequestTransform(), /** * Only requests that will be issued with a "Host" header are allowed. */ - requireHostHeaderTranform(), + allowIpHost ? asRequest : requireHostHeaderTransform(), + + /** + * Prevent using http:, file: or data: protocols. + */ + protocolCheckRequestTransform({ + 'about:': false, + 'data:': allowData, + 'file:': false, + 'http:': allowHttp && { allowCustomPort }, + 'https:': { allowCustomPort }, + }), /** * Disallow fetching from domains we know are not atproto/OIDC client @@ -65,7 +79,7 @@ export function safeFetchWrap({ * input, we need to make sure that the request is not vulnerable to SSRF * attacks. */ - ssrfProtection ? ssrfFetchWrap({ fetch }) : fetch, + allowPrivateIps ? fetch : unicastFetchWrap({ fetch }), ), /** diff --git a/packages/internal/fetch-node/src/ssrf.ts b/packages/internal/fetch-node/src/ssrf.ts deleted file mode 100644 index 7879ad9782b..00000000000 --- a/packages/internal/fetch-node/src/ssrf.ts +++ /dev/null @@ -1,214 +0,0 @@ -import dns, { LookupAddress } from 'node:dns' -import { LookupFunction } from 'node:net' - -import { - Fetch, - FetchContext, - FetchRequestError, - toRequestTransformer, -} from '@atproto-labs/fetch' -import ipaddr from 'ipaddr.js' -import { isValid as isValidDomain } from 'psl' -import { Agent } from 'undici' - -const { IPv4, IPv6 } = ipaddr - -const [NODE_VERSION] = process.versions.node.split('.').map(Number) - -export type SsrfFetchWrapOptions = { - allowCustomPort?: boolean - allowUnknownTld?: boolean - fetch?: Fetch -} - -/** - * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/} - */ -export function ssrfFetchWrap({ - allowCustomPort = false, - allowUnknownTld = false, - fetch = globalThis.fetch, -}: SsrfFetchWrapOptions): Fetch { - const ssrfAgent = new Agent({ connect: { lookup } }) - - return toRequestTransformer(async function ( - this: C, - request, - ): Promise { - const url = new URL(request.url) - - if (url.protocol === 'data:') { - // No SSRF issue - return fetch.call(this, request) - } - - if (url.protocol === 'http:' || url.protocol === 'https:') { - // @ts-expect-error non-standard option - if (request.dispatcher) { - throw new FetchRequestError( - request, - 500, - 'SSRF protection cannot be used with a custom request dispatcher', - ) - } - - // Check port (OWASP) - if (url.port && !allowCustomPort) { - throw new FetchRequestError( - request, - 400, - 'Request port must be omitted or standard when SSRF is enabled', - ) - } - - // Disable HTTP redirections (OWASP) - if (request.redirect === 'follow') { - throw new FetchRequestError( - request, - 500, - 'Request redirect must be "error" or "manual" when SSRF is enabled', - ) - } - - // If the hostname is an IP address, it must be a unicast address. - const ip = parseIpHostname(url.hostname) - if (ip) { - if (ip.range() !== 'unicast') { - throw new FetchRequestError( - request, - 400, - 'Hostname resolved to non-unicast address', - ) - } - // No additional check required - return fetch.call(this, request) - } - - if (allowUnknownTld !== true && !isValidDomain(url.hostname)) { - throw new FetchRequestError( - request, - 400, - 'Hostname is not a public domain', - ) - } - - // Else hostname is a domain name, use DNS lookup to check if it resolves - // to a unicast address - - if (NODE_VERSION < 21) { - // Note: due to the issue nodejs/undici#2828 (fixed in undici >=6.7.0, - // Node >=21), the "dispatcher" property of the request object will not - // be used by fetch(). As a workaround, we pass the dispatcher as second - // argument to fetch() here, and make sure it is used (which might not be - // the case if a custom fetch() function is used). - - if (fetch === globalThis.fetch) { - // If the global fetch function is used, we can pass the dispatcher - // singleton directly to the fetch function as we know it will be - // used. - - // @ts-expect-error non-standard option - return fetch.call(this, request, { dispatcher: ssrfAgent }) - } - - let didLookup = false - const dispatcher = new Agent({ - connect: { - lookup(...args) { - didLookup = true - lookup(...args) - }, - }, - }) - - try { - // @ts-expect-error non-standard option - return await fetch.call(this, request, { dispatcher }) - } finally { - // Free resources (we cannot await here since the response was not - // consumed yet). - void dispatcher.close().catch((err) => { - // No biggie, but let's still log it - console.warn('Failed to close dispatcher', err) - }) - - if (!didLookup) { - // If you encounter this error, either upgrade to Node.js >=21 or - // make sure that the requestInit object is passed as second - // argument to the global fetch function. - - // eslint-disable-next-line no-unsafe-finally - throw new FetchRequestError( - request, - 500, - 'Unable to enforce SSRF protection', - ) - } - } - } - - // @ts-expect-error non-standard option - return fetch(new Request(request, { dispatcher: ssrfAgent })) - } - - // blob: about: file: all should be rejected - throw new FetchRequestError( - request, - 400, - `Forbidden protocol "${url.protocol}"`, - ) - }) -} - -function parseIpHostname( - hostname: string, -): ipaddr.IPv4 | ipaddr.IPv6 | undefined { - if (IPv4.isIPv4(hostname)) { - return IPv4.parse(hostname) - } - - if (hostname.startsWith('[') && hostname.endsWith(']')) { - return IPv6.parse(hostname.slice(1, -1)) - } - - return undefined -} - -function lookup( - hostname: string, - options: dns.LookupOptions, - callback: Parameters[2], -) { - dns.lookup(hostname, options, (err, address, family) => { - if (err) { - callback(err, address, family) - } else { - const ips = Array.isArray(address) - ? address.map(parseLookupAddress) - : [parseLookupAddress({ address, family })] - - if (ips.some((ip) => ip.range() !== 'unicast')) { - callback( - new Error('Hostname resolved to non-unicast address'), - address, - family, - ) - } else { - callback(null, address, family) - } - } - }) -} - -function parseLookupAddress({ - address, - family, -}: LookupAddress): ipaddr.IPv4 | ipaddr.IPv6 { - const ip = family === 4 ? IPv4.parse(address) : IPv6.parse(address) - - if (ip instanceof IPv6 && ip.isIPv4MappedAddress()) { - return ip.toIPv4Address() - } else { - return ip - } -} diff --git a/packages/internal/fetch-node/src/unicast.ts b/packages/internal/fetch-node/src/unicast.ts new file mode 100644 index 00000000000..941d75a2f21 --- /dev/null +++ b/packages/internal/fetch-node/src/unicast.ts @@ -0,0 +1,204 @@ +import dns, { LookupAddress } from 'node:dns' +import { LookupFunction } from 'node:net' + +import { + asRequest, + extractUrl, + Fetch, + FetchContext, + FetchRequestError, +} from '@atproto-labs/fetch' +import ipaddr from 'ipaddr.js' +import { isValid as isValidDomain } from 'psl' +import { Agent, Client } from 'undici' + +import { isUnicastIp } from './util.js' + +const { IPv4, IPv6 } = ipaddr + +export type SsrfFetchWrapOptions = { + fetch?: Fetch +} + +/** + * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/} + */ +export function unicastFetchWrap({ + fetch = globalThis.fetch, +}: SsrfFetchWrapOptions): Fetch { + // In order to enforce the SSRF protection, we need to use a custom dispatcher + // that uses "unicastLookup" to resolve the hostname to a unicast IP address. + + // In case a custom "fetch" function is passed here, we have no assurance that + // the dispatcher will be used to make the request. Because of this, in case a + // custom fetch method is passed, we will use a on-time use dispatcher that + // ensures that "unicastLookup" gets called to resolve the hostname to an IP + // address and ensure that it is a unicast address. + + // Sadly, this means that we cannot use "keepAlive" connections, as the method + // used to ensure that "unicastLookup" gets called requires to create a new + // dispatcher for each request. + + // @TODO: find a way to use a re-usable dispatcher with a custom fetch method. + + if (fetch === globalThis.fetch) { + const dispatcher = new Agent({ + allowH2: true, + connect: { keepAlive: true, lookup: unicastLookup }, + }) + + return async function (input, init): Promise { + if (init?.dispatcher) { + throw new FetchRequestError( + asRequest(input, init), + 500, + 'SSRF protection cannot be used with a custom request dispatcher', + ) + } + + const url = extractUrl(input) + + if (url.hostname && isUnicastIp(url.hostname) === false) { + throw new FetchRequestError( + asRequest(input, init), + 400, + 'Hostname is a non-unicast address', + ) + } + + // @ts-expect-error non-standard option + return fetch.call(this, input, { ...init, dispatcher }) + } + } else { + return async function (input, init): Promise { + if (init?.dispatcher) { + throw new FetchRequestError( + asRequest(input, init), + 500, + 'SSRF protection cannot be used with a custom request dispatcher', + ) + } + + const url = extractUrl(input) + + if (!url.hostname) { + return fetch.call(this, input, init) + } + + switch (isUnicastIp(url.hostname)) { + case true: { + // hostname is a unicast address, safe to proceed. + return fetch.call(this, input, init) + } + + case false: { + throw new FetchRequestError( + asRequest(input, init), + 400, + 'Hostname is a non-unicast address', + ) + } + + case undefined: { + // hostname is a domain name, using the dispatcher defined above + // will result in the DNS lookup being performed, ensuring that the + // hostname resolves to a unicast address. + + let didLookup = false + const dispatcher = new Client(url.origin, { + // Do *not* enable H2 here, as it will cause an error (the client + // will terminate the connection before the response is consumed). + // https://github.com/nodejs/undici/issues/3671 + connect: { + keepAlive: false, // Client will be used once + lookup(...args) { + didLookup = true + unicastLookup(...args) + }, + }, + }) + + const headers = new Headers(init?.headers) + headers.set('connection', 'close') // Proactively close the connection + + try { + return await fetch.call(this, input, { + ...init, + headers, + // @ts-expect-error non-standard option + dispatcher, + }) + } finally { + // Free resources (we cannot await here since the response was not + // consumed yet). + void dispatcher.close().catch((err) => { + // No biggie, but let's still log it + console.warn('Failed to close dispatcher', err) + }) + + if (!didLookup) { + // If you encounter this error, either upgrade to Node.js >=21 or + // make sure that the dispatcher passed through the requestInit + // object ends up being used to make the request. + + // eslint-disable-next-line no-unsafe-finally + throw new FetchRequestError( + asRequest(input, init), + 500, + 'Unable to enforce SSRF protection', + ) + } + } + } + } + } + } +} + +export function unicastLookup( + hostname: string, + options: dns.LookupOptions, + callback: Parameters[2], +) { + if (!isValidDomain(hostname)) { + callback(new Error('Hostname is not a public domain'), '') + return + } + + dns.lookup(hostname, options, (err, address, family) => { + if (err) { + callback(err, address, family) + } else { + const ips = Array.isArray(address) + ? address.map(parseLookupAddress) + : [parseLookupAddress({ address, family })] + + if (ips.some(isNotUnicast)) { + callback( + new Error('Hostname resolved to non-unicast address'), + address, + family, + ) + } else { + callback(null, address, family) + } + } + }) +} + +function isNotUnicast(ip: ipaddr.IPv4 | ipaddr.IPv6): boolean { + return ip.range() !== 'unicast' +} + +function parseLookupAddress({ + address, + family, +}: LookupAddress): ipaddr.IPv4 | ipaddr.IPv6 { + const ip = family === 4 ? IPv4.parse(address) : IPv6.parse(address) + + if (ip instanceof IPv6 && ip.isIPv4MappedAddress()) { + return ip.toIPv4Address() + } else { + return ip + } +} diff --git a/packages/internal/fetch-node/src/util.ts b/packages/internal/fetch-node/src/util.ts new file mode 100644 index 00000000000..d2889d9539c --- /dev/null +++ b/packages/internal/fetch-node/src/util.ts @@ -0,0 +1,22 @@ +import ipaddr from 'ipaddr.js' + +const { IPv4, IPv6 } = ipaddr + +function parseIpHostname( + hostname: string, +): ipaddr.IPv4 | ipaddr.IPv6 | undefined { + if (IPv4.isIPv4(hostname)) { + return IPv4.parse(hostname) + } + + if (hostname.startsWith('[') && hostname.endsWith(']')) { + return IPv6.parse(hostname.slice(1, -1)) + } + + return undefined +} + +export function isUnicastIp(hostname: string): boolean | undefined { + const ip = parseIpHostname(hostname) + return ip ? ip.range() === 'unicast' : undefined +} diff --git a/packages/internal/fetch/CHANGELOG.md b/packages/internal/fetch/CHANGELOG.md index 820533c332b..0c495f02274 100644 --- a/packages/internal/fetch/CHANGELOG.md +++ b/packages/internal/fetch/CHANGELOG.md @@ -1,5 +1,15 @@ # @atproto-labs/fetch +## 0.1.1 + +### Patch Changes + +- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose extractUrl utility + +- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add redirectCheckRequestTransform utility to prevent request redirects + +- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow customizing fetch logging function + ## 0.1.0 ### Minor Changes diff --git a/packages/internal/fetch/package.json b/packages/internal/fetch/package.json index ac68d0fcf03..55c58c36299 100644 --- a/packages/internal/fetch/package.json +++ b/packages/internal/fetch/package.json @@ -1,6 +1,6 @@ { "name": "@atproto-labs/fetch", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "description": "Isomorphic wrapper utilities for fetch API", "keywords": [ diff --git a/packages/internal/fetch/src/fetch-request.ts b/packages/internal/fetch/src/fetch-request.ts index 6d84ec8cb2a..eabe02851f7 100644 --- a/packages/internal/fetch/src/fetch-request.ts +++ b/packages/internal/fetch/src/fetch-request.ts @@ -1,6 +1,6 @@ import { FetchError } from './fetch-error.js' import { asRequest } from './fetch.js' -import { isIp } from './util.js' +import { extractUrl, isIp } from './util.js' export class FetchRequestError extends FetchError { constructor( @@ -18,26 +18,51 @@ export class FetchRequestError extends FetchError { } } -const extractUrl = (input: Request | string | URL) => - typeof input === 'string' - ? new URL(input) - : input instanceof URL - ? input - : new URL(input.url) - -export function protocolCheckRequestTransform(protocols: Iterable) { - const allowedProtocols = new Set(protocols) - +export function protocolCheckRequestTransform(protocols: { + 'about:'?: boolean + 'blob:'?: boolean + 'data:'?: boolean + 'file:'?: boolean + 'http:'?: boolean | { allowCustomPort: boolean } + 'https:'?: boolean | { allowCustomPort: boolean } +}) { return (input: Request | string | URL, init?: RequestInit) => { - const { protocol } = extractUrl(input) + const { protocol, port } = extractUrl(input) const request = asRequest(input, init) - if (!allowedProtocols.has(protocol)) { + const config: undefined | boolean | { allowCustomPort?: boolean } = + Object.hasOwn(protocols, protocol) ? protocols[protocol] : undefined + + if (!config) { + throw new FetchRequestError( + request, + 400, + `Forbidden protocol "${protocol}"`, + ) + } else if (config === true) { + // Safe to proceed + } else if (!config['allowCustomPort'] && port !== '') { throw new FetchRequestError( request, 400, - `"${protocol}" protocol is not allowed`, + `Custom ${protocol} ports not allowed`, + ) + } + + return request + } +} + +export function redirectCheckRequestTransform() { + return (input: Request | string | URL, init?: RequestInit) => { + const request = asRequest(input, init) + + if (request.redirect === 'follow') { + throw new FetchRequestError( + request, + 500, + 'Request redirect must be "error" or "manual"', ) } @@ -45,7 +70,7 @@ export function protocolCheckRequestTransform(protocols: Iterable) { } } -export function requireHostHeaderTranform() { +export function requireHostHeaderTransform() { return (input: Request | string | URL, init?: RequestInit) => { // Note that fetch() will automatically add the Host header from the URL and // discard any Host header manually set in the request. @@ -90,7 +115,7 @@ export function forbiddenDomainNameRequestTransform( // Optimization: if no forbidden domain names are provided, we can skip the // check entirely. if (denySet.size === 0) { - return async (request) => request + return asRequest } return async (input: Request | string | URL, init?: RequestInit) => { diff --git a/packages/internal/fetch/src/fetch-wrap.ts b/packages/internal/fetch/src/fetch-wrap.ts index 71291003b81..e619aac863d 100644 --- a/packages/internal/fetch/src/fetch-wrap.ts +++ b/packages/internal/fetch/src/fetch-wrap.ts @@ -3,29 +3,57 @@ import { Fetch, FetchContext, toRequestTransformer } from './fetch.js' import { TransformedResponse } from './transformed-response.js' import { padLines, stringifyMessage } from './util.js' -export function loggedFetch( - fetch: Fetch = globalThis.fetch, -) { +type LogFn = (...args: Args) => void | PromiseLike + +export function loggedFetch({ + fetch = globalThis.fetch as Fetch, + logRequest = true as boolean | LogFn<[request: Request]>, + logResponse = true as boolean | LogFn<[response: Response, request: Request]>, + logError = true as boolean | LogFn<[error: unknown, request: Request]>, +}) { + const onRequest = + logRequest === true + ? async (request) => { + const requestMessage = await stringifyMessage(request) + console.info( + `> ${request.method} ${request.url}\n${padLines(requestMessage, ' ')}`, + ) + } + : logRequest || undefined + + const onResponse = + logResponse === true + ? async (response) => { + const responseMessage = await stringifyMessage(response.clone()) + console.info( + `< HTTP/1.1 ${response.status} ${response.statusText}\n${padLines(responseMessage, ' ')}`, + ) + } + : logResponse || undefined + + const onError = + logError === true + ? async (error) => { + console.error(`< Error:`, error) + } + : logError || undefined + + if (!onRequest && !onResponse && !onError) return fetch + return toRequestTransformer(async function ( this: C, request, ): Promise { - const requestMessage = await stringifyMessage(request) - console.info( - `> ${request.method} ${request.url}\n${padLines(requestMessage, ' ')}`, - ) + if (onRequest) await onRequest(request) try { const response = await fetch.call(this, request) - const responseMessage = await stringifyMessage(response.clone()) - console.info( - `< HTTP/1.1 ${response.status} ${response.statusText}\n${padLines(responseMessage, ' ')}`, - ) + if (onResponse) await onResponse(response, request) return response } catch (error) { - console.error(`< Error:`, error) + if (onError) await onError(error, request) throw error } diff --git a/packages/internal/fetch/src/util.ts b/packages/internal/fetch/src/util.ts index ffc518a2b2a..67d5a81e28f 100644 --- a/packages/internal/fetch/src/util.ts +++ b/packages/internal/fetch/src/util.ts @@ -167,3 +167,10 @@ async function stringifyBody(body: Body) { return '[Body could not be read]' } } + +export const extractUrl = (input: Request | string | URL) => + typeof input === 'string' + ? new URL(input) + : input instanceof URL + ? input + : new URL(input.url) diff --git a/packages/internal/handle-resolver-node/CHANGELOG.md b/packages/internal/handle-resolver-node/CHANGELOG.md index 6e8933bc61c..04e365c6580 100644 --- a/packages/internal/handle-resolver-node/CHANGELOG.md +++ b/packages/internal/handle-resolver-node/CHANGELOG.md @@ -1,5 +1,27 @@ # @atproto-labs/handle-resolver-node +## 0.1.5 + +### Patch Changes + +- Updated dependencies [[`8943c1008`](https://github.com/bluesky-social/atproto/commit/8943c10082702bbc0fc150237c6cc421251afd51)]: + - @atproto-labs/fetch-node@0.1.2 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]: + - @atproto-labs/fetch-node@0.1.1 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [[`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c)]: + - @atproto/did@0.1.2 + - @atproto-labs/handle-resolver@0.1.3 + ## 0.1.2 ### Patch Changes diff --git a/packages/internal/handle-resolver-node/package.json b/packages/internal/handle-resolver-node/package.json index 8488319d66a..2f9492a33ae 100644 --- a/packages/internal/handle-resolver-node/package.json +++ b/packages/internal/handle-resolver-node/package.json @@ -1,6 +1,6 @@ { "name": "@atproto-labs/handle-resolver-node", - "version": "0.1.2", + "version": "0.1.5", "license": "MIT", "description": "Node specific ATProto handle to DID resolver", "keywords": [ diff --git a/packages/internal/handle-resolver/CHANGELOG.md b/packages/internal/handle-resolver/CHANGELOG.md index 723ffd691ce..da91f2546e7 100644 --- a/packages/internal/handle-resolver/CHANGELOG.md +++ b/packages/internal/handle-resolver/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto-labs/handle-resolver +## 0.1.3 + +### Patch Changes + +- Updated dependencies [[`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c)]: + - @atproto/did@0.1.2 + ## 0.1.2 ### Patch Changes diff --git a/packages/internal/handle-resolver/package.json b/packages/internal/handle-resolver/package.json index d31e93b6fe7..f90d1634f99 100644 --- a/packages/internal/handle-resolver/package.json +++ b/packages/internal/handle-resolver/package.json @@ -1,6 +1,6 @@ { "name": "@atproto-labs/handle-resolver", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "description": "Isomorphic ATProto handle to DID resolver", "keywords": [ diff --git a/packages/internal/identity-resolver/CHANGELOG.md b/packages/internal/identity-resolver/CHANGELOG.md index 16526b20802..43fd758f05f 100644 --- a/packages/internal/identity-resolver/CHANGELOG.md +++ b/packages/internal/identity-resolver/CHANGELOG.md @@ -1,5 +1,20 @@ # @atproto-labs/identity-resolver +## 0.1.4 + +### Patch Changes + +- Updated dependencies []: + - @atproto-labs/did-resolver@0.1.4 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies []: + - @atproto-labs/did-resolver@0.1.3 + - @atproto-labs/handle-resolver@0.1.3 + ## 0.1.2 ### Patch Changes diff --git a/packages/internal/identity-resolver/package.json b/packages/internal/identity-resolver/package.json index 4cfe7684b20..67ee4c9052e 100644 --- a/packages/internal/identity-resolver/package.json +++ b/packages/internal/identity-resolver/package.json @@ -1,6 +1,6 @@ { "name": "@atproto-labs/identity-resolver", - "version": "0.1.2", + "version": "0.1.4", "license": "MIT", "description": "A library resolving ATPROTO identities", "keywords": [ diff --git a/packages/lex-cli/CHANGELOG.md b/packages/lex-cli/CHANGELOG.md index 735c481db47..501e7f52a58 100644 --- a/packages/lex-cli/CHANGELOG.md +++ b/packages/lex-cli/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/lex-cli +## 0.5.1 + +### Patch Changes + +- Updated dependencies [[`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94)]: + - @atproto/lexicon@0.4.2 + ## 0.5.0 ### Minor Changes diff --git a/packages/lex-cli/package.json b/packages/lex-cli/package.json index efbbb346c32..b0897a39e43 100644 --- a/packages/lex-cli/package.json +++ b/packages/lex-cli/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/lex-cli", - "version": "0.5.0", + "version": "0.5.1", "license": "MIT", "description": "TypeScript codegen tool for atproto Lexicon schemas", "keywords": [ diff --git a/packages/lexicon/CHANGELOG.md b/packages/lexicon/CHANGELOG.md index b3960fcba62..3f7e5239979 100644 --- a/packages/lexicon/CHANGELOG.md +++ b/packages/lexicon/CHANGELOG.md @@ -1,5 +1,14 @@ # @atproto/lexicon +## 0.4.2 + +### Patch Changes + +- [#2817](https://github.com/bluesky-social/atproto/pull/2817) [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94) Thanks [@gaearon](https://github.com/gaearon)! - Add fast path skipping grapheme counting + +- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6)]: + - @atproto/common-web@0.3.1 + ## 0.4.1 ### Patch Changes diff --git a/packages/lexicon/package.json b/packages/lexicon/package.json index c9c2de88fcb..66c86c26ade 100644 --- a/packages/lexicon/package.json +++ b/packages/lexicon/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/lexicon", - "version": "0.4.1", + "version": "0.4.2", "license": "MIT", "description": "atproto Lexicon schema language library", "keywords": [ diff --git a/packages/lexicon/src/validators/primitives.ts b/packages/lexicon/src/validators/primitives.ts index ece7e06237c..007dc7be234 100644 --- a/packages/lexicon/src/validators/primitives.ts +++ b/packages/lexicon/src/validators/primitives.ts @@ -197,50 +197,90 @@ export function string( } } - // maxLength - if (typeof def.maxLength === 'number') { - if (utf8Len(value) > def.maxLength) { - return { - success: false, - error: new ValidationError( - `${path} must not be longer than ${def.maxLength} characters`, - ), + // maxLength and minLength + if (typeof def.maxLength === 'number' || typeof def.minLength === 'number') { + const len = utf8Len(value) + + if (typeof def.maxLength === 'number') { + if (len > def.maxLength) { + return { + success: false, + error: new ValidationError( + `${path} must not be longer than ${def.maxLength} characters`, + ), + } } } - } - // minLength - if (typeof def.minLength === 'number') { - if (utf8Len(value) < def.minLength) { - return { - success: false, - error: new ValidationError( - `${path} must not be shorter than ${def.minLength} characters`, - ), + if (typeof def.minLength === 'number') { + if (len < def.minLength) { + return { + success: false, + error: new ValidationError( + `${path} must not be shorter than ${def.minLength} characters`, + ), + } } } } - // maxGraphemes - if (typeof def.maxGraphemes === 'number') { - if (graphemeLen(value) > def.maxGraphemes) { - return { - success: false, - error: new ValidationError( - `${path} must not be longer than ${def.maxGraphemes} graphemes`, - ), + // maxGraphemes and minGraphemes + if ( + typeof def.maxGraphemes === 'number' || + typeof def.minGraphemes === 'number' + ) { + let needsMaxGraphemesCheck = false + let needsMinGraphemesCheck = false + + if (typeof def.maxGraphemes === 'number') { + if (value.length <= def.maxGraphemes) { + // If the JavaScript string length (UTF-16) is within the maximum limit, + // its grapheme length (which <= .length) will also be within. + needsMaxGraphemesCheck = false + } else { + needsMaxGraphemesCheck = true } } - } - // minGraphemes - if (typeof def.minGraphemes === 'number') { - if (graphemeLen(value) < def.minGraphemes) { - return { - success: false, - error: new ValidationError( - `${path} must not be shorter than ${def.minGraphemes} graphemes`, - ), + if (typeof def.minGraphemes === 'number') { + if (value.length < def.minGraphemes) { + // If the JavaScript string length (UTF-16) is below the minimal limit, + // its grapheme length (which <= .length) will also be below. + // Fail early. + return { + success: false, + error: new ValidationError( + `${path} must not be shorter than ${def.minGraphemes} graphemes`, + ), + } + } else { + needsMinGraphemesCheck = true + } + } + + if (needsMaxGraphemesCheck || needsMinGraphemesCheck) { + const len = graphemeLen(value) + + if (typeof def.maxGraphemes === 'number') { + if (len > def.maxGraphemes) { + return { + success: false, + error: new ValidationError( + `${path} must not be longer than ${def.maxGraphemes} graphemes`, + ), + } + } + } + + if (typeof def.minGraphemes === 'number') { + if (len < def.minGraphemes) { + return { + success: false, + error: new ValidationError( + `${path} must not be shorter than ${def.minGraphemes} graphemes`, + ), + } + } } } } diff --git a/packages/lexicon/tests/general.test.ts b/packages/lexicon/tests/general.test.ts index ca9cb44dc34..2d493a23d09 100644 --- a/packages/lexicon/tests/general.test.ts +++ b/packages/lexicon/tests/general.test.ts @@ -592,20 +592,97 @@ describe('Record validation', () => { }) it('Applies grapheme string length constraint', () => { + // Shorter than two graphemes + expect(() => + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: '', + }), + ).toThrow('Record/string must not be shorter than 2 graphemes') + expect(() => + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: '\u0301\u0301\u0301', // Three combining acute accents + }), + ).toThrow('Record/string must not be shorter than 2 graphemes') + expect(() => + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: 'a', + }), + ).toThrow('Record/string must not be shorter than 2 graphemes') + expect(() => + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: 'a\u0301\u0301\u0301\u0301', // 'á́́́' ('a' with four combining acute accents) + }), + ).toThrow('Record/string must not be shorter than 2 graphemes') + expect(() => + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: '5\uFE0F', // '5️' with emoji presentation + }), + ).toThrow('Record/string must not be shorter than 2 graphemes') + expect(() => + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: '👨‍👩‍👧‍👧', + }), + ).toThrow('Record/string must not be shorter than 2 graphemes') + + // Two to four graphemes + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: 'ab', + }) + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: 'a\u0301b', // 'áb' with combining accent + }) + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: 'a\u0301b\u0301', // 'áb́' + }) + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: '😀😀', + }) lex.assertValidRecord('com.example.stringLengthGrapheme', { $type: 'com.example.stringLengthGrapheme', string: '12👨‍👩‍👧‍👧', }) + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: 'abcd', + }) + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: 'a\u0301b\u0301c\u0301d\u0301', // 'áb́ćd́' + }) + + // Longer than four graphemes expect(() => lex.assertValidRecord('com.example.stringLengthGrapheme', { $type: 'com.example.stringLengthGrapheme', - string: '👨‍👩‍👧‍👧', + string: 'abcde', }), - ).toThrow('Record/string must not be shorter than 2 graphemes') + ).toThrow('Record/string must not be longer than 4 graphemes') expect(() => lex.assertValidRecord('com.example.stringLengthGrapheme', { $type: 'com.example.stringLengthGrapheme', - string: '12345', + string: 'a\u0301b\u0301c\u0301d\u0301e\u0301', // 'áb́ćd́é' + }), + ).toThrow('Record/string must not be longer than 4 graphemes') + expect(() => + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: '😀😀😀😀😀', + }), + ).toThrow('Record/string must not be longer than 4 graphemes') + expect(() => + lex.assertValidRecord('com.example.stringLengthGrapheme', { + $type: 'com.example.stringLengthGrapheme', + string: 'ab😀de', }), ).toThrow('Record/string must not be longer than 4 graphemes') }) diff --git a/packages/oauth/oauth-client-browser/CHANGELOG.md b/packages/oauth/oauth-client-browser/CHANGELOG.md index 3da2c4e04b8..297a05e26e1 100644 --- a/packages/oauth/oauth-client-browser/CHANGELOG.md +++ b/packages/oauth/oauth-client-browser/CHANGELOG.md @@ -1,5 +1,77 @@ # @atproto/oauth-client-browser +## 0.2.2 + +### Patch Changes + +- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve invalid client_id error messages from BrowserOAuthClient.from() + +- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Do not strip query string from URL after oauth redirect in fragment mode + +- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Relax type restriction on clientId option + +- Updated dependencies [[`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8)]: + - @atproto/oauth-types@0.1.5 + - @atproto/oauth-client@0.2.2 + - @atproto-labs/did-resolver@0.1.4 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c)]: + - @atproto/did@0.1.2 + - @atproto-labs/did-resolver@0.1.3 + - @atproto-labs/handle-resolver@0.1.3 + - @atproto/oauth-client@0.2.1 + +## 0.2.0 + +### Minor Changes + +- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The `OAuthClient` (and runtime specific sub-classes) no longer return @atproto/api `Agent` instances. Instead, they return `OAuthSession` instances that can be used to instantiate the `Agent` class. + +- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove "openid" compatibility. The reason is that although we were technically "openid" compatible, ATProto identifiers are distributed identifiers. When a client relies on OpenID to authenticate users, it will use the auth provider in combination with the identifier to uniquely identify the user. Since ATProto identifiers are meant to be able to move from one provider to the other, OpenID compatibility could break authentication after a user was migrated to a different provider. + + The way OpenID compliant clients would adapt to this particularity would typically be to remove the provider + identifier combination and use the identifier alone. While this is indeed the right way to handle ATProto identifiers, it requires more work to avoid impersonation. In particular, when obtaining a user identifier, the client **must** verify that the issuer of the identity token is indeed the server responsible for that user. This mechanism being not enforced by the OpenID standard, OpenID compatibility could lead to security issues. For this reason, we decided to remove OpenID compatibility from the OAuth provider. + + Note that a trusted central authority could still offer OpenID compatibility by relying on ATProto's regular OAuth flow under the hood. This capability is out of the scope of this library. + +### Patch Changes + +- Updated dependencies [[`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]: + - @atproto/oauth-client@0.2.0 + - @atproto/oauth-types@0.1.4 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies []: + - @atproto/oauth-client@0.1.7 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies []: + - @atproto/oauth-client@0.1.6 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies [[`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`3ebcd4e61`](https://github.com/bluesky-social/atproto/commit/3ebcd4e6161291d3649d7f8a9c5ee4ac26d590a2), [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb)]: + - @atproto/oauth-client@0.1.5 + - @atproto/oauth-types@0.1.3 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [[`04112783d`](https://github.com/bluesky-social/atproto/commit/04112783db17f865c9e2b673190f77dd0b7461e3)]: + - @atproto/oauth-client@0.1.4 + ## 0.1.3 ### Patch Changes diff --git a/packages/oauth/oauth-client-browser/README.md b/packages/oauth/oauth-client-browser/README.md index 344987bc88e..ce10c240ac5 100644 --- a/packages/oauth/oauth-client-browser/README.md +++ b/packages/oauth/oauth-client-browser/README.md @@ -1,11 +1,12 @@ # atproto OAuth Client for the Browser -This package provides an OAuth bases `@atproto/api` agent interface for the -browser. It implements all the OAuth features required by [ATPROTO] (PKCE, DPoP, +This package provides a browser specific OAuth client implementation for +atproto. It implements all the OAuth features required by [ATPROTO] (PKCE, DPoP, etc.). `@atproto/oauth-client-browser` is designed for front-end applications that do -not have a backend server to manage OAuth sessions. +not have a backend server to manage OAuth sessions, a.k.a "Single Page +Applications" (SPA). > [!IMPORTANT] > @@ -44,7 +45,7 @@ needs of your application and must respect the [ATPROTO] spec. "tos_uri": "https://my-app.com/tos", "policy_uri": "https://my-app.com/policy", "redirect_uris": ["https://my-app.com/callback"], - "scope": "profile email offline_access", + "scope": "atproto", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none", @@ -163,22 +164,24 @@ initialize itself. Note that this operation must be performed once (and **only once**) whenever the web app is loaded. ```typescript -const result: undefined | { agent: OAuthAgent; state?: string } = +const result: undefined | { session: OAuthSession; state?: string } = await client.init() if (result) { - const { agent, state } = result + const { session, state } = result if (state != null) { - console.log(`${agent.sub} was successfully authenticated (state: ${state})`) + console.log( + `${session.sub} was successfully authenticated (state: ${state})`, + ) } else { - console.log(`${agent.sub} was restored (last active session)`) + console.log(`${session.sub} was restored (last active session)`) } } ``` The return value can be used to determine if the client was able to restore the -last used session (`agent` is defined) or if the current navigation is the -result of an authorization redirect (both `agent` and `state` are defined). +last used session (`session` is defined) or if the current navigation is the +result of an authorization redirect (both `session` and `state` are defined). ### Initiating an OAuth flow @@ -217,18 +220,21 @@ the OAuth server). The promise will reject if the user cancels the sign in When the user is redirected back to the application, the OAuth response will be available in the URL. The `BrowserOAuthClient` will automatically detect the -response and handle it when `client.init()` is called. +response and handle it when `client.init()` is called. Alternatively, the +application can manually handle the response using the +`client.callback(urlQueryParams)` method. ### Restoring a session -The client keeps an internal store of all the sessions that it manages. -Regardless of the agent that was returned from the `client.init()` call, -any other session can be loaded into a new agent using the `client.restore()` -method. +The client keeps track of all the sessions that it manages through an internal +store. Regardless of the session that was returned from the `client.init()` +call, any other session can be loaded using the `client.restore()` method. This +method will throw an error if the session is no longer available or if it has +become expired. ```ts -const aliceAgent = await client.restore('did:plc:alice') -const bobAgent = await client.restore('did:plc:bob') +const aliceSession = await client.restore('did:plc:alice') +const bobSession = await client.restore('did:plc:bob') ``` In its current form, the client does not expose methods to list all sessions @@ -256,16 +262,19 @@ client.addEventListener( ## Usage with `@atproto/api` -The `@atproto/api` package provides a way to interact with the `com.atproto` and -`app.bsky` XRPC lexicons through the `Agent` interface. The `agent` returned -by the `BrowserOAuthClient` extend the `Agent` class, allowing to use the -`BrowserOAuthClient` as a regular `Agent` (akin to `AtpAgent` class -instances). +The `@atproto/api` package provides a way to interact with multiple Bluesky +specific XRPC lexicons (`com.atproto`, `app.bsky`, `chat.bsky`, `tools.ozone`) +through the `Agent` interface. The `oauthSession` returned by the +`BrowserOAuthClient` can be used to instantiate an `Agent` instance. ```typescript -const aliceAgent = await client.restore('did:plc:alice') +import { Agent } from '@atproto/api' -await aliceAgent.getProfile({ actor: aliceAgent.did }) +const session = await client.restore('did:plc:alice') + +const agent = new Agent(session) + +await agent.getProfile({ actor: agent.accountDid }) ``` Any refresh of the credentials will happen under the hood, and the new tokens diff --git a/packages/oauth/oauth-client-browser/example/src/app.tsx b/packages/oauth/oauth-client-browser/example/src/app.tsx index 3625d2f67ca..82e01236170 100644 --- a/packages/oauth/oauth-client-browser/example/src/app.tsx +++ b/packages/oauth/oauth-client-browser/example/src/app.tsx @@ -1,8 +1,18 @@ import { useCallback, useState } from 'react' import { useAuthContext } from './auth/auth-provider' +import { OAuthSession } from '@atproto/oauth-client' function App() { - const { pdsAgent, signOut } = useAuthContext() + const { pdsAgent, signOut, refresh } = useAuthContext() + + const hasTokenInfo = pdsAgent.sessionManager instanceof OAuthSession + + const [tokeninfo, setTokeninfo] = useState(undefined) + const loadTokeninfo = useCallback(async () => { + if (pdsAgent.sessionManager instanceof OAuthSession) { + setTokeninfo(await pdsAgent.sessionManager.getTokenInfo()) + } + }, [pdsAgent]) // A call that requires to be authenticated const [serviceAuth, setServiceAuth] = useState(undefined) @@ -30,6 +40,19 @@ function App() {

Logged in!

+ {hasTokenInfo && ( + <> + + +
+              {tokeninfo !== undefined
+                ? JSON.stringify(tokeninfo, undefined, 2)
+                : null}
+            
+
+ + )} +
@@ -46,6 +69,8 @@ function App() {
         
+ +
) diff --git a/packages/oauth/oauth-client-browser/example/src/auth/auth-form.tsx b/packages/oauth/oauth-client-browser/example/src/auth/auth-form.tsx index 336ef420f45..acec28717c5 100644 --- a/packages/oauth/oauth-client-browser/example/src/auth/auth-form.tsx +++ b/packages/oauth/oauth-client-browser/example/src/auth/auth-form.tsx @@ -1,6 +1,9 @@ -import { useCallback, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' -import { AtpSignIn, AtpSignInForm } from './atp/atp-sign-in-form' +import { + AtpSignIn, + CredentialSignInForm, +} from './credential/credential-sign-in-form' import { OAuthSignIn, OAuthSignInForm } from './oauth/oauth-sign-in-form' export function AuthForm({ @@ -10,19 +13,20 @@ export function AuthForm({ atpSignIn?: AtpSignIn oauthSignIn?: OAuthSignIn }) { - const defaultMethod = useCallback( - () => (oauthSignIn ? 'oauth' : atpSignIn ? 'atp' : undefined), - [], - ) + const defaultMethod = oauthSignIn + ? 'oauth' + : atpSignIn + ? 'credential' + : undefined - const [method, setMethod] = useState( + const [method, setMethod] = useState( defaultMethod, ) useEffect(() => { if (method === 'oauth' && !oauthSignIn) { setMethod(defaultMethod) - } else if (method === 'atp' && !atpSignIn) { + } else if (method === 'credential' && !atpSignIn) { setMethod(defaultMethod) } else if (!method) { setMethod(defaultMethod) @@ -45,9 +49,9 @@ export function AuthForm({