From 4e04aaf224efc6bfe13aace84414a1af51339e56 Mon Sep 17 00:00:00 2001 From: Luke VanderHart Date: Sun, 2 Feb 2025 13:20:39 -0500 Subject: [PATCH 1/8] platform-agnostic http api - create simple HTTP protocol to allow for alternate impls - use callbacks as lowest common denominator for async code - simple interceptor pattern to facilitate auth and other content types --- README.md | 46 +- .../atproto-xrpc-openapi.2024-12-18.json | 23911 ---------------- src/net/gosha/atproto/client.clj | 166 - src/net/gosha/atproto/client.cljc | 166 + src/net/gosha/atproto/impl/jvm.clj | 39 + src/net/gosha/atproto/interceptor.cljc | 120 + 6 files changed, 358 insertions(+), 24090 deletions(-) delete mode 100644 resources/atproto-xrpc-openapi.2024-12-18.json delete mode 100644 src/net/gosha/atproto/client.clj create mode 100644 src/net/gosha/atproto/client.cljc create mode 100644 src/net/gosha/atproto/impl/jvm.clj create mode 100644 src/net/gosha/atproto/interceptor.cljc diff --git a/README.md b/README.md index fe83d3b..4054ef5 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ Work very much in progress +Multi-platform codebase designed to work in Clojure, ClojureScript and +ClojureDart. + ## Progress | Feature | Status | @@ -26,30 +29,47 @@ Work very much in progress ## Usage -### HTTP client +### ATProto client + +The workflow for utilizing the client is to: + +1. Obtain a session by specifying the ATProto endpoint and (optionally) authentication credentials. +2. Use the session to make `query` or `procedure` calls to [ATProto](https://atproto.com/specs/xrpc#lexicon-http-endpoints) or [Bluesky](https://docs.bsky.app/docs/category/http-reference) endpoints. + +A session is a thread-safe, stateful object containing the information required to make ATProto HTTP requests. -The client is using [Martian](https://github.com/oliyh/martian/) under the hood to handle the HTTP endpoints [published](https://github.com/bluesky-social/bsky-docs/tree/main/atproto-openapi-types) by the Bsky team in OpenAPI format +All calls use the "NSID" of the query or procedure, and a Clojure map of parameters/input. All queries are asynchronous, and return immediately. The return value depends on platform: + +- Clojure: a Clojure promise. +- ClojureScript: a core.async channel. +- ClojureDart: a Dart Watchable. + +You can also provide a `:channel`, `:callback` or `:promise` keyword option to recieve the return value. Not all options are supported on all platforms. ```clojure (require '[net.gosha.atproto.client :as at]) ;; Unauthenticated client -(def session (at/init :base-url "https://public.api.bsky.app")) - -;; Authenticated client -(def session (at/init :username "me.bsky.social" - :app-password "SECRET" - :base-url "https://bsky.social")) +(def session (at/init "https://public.api.bsky.app")) +;; Password-based authenticated client +(def session (at/init "https://bsky.social" + :identifier "me.bsky.social" + :password "SECRET")) ;; Bluesky endpoints and their query params can be found here: ;; https://docs.bsky.app/docs/category/http-reference -(let [resp (at/call session :app.bsky.actor.get-profile {:actor "gosha.net"})] - (select-keys (:body @resp) [:handle :displayName :createdAt :followersCount])) + +@(at/query session :app.bsky.actor.get-profile {:actor "gosha.net"}) ;; => {:handle "gosha.net", -;; :displayName "Gosha ⚡", -;; :createdAt "2023-05-08T19:08:05.781Z", -;; :followersCount 617} +;; :displayName "Gosha ⚡", +;; :did "did:plc:ypjjs7u7owjb7xmueb2iw37u", +;; ......} + +;; Using core.async +(def result (async/chan)) +(at/query sess :app.bsky.actor.getProfile {:actor "gosha.net"} :channel result) +(async/) to filter by. If not specified, all events are returned.", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "createdBy", - "in": "query", - "required": false, - "schema": { - "type": "string", - "format": "did" - } - }, - { - "name": "sortDirection", - "in": "query", - "description": "Sort direction for the events. Defaults to descending order of created at timestamp.", - "required": false, - "schema": { - "type": "string", - "description": "Sort direction for the events. Defaults to descending order of created at timestamp.", - "default": "desc" - } - }, - { - "name": "createdAfter", - "in": "query", - "description": "Retrieve events created after a given timestamp", - "required": false, - "schema": { - "type": "string", - "description": "Retrieve events created after a given timestamp", - "format": "date-time" - } - }, - { - "name": "createdBefore", - "in": "query", - "description": "Retrieve events created before a given timestamp", - "required": false, - "schema": { - "type": "string", - "description": "Retrieve events created before a given timestamp", - "format": "date-time" - } - }, - { - "name": "subject", - "in": "query", - "required": false, - "schema": { - "type": "string", - "format": "uri" - } - }, - { - "name": "collections", - "in": "query", - "description": "If specified, only events where the subject belongs to the given collections will be returned. When subjectType is set to 'account', this will be ignored.", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "nsid" - }, - "maxItems": 20 - } - }, - { - "name": "subjectType", - "in": "query", - "description": "If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.", - "required": false, - "schema": { - "type": "string", - "description": "If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.", - "enum": [ - "account", - "record" - ] - } - }, - { - "name": "includeAllUserRecords", - "in": "query", - "description": "If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned.", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned." - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - } - }, - { - "name": "hasComment", - "in": "query", - "description": "If true, only events with comments are returned", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, only events with comments are returned" - } - }, - { - "name": "comment", - "in": "query", - "description": "If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition.", - "required": false, - "schema": { - "type": "string", - "description": "If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition." - } - }, - { - "name": "addedLabels", - "in": "query", - "description": "If specified, only events where all of these labels were added are returned", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "removedLabels", - "in": "query", - "description": "If specified, only events where all of these labels were removed are returned", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "addedTags", - "in": "query", - "description": "If specified, only events where all of these tags were added are returned", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "removedTags", - "in": "query", - "description": "If specified, only events where all of these tags were removed are returned", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "reportTypes", - "in": "query", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "events" - ], - "properties": { - "cursor": { - "type": "string" - }, - "events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventView" - } - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.moderation.queryStatuses": { - "get": { - "tags": [ - "tools.ozone.moderation" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nView moderation statuses of subjects (record or repo).", - "operationId": "tools.ozone.moderation.queryStatuses", - "security": [ - { - "Bearer": [] - } - ], - "parameters": [ - { - "name": "includeAllUserRecords", - "in": "query", - "description": "All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned.", - "required": false, - "schema": { - "type": "boolean", - "description": "All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned." - } - }, - { - "name": "subject", - "in": "query", - "description": "The subject to get the status for.", - "required": false, - "schema": { - "type": "string", - "description": "The subject to get the status for.", - "format": "uri" - } - }, - { - "name": "comment", - "in": "query", - "description": "Search subjects by keyword from comments", - "required": false, - "schema": { - "type": "string", - "description": "Search subjects by keyword from comments" - } - }, - { - "name": "reportedAfter", - "in": "query", - "description": "Search subjects reported after a given timestamp", - "required": false, - "schema": { - "type": "string", - "description": "Search subjects reported after a given timestamp", - "format": "date-time" - } - }, - { - "name": "reportedBefore", - "in": "query", - "description": "Search subjects reported before a given timestamp", - "required": false, - "schema": { - "type": "string", - "description": "Search subjects reported before a given timestamp", - "format": "date-time" - } - }, - { - "name": "reviewedAfter", - "in": "query", - "description": "Search subjects reviewed after a given timestamp", - "required": false, - "schema": { - "type": "string", - "description": "Search subjects reviewed after a given timestamp", - "format": "date-time" - } - }, - { - "name": "hostingDeletedAfter", - "in": "query", - "description": "Search subjects where the associated record/account was deleted after a given timestamp", - "required": false, - "schema": { - "type": "string", - "description": "Search subjects where the associated record/account was deleted after a given timestamp", - "format": "date-time" - } - }, - { - "name": "hostingDeletedBefore", - "in": "query", - "description": "Search subjects where the associated record/account was deleted before a given timestamp", - "required": false, - "schema": { - "type": "string", - "description": "Search subjects where the associated record/account was deleted before a given timestamp", - "format": "date-time" - } - }, - { - "name": "hostingUpdatedAfter", - "in": "query", - "description": "Search subjects where the associated record/account was updated after a given timestamp", - "required": false, - "schema": { - "type": "string", - "description": "Search subjects where the associated record/account was updated after a given timestamp", - "format": "date-time" - } - }, - { - "name": "hostingUpdatedBefore", - "in": "query", - "description": "Search subjects where the associated record/account was updated before a given timestamp", - "required": false, - "schema": { - "type": "string", - "description": "Search subjects where the associated record/account was updated before a given timestamp", - "format": "date-time" - } - }, - { - "name": "hostingStatuses", - "in": "query", - "description": "Search subjects by the status of the associated record/account", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "reviewedBefore", - "in": "query", - "description": "Search subjects reviewed before a given timestamp", - "required": false, - "schema": { - "type": "string", - "description": "Search subjects reviewed before a given timestamp", - "format": "date-time" - } - }, - { - "name": "includeMuted", - "in": "query", - "description": "By default, we don't include muted subjects in the results. Set this to true to include them.", - "required": false, - "schema": { - "type": "boolean", - "description": "By default, we don't include muted subjects in the results. Set this to true to include them." - } - }, - { - "name": "onlyMuted", - "in": "query", - "description": "When set to true, only muted subjects and reporters will be returned.", - "required": false, - "schema": { - "type": "boolean", - "description": "When set to true, only muted subjects and reporters will be returned." - } - }, - { - "name": "reviewState", - "in": "query", - "description": "Specify when fetching subjects in a certain state", - "required": false, - "schema": { - "type": "string", - "description": "Specify when fetching subjects in a certain state" - } - }, - { - "name": "ignoreSubjects", - "in": "query", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "uri" - } - } - }, - { - "name": "lastReviewedBy", - "in": "query", - "description": "Get all subject statuses that were reviewed by a specific moderator", - "required": false, - "schema": { - "type": "string", - "description": "Get all subject statuses that were reviewed by a specific moderator", - "format": "did" - } - }, - { - "name": "sortField", - "in": "query", - "required": false, - "schema": { - "type": "string", - "default": "lastReportedAt" - } - }, - { - "name": "sortDirection", - "in": "query", - "required": false, - "schema": { - "type": "string", - "default": "desc" - } - }, - { - "name": "takendown", - "in": "query", - "description": "Get subjects that were taken down", - "required": false, - "schema": { - "type": "boolean", - "description": "Get subjects that were taken down" - } - }, - { - "name": "appealed", - "in": "query", - "description": "Get subjects in unresolved appealed status", - "required": false, - "schema": { - "type": "boolean", - "description": "Get subjects in unresolved appealed status" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - } - }, - { - "name": "tags", - "in": "query", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "excludeTags", - "in": "query", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "collections", - "in": "query", - "description": "If specified, subjects belonging to the given collections will be returned. When subjectType is set to 'account', this will be ignored.", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "nsid" - }, - "maxItems": 20 - } - }, - { - "name": "subjectType", - "in": "query", - "description": "If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.", - "required": false, - "schema": { - "type": "string", - "description": "If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.", - "enum": [ - "account", - "record" - ] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "subjectStatuses" - ], - "properties": { - "cursor": { - "type": "string" - }, - "subjectStatuses": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.subjectStatusView" - } - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.moderation.searchRepos": { - "get": { - "tags": [ - "tools.ozone.moderation" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nFind repositories based on a search term.", - "operationId": "tools.ozone.moderation.searchRepos", - "security": [ - { - "Bearer": [] - } - ], - "parameters": [ - { - "name": "q", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "repos" - ], - "properties": { - "cursor": { - "type": "string" - }, - "repos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.repoView" - } - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.server.getConfig": { - "get": { - "tags": [ - "tools.ozone.server" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nGet details about ozone's server configuration.", - "operationId": "tools.ozone.server.getConfig", - "security": [ - { - "Bearer": [] - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "appview": { - "$ref": "#/components/schemas/tools.ozone.server.getConfig.serviceConfig" - }, - "pds": { - "$ref": "#/components/schemas/tools.ozone.server.getConfig.serviceConfig" - }, - "blobDivert": { - "$ref": "#/components/schemas/tools.ozone.server.getConfig.serviceConfig" - }, - "chat": { - "$ref": "#/components/schemas/tools.ozone.server.getConfig.serviceConfig" - }, - "viewer": { - "$ref": "#/components/schemas/tools.ozone.server.getConfig.viewerConfig" - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.set.addValues": { - "post": { - "tags": [ - "tools.ozone.set" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nAdd values to a specific set. Attempting to add values to a set that does not exist will result in an error.", - "operationId": "tools.ozone.set.addValues", - "security": [ - { - "Bearer": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name", - "values" - ], - "properties": { - "name": { - "type": "string", - "description": "Name of the set to add values to" - }, - "values": { - "type": "array", - "items": { - "type": "string" - }, - "maxItems": 1000 - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.set.deleteSet": { - "post": { - "tags": [ - "tools.ozone.set" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nDelete an entire set. Attempting to delete a set that does not exist will result in an error.", - "operationId": "tools.ozone.set.deleteSet", - "security": [ - { - "Bearer": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string", - "description": "Name of the set to delete" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "_unknown_": { - "type": "object", - "properties": {} - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken", - "SetNotFound" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.set.deleteValues": { - "post": { - "tags": [ - "tools.ozone.set" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nDelete values from a specific set. Attempting to delete values that are not in the set will not result in an error", - "operationId": "tools.ozone.set.deleteValues", - "security": [ - { - "Bearer": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name", - "values" - ], - "properties": { - "name": { - "type": "string", - "description": "Name of the set to delete values from" - }, - "values": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken", - "SetNotFound" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.set.getValues": { - "get": { - "tags": [ - "tools.ozone.set" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nGet a specific set and its values", - "operationId": "tools.ozone.set.getValues", - "security": [ - { - "Bearer": [] - } - ], - "parameters": [ - { - "name": "name", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 1000, - "default": 100 - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "set", - "values" - ], - "properties": { - "set": { - "$ref": "#/components/schemas/tools.ozone.set.defs.setView" - }, - "values": { - "type": "array", - "items": { - "type": "string" - } - }, - "cursor": { - "type": "string" - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken", - "SetNotFound" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.set.querySets": { - "get": { - "tags": [ - "tools.ozone.set" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nQuery available sets", - "operationId": "tools.ozone.set.querySets", - "security": [ - { - "Bearer": [] - } - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "namePrefix", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "sortBy", - "in": "query", - "required": false, - "schema": { - "type": "string", - "default": "name" - } - }, - { - "name": "sortDirection", - "in": "query", - "description": "Defaults to ascending order of name field.", - "required": false, - "schema": { - "type": "string", - "description": "Defaults to ascending order of name field.", - "default": "asc" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "sets" - ], - "properties": { - "sets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tools.ozone.set.defs.setView" - } - }, - "cursor": { - "type": "string" - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.set.upsertSet": { - "post": { - "tags": [ - "tools.ozone.set" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nCreate or update set metadata", - "operationId": "tools.ozone.set.upsertSet", - "security": [ - { - "Bearer": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tools.ozone.set.defs.set" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tools.ozone.set.defs.setView" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.setting.listOptions": { - "get": { - "tags": [ - "tools.ozone.setting" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nList settings with optional filtering", - "operationId": "tools.ozone.setting.listOptions", - "security": [ - { - "Bearer": [] - } - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "scope", - "in": "query", - "required": false, - "schema": { - "type": "string", - "default": "instance", - "enum": [ - "instance", - "personal" - ] - } - }, - { - "name": "prefix", - "in": "query", - "description": "Filter keys by prefix", - "required": false, - "schema": { - "type": "string", - "description": "Filter keys by prefix" - } - }, - { - "name": "keys", - "in": "query", - "description": "Filter for only the specified keys. Ignored if prefix is provided", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "nsid" - }, - "maxItems": 100 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "options" - ], - "properties": { - "cursor": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tools.ozone.setting.defs.option" - } - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.setting.removeOptions": { - "post": { - "tags": [ - "tools.ozone.setting" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nDelete settings by key", - "operationId": "tools.ozone.setting.removeOptions", - "security": [ - { - "Bearer": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "keys", - "scope" - ], - "properties": { - "keys": { - "type": "array", - "items": { - "type": "string", - "format": "nsid" - }, - "maxItems": 200 - }, - "scope": { - "type": "string", - "enum": [ - "instance", - "personal" - ] - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "_unknown_": { - "type": "object", - "properties": {} - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.setting.upsertOption": { - "post": { - "tags": [ - "tools.ozone.setting" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nCreate or update setting option", - "operationId": "tools.ozone.setting.upsertOption", - "security": [ - { - "Bearer": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "key", - "scope", - "value" - ], - "properties": { - "key": { - "type": "string", - "format": "nsid" - }, - "scope": { - "type": "string", - "enum": [ - "instance", - "personal" - ] - }, - "value": {}, - "description": { - "type": "string", - "maxLength": 2000 - }, - "managerRole": { - "type": "string", - "enum": [ - "tools.ozone.team.defs#roleModerator", - "tools.ozone.team.defs#roleTriage", - "tools.ozone.team.defs#roleAdmin" - ] - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "option" - ], - "properties": { - "option": { - "$ref": "#/components/schemas/tools.ozone.setting.defs.option" - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.signature.findCorrelation": { - "get": { - "tags": [ - "tools.ozone.signature" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nFind all correlated threat signatures between 2 or more accounts.", - "operationId": "tools.ozone.signature.findCorrelation", - "security": [ - { - "Bearer": [] - } - ], - "parameters": [ - { - "name": "dids", - "in": "query", - "required": true, - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "did" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "details" - ], - "properties": { - "details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tools.ozone.signature.defs.sigDetail" - } - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.signature.findRelatedAccounts": { - "get": { - "tags": [ - "tools.ozone.signature" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nGet accounts that share some matching threat signatures with the root account.", - "operationId": "tools.ozone.signature.findRelatedAccounts", - "security": [ - { - "Bearer": [] - } - ], - "parameters": [ - { - "name": "did", - "in": "query", - "required": true, - "schema": { - "type": "string", - "format": "did" - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "accounts" - ], - "properties": { - "cursor": { - "type": "string" - }, - "accounts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tools.ozone.signature.findRelatedAccounts.relatedAccount" - } - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.signature.searchAccounts": { - "get": { - "tags": [ - "tools.ozone.signature" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nSearch for accounts that match one or more threat signature values.", - "operationId": "tools.ozone.signature.searchAccounts", - "security": [ - { - "Bearer": [] - } - ], - "parameters": [ - { - "name": "values", - "in": "query", - "required": true, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "accounts" - ], - "properties": { - "cursor": { - "type": "string" - }, - "accounts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.admin.defs.accountView" - } - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.team.addMember": { - "post": { - "tags": [ - "tools.ozone.team" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nAdd a member to the ozone team. Requires admin role.", - "operationId": "tools.ozone.team.addMember", - "security": [ - { - "Bearer": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "did", - "role" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "role": { - "type": "string", - "enum": [ - "tools.ozone.team.defs#roleAdmin", - "tools.ozone.team.defs#roleModerator", - "tools.ozone.team.defs#roleTriage" - ] - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tools.ozone.team.defs.member" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken", - "MemberAlreadyExists" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.team.deleteMember": { - "post": { - "tags": [ - "tools.ozone.team" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nDelete a member from ozone team. Requires admin role.", - "operationId": "tools.ozone.team.deleteMember", - "security": [ - { - "Bearer": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "did" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken", - "MemberNotFound", - "CannotDeleteSelf" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.team.listMembers": { - "get": { - "tags": [ - "tools.ozone.team" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nList all members with access to the ozone service.", - "operationId": "tools.ozone.team.listMembers", - "security": [ - { - "Bearer": [] - } - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "members" - ], - "properties": { - "cursor": { - "type": "string" - }, - "members": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tools.ozone.team.defs.member" - } - } - } - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/xrpc/tools.ozone.team.updateMember": { - "post": { - "tags": [ - "tools.ozone.team" - ], - "description": "*This endpoint is part of the [Ozone moderation service](https://ozone.tools/) APIs. Requests usually require authentication, are directed to the user's PDS intance, and proxied to the Ozone instance indicated by the DID in the service proxying header. Admin authenentication may also be possible, with request sent directly to the Ozone instance.*\n\n*To learn more about calling atproto API endpoints like this one, see the [API Hosts and Auth](/docs/advanced-guides/api-directory) guide.*\n\nUpdate a member in the ozone service. Requires admin role.", - "operationId": "tools.ozone.team.updateMember", - "security": [ - { - "Bearer": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "did" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "disabled": { - "type": "boolean" - }, - "role": { - "type": "string", - "enum": [ - "tools.ozone.team.defs#roleAdmin", - "tools.ozone.team.defs#roleModerator", - "tools.ozone.team.defs#roleTriage" - ] - } - } - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tools.ozone.team.defs.member" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "InvalidRequest", - "ExpiredToken", - "InvalidToken", - "MemberNotFound" - ] - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "error", - "message" - ], - "properties": { - "error": { - "const": "AuthMissing" - }, - "message": { - "type": "string" - } - } - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "app.bsky.actor.defs.profileViewBasic": { - "type": "object", - "required": [ - "did", - "handle" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "handle": { - "type": "string", - "format": "handle" - }, - "displayName": { - "type": "string", - "maxLength": 640 - }, - "avatar": { - "type": "string", - "format": "uri" - }, - "associated": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileAssociated" - }, - "viewer": { - "$ref": "#/components/schemas/app.bsky.actor.defs.viewerState" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.actor.defs.profileView": { - "type": "object", - "required": [ - "did", - "handle" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "handle": { - "type": "string", - "format": "handle" - }, - "displayName": { - "type": "string", - "maxLength": 640 - }, - "description": { - "type": "string", - "maxLength": 2560 - }, - "avatar": { - "type": "string", - "format": "uri" - }, - "associated": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileAssociated" - }, - "indexedAt": { - "type": "string", - "format": "date-time" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "viewer": { - "$ref": "#/components/schemas/app.bsky.actor.defs.viewerState" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - } - } - }, - "app.bsky.actor.defs.profileViewDetailed": { - "type": "object", - "required": [ - "did", - "handle" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "handle": { - "type": "string", - "format": "handle" - }, - "displayName": { - "type": "string", - "maxLength": 640 - }, - "description": { - "type": "string", - "maxLength": 2560 - }, - "avatar": { - "type": "string", - "format": "uri" - }, - "banner": { - "type": "string", - "format": "uri" - }, - "followersCount": { - "type": "integer" - }, - "followsCount": { - "type": "integer" - }, - "postsCount": { - "type": "integer" - }, - "associated": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileAssociated" - }, - "joinedViaStarterPack": { - "$ref": "#/components/schemas/app.bsky.graph.defs.starterPackViewBasic" - }, - "indexedAt": { - "type": "string", - "format": "date-time" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "viewer": { - "$ref": "#/components/schemas/app.bsky.actor.defs.viewerState" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - }, - "pinnedPost": { - "$ref": "#/components/schemas/com.atproto.repo.strongRef" - } - } - }, - "app.bsky.actor.defs.profileAssociated": { - "type": "object", - "properties": { - "lists": { - "type": "integer" - }, - "feedgens": { - "type": "integer" - }, - "starterPacks": { - "type": "integer" - }, - "labeler": { - "type": "boolean" - }, - "chat": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileAssociatedChat" - } - } - }, - "app.bsky.actor.defs.profileAssociatedChat": { - "type": "object", - "required": [ - "allowIncoming" - ], - "properties": { - "allowIncoming": { - "type": "string", - "enum": [ - "all", - "none", - "following" - ] - } - } - }, - "app.bsky.actor.defs.viewerState": { - "type": "object", - "description": "Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.", - "properties": { - "muted": { - "type": "boolean" - }, - "mutedByList": { - "$ref": "#/components/schemas/app.bsky.graph.defs.listViewBasic" - }, - "blockedBy": { - "type": "boolean" - }, - "blocking": { - "type": "string", - "format": "at-uri" - }, - "blockingByList": { - "$ref": "#/components/schemas/app.bsky.graph.defs.listViewBasic" - }, - "following": { - "type": "string", - "format": "at-uri" - }, - "followedBy": { - "type": "string", - "format": "at-uri" - }, - "knownFollowers": { - "$ref": "#/components/schemas/app.bsky.actor.defs.knownFollowers" - } - } - }, - "app.bsky.actor.defs.knownFollowers": { - "type": "object", - "description": "The subject's followers whom you also follow", - "required": [ - "count", - "followers" - ], - "properties": { - "count": { - "type": "integer" - }, - "followers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileViewBasic" - }, - "maxItems": 5 - } - } - }, - "app.bsky.actor.defs.preferences": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.actor.defs.adultContentPref" - }, - { - "$ref": "#/components/schemas/app.bsky.actor.defs.contentLabelPref" - }, - { - "$ref": "#/components/schemas/app.bsky.actor.defs.savedFeedsPref" - }, - { - "$ref": "#/components/schemas/app.bsky.actor.defs.savedFeedsPrefV2" - }, - { - "$ref": "#/components/schemas/app.bsky.actor.defs.personalDetailsPref" - }, - { - "$ref": "#/components/schemas/app.bsky.actor.defs.feedViewPref" - }, - { - "$ref": "#/components/schemas/app.bsky.actor.defs.threadViewPref" - }, - { - "$ref": "#/components/schemas/app.bsky.actor.defs.interestsPref" - }, - { - "$ref": "#/components/schemas/app.bsky.actor.defs.mutedWordsPref" - }, - { - "$ref": "#/components/schemas/app.bsky.actor.defs.hiddenPostsPref" - }, - { - "$ref": "#/components/schemas/app.bsky.actor.defs.bskyAppStatePref" - }, - { - "$ref": "#/components/schemas/app.bsky.actor.defs.labelersPref" - } - ] - } - }, - "app.bsky.actor.defs.adultContentPref": { - "type": "object", - "required": [ - "enabled" - ], - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "app.bsky.actor.defs.contentLabelPref": { - "type": "object", - "required": [ - "label", - "visibility" - ], - "properties": { - "labelerDid": { - "type": "string", - "description": "Which labeler does this preference apply to? If undefined, applies globally.", - "format": "did" - }, - "label": { - "type": "string" - }, - "visibility": { - "type": "string", - "enum": [ - "ignore", - "show", - "warn", - "hide" - ] - } - } - }, - "app.bsky.actor.defs.savedFeed": { - "type": "object", - "required": [ - "id", - "type", - "value", - "pinned" - ], - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "feed", - "list", - "timeline" - ] - }, - "value": { - "type": "string" - }, - "pinned": { - "type": "boolean" - } - } - }, - "app.bsky.actor.defs.savedFeedsPrefV2": { - "type": "object", - "required": [ - "items" - ], - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.actor.defs.savedFeed" - } - } - } - }, - "app.bsky.actor.defs.savedFeedsPref": { - "type": "object", - "required": [ - "pinned", - "saved" - ], - "properties": { - "pinned": { - "type": "array", - "items": { - "type": "string", - "format": "at-uri" - } - }, - "saved": { - "type": "array", - "items": { - "type": "string", - "format": "at-uri" - } - }, - "timelineIndex": { - "type": "integer" - } - } - }, - "app.bsky.actor.defs.personalDetailsPref": { - "type": "object", - "properties": { - "birthDate": { - "type": "string", - "description": "The birth date of account owner.", - "format": "date-time" - } - } - }, - "app.bsky.actor.defs.feedViewPref": { - "type": "object", - "required": [ - "feed" - ], - "properties": { - "feed": { - "type": "string", - "description": "The URI of the feed, or an identifier which describes the feed." - }, - "hideReplies": { - "type": "boolean", - "description": "Hide replies in the feed." - }, - "hideRepliesByUnfollowed": { - "type": "boolean", - "description": "Hide replies in the feed if they are not by followed users.", - "default": true - }, - "hideRepliesByLikeCount": { - "type": "integer" - }, - "hideReposts": { - "type": "boolean", - "description": "Hide reposts in the feed." - }, - "hideQuotePosts": { - "type": "boolean", - "description": "Hide quote posts in the feed." - } - } - }, - "app.bsky.actor.defs.threadViewPref": { - "type": "object", - "properties": { - "sort": { - "type": "string", - "description": "Sorting mode for threads.", - "enum": [ - "oldest", - "newest", - "most-likes", - "random", - "hotness" - ] - }, - "prioritizeFollowedUsers": { - "type": "boolean", - "description": "Show followed users at the top of all replies." - } - } - }, - "app.bsky.actor.defs.interestsPref": { - "type": "object", - "required": [ - "tags" - ], - "properties": { - "tags": { - "type": "array", - "items": { - "type": "string", - "maxLength": 640 - }, - "maxItems": 100 - } - } - }, - "app.bsky.actor.defs.mutedWordTarget": { - "type": "string", - "maxLength": 640, - "enum": [ - "content", - "tag" - ] - }, - "app.bsky.actor.defs.mutedWord": { - "type": "object", - "description": "A word that the account owner has muted.", - "required": [ - "value", - "targets" - ], - "properties": { - "id": { - "type": "string" - }, - "value": { - "type": "string", - "description": "The muted word itself.", - "maxLength": 10000 - }, - "targets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.actor.defs.mutedWordTarget" - } - }, - "actorTarget": { - "type": "string", - "description": "Groups of users to apply the muted word to. If undefined, applies to all users.", - "default": "all", - "enum": [ - "all", - "exclude-following" - ] - }, - "expiresAt": { - "type": "string", - "description": "The date and time at which the muted word will expire and no longer be applied.", - "format": "date-time" - } - } - }, - "app.bsky.actor.defs.mutedWordsPref": { - "type": "object", - "required": [ - "items" - ], - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.actor.defs.mutedWord" - } - } - } - }, - "app.bsky.actor.defs.hiddenPostsPref": { - "type": "object", - "required": [ - "items" - ], - "properties": { - "items": { - "type": "array", - "items": { - "type": "string", - "format": "at-uri" - } - } - } - }, - "app.bsky.actor.defs.labelersPref": { - "type": "object", - "required": [ - "labelers" - ], - "properties": { - "labelers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.actor.defs.labelerPrefItem" - } - } - } - }, - "app.bsky.actor.defs.labelerPrefItem": { - "type": "object", - "required": [ - "did" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - } - } - }, - "app.bsky.actor.defs.bskyAppStatePref": { - "type": "object", - "description": "A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this.", - "properties": { - "activeProgressGuide": { - "$ref": "#/components/schemas/app.bsky.actor.defs.bskyAppProgressGuide" - }, - "queuedNudges": { - "type": "array", - "items": { - "type": "string", - "maxLength": 100 - }, - "maxItems": 1000 - }, - "nuxs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.actor.defs.nux" - }, - "maxItems": 100 - } - } - }, - "app.bsky.actor.defs.bskyAppProgressGuide": { - "type": "object", - "description": "If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress.", - "required": [ - "guide" - ], - "properties": { - "guide": { - "type": "string", - "maxLength": 100 - } - } - }, - "app.bsky.actor.defs.nux": { - "type": "object", - "description": "A new user experiences (NUX) storage object", - "required": [ - "id", - "completed" - ], - "properties": { - "id": { - "type": "string", - "maxLength": 100 - }, - "completed": { - "type": "boolean" - }, - "data": { - "type": "string", - "description": "Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.", - "maxLength": 3000 - }, - "expiresAt": { - "type": "string", - "description": "The date and time at which the NUX will expire and should be considered completed.", - "format": "date-time" - } - } - }, - "app.bsky.actor.profile": { - "type": "object", - "properties": { - "displayName": { - "type": "string", - "maxLength": 640 - }, - "description": { - "type": "string", - "description": "Free-form profile description text.", - "maxLength": 2560 - }, - "avatar": { - "type": "string", - "format": "binary", - "maxLength": 1000000 - }, - "banner": { - "type": "string", - "format": "binary", - "maxLength": 1000000 - }, - "labels": { - "oneOf": [ - { - "$ref": "#/components/schemas/com.atproto.label.defs.selfLabels" - } - ] - }, - "joinedViaStarterPack": { - "$ref": "#/components/schemas/com.atproto.repo.strongRef" - }, - "pinnedPost": { - "$ref": "#/components/schemas/com.atproto.repo.strongRef" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.embed.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 - } - } - }, - "app.bsky.embed.external": { - "type": "object", - "description": "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).", - "required": [ - "external" - ], - "properties": { - "external": { - "$ref": "#/components/schemas/app.bsky.embed.external.external" - } - } - }, - "app.bsky.embed.external.external": { - "type": "object", - "required": [ - "uri", - "title", - "description" - ], - "properties": { - "uri": { - "type": "string", - "format": "uri" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "thumb": { - "type": "string", - "format": "binary", - "maxLength": 1000000 - } - } - }, - "app.bsky.embed.external.view": { - "type": "object", - "required": [ - "external" - ], - "properties": { - "external": { - "$ref": "#/components/schemas/app.bsky.embed.external.viewExternal" - } - } - }, - "app.bsky.embed.external.viewExternal": { - "type": "object", - "required": [ - "uri", - "title", - "description" - ], - "properties": { - "uri": { - "type": "string", - "format": "uri" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "thumb": { - "type": "string", - "format": "uri" - } - } - }, - "app.bsky.embed.images": { - "type": "object", - "required": [ - "images" - ], - "properties": { - "images": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.embed.images.image" - }, - "maxItems": 4 - } - } - }, - "app.bsky.embed.images.image": { - "type": "object", - "required": [ - "image", - "alt" - ], - "properties": { - "image": { - "type": "string", - "format": "binary", - "maxLength": 1000000 - }, - "alt": { - "type": "string", - "description": "Alt text description of the image, for accessibility." - }, - "aspectRatio": { - "$ref": "#/components/schemas/app.bsky.embed.defs.aspectRatio" - } - } - }, - "app.bsky.embed.images.view": { - "type": "object", - "required": [ - "images" - ], - "properties": { - "images": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.embed.images.viewImage" - }, - "maxItems": 4 - } - } - }, - "app.bsky.embed.images.viewImage": { - "type": "object", - "required": [ - "thumb", - "fullsize", - "alt" - ], - "properties": { - "thumb": { - "type": "string", - "description": "Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View.", - "format": "uri" - }, - "fullsize": { - "type": "string", - "description": "Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View.", - "format": "uri" - }, - "alt": { - "type": "string", - "description": "Alt text description of the image, for accessibility." - }, - "aspectRatio": { - "$ref": "#/components/schemas/app.bsky.embed.defs.aspectRatio" - } - } - }, - "app.bsky.embed.record": { - "type": "object", - "required": [ - "record" - ], - "properties": { - "record": { - "$ref": "#/components/schemas/com.atproto.repo.strongRef" - } - } - }, - "app.bsky.embed.record.view": { - "type": "object", - "required": [ - "record" - ], - "properties": { - "record": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.embed.record.viewRecord" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.record.viewNotFound" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.record.viewBlocked" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.record.viewDetached" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.defs.generatorView" - }, - { - "$ref": "#/components/schemas/app.bsky.graph.defs.listView" - }, - { - "$ref": "#/components/schemas/app.bsky.labeler.defs.labelerView" - }, - { - "$ref": "#/components/schemas/app.bsky.graph.defs.starterPackViewBasic" - } - ] - } - } - }, - "app.bsky.embed.record.viewRecord": { - "type": "object", - "required": [ - "uri", - "cid", - "author", - "value", - "indexedAt" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "author": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileViewBasic" - }, - "value": {}, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - }, - "replyCount": { - "type": "integer" - }, - "repostCount": { - "type": "integer" - }, - "likeCount": { - "type": "integer" - }, - "quoteCount": { - "type": "integer" - }, - "embeds": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.embed.images.view" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.video.view" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.external.view" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.record.view" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.recordWithMedia.view" - } - ] - } - }, - "indexedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.embed.record.viewNotFound": { - "type": "object", - "required": [ - "uri", - "notFound" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "notFound": { - "type": "boolean", - "default": true - } - } - }, - "app.bsky.embed.record.viewBlocked": { - "type": "object", - "required": [ - "uri", - "blocked", - "author" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "blocked": { - "type": "boolean", - "default": true - }, - "author": { - "$ref": "#/components/schemas/app.bsky.feed.defs.blockedAuthor" - } - } - }, - "app.bsky.embed.record.viewDetached": { - "type": "object", - "required": [ - "uri", - "detached" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "detached": { - "type": "boolean", - "default": true - } - } - }, - "app.bsky.embed.recordWithMedia": { - "type": "object", - "required": [ - "record", - "media" - ], - "properties": { - "record": { - "$ref": "#/components/schemas/app.bsky.embed.record" - }, - "media": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.embed.images" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.video" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.external" - } - ] - } - } - }, - "app.bsky.embed.recordWithMedia.view": { - "type": "object", - "required": [ - "record", - "media" - ], - "properties": { - "record": { - "$ref": "#/components/schemas/app.bsky.embed.record.view" - }, - "media": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.embed.images.view" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.video.view" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.external.view" - } - ] - } - } - }, - "app.bsky.embed.video": { - "type": "object", - "required": [ - "video" - ], - "properties": { - "video": { - "type": "string", - "format": "binary", - "maxLength": 50000000 - }, - "captions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.embed.video.caption" - }, - "maxItems": 20 - }, - "alt": { - "type": "string", - "description": "Alt text description of the video, for accessibility.", - "maxLength": 10000 - }, - "aspectRatio": { - "$ref": "#/components/schemas/app.bsky.embed.defs.aspectRatio" - } - } - }, - "app.bsky.embed.video.caption": { - "type": "object", - "required": [ - "lang", - "file" - ], - "properties": { - "lang": { - "type": "string", - "format": "language" - }, - "file": { - "type": "string", - "format": "binary", - "maxLength": 20000 - } - } - }, - "app.bsky.embed.video.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", - "maxLength": 10000 - }, - "aspectRatio": { - "$ref": "#/components/schemas/app.bsky.embed.defs.aspectRatio" - } - } - }, - "app.bsky.feed.defs.postView": { - "WARNING": "Schema Docs Have Been Truncated!" - }, - "app.bsky.feed.defs.viewerState": { - "type": "object", - "description": "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.", - "properties": { - "repost": { - "type": "string", - "format": "at-uri" - }, - "like": { - "type": "string", - "format": "at-uri" - }, - "threadMuted": { - "type": "boolean" - }, - "replyDisabled": { - "type": "boolean" - }, - "embeddingDisabled": { - "type": "boolean" - }, - "pinned": { - "type": "boolean" - } - } - }, - "app.bsky.feed.defs.feedViewPost": { - "type": "object", - "required": [ - "post" - ], - "properties": { - "post": { - "$ref": "#/components/schemas/app.bsky.feed.defs.postView" - }, - "reply": { - "$ref": "#/components/schemas/app.bsky.feed.defs.replyRef" - }, - "reason": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.feed.defs.reasonRepost" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.defs.reasonPin" - } - ] - }, - "feedContext": { - "type": "string", - "description": "Context provided by feed generator that may be passed back alongside interactions.", - "maxLength": 2000 - } - } - }, - "app.bsky.feed.defs.replyRef": { - "type": "object", - "required": [ - "root", - "parent" - ], - "properties": { - "root": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.feed.defs.postView" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.defs.notFoundPost" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.defs.blockedPost" - } - ] - }, - "parent": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.feed.defs.postView" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.defs.notFoundPost" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.defs.blockedPost" - } - ] - }, - "grandparentAuthor": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileViewBasic" - } - } - }, - "app.bsky.feed.defs.reasonRepost": { - "type": "object", - "required": [ - "by", - "indexedAt" - ], - "properties": { - "by": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileViewBasic" - }, - "indexedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.feed.defs.reasonPin": { - "type": "object", - "properties": {} - }, - "app.bsky.feed.defs.threadViewPost": { - "type": "object", - "required": [ - "post" - ], - "properties": { - "post": { - "$ref": "#/components/schemas/app.bsky.feed.defs.postView" - }, - "parent": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.feed.defs.threadViewPost" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.defs.notFoundPost" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.defs.blockedPost" - } - ] - }, - "replies": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.feed.defs.threadViewPost" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.defs.notFoundPost" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.defs.blockedPost" - } - ] - } - } - } - }, - "app.bsky.feed.defs.notFoundPost": { - "type": "object", - "required": [ - "uri", - "notFound" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "notFound": { - "type": "boolean", - "default": true - } - } - }, - "app.bsky.feed.defs.blockedPost": { - "type": "object", - "required": [ - "uri", - "blocked", - "author" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "blocked": { - "type": "boolean", - "default": true - }, - "author": { - "$ref": "#/components/schemas/app.bsky.feed.defs.blockedAuthor" - } - } - }, - "app.bsky.feed.defs.blockedAuthor": { - "type": "object", - "required": [ - "did" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "viewer": { - "$ref": "#/components/schemas/app.bsky.actor.defs.viewerState" - } - } - }, - "app.bsky.feed.defs.generatorView": { - "type": "object", - "required": [ - "uri", - "cid", - "did", - "creator", - "displayName", - "indexedAt" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "did": { - "type": "string", - "format": "did" - }, - "creator": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileView" - }, - "displayName": { - "type": "string" - }, - "description": { - "type": "string", - "maxLength": 3000 - }, - "descriptionFacets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.richtext.facet" - } - }, - "avatar": { - "type": "string", - "format": "uri" - }, - "likeCount": { - "type": "integer", - "minimum": 0 - }, - "acceptsInteractions": { - "type": "boolean" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - }, - "viewer": { - "$ref": "#/components/schemas/app.bsky.feed.defs.generatorViewerState" - }, - "indexedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.feed.defs.generatorViewerState": { - "type": "object", - "properties": { - "like": { - "type": "string", - "format": "at-uri" - } - } - }, - "app.bsky.feed.defs.skeletonFeedPost": { - "type": "object", - "required": [ - "post" - ], - "properties": { - "post": { - "type": "string", - "format": "at-uri" - }, - "reason": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.feed.defs.skeletonReasonRepost" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.defs.skeletonReasonPin" - } - ] - }, - "feedContext": { - "type": "string", - "description": "Context that will be passed through to client and may be passed to feed generator back alongside interactions.", - "maxLength": 2000 - } - } - }, - "app.bsky.feed.defs.skeletonReasonRepost": { - "type": "object", - "required": [ - "repost" - ], - "properties": { - "repost": { - "type": "string", - "format": "at-uri" - } - } - }, - "app.bsky.feed.defs.skeletonReasonPin": { - "type": "object", - "properties": {} - }, - "app.bsky.feed.defs.threadgateView": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "record": {}, - "lists": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.graph.defs.listViewBasic" - } - } - } - }, - "app.bsky.feed.defs.interaction": { - "type": "object", - "properties": { - "item": { - "type": "string", - "format": "at-uri" - }, - "event": { - "type": "string", - "enum": [ - "app.bsky.feed.defs#requestLess", - "app.bsky.feed.defs#requestMore", - "app.bsky.feed.defs#clickthroughItem", - "app.bsky.feed.defs#clickthroughAuthor", - "app.bsky.feed.defs#clickthroughReposter", - "app.bsky.feed.defs#clickthroughEmbed", - "app.bsky.feed.defs#interactionSeen", - "app.bsky.feed.defs#interactionLike", - "app.bsky.feed.defs#interactionRepost", - "app.bsky.feed.defs#interactionReply", - "app.bsky.feed.defs#interactionQuote", - "app.bsky.feed.defs#interactionShare" - ] - }, - "feedContext": { - "type": "string", - "description": "Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton.", - "maxLength": 2000 - } - } - }, - "app.bsky.feed.defs.requestLess": { - "type": "string", - "format": "token", - "description": "Request that less content like the given feed item be shown in the feed" - }, - "app.bsky.feed.defs.requestMore": { - "type": "string", - "format": "token", - "description": "Request that more content like the given feed item be shown in the feed" - }, - "app.bsky.feed.defs.clickthroughItem": { - "type": "string", - "format": "token", - "description": "User clicked through to the feed item" - }, - "app.bsky.feed.defs.clickthroughAuthor": { - "type": "string", - "format": "token", - "description": "User clicked through to the author of the feed item" - }, - "app.bsky.feed.defs.clickthroughReposter": { - "type": "string", - "format": "token", - "description": "User clicked through to the reposter of the feed item" - }, - "app.bsky.feed.defs.clickthroughEmbed": { - "type": "string", - "format": "token", - "description": "User clicked through to the embedded content of the feed item" - }, - "app.bsky.feed.defs.interactionSeen": { - "type": "string", - "format": "token", - "description": "Feed item was seen by user" - }, - "app.bsky.feed.defs.interactionLike": { - "type": "string", - "format": "token", - "description": "User liked the feed item" - }, - "app.bsky.feed.defs.interactionRepost": { - "type": "string", - "format": "token", - "description": "User reposted the feed item" - }, - "app.bsky.feed.defs.interactionReply": { - "type": "string", - "format": "token", - "description": "User replied to the feed item" - }, - "app.bsky.feed.defs.interactionQuote": { - "type": "string", - "format": "token", - "description": "User quoted the feed item" - }, - "app.bsky.feed.defs.interactionShare": { - "type": "string", - "format": "token", - "description": "User shared the feed item" - }, - "app.bsky.feed.describeFeedGenerator.feed": { - "type": "object", - "required": [ - "uri" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - } - } - }, - "app.bsky.feed.describeFeedGenerator.links": { - "type": "object", - "properties": { - "privacyPolicy": { - "type": "string" - }, - "termsOfService": { - "type": "string" - } - } - }, - "app.bsky.feed.generator": { - "type": "object", - "required": [ - "did", - "displayName", - "createdAt" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "displayName": { - "type": "string", - "maxLength": 240 - }, - "description": { - "type": "string", - "maxLength": 3000 - }, - "descriptionFacets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.richtext.facet" - } - }, - "avatar": { - "type": "string", - "format": "binary", - "maxLength": 1000000 - }, - "acceptsInteractions": { - "type": "boolean", - "description": "Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions" - }, - "labels": { - "oneOf": [ - { - "$ref": "#/components/schemas/com.atproto.label.defs.selfLabels" - } - ] - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.feed.getLikes.like": { - "type": "object", - "required": [ - "indexedAt", - "createdAt", - "actor" - ], - "properties": { - "indexedAt": { - "type": "string", - "format": "date-time" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "actor": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileView" - } - } - }, - "app.bsky.feed.like": { - "type": "object", - "required": [ - "subject", - "createdAt" - ], - "properties": { - "subject": { - "$ref": "#/components/schemas/com.atproto.repo.strongRef" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.feed.post": { - "type": "object", - "required": [ - "text", - "createdAt" - ], - "properties": { - "text": { - "type": "string", - "description": "The primary post content. May be an empty string, if there are embeds.", - "maxLength": 3000 - }, - "facets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.richtext.facet" - } - }, - "reply": { - "$ref": "#/components/schemas/app.bsky.feed.post.replyRef" - }, - "embed": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.embed.images" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.video" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.external" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.record" - }, - { - "$ref": "#/components/schemas/app.bsky.embed.recordWithMedia" - } - ] - }, - "langs": { - "type": "array", - "items": { - "type": "string", - "format": "language" - }, - "maxItems": 3 - }, - "labels": { - "oneOf": [ - { - "$ref": "#/components/schemas/com.atproto.label.defs.selfLabels" - } - ] - }, - "tags": { - "type": "array", - "items": { - "type": "string", - "maxLength": 640 - }, - "maxItems": 8 - }, - "createdAt": { - "type": "string", - "description": "Client-declared timestamp when this post was originally created.", - "format": "date-time" - } - } - }, - "app.bsky.feed.post.replyRef": { - "type": "object", - "required": [ - "root", - "parent" - ], - "properties": { - "root": { - "$ref": "#/components/schemas/com.atproto.repo.strongRef" - }, - "parent": { - "$ref": "#/components/schemas/com.atproto.repo.strongRef" - } - } - }, - "app.bsky.feed.postgate": { - "type": "object", - "required": [ - "post", - "createdAt" - ], - "properties": { - "createdAt": { - "type": "string", - "format": "date-time" - }, - "post": { - "type": "string", - "description": "Reference (AT-URI) to the post record.", - "format": "at-uri" - }, - "detachedEmbeddingUris": { - "type": "array", - "items": { - "type": "string", - "format": "at-uri" - }, - "maxItems": 50 - }, - "embeddingRules": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.feed.postgate.disableRule" - } - ] - }, - "maxItems": 5 - } - } - }, - "app.bsky.feed.postgate.disableRule": { - "type": "object", - "description": "Disables embedding of this post.", - "properties": {} - }, - "app.bsky.feed.repost": { - "type": "object", - "required": [ - "subject", - "createdAt" - ], - "properties": { - "subject": { - "$ref": "#/components/schemas/com.atproto.repo.strongRef" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.feed.threadgate": { - "type": "object", - "required": [ - "post", - "createdAt" - ], - "properties": { - "post": { - "type": "string", - "description": "Reference (AT-URI) to the post record.", - "format": "at-uri" - }, - "allow": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.feed.threadgate.mentionRule" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.threadgate.followingRule" - }, - { - "$ref": "#/components/schemas/app.bsky.feed.threadgate.listRule" - } - ] - }, - "maxItems": 5 - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "hiddenReplies": { - "type": "array", - "items": { - "type": "string", - "format": "at-uri" - }, - "maxItems": 50 - } - } - }, - "app.bsky.feed.threadgate.mentionRule": { - "type": "object", - "description": "Allow replies from actors mentioned in your post.", - "properties": {} - }, - "app.bsky.feed.threadgate.followingRule": { - "type": "object", - "description": "Allow replies from actors you follow.", - "properties": {} - }, - "app.bsky.feed.threadgate.listRule": { - "type": "object", - "description": "Allow replies from actors on a list.", - "required": [ - "list" - ], - "properties": { - "list": { - "type": "string", - "format": "at-uri" - } - } - }, - "app.bsky.graph.block": { - "type": "object", - "required": [ - "subject", - "createdAt" - ], - "properties": { - "subject": { - "type": "string", - "description": "DID of the account to be blocked.", - "format": "did" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.graph.defs.listViewBasic": { - "type": "object", - "required": [ - "uri", - "cid", - "name", - "purpose" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 64 - }, - "purpose": { - "$ref": "#/components/schemas/app.bsky.graph.defs.listPurpose" - }, - "avatar": { - "type": "string", - "format": "uri" - }, - "listItemCount": { - "type": "integer", - "minimum": 0 - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - }, - "viewer": { - "$ref": "#/components/schemas/app.bsky.graph.defs.listViewerState" - }, - "indexedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.graph.defs.listView": { - "type": "object", - "required": [ - "uri", - "cid", - "creator", - "name", - "purpose", - "indexedAt" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "creator": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileView" - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 64 - }, - "purpose": { - "$ref": "#/components/schemas/app.bsky.graph.defs.listPurpose" - }, - "description": { - "type": "string", - "maxLength": 3000 - }, - "descriptionFacets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.richtext.facet" - } - }, - "avatar": { - "type": "string", - "format": "uri" - }, - "listItemCount": { - "type": "integer", - "minimum": 0 - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - }, - "viewer": { - "$ref": "#/components/schemas/app.bsky.graph.defs.listViewerState" - }, - "indexedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.graph.defs.listItemView": { - "type": "object", - "required": [ - "uri", - "subject" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "subject": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileView" - } - } - }, - "app.bsky.graph.defs.starterPackView": { - "type": "object", - "required": [ - "uri", - "cid", - "record", - "creator", - "indexedAt" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "record": {}, - "creator": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileViewBasic" - }, - "list": { - "$ref": "#/components/schemas/app.bsky.graph.defs.listViewBasic" - }, - "listItemsSample": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.graph.defs.listItemView" - }, - "maxItems": 12 - }, - "feeds": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.feed.defs.generatorView" - }, - "maxItems": 3 - }, - "joinedWeekCount": { - "type": "integer", - "minimum": 0 - }, - "joinedAllTimeCount": { - "type": "integer", - "minimum": 0 - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - }, - "indexedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.graph.defs.starterPackViewBasic": { - "type": "object", - "required": [ - "uri", - "cid", - "record", - "creator", - "indexedAt" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "record": {}, - "creator": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileViewBasic" - }, - "listItemCount": { - "type": "integer", - "minimum": 0 - }, - "joinedWeekCount": { - "type": "integer", - "minimum": 0 - }, - "joinedAllTimeCount": { - "type": "integer", - "minimum": 0 - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - }, - "indexedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.graph.defs.listPurpose": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.graph.defs.modlist" - }, - { - "$ref": "#/components/schemas/app.bsky.graph.defs.curatelist" - }, - { - "$ref": "#/components/schemas/app.bsky.graph.defs.referencelist" - } - ] - }, - "app.bsky.graph.defs.modlist": { - "type": "string", - "format": "token", - "description": "A list of actors to apply an aggregate moderation action (mute/block) on." - }, - "app.bsky.graph.defs.curatelist": { - "type": "string", - "format": "token", - "description": "A list of actors used for curation purposes such as list feeds or interaction gating." - }, - "app.bsky.graph.defs.referencelist": { - "type": "string", - "format": "token", - "description": "A list of actors used for only for reference purposes such as within a starter pack." - }, - "app.bsky.graph.defs.listViewerState": { - "type": "object", - "properties": { - "muted": { - "type": "boolean" - }, - "blocked": { - "type": "string", - "format": "at-uri" - } - } - }, - "app.bsky.graph.defs.notFoundActor": { - "type": "object", - "description": "indicates that a handle or DID could not be resolved", - "required": [ - "actor", - "notFound" - ], - "properties": { - "actor": { - "type": "string", - "format": "at-identifier" - }, - "notFound": { - "type": "boolean", - "default": true - } - } - }, - "app.bsky.graph.defs.relationship": { - "type": "object", - "description": "lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)", - "required": [ - "did" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "following": { - "type": "string", - "description": "if the actor follows this DID, this is the AT-URI of the follow record", - "format": "at-uri" - }, - "followedBy": { - "type": "string", - "description": "if the actor is followed by this DID, contains the AT-URI of the follow record", - "format": "at-uri" - } - } - }, - "app.bsky.graph.follow": { - "type": "object", - "required": [ - "subject", - "createdAt" - ], - "properties": { - "subject": { - "type": "string", - "format": "did" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.graph.list": { - "type": "object", - "required": [ - "name", - "purpose", - "createdAt" - ], - "properties": { - "purpose": { - "$ref": "#/components/schemas/app.bsky.graph.defs.listPurpose" - }, - "name": { - "type": "string", - "description": "Display name for list; can not be empty.", - "minLength": 1, - "maxLength": 64 - }, - "description": { - "type": "string", - "maxLength": 3000 - }, - "descriptionFacets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.richtext.facet" - } - }, - "avatar": { - "type": "string", - "format": "binary", - "maxLength": 1000000 - }, - "labels": { - "oneOf": [ - { - "$ref": "#/components/schemas/com.atproto.label.defs.selfLabels" - } - ] - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.graph.listblock": { - "type": "object", - "required": [ - "subject", - "createdAt" - ], - "properties": { - "subject": { - "type": "string", - "description": "Reference (AT-URI) to the mod list record.", - "format": "at-uri" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.graph.listitem": { - "type": "object", - "required": [ - "subject", - "list", - "createdAt" - ], - "properties": { - "subject": { - "type": "string", - "description": "The account which is included on the list.", - "format": "did" - }, - "list": { - "type": "string", - "description": "Reference (AT-URI) to the list record (app.bsky.graph.list).", - "format": "at-uri" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.graph.starterpack": { - "type": "object", - "required": [ - "name", - "list", - "createdAt" - ], - "properties": { - "name": { - "type": "string", - "description": "Display name for starter pack; can not be empty.", - "minLength": 1, - "maxLength": 500 - }, - "description": { - "type": "string", - "maxLength": 3000 - }, - "descriptionFacets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.richtext.facet" - } - }, - "list": { - "type": "string", - "description": "Reference (AT-URI) to the list record.", - "format": "at-uri" - }, - "feeds": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.graph.starterpack.feedItem" - }, - "maxItems": 3 - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.graph.starterpack.feedItem": { - "type": "object", - "required": [ - "uri" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - } - } - }, - "app.bsky.labeler.defs.labelerView": { - "type": "object", - "required": [ - "uri", - "cid", - "creator", - "indexedAt" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "creator": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileView" - }, - "likeCount": { - "type": "integer", - "minimum": 0 - }, - "viewer": { - "$ref": "#/components/schemas/app.bsky.labeler.defs.labelerViewerState" - }, - "indexedAt": { - "type": "string", - "format": "date-time" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - } - } - }, - "app.bsky.labeler.defs.labelerViewDetailed": { - "type": "object", - "required": [ - "uri", - "cid", - "creator", - "policies", - "indexedAt" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "creator": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileView" - }, - "policies": { - "$ref": "#/components/schemas/app.bsky.labeler.defs.labelerPolicies" - }, - "likeCount": { - "type": "integer", - "minimum": 0 - }, - "viewer": { - "$ref": "#/components/schemas/app.bsky.labeler.defs.labelerViewerState" - }, - "indexedAt": { - "type": "string", - "format": "date-time" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - } - } - }, - "app.bsky.labeler.defs.labelerViewerState": { - "type": "object", - "properties": { - "like": { - "type": "string", - "format": "at-uri" - } - } - }, - "app.bsky.labeler.defs.labelerPolicies": { - "type": "object", - "required": [ - "labelValues" - ], - "properties": { - "labelValues": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.labelValue" - } - }, - "labelValueDefinitions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.labelValueDefinition" - } - } - } - }, - "app.bsky.labeler.service": { - "type": "object", - "required": [ - "policies", - "createdAt" - ], - "properties": { - "policies": { - "$ref": "#/components/schemas/app.bsky.labeler.defs.labelerPolicies" - }, - "labels": { - "oneOf": [ - { - "$ref": "#/components/schemas/com.atproto.label.defs.selfLabels" - } - ] - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "app.bsky.notification.listNotifications.notification": { - "type": "object", - "required": [ - "uri", - "cid", - "author", - "reason", - "record", - "isRead", - "indexedAt" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "author": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileView" - }, - "reason": { - "type": "string", - "description": "Expected values are 'like', 'repost', 'follow', 'mention', 'reply', 'quote', and 'starterpack-joined'.", - "enum": [ - "like", - "repost", - "follow", - "mention", - "reply", - "quote", - "starterpack-joined" - ] - }, - "reasonSubject": { - "type": "string", - "format": "at-uri" - }, - "record": {}, - "isRead": { - "type": "boolean" - }, - "indexedAt": { - "type": "string", - "format": "date-time" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - } - } - }, - "app.bsky.richtext.facet": { - "type": "object", - "description": "Annotation of a sub-string within rich text.", - "required": [ - "index", - "features" - ], - "properties": { - "index": { - "$ref": "#/components/schemas/app.bsky.richtext.facet.byteSlice" - }, - "features": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.richtext.facet.mention" - }, - { - "$ref": "#/components/schemas/app.bsky.richtext.facet.link" - }, - { - "$ref": "#/components/schemas/app.bsky.richtext.facet.tag" - } - ] - } - } - } - }, - "app.bsky.richtext.facet.mention": { - "type": "object", - "description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.", - "required": [ - "did" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - } - } - }, - "app.bsky.richtext.facet.link": { - "type": "object", - "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.", - "required": [ - "uri" - ], - "properties": { - "uri": { - "type": "string", - "format": "uri" - } - } - }, - "app.bsky.richtext.facet.tag": { - "type": "object", - "description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').", - "required": [ - "tag" - ], - "properties": { - "tag": { - "type": "string", - "maxLength": 640 - } - } - }, - "app.bsky.richtext.facet.byteSlice": { - "type": "object", - "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.", - "required": [ - "byteStart", - "byteEnd" - ], - "properties": { - "byteStart": { - "type": "integer", - "minimum": 0 - }, - "byteEnd": { - "type": "integer", - "minimum": 0 - } - } - }, - "app.bsky.video.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.", - "enum": [ - "JOB_STATE_COMPLETED", - "JOB_STATE_FAILED" - ] - }, - "progress": { - "type": "integer", - "minimum": 0, - "maximum": 100 - }, - "blob": { - "type": "string", - "format": "binary" - }, - "error": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "chat.bsky.actor.declaration": { - "type": "object", - "required": [ - "allowIncoming" - ], - "properties": { - "allowIncoming": { - "type": "string", - "enum": [ - "all", - "none", - "following" - ] - } - } - }, - "chat.bsky.actor.defs.profileViewBasic": { - "type": "object", - "required": [ - "did", - "handle" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "handle": { - "type": "string", - "format": "handle" - }, - "displayName": { - "type": "string", - "maxLength": 640 - }, - "avatar": { - "type": "string", - "format": "uri" - }, - "associated": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileAssociated" - }, - "viewer": { - "$ref": "#/components/schemas/app.bsky.actor.defs.viewerState" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - }, - "chatDisabled": { - "type": "boolean", - "description": "Set to true when the actor cannot actively participate in converations" - } - } - }, - "chat.bsky.convo.defs.messageRef": { - "type": "object", - "required": [ - "did", - "messageId", - "convoId" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "convoId": { - "type": "string" - }, - "messageId": { - "type": "string" - } - } - }, - "chat.bsky.convo.defs.messageInput": { - "type": "object", - "required": [ - "text" - ], - "properties": { - "text": { - "type": "string", - "maxLength": 10000 - }, - "facets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.richtext.facet" - } - }, - "embed": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.embed.record" - } - ] - } - } - }, - "chat.bsky.convo.defs.messageView": { - "type": "object", - "required": [ - "id", - "rev", - "text", - "sender", - "sentAt" - ], - "properties": { - "id": { - "type": "string" - }, - "rev": { - "type": "string" - }, - "text": { - "type": "string", - "maxLength": 10000 - }, - "facets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/app.bsky.richtext.facet" - } - }, - "embed": { - "oneOf": [ - { - "$ref": "#/components/schemas/app.bsky.embed.record.view" - } - ] - }, - "sender": { - "$ref": "#/components/schemas/chat.bsky.convo.defs.messageViewSender" - }, - "sentAt": { - "type": "string", - "format": "date-time" - } - } - }, - "chat.bsky.convo.defs.deletedMessageView": { - "type": "object", - "required": [ - "id", - "rev", - "sender", - "sentAt" - ], - "properties": { - "id": { - "type": "string" - }, - "rev": { - "type": "string" - }, - "sender": { - "$ref": "#/components/schemas/chat.bsky.convo.defs.messageViewSender" - }, - "sentAt": { - "type": "string", - "format": "date-time" - } - } - }, - "chat.bsky.convo.defs.messageViewSender": { - "type": "object", - "required": [ - "did" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - } - } - }, - "chat.bsky.convo.defs.convoView": { - "type": "object", - "required": [ - "id", - "rev", - "members", - "muted", - "unreadCount" - ], - "properties": { - "id": { - "type": "string" - }, - "rev": { - "type": "string" - }, - "members": { - "type": "array", - "items": { - "$ref": "#/components/schemas/chat.bsky.actor.defs.profileViewBasic" - } - }, - "lastMessage": { - "oneOf": [ - { - "$ref": "#/components/schemas/chat.bsky.convo.defs.messageView" - }, - { - "$ref": "#/components/schemas/chat.bsky.convo.defs.deletedMessageView" - } - ] - }, - "muted": { - "type": "boolean" - }, - "opened": { - "type": "boolean" - }, - "unreadCount": { - "type": "integer" - } - } - }, - "chat.bsky.convo.defs.logBeginConvo": { - "type": "object", - "required": [ - "rev", - "convoId" - ], - "properties": { - "rev": { - "type": "string" - }, - "convoId": { - "type": "string" - } - } - }, - "chat.bsky.convo.defs.logLeaveConvo": { - "type": "object", - "required": [ - "rev", - "convoId" - ], - "properties": { - "rev": { - "type": "string" - }, - "convoId": { - "type": "string" - } - } - }, - "chat.bsky.convo.defs.logCreateMessage": { - "type": "object", - "required": [ - "rev", - "convoId", - "message" - ], - "properties": { - "rev": { - "type": "string" - }, - "convoId": { - "type": "string" - }, - "message": { - "oneOf": [ - { - "$ref": "#/components/schemas/chat.bsky.convo.defs.messageView" - }, - { - "$ref": "#/components/schemas/chat.bsky.convo.defs.deletedMessageView" - } - ] - } - } - }, - "chat.bsky.convo.defs.logDeleteMessage": { - "type": "object", - "required": [ - "rev", - "convoId", - "message" - ], - "properties": { - "rev": { - "type": "string" - }, - "convoId": { - "type": "string" - }, - "message": { - "oneOf": [ - { - "$ref": "#/components/schemas/chat.bsky.convo.defs.messageView" - }, - { - "$ref": "#/components/schemas/chat.bsky.convo.defs.deletedMessageView" - } - ] - } - } - }, - "chat.bsky.convo.sendMessageBatch.batchItem": { - "type": "object", - "required": [ - "convoId", - "message" - ], - "properties": { - "convoId": { - "type": "string" - }, - "message": { - "$ref": "#/components/schemas/chat.bsky.convo.defs.messageInput" - } - } - }, - "chat.bsky.moderation.getActorMetadata.metadata": { - "type": "object", - "required": [ - "messagesSent", - "messagesReceived", - "convos", - "convosStarted" - ], - "properties": { - "messagesSent": { - "type": "integer" - }, - "messagesReceived": { - "type": "integer" - }, - "convos": { - "type": "integer" - }, - "convosStarted": { - "type": "integer" - } - } - }, - "com.atproto.admin.defs.statusAttr": { - "type": "object", - "required": [ - "applied" - ], - "properties": { - "applied": { - "type": "boolean" - }, - "ref": { - "type": "string" - } - } - }, - "com.atproto.admin.defs.accountView": { - "type": "object", - "required": [ - "did", - "handle", - "indexedAt" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "handle": { - "type": "string", - "format": "handle" - }, - "email": { - "type": "string" - }, - "relatedRecords": { - "type": "array", - "items": {} - }, - "indexedAt": { - "type": "string", - "format": "date-time" - }, - "invitedBy": { - "$ref": "#/components/schemas/com.atproto.server.defs.inviteCode" - }, - "invites": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.server.defs.inviteCode" - } - }, - "invitesDisabled": { - "type": "boolean" - }, - "emailConfirmedAt": { - "type": "string", - "format": "date-time" - }, - "inviteNote": { - "type": "string" - }, - "deactivatedAt": { - "type": "string", - "format": "date-time" - }, - "threatSignatures": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.admin.defs.threatSignature" - } - } - } - }, - "com.atproto.admin.defs.repoRef": { - "type": "object", - "required": [ - "did" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - } - } - }, - "com.atproto.admin.defs.repoBlobRef": { - "type": "object", - "required": [ - "did", - "cid" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "recordUri": { - "type": "string", - "format": "at-uri" - } - } - }, - "com.atproto.admin.defs.threatSignature": { - "type": "object", - "required": [ - "property", - "value" - ], - "properties": { - "property": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "com.atproto.label.defs.label": { - "type": "object", - "description": "Metadata tag on an atproto resource (eg, repo or record).", - "required": [ - "src", - "uri", - "val", - "cts" - ], - "properties": { - "ver": { - "type": "integer" - }, - "src": { - "type": "string", - "description": "DID of the actor who created this label.", - "format": "did" - }, - "uri": { - "type": "string", - "description": "AT URI of the record, repository (account), or other resource that this label applies to.", - "format": "uri" - }, - "cid": { - "type": "string", - "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to.", - "format": "cid" - }, - "val": { - "type": "string", - "description": "The short string name of the value or type of this label.", - "maxLength": 128 - }, - "neg": { - "type": "boolean", - "description": "If true, this is a negation label, overwriting a previous label." - }, - "cts": { - "type": "string", - "description": "Timestamp when this label was created.", - "format": "date-time" - }, - "exp": { - "type": "string", - "description": "Timestamp at which this label expires (no longer applies).", - "format": "date-time" - }, - "sig": { - "type": "string", - "format": "byte", - "description": "Signature of dag-cbor encoded label." - } - } - }, - "com.atproto.label.defs.selfLabels": { - "type": "object", - "description": "Metadata tags on an atproto record, published by the author within the record.", - "required": [ - "values" - ], - "properties": { - "values": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.selfLabel" - }, - "maxItems": 10 - } - } - }, - "com.atproto.label.defs.selfLabel": { - "type": "object", - "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.", - "required": [ - "val" - ], - "properties": { - "val": { - "type": "string", - "description": "The short string name of the value or type of this label.", - "maxLength": 128 - } - } - }, - "com.atproto.label.defs.labelValueDefinition": { - "type": "object", - "description": "Declares a label value and its expected interpretations and behaviors.", - "required": [ - "identifier", - "severity", - "blurs", - "locales" - ], - "properties": { - "identifier": { - "type": "string", - "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", - "maxLength": 100 - }, - "severity": { - "type": "string", - "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", - "enum": [ - "inform", - "alert", - "none" - ] - }, - "blurs": { - "type": "string", - "description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", - "enum": [ - "content", - "media", - "none" - ] - }, - "defaultSetting": { - "type": "string", - "description": "The default setting for this label.", - "default": "warn", - "enum": [ - "ignore", - "warn", - "hide" - ] - }, - "adultOnly": { - "type": "boolean", - "description": "Does the user need to have adult content enabled in order to configure this label?" - }, - "locales": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.labelValueDefinitionStrings" - } - } - } - }, - "com.atproto.label.defs.labelValueDefinitionStrings": { - "type": "object", - "description": "Strings which describe the label in the UI, localized into a specific language.", - "required": [ - "lang", - "name", - "description" - ], - "properties": { - "lang": { - "type": "string", - "description": "The code of the language these strings are written in.", - "format": "language" - }, - "name": { - "type": "string", - "description": "A short human-readable name for the label.", - "maxLength": 640 - }, - "description": { - "type": "string", - "description": "A longer description of what the label means and why it might be applied.", - "maxLength": 100000 - } - } - }, - "com.atproto.label.defs.labelValue": { - "type": "string", - "enum": [ - "!hide", - "!no-promote", - "!warn", - "!no-unauthenticated", - "dmca-violation", - "doxxing", - "porn", - "sexual", - "nudity", - "nsfl", - "gore" - ] - }, - "com.atproto.label.subscribeLabels.labels": { - "type": "object", - "required": [ - "seq", - "labels" - ], - "properties": { - "seq": { - "type": "integer" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - } - } - }, - "com.atproto.label.subscribeLabels.info": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string", - "enum": [ - "OutdatedCursor" - ] - }, - "message": { - "type": "string" - } - } - }, - "com.atproto.moderation.defs.reasonType": { - "oneOf": [ - { - "$ref": "#/components/schemas/com.atproto.moderation.defs.reasonSpam" - }, - { - "$ref": "#/components/schemas/com.atproto.moderation.defs.reasonViolation" - }, - { - "$ref": "#/components/schemas/com.atproto.moderation.defs.reasonMisleading" - }, - { - "$ref": "#/components/schemas/com.atproto.moderation.defs.reasonSexual" - }, - { - "$ref": "#/components/schemas/com.atproto.moderation.defs.reasonRude" - }, - { - "$ref": "#/components/schemas/com.atproto.moderation.defs.reasonOther" - }, - { - "$ref": "#/components/schemas/com.atproto.moderation.defs.reasonAppeal" - } - ] - }, - "com.atproto.moderation.defs.reasonSpam": { - "type": "string", - "format": "token", - "description": "Spam: frequent unwanted promotion, replies, mentions" - }, - "com.atproto.moderation.defs.reasonViolation": { - "type": "string", - "format": "token", - "description": "Direct violation of server rules, laws, terms of service" - }, - "com.atproto.moderation.defs.reasonMisleading": { - "type": "string", - "format": "token", - "description": "Misleading identity, affiliation, or content" - }, - "com.atproto.moderation.defs.reasonSexual": { - "type": "string", - "format": "token", - "description": "Unwanted or mislabeled sexual content" - }, - "com.atproto.moderation.defs.reasonRude": { - "type": "string", - "format": "token", - "description": "Rude, harassing, explicit, or otherwise unwelcoming behavior" - }, - "com.atproto.moderation.defs.reasonOther": { - "type": "string", - "format": "token", - "description": "Other: reports not falling under another report category" - }, - "com.atproto.moderation.defs.reasonAppeal": { - "type": "string", - "format": "token", - "description": "Appeal: appeal a previously taken moderation action" - }, - "com.atproto.repo.applyWrites.create": { - "type": "object", - "description": "Operation which creates a new record.", - "required": [ - "collection", - "value" - ], - "properties": { - "collection": { - "type": "string", - "format": "nsid" - }, - "rkey": { - "type": "string", - "maxLength": 512 - }, - "value": {} - } - }, - "com.atproto.repo.applyWrites.update": { - "type": "object", - "description": "Operation which updates an existing record.", - "required": [ - "collection", - "rkey", - "value" - ], - "properties": { - "collection": { - "type": "string", - "format": "nsid" - }, - "rkey": { - "type": "string" - }, - "value": {} - } - }, - "com.atproto.repo.applyWrites.delete": { - "type": "object", - "description": "Operation which deletes an existing record.", - "required": [ - "collection", - "rkey" - ], - "properties": { - "collection": { - "type": "string", - "format": "nsid" - }, - "rkey": { - "type": "string" - } - } - }, - "com.atproto.repo.applyWrites.createResult": { - "type": "object", - "required": [ - "uri", - "cid" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "validationStatus": { - "type": "string", - "enum": [ - "valid", - "unknown" - ] - } - } - }, - "com.atproto.repo.applyWrites.updateResult": { - "type": "object", - "required": [ - "uri", - "cid" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "validationStatus": { - "type": "string", - "enum": [ - "valid", - "unknown" - ] - } - } - }, - "com.atproto.repo.applyWrites.deleteResult": { - "type": "object", - "required": [], - "properties": {} - }, - "com.atproto.repo.defs.commitMeta": { - "type": "object", - "required": [ - "cid", - "rev" - ], - "properties": { - "cid": { - "type": "string", - "format": "cid" - }, - "rev": { - "type": "string" - } - } - }, - "com.atproto.repo.listMissingBlobs.recordBlob": { - "type": "object", - "required": [ - "cid", - "recordUri" - ], - "properties": { - "cid": { - "type": "string", - "format": "cid" - }, - "recordUri": { - "type": "string", - "format": "at-uri" - } - } - }, - "com.atproto.repo.listRecords.record": { - "type": "object", - "required": [ - "uri", - "cid", - "value" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "value": {} - } - }, - "com.atproto.repo.strongRef": { - "type": "object", - "required": [ - "uri", - "cid" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - } - } - }, - "com.atproto.server.createAppPassword.appPassword": { - "type": "object", - "required": [ - "name", - "password", - "createdAt" - ], - "properties": { - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "privileged": { - "type": "boolean" - } - } - }, - "com.atproto.server.createInviteCodes.accountCodes": { - "type": "object", - "required": [ - "account", - "codes" - ], - "properties": { - "account": { - "type": "string" - }, - "codes": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "com.atproto.server.defs.inviteCode": { - "type": "object", - "required": [ - "code", - "available", - "disabled", - "forAccount", - "createdBy", - "createdAt", - "uses" - ], - "properties": { - "code": { - "type": "string" - }, - "available": { - "type": "integer" - }, - "disabled": { - "type": "boolean" - }, - "forAccount": { - "type": "string" - }, - "createdBy": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "uses": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.server.defs.inviteCodeUse" - } - } - } - }, - "com.atproto.server.defs.inviteCodeUse": { - "type": "object", - "required": [ - "usedBy", - "usedAt" - ], - "properties": { - "usedBy": { - "type": "string", - "format": "did" - }, - "usedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "com.atproto.server.describeServer.links": { - "type": "object", - "properties": { - "privacyPolicy": { - "type": "string", - "format": "uri" - }, - "termsOfService": { - "type": "string", - "format": "uri" - } - } - }, - "com.atproto.server.describeServer.contact": { - "type": "object", - "properties": { - "email": { - "type": "string" - } - } - }, - "com.atproto.server.listAppPasswords.appPassword": { - "type": "object", - "required": [ - "name", - "createdAt" - ], - "properties": { - "name": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "privileged": { - "type": "boolean" - } - } - }, - "com.atproto.sync.listRepos.repo": { - "type": "object", - "required": [ - "did", - "head", - "rev" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "head": { - "type": "string", - "description": "Current repo commit CID", - "format": "cid" - }, - "rev": { - "type": "string" - }, - "active": { - "type": "boolean" - }, - "status": { - "type": "string", - "description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.", - "enum": [ - "takendown", - "suspended", - "deactivated" - ] - } - } - }, - "com.atproto.sync.subscribeRepos.commit": { - "type": "object", - "description": "Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.", - "required": [ - "seq", - "rebase", - "tooBig", - "repo", - "commit", - "rev", - "since", - "blocks", - "ops", - "blobs", - "time" - ], - "properties": { - "seq": { - "type": "integer" - }, - "tooBig": { - "type": "boolean", - "description": "Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data." - }, - "repo": { - "type": "string", - "description": "The repo this event comes from.", - "format": "did" - }, - "commit": { - "type": "string", - "format": "cid-link" - }, - "rev": { - "type": "string", - "description": "The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event." - }, - "since": { - "type": "string", - "description": "The rev of the last emitted commit from this repo (if any)." - }, - "blocks": { - "type": "string", - "format": "byte", - "description": "CAR file containing relevant blocks, as a diff since the previous repo state.", - "maxLength": 1000000 - }, - "ops": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.sync.subscribeRepos.repoOp" - }, - "maxItems": 200 - }, - "blobs": { - "type": "array", - "items": { - "type": "string", - "format": "cid-link" - } - }, - "time": { - "type": "string", - "description": "Timestamp of when this message was originally broadcast.", - "format": "date-time" - } - } - }, - "com.atproto.sync.subscribeRepos.identity": { - "type": "object", - "description": "Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.", - "required": [ - "seq", - "did", - "time" - ], - "properties": { - "seq": { - "type": "integer" - }, - "did": { - "type": "string", - "format": "did" - }, - "time": { - "type": "string", - "format": "date-time" - }, - "handle": { - "type": "string", - "description": "The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details.", - "format": "handle" - } - } - }, - "com.atproto.sync.subscribeRepos.account": { - "type": "object", - "description": "Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active.", - "required": [ - "seq", - "did", - "time", - "active" - ], - "properties": { - "seq": { - "type": "integer" - }, - "did": { - "type": "string", - "format": "did" - }, - "time": { - "type": "string", - "format": "date-time" - }, - "active": { - "type": "boolean", - "description": "Indicates that the account has a repository which can be fetched from the host that emitted this event." - }, - "status": { - "type": "string", - "description": "If active=false, this optional field indicates a reason for why the account is not active.", - "enum": [ - "takendown", - "suspended", - "deleted", - "deactivated" - ] - } - } - }, - "com.atproto.sync.subscribeRepos.info": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string", - "enum": [ - "OutdatedCursor" - ] - }, - "message": { - "type": "string" - } - } - }, - "com.atproto.sync.subscribeRepos.repoOp": { - "type": "object", - "description": "A repo operation, ie a mutation of a single record.", - "required": [ - "action", - "path", - "cid" - ], - "properties": { - "action": { - "type": "string", - "enum": [ - "create", - "update", - "delete" - ] - }, - "path": { - "type": "string" - }, - "cid": { - "type": "string", - "format": "cid-link" - } - } - }, - "tools.ozone.communication.defs.templateView": { - "type": "object", - "required": [ - "id", - "name", - "contentMarkdown", - "disabled", - "lastUpdatedBy", - "createdAt", - "updatedAt" - ], - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string", - "description": "Name of the template." - }, - "subject": { - "type": "string", - "description": "Content of the template, can contain markdown and variable placeholders." - }, - "contentMarkdown": { - "type": "string", - "description": "Subject of the message, used in emails." - }, - "disabled": { - "type": "boolean" - }, - "lang": { - "type": "string", - "description": "Message language.", - "format": "language" - }, - "lastUpdatedBy": { - "type": "string", - "description": "DID of the user who last updated the template.", - "format": "did" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "tools.ozone.moderation.defs.modEventView": { - "type": "object", - "required": [ - "id", - "event", - "subject", - "subjectBlobCids", - "createdBy", - "createdAt" - ], - "properties": { - "id": { - "type": "integer" - }, - "event": { - "oneOf": [ - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventTakedown" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventReverseTakedown" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventComment" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventReport" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventLabel" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventAcknowledge" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventEscalate" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventMute" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventUnmute" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventMuteReporter" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventUnmuteReporter" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventEmail" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventResolveAppeal" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventDivert" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventTag" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.accountEvent" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.identityEvent" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.recordEvent" - } - ] - }, - "subject": { - "oneOf": [ - { - "$ref": "#/components/schemas/com.atproto.admin.defs.repoRef" - }, - { - "$ref": "#/components/schemas/com.atproto.repo.strongRef" - }, - { - "$ref": "#/components/schemas/chat.bsky.convo.defs.messageRef" - } - ] - }, - "subjectBlobCids": { - "type": "array", - "items": { - "type": "string" - } - }, - "createdBy": { - "type": "string", - "format": "did" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "creatorHandle": { - "type": "string" - }, - "subjectHandle": { - "type": "string" - } - } - }, - "tools.ozone.moderation.defs.modEventViewDetail": { - "type": "object", - "required": [ - "id", - "event", - "subject", - "subjectBlobs", - "createdBy", - "createdAt" - ], - "properties": { - "id": { - "type": "integer" - }, - "event": { - "oneOf": [ - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventTakedown" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventReverseTakedown" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventComment" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventReport" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventLabel" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventAcknowledge" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventEscalate" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventMute" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventUnmute" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventMuteReporter" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventUnmuteReporter" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventEmail" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventResolveAppeal" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventDivert" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.modEventTag" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.accountEvent" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.identityEvent" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.recordEvent" - } - ] - }, - "subject": { - "oneOf": [ - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.repoView" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.repoViewNotFound" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.recordView" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.recordViewNotFound" - } - ] - }, - "subjectBlobs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.blobView" - } - }, - "createdBy": { - "type": "string", - "format": "did" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "tools.ozone.moderation.defs.subjectStatusView": { - "type": "object", - "required": [ - "id", - "subject", - "createdAt", - "updatedAt", - "reviewState" - ], - "properties": { - "id": { - "type": "integer" - }, - "subject": { - "oneOf": [ - { - "$ref": "#/components/schemas/com.atproto.admin.defs.repoRef" - }, - { - "$ref": "#/components/schemas/com.atproto.repo.strongRef" - } - ] - }, - "hosting": { - "oneOf": [ - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.accountHosting" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.recordHosting" - } - ] - }, - "subjectBlobCids": { - "type": "array", - "items": { - "type": "string", - "format": "cid" - } - }, - "subjectRepoHandle": { - "type": "string" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp referencing when the last update was made to the moderation status of the subject", - "format": "date-time" - }, - "createdAt": { - "type": "string", - "description": "Timestamp referencing the first moderation status impacting event was emitted on the subject", - "format": "date-time" - }, - "reviewState": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.subjectReviewState" - }, - "comment": { - "type": "string", - "description": "Sticky comment on the subject." - }, - "muteUntil": { - "type": "string", - "format": "date-time" - }, - "muteReportingUntil": { - "type": "string", - "format": "date-time" - }, - "lastReviewedBy": { - "type": "string", - "format": "did" - }, - "lastReviewedAt": { - "type": "string", - "format": "date-time" - }, - "lastReportedAt": { - "type": "string", - "format": "date-time" - }, - "lastAppealedAt": { - "type": "string", - "description": "Timestamp referencing when the author of the subject appealed a moderation action", - "format": "date-time" - }, - "takendown": { - "type": "boolean" - }, - "appealed": { - "type": "boolean", - "description": "True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators." - }, - "suspendUntil": { - "type": "string", - "format": "date-time" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "tools.ozone.moderation.defs.subjectReviewState": { - "type": "string", - "enum": [ - "#reviewOpen", - "#reviewEscalated", - "#reviewClosed", - "#reviewNone" - ] - }, - "tools.ozone.moderation.defs.reviewOpen": { - "type": "string", - "format": "token", - "description": "Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator" - }, - "tools.ozone.moderation.defs.reviewEscalated": { - "type": "string", - "format": "token", - "description": "Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator" - }, - "tools.ozone.moderation.defs.reviewClosed": { - "type": "string", - "format": "token", - "description": "Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator" - }, - "tools.ozone.moderation.defs.reviewNone": { - "type": "string", - "format": "token", - "description": "Moderator review status of a subject: Unnecessary. Indicates that the subject does not need a review at the moment but there is probably some moderation related metadata available for it" - }, - "tools.ozone.moderation.defs.modEventTakedown": { - "type": "object", - "description": "Take down a subject permanently or temporarily", - "properties": { - "comment": { - "type": "string" - }, - "durationInHours": { - "type": "integer" - }, - "acknowledgeAccountSubjects": { - "type": "boolean", - "description": "If true, all other reports on content authored by this account will be resolved (acknowledged)." - } - } - }, - "tools.ozone.moderation.defs.modEventReverseTakedown": { - "type": "object", - "description": "Revert take down action on a subject", - "properties": { - "comment": { - "type": "string", - "description": "Describe reasoning behind the reversal." - } - } - }, - "tools.ozone.moderation.defs.modEventResolveAppeal": { - "type": "object", - "description": "Resolve appeal on a subject", - "properties": { - "comment": { - "type": "string", - "description": "Describe resolution." - } - } - }, - "tools.ozone.moderation.defs.modEventComment": { - "type": "object", - "description": "Add a comment to a subject", - "required": [ - "comment" - ], - "properties": { - "comment": { - "type": "string" - }, - "sticky": { - "type": "boolean", - "description": "Make the comment persistent on the subject" - } - } - }, - "tools.ozone.moderation.defs.modEventReport": { - "type": "object", - "description": "Report a subject", - "required": [ - "reportType" - ], - "properties": { - "comment": { - "type": "string" - }, - "isReporterMuted": { - "type": "boolean", - "description": "Set to true if the reporter was muted from reporting at the time of the event. These reports won't impact the reviewState of the subject." - }, - "reportType": { - "$ref": "#/components/schemas/com.atproto.moderation.defs.reasonType" - } - } - }, - "tools.ozone.moderation.defs.modEventLabel": { - "type": "object", - "description": "Apply/Negate labels on a subject", - "required": [ - "createLabelVals", - "negateLabelVals" - ], - "properties": { - "comment": { - "type": "string" - }, - "createLabelVals": { - "type": "array", - "items": { - "type": "string" - } - }, - "negateLabelVals": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "tools.ozone.moderation.defs.modEventAcknowledge": { - "type": "object", - "properties": { - "comment": { - "type": "string" - } - } - }, - "tools.ozone.moderation.defs.modEventEscalate": { - "type": "object", - "properties": { - "comment": { - "type": "string" - } - } - }, - "tools.ozone.moderation.defs.modEventMute": { - "type": "object", - "description": "Mute incoming reports on a subject", - "required": [ - "durationInHours" - ], - "properties": { - "comment": { - "type": "string" - }, - "durationInHours": { - "type": "integer" - } - } - }, - "tools.ozone.moderation.defs.modEventUnmute": { - "type": "object", - "description": "Unmute action on a subject", - "properties": { - "comment": { - "type": "string", - "description": "Describe reasoning behind the reversal." - } - } - }, - "tools.ozone.moderation.defs.modEventMuteReporter": { - "type": "object", - "description": "Mute incoming reports from an account", - "properties": { - "comment": { - "type": "string" - }, - "durationInHours": { - "type": "integer" - } - } - }, - "tools.ozone.moderation.defs.modEventUnmuteReporter": { - "type": "object", - "description": "Unmute incoming reports from an account", - "properties": { - "comment": { - "type": "string", - "description": "Describe reasoning behind the reversal." - } - } - }, - "tools.ozone.moderation.defs.modEventEmail": { - "type": "object", - "description": "Keep a log of outgoing email to a user", - "required": [ - "subjectLine" - ], - "properties": { - "subjectLine": { - "type": "string", - "description": "The subject line of the email sent to the user." - }, - "content": { - "type": "string", - "description": "The content of the email sent to the user." - }, - "comment": { - "type": "string", - "description": "Additional comment about the outgoing comm." - } - } - }, - "tools.ozone.moderation.defs.modEventDivert": { - "type": "object", - "description": "Divert a record's blobs to a 3rd party service for further scanning/tagging", - "properties": { - "comment": { - "type": "string" - } - } - }, - "tools.ozone.moderation.defs.modEventTag": { - "type": "object", - "description": "Add/Remove a tag on a subject", - "required": [ - "add", - "remove" - ], - "properties": { - "add": { - "type": "array", - "items": { - "type": "string" - } - }, - "remove": { - "type": "array", - "items": { - "type": "string" - } - }, - "comment": { - "type": "string", - "description": "Additional comment about added/removed tags." - } - } - }, - "tools.ozone.moderation.defs.accountEvent": { - "type": "object", - "description": "Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.", - "required": [ - "timestamp", - "active" - ], - "properties": { - "comment": { - "type": "string" - }, - "active": { - "type": "boolean", - "description": "Indicates that the account has a repository which can be fetched from the host that emitted this event." - }, - "status": { - "type": "string", - "enum": [ - "unknown", - "deactivated", - "deleted", - "takendown", - "suspended", - "tombstoned" - ] - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "tools.ozone.moderation.defs.identityEvent": { - "type": "object", - "description": "Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.", - "required": [ - "timestamp" - ], - "properties": { - "comment": { - "type": "string" - }, - "handle": { - "type": "string", - "format": "handle" - }, - "pdsHost": { - "type": "string", - "format": "uri" - }, - "tombstone": { - "type": "boolean" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "tools.ozone.moderation.defs.recordEvent": { - "type": "object", - "description": "Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.", - "required": [ - "timestamp", - "op" - ], - "properties": { - "comment": { - "type": "string" - }, - "op": { - "type": "string", - "enum": [ - "create", - "update", - "delete" - ] - }, - "cid": { - "type": "string", - "format": "cid" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "tools.ozone.moderation.defs.repoView": { - "type": "object", - "required": [ - "did", - "handle", - "relatedRecords", - "indexedAt", - "moderation" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "handle": { - "type": "string", - "format": "handle" - }, - "email": { - "type": "string" - }, - "relatedRecords": { - "type": "array", - "items": {} - }, - "indexedAt": { - "type": "string", - "format": "date-time" - }, - "moderation": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.moderation" - }, - "invitedBy": { - "$ref": "#/components/schemas/com.atproto.server.defs.inviteCode" - }, - "invitesDisabled": { - "type": "boolean" - }, - "inviteNote": { - "type": "string" - }, - "deactivatedAt": { - "type": "string", - "format": "date-time" - }, - "threatSignatures": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.admin.defs.threatSignature" - } - } - } - }, - "tools.ozone.moderation.defs.repoViewDetail": { - "type": "object", - "required": [ - "did", - "handle", - "relatedRecords", - "indexedAt", - "moderation" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "handle": { - "type": "string", - "format": "handle" - }, - "email": { - "type": "string" - }, - "relatedRecords": { - "type": "array", - "items": {} - }, - "indexedAt": { - "type": "string", - "format": "date-time" - }, - "moderation": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.moderationDetail" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - }, - "invitedBy": { - "$ref": "#/components/schemas/com.atproto.server.defs.inviteCode" - }, - "invites": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.server.defs.inviteCode" - } - }, - "invitesDisabled": { - "type": "boolean" - }, - "inviteNote": { - "type": "string" - }, - "emailConfirmedAt": { - "type": "string", - "format": "date-time" - }, - "deactivatedAt": { - "type": "string", - "format": "date-time" - }, - "threatSignatures": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.admin.defs.threatSignature" - } - } - } - }, - "tools.ozone.moderation.defs.repoViewNotFound": { - "type": "object", - "required": [ - "did" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - } - } - }, - "tools.ozone.moderation.defs.recordView": { - "type": "object", - "required": [ - "uri", - "cid", - "value", - "blobCids", - "indexedAt", - "moderation", - "repo" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "value": {}, - "blobCids": { - "type": "array", - "items": { - "type": "string", - "format": "cid" - } - }, - "indexedAt": { - "type": "string", - "format": "date-time" - }, - "moderation": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.moderation" - }, - "repo": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.repoView" - } - } - }, - "tools.ozone.moderation.defs.recordViewDetail": { - "type": "object", - "required": [ - "uri", - "cid", - "value", - "blobs", - "indexedAt", - "moderation", - "repo" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - }, - "cid": { - "type": "string", - "format": "cid" - }, - "value": {}, - "blobs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.blobView" - } - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/com.atproto.label.defs.label" - } - }, - "indexedAt": { - "type": "string", - "format": "date-time" - }, - "moderation": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.moderationDetail" - }, - "repo": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.repoView" - } - } - }, - "tools.ozone.moderation.defs.recordViewNotFound": { - "type": "object", - "required": [ - "uri" - ], - "properties": { - "uri": { - "type": "string", - "format": "at-uri" - } - } - }, - "tools.ozone.moderation.defs.moderation": { - "type": "object", - "properties": { - "subjectStatus": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.subjectStatusView" - } - } - }, - "tools.ozone.moderation.defs.moderationDetail": { - "type": "object", - "properties": { - "subjectStatus": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.subjectStatusView" - } - } - }, - "tools.ozone.moderation.defs.blobView": { - "type": "object", - "required": [ - "cid", - "mimeType", - "size", - "createdAt" - ], - "properties": { - "cid": { - "type": "string", - "format": "cid" - }, - "mimeType": { - "type": "string" - }, - "size": { - "type": "integer" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "details": { - "oneOf": [ - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.imageDetails" - }, - { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.videoDetails" - } - ] - }, - "moderation": { - "$ref": "#/components/schemas/tools.ozone.moderation.defs.moderation" - } - } - }, - "tools.ozone.moderation.defs.imageDetails": { - "type": "object", - "required": [ - "width", - "height" - ], - "properties": { - "width": { - "type": "integer" - }, - "height": { - "type": "integer" - } - } - }, - "tools.ozone.moderation.defs.videoDetails": { - "type": "object", - "required": [ - "width", - "height", - "length" - ], - "properties": { - "width": { - "type": "integer" - }, - "height": { - "type": "integer" - }, - "length": { - "type": "integer" - } - } - }, - "tools.ozone.moderation.defs.accountHosting": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "status": { - "type": "string", - "enum": [ - "takendown", - "suspended", - "deleted", - "deactivated", - "unknown" - ] - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "deletedAt": { - "type": "string", - "format": "date-time" - }, - "deactivatedAt": { - "type": "string", - "format": "date-time" - }, - "reactivatedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "tools.ozone.moderation.defs.recordHosting": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "status": { - "type": "string", - "enum": [ - "deleted", - "unknown" - ] - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "deletedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "tools.ozone.server.getConfig.serviceConfig": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - } - } - }, - "tools.ozone.server.getConfig.viewerConfig": { - "type": "object", - "properties": { - "role": { - "type": "string", - "enum": [ - "tools.ozone.team.defs#roleAdmin", - "tools.ozone.team.defs#roleModerator", - "tools.ozone.team.defs#roleTriage" - ] - } - } - }, - "tools.ozone.set.defs.set": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string", - "minLength": 3, - "maxLength": 128 - }, - "description": { - "type": "string", - "maxLength": 10240 - } - } - }, - "tools.ozone.set.defs.setView": { - "type": "object", - "required": [ - "name", - "setSize", - "createdAt", - "updatedAt" - ], - "properties": { - "name": { - "type": "string", - "minLength": 3, - "maxLength": 128 - }, - "description": { - "type": "string", - "maxLength": 10240 - }, - "setSize": { - "type": "integer" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "tools.ozone.setting.defs.option": { - "type": "object", - "required": [ - "key", - "value", - "did", - "scope", - "createdBy", - "lastUpdatedBy" - ], - "properties": { - "key": { - "type": "string", - "format": "nsid" - }, - "did": { - "type": "string", - "format": "did" - }, - "value": {}, - "description": { - "type": "string", - "maxLength": 10240 - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "managerRole": { - "type": "string", - "enum": [ - "tools.ozone.team.defs#roleModerator", - "tools.ozone.team.defs#roleTriage", - "tools.ozone.team.defs#roleAdmin" - ] - }, - "scope": { - "type": "string", - "enum": [ - "instance", - "personal" - ] - }, - "createdBy": { - "type": "string", - "format": "did" - }, - "lastUpdatedBy": { - "type": "string", - "format": "did" - } - } - }, - "tools.ozone.signature.defs.sigDetail": { - "type": "object", - "required": [ - "property", - "value" - ], - "properties": { - "property": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "tools.ozone.signature.findRelatedAccounts.relatedAccount": { - "type": "object", - "required": [ - "account" - ], - "properties": { - "account": { - "$ref": "#/components/schemas/com.atproto.admin.defs.accountView" - }, - "similarities": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tools.ozone.signature.defs.sigDetail" - } - } - } - }, - "tools.ozone.team.defs.member": { - "type": "object", - "required": [ - "did", - "role" - ], - "properties": { - "did": { - "type": "string", - "format": "did" - }, - "disabled": { - "type": "boolean" - }, - "profile": { - "$ref": "#/components/schemas/app.bsky.actor.defs.profileViewDetailed" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "lastUpdatedBy": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "#roleAdmin", - "#roleModerator", - "#roleTriage" - ] - } - } - }, - "tools.ozone.team.defs.roleAdmin": { - "type": "string", - "format": "token", - "description": "Admin role. Highest level of access, can perform all actions." - }, - "tools.ozone.team.defs.roleModerator": { - "type": "string", - "format": "token", - "description": "Moderator role. Can perform most actions." - }, - "tools.ozone.team.defs.roleTriage": { - "type": "string", - "format": "token", - "description": "Triage role. Mostly intended for monitoring and escalating issues." - } - }, - "securitySchemes": { - "Bearer": { - "type": "http", - "scheme": "bearer" - } - } - }, - "tags": [ - { - "name": "app.bsky.actor" - }, - { - "name": "app.bsky.embed" - }, - { - "name": "app.bsky.feed" - }, - { - "name": "app.bsky.graph" - }, - { - "name": "app.bsky.labeler" - }, - { - "name": "app.bsky.notification" - }, - { - "name": "app.bsky.richtext" - }, - { - "name": "app.bsky.unspecced" - }, - { - "name": "app.bsky.video" - }, - { - "name": "chat.bsky.actor" - }, - { - "name": "chat.bsky.convo" - }, - { - "name": "chat.bsky.moderation" - }, - { - "name": "com.atproto.admin" - }, - { - "name": "com.atproto.identity" - }, - { - "name": "com.atproto.label" - }, - { - "name": "com.atproto.moderation" - }, - { - "name": "com.atproto.repo" - }, - { - "name": "com.atproto.server" - }, - { - "name": "com.atproto.sync" - }, - { - "name": "com.atproto.temp" - }, - { - "name": "tools.ozone.communication" - }, - { - "name": "tools.ozone.moderation" - }, - { - "name": "tools.ozone.server" - }, - { - "name": "tools.ozone.set" - }, - { - "name": "tools.ozone.setting" - }, - { - "name": "tools.ozone.signature" - }, - { - "name": "tools.ozone.team" - } - ] -} diff --git a/src/net/gosha/atproto/client.clj b/src/net/gosha/atproto/client.clj deleted file mode 100644 index 613acfe..0000000 --- a/src/net/gosha/atproto/client.clj +++ /dev/null @@ -1,166 +0,0 @@ -(ns net.gosha.atproto.client - (:require - [clojure.core.async :as a] - [clojure.spec.alpha :as s] - [clojure.string :as str] - [charred.api :as json] - [clojure.tools.logging :as log] - [martian.core :as martian] - [martian.httpkit :as martian-http])) - -(s/def ::username string?) -(s/def ::base-url string?) -(s/def ::app-password string?) -(s/def ::openapi-spec string?) -(s/def ::config (s/keys :req-un [::base-url ::openapi-spec] - :opt-un [::app-password ::username ::openapi-spec])) - -(def ^{:doc "map of config keys to env vars that can set them"} - env-keys - {:openapi-spec "ATPROTO_OPENAPI_SPEC" - :base-url "ATPROTO_BASE_URL" - :username "ATPROTO_USERNAME" - :app-password "ATPROTO_APP_PASSWORD"}) - -(def ^{:private true} add-authentication-header - {:name ::add-authentication-header - :enter (fn [ctx] - (let [supplied-auth (get-in ctx [:params :headers "Authorization"])] - (assoc-in ctx [:request :headers "Authorization"] - (if supplied-auth - supplied-auth - (str "Bearer " (:access @(:tokens (:opts ctx))))))))}) - -(defn- build-config - "Build a configuration map based on defaults, env vars and provided values" - [& {:as user-config}] - (-> {:openapi-spec "atproto-xrpc-openapi.2024-12-18.json"} - (into (map (fn [[cfg-key env-var]] - (when-let [val (System/getenv env-var)] - {cfg-key val})) - env-keys)) - (merge user-config))) - -(defn- decode-jwt [token] - (let [decoder (java.util.Base64/getUrlDecoder) - payload (second (str/split token #"\.")) - bytes (.getBytes payload "UTF-8")] - (-> (.decode decoder bytes) - String. - json/read-json))) - -(defn- expired? [token] - (if (nil? token) - false - (let [exp (get (decode-jwt token) "exp") - now (quot (inst-ms (java.time.Instant/now)) 1000)] - (<= exp now)))) - -(defn authenticate - "Given an api session, authenticate with the atproto API using an app password - and return an authenticated api session. - - Note that the session will only work as long as the token is valid. If the - token expires, authenticate will need to be called again." - [session] - (let [config (:config (:opts session)) - {:keys [username app-password openapi-spec base-url]} config - response @(martian/response-for - session - :com.atproto.server.create-session - {:identifier username :password app-password}) - access-token (get-in response [:body :accessJwt]) - refresh-token (get-in response [:body :refreshJwt])] - (when-not access-token - (if (:status response) - (throw (ex-info (format "Authorization failed (%s)" (:status response)) - {:response response})) - (throw (ex-info "Authorization failed (unknown error)" - {:response response})))) - (martian-http/bootstrap-openapi - openapi-spec - {:server-url base-url - :tokens (atom {:access access-token :refresh refresh-token}) - :config config - :interceptors (cons add-authentication-header - martian-http/default-interceptors)}))) - -(defn refresh-token! - "Given an api session, refresh the access token using the refresh token, and - update the tokens atom" - [session] - (let [tokens (:tokens (:opts session)) - response @(martian/response-for - session - :com.atproto.server.refresh-session - {:headers { "Authorization" (str "Bearer " (:refresh @tokens))}}) - access-token (get-in response [:body :accessJwt]) - refresh-token (get-in response [:body :refreshJwt])] - (when-not access-token - (if (:status response) - (throw (ex-info (format "Failed refreshing token (%s)" (:status response)) - {:body response})) - (throw (ex-info "Failed refreshing token (unknown error)" - {:body response})))) - (reset! tokens {:access access-token :refresh refresh-token}) - )) - -(defn init - "Create and return a new api session. Valid configuration options are: - - :username - (optional) Bluesky username. - :app-password - (optional) Bluesky appplication-specific password. - :base-url - Bluesky endpoint. Mandatory. Use 'https://public.api.bsky.app' - for unauthenticated access, 'https://bsky.social' for - authenticated, or a different endpoint if you know what you're - doing. - :openapi-spec - OpenAPI JSON specification. Defaults to the included spec. - - All options can be specifed via an environment variable prefixed by `ATPROTO_` - (e.g. ATPROTO_BASE_URL)." - [& {:as options}] - (let [{:keys [openapi-spec - base-url - username] :as config} (build-config options)] - (when-not (s/valid? ::config config) - (throw (ex-info "Invalid configuration" - {:errors (s/explain-str ::config config)}))) - (let [session (martian-http/bootstrap-openapi openapi-spec - {:server-url base-url - :config config})] - (if username - (authenticate session) - session)))) - -(defn call - "Make an HTTP request to the atproto API. - - - `session` The API session returned by `init`. - - `endpoint` API endpoint for the format :com.atproto.server.get-session - - `opts` Map of options to pass to the endpoint" - [session endpoint & {:as opts}] - (let [tokens (:tokens (:opts session))] - (when (and tokens - (expired? (:access @tokens))) - (refresh-token! session))) - (let [res @(martian/response-for session endpoint opts) - error (-> res :body :error)] - (if (= error "ExpiredToken") - (do - (log/info "Access token expired, refreshing.") - (refresh-token! session) - (call session endpoint opts)) - res))) - -(defn call-async - "Like `call`, but returns a core.async channel instead of a IBlockingDeref. - - If an exception is thrown, it will be placed on the channel." - [session endpoint & {:as opts}] - (let [ch (a/promise-chan)] - (future - (try - (a/>!! ch @(call session endpoint opts)) - (catch Exception e - (a/>!! ch e)))) - ch)) diff --git a/src/net/gosha/atproto/client.cljc b/src/net/gosha/atproto/client.cljc new file mode 100644 index 0000000..862af5e --- /dev/null +++ b/src/net/gosha/atproto/client.cljc @@ -0,0 +1,166 @@ +(ns net.gosha.atproto.client + "Cross-platform API" + (:require [net.gosha.atproto.interceptor :as i] + #?@(:cljd [] :default [[clojure.core.async :as a]]) + #?(:clj [net.gosha.atproto.impl.jvm :as jvm]))) + +(defn- success? + [code] + (and (number? code) + (<= 200 code 299))) + +(defn- http-error-map + "Given an unsuccessful HTTP response, convert to an error map" + [resp] + {:error (str "HTTP " (:status resp)) + :http-response resp}) + +(defn- impl-interceptors + "Return implementation-specific HTTP & content type interceptors" + [] + #?(:clj [jvm/json-interceptor, jvm/httpkit-handler])) + +(defn- add-auth-header + [ctx token] + (assoc-in ctx [::i/request :headers "Authorization"] + (str "Bearer " token))) + +(declare procedure) + +(defn- authenticate + [ctx identifier password auth-atom] + (procedure (-> ctx + (assoc :skip-auth true) + (dissoc ::i/response)) + :com.atproto.server.createSession + {:identifier identifier :password password} + :callback + (fn auth-result [resp] + (if (:error resp) + (i/continue (assoc ctx ::i/response resp)) + (do + (reset! auth-atom resp) + (i/continue + (add-auth-header ctx (:accessJwt resp)))))))) + +(defn- refresh + [ctx identifier password auth-atom] + (procedure (-> ctx + (assoc :skip-auth true) + (dissoc ::i/response) + (add-auth-header (:refreshJwt @auth-atom))) + :com.atproto.server.refreshSession + :callback + (fn reauth-result [resp] + (if (:error resp) + ;; Try a full reauthenticate if refresh fails + (authenticate ctx identifier password auth-atom) + (do + (reset! auth-atom resp) + (i/continue + (add-auth-header ctx (:accessJwt resp)))))))) + +(defn- password-auth-interceptor + "Construct an interceptor for password-based authentication" + [identifier password] + (let [auth (atom nil)] + {::i/name ::password-auth + ::i/enter (fn enter-app-password [ctx] + (if (:skip-auth ctx) + ctx + (if-let [{token :accessJwt} @auth] + (add-auth-header ctx token) + (authenticate ctx identifier password auth)))) + ::i/leave (fn leave-app-password [{:keys [::i/response] :as ctx}] + ctx + (if-not (= "ExpiredToken" (-> response :body :error)) + ctx + (refresh ctx identifier password auth)))})) + + +(def xrpc-interceptor + "Construct an interceptor that converts XRPC requests to HTTP requests against + the provided endpoint. + + An XRPC request has a :nsid and either :parameters or :input. :parameters + indicates an XRPC 'query' (GET request) while `input` indicates a 'procedure' + (POST request)." + {::i/name ::xrpc + ::i/enter (fn xrpc-enter [{:keys [::i/request :endpoint] :as ctx}] + (let [url (str endpoint "/xrpc/" (name (:nsid request))) + req (if (:input request) + {:method :post + :body (:input request) + :headers {"content-type" "application/json"}} + {:method :get + :query-params (:parameters request)})] + (assoc ctx ::i/request (assoc req :url url)))) + ::i/leave (fn xrpc-leave [{:keys [::i/response] :as ctx}] + (cond + (:error response) + ctx + + (success? (:status response)) + (assoc ctx ::i/response (:body response)) + + (:error (:body response)) + (assoc ctx ::i/response (:body response)) + + :else + (assoc ctx ::i/response (http-error-map response))))}) + +(defn init + "Initialize an ATProto session using the given endpoint and options. Valid + options are: + + - `:identifier` + `:password` - Password-based authentication" + [endpoint & {:keys [:identifier :password] :as opts}] + (let [interceptors (concat + [xrpc-interceptor] + (when identifier + [(password-auth-interceptor identifier password)]) + (impl-interceptors))] + {:endpoint endpoint + :interceptors interceptors})) + +(defn- exec + "Given an XRPC request, execute it against the specified session. The + mechanism for returning results is specified via a :channel, :callback, or + :promise keyword arg, defaulting to a platform-appropriate type." + [session request & {:keys [:channel :callback :promise] :as opts}] + (let [promise #?(:clj (if (empty? opts) (clojure.core/promise) promise) + :default promise) + channel #?(:cljs (if (empty? opts) (a/chan) channel) + :default channel) + cb (cond + channel #?(:clj #(a/>!! channel (::i/response %)) + :cljs #(a/go (a/>! channel (::i/response %))) + :cljd #(throw (ex-info + "core.async not supported" {}))) + promise #?(:clj #(deliver promise (::i/response %)) + :default #(throw (ex-info + "JVM promises not supported" {}))) + callback #(callback (::i/response %)))] + (i/execute (assoc session + ::i/queue (:interceptors session) + ::i/request request) cb) + (or channel promise))) + +(defn query + "Query using the provided NSID and parameters. The mechanism for returning + results is specified via a :channel, :callback, or :promise keyword arg, + which is returned. Defaults to a platform-appropriate type." + [session nsid parameters & {:as opts}] + (exec session {:nsid nsid :parameters parameters} opts)) + +(defn procedure + "Execute a procedure using the provided NSID and parameters. The mechanism + for returning results is specified via a :channel, :callback, or :promise + keyword arg which is returned. Defaults to a platform-appropriate type." + [session nsid input & {:as opts}] + (exec session {:nsid nsid :input input} opts)) + +;; TODO: Validate by reading Lexicon files, and converting to schema/spec/other +(defn validate + [nsid parameters-or-input] + (throw (ex-info "Validation not yet implemented" {}))) diff --git a/src/net/gosha/atproto/impl/jvm.clj b/src/net/gosha/atproto/impl/jvm.clj new file mode 100644 index 0000000..5f1197e --- /dev/null +++ b/src/net/gosha/atproto/impl/jvm.clj @@ -0,0 +1,39 @@ +(ns net.gosha.atproto.impl.jvm + "JVM interceptor implementations" + (:require [charred.api :as json] + [net.gosha.atproto.interceptor :as i] + [org.httpkit.client :as http])) + +(defn- json-content-type? + "Test if a request or response should be interpreted as json" + [req-or-resp] + (when-let [ct (:content-type (:headers req-or-resp))] + (.startsWith ct "application/json"))) + +(def json-interceptor + "Interceptor for JSON request and response bodies" + {::i/name ::json + ::i/enter (fn enter-json [{:keys [::i/request] :as ctx}] + (if (and (:body request) + (or (json-content-type? request) + (not (:content-type (:headers request))))) + (update-in ctx [::i/request :body] json/write-json-str) + ctx)) + ::i/leave (fn leave-json [{:keys [::i/response] :as ctx}] + (if (json-content-type? response) + (update-in ctx [::i/response :body] + #(json/read-json % :key-fn keyword)) + ctx))}) + +(def httpkit-handler + "Interceptor to handle HTTP requests using httpkit" + {::i/name ::http + ::i/enter (fn enter-httpkit [ctx] + (http/request (::i/request ctx) + (fn [{:keys [error] :as resp}] + (if error + (i/continue (assoc ctx ::i/response + {:error (.getName (.getClass error)) + :message (.getMessage error) + :exception error})) + (i/continue (assoc ctx ::i/response resp))))))}) diff --git a/src/net/gosha/atproto/interceptor.cljc b/src/net/gosha/atproto/interceptor.cljc new file mode 100644 index 0000000..609c674 --- /dev/null +++ b/src/net/gosha/atproto/interceptor.cljc @@ -0,0 +1,120 @@ +(ns net.gosha.atproto.interceptor + "Simplified, callback-based implementation of the interceptor pattern. + + Interceptors are maps with ::enter and/or ::leave keys, with _interceptor + functions_ as values. An interceptor function takes a context as its + argument, and either (a) returns an updated context, or (b) calls + `continue` with an updated context (possibly asynchronously.) If it calls + continue, it should return nil to avoid double-processing the context. + + Execution flow is as described in the Pedestal documentation + (http://pedestal.io/pedestal/0.6/reference/interceptors.html): + + ``` + Logically speaking, interceptors form a queue. During the :enter phase, + the next interceptor is popped off the queue, pushed onto the leave stack, + and it’s :enter function, if any, is executed. + + Once the handler (or other interceptor) adds a ::response to the context, + the chain logic switches to ::leave mode: it pops interceptors off the leave + stack and invokes the :leave function, if any. + + Because it’s the leave stack the :leave functions are invoked in the opposite + order from the :enter functions. + + Both the queue and the stack reside in the context map. Since interceptors can + modify the context map, that means they can change the plan of execution for + the rest of the request! Interceptors are allowed to enqueue more interceptors + to be called, or they can terminate the request. + + This process, of running all the interceptor ::enter functions, + then running the interceptor ::leave functions, is called executing the + interceptor chain. + ``` + + Context maps may have the following keys: + + - ::queue - Interceptor queue + - ::stack - Interceptor stack + - ::request - The request object + - ::response - The response object (present only for `leave` phase.) + + Errors are represented as response objects with an `:error` key indicating + the error type, and a `:message` key with a human-readable error message.") + +(declare continue) + +(defn- try-invoke + "Helper function to invoke the given phase on an interceptor with appropriate + error handling." + [i phase ctx] + (try + (let [f (get i phase)] + (if (not f) + ctx + (let [ret (f ctx)] + (if (and ret (not (and (map? ret) (contains? ret ::request)))) + {::request ::missing + ::response {:error "Invalid Interceptor Context" + :message (str "Phase " phase " of " (::name i) + " returned a non-context value.") + :return-value ret}} + ret)))) + #?(:clj (catch Throwable t + (assoc ctx ::response + {:error (.getName (.getClass t)) + :message (.getMessage t) + :exception t + :phase phase + :interceptor (::name i)}))))) + +(defn- leave + "Execute the leave phase of an interceptor context." + [{:keys [::queue ::stack] :as ctx}] + (if (empty? stack) + ctx + (let [current (first stack) + ctx' (assoc ctx ::stack (rest stack)) + ctx'' (try-invoke current ::leave ctx')] + (when ctx'' (continue ctx''))))) + +(defn- enter + "Execute the enter phase of an interceptor context." + [{:keys [::queue ::stack] :as ctx}] + (if (empty? queue) + (leave ctx) + (let [current (first queue) + ctx' (assoc ctx ::stack (cons current stack) ::queue (rest queue)) + ctx'' (try-invoke current ::enter ctx')] + (when ctx'' (continue ctx''))))) + +(defn- first-index-of + [seq pred] + (first (keep-indexed (fn [i v] + (when (pred v) i)) seq))) + +(defn insert-after + "Given a context, insert the given interceptor into the queue immediately + before the interceptor with the provided name. Throws an exception if no such + interceptor exists" + [ctx interceptor name] + (let [[before after] (split-at (inc (first-index-of (::stack ctx) + #(= name (::name %)))) + (::stack ctx))] + (assoc ctx ::stack (concat before [interceptor] after)))) + +(defn continue + "Continue processing the interceptor chain of the provided context. + + This function may be called rather than returning an updated context from an + interceptor." + [{:keys [::response] :as ctx}] + (if response (leave ctx) (enter ctx))) + +(defn execute + "Execute an interceptor chain starting with the context, passing the + final context to the provided callback." + [ctx callback] + (let [final {::name ::execute ::leave callback} + ctx (update ctx ::queue #(cons final %))] + (enter ctx))) From ace9a77db4fe2e1a360c637801820db8633ce7f6 Mon Sep 17 00:00:00 2001 From: Luke VanderHart Date: Mon, 3 Feb 2025 15:23:02 -0500 Subject: [PATCH 2/8] remove unused deps --- deps.edn | 3 --- 1 file changed, 3 deletions(-) diff --git a/deps.edn b/deps.edn index 21b702a..ea533e6 100644 --- a/deps.edn +++ b/deps.edn @@ -4,8 +4,5 @@ org.clojure/core.async {:mvn/version "1.6.681"} com.cnuernber/charred {:mvn/version "1.034"} http-kit/http-kit {:mvn/version "2.8.0"} - metosin/reitit {:mvn/version "0.7.2"} - com.github.oliyh/martian {:mvn/version "0.1.30"} - com.github.oliyh/martian-httpkit {:mvn/version "0.1.30"} org.java-websocket/Java-WebSocket {:mvn/version "1.6.0"} org.slf4j/slf4j-simple {:mvn/version "2.0.16"}}} From 142e1a564ba05b5aeef0961b4913aad94147acc8 Mon Sep 17 00:00:00 2001 From: Luke VanderHart Date: Wed, 5 Feb 2025 10:49:22 -0500 Subject: [PATCH 3/8] fix readme, add debug info as metadata --- README.md | 2 +- src/net/gosha/atproto/client.cljc | 13 +++++++++---- src/net/gosha/atproto/impl/jvm.clj | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4054ef5..e868596 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ You can also provide a `:channel`, `:callback` or `:promise` keyword option to r ;; Bluesky endpoints and their query params can be found here: ;; https://docs.bsky.app/docs/category/http-reference -@(at/query session :app.bsky.actor.get-profile {:actor "gosha.net"}) +@(at/query session :app.bsky.actor.getProfile {:actor "gosha.net"}) ;; => {:handle "gosha.net", ;; :displayName "Gosha ⚡", ;; :did "did:plc:ypjjs7u7owjb7xmueb2iw37u", diff --git a/src/net/gosha/atproto/client.cljc b/src/net/gosha/atproto/client.cljc index 862af5e..0ffc37c 100644 --- a/src/net/gosha/atproto/client.cljc +++ b/src/net/gosha/atproto/client.cljc @@ -123,6 +123,11 @@ {:endpoint endpoint :interceptors interceptors})) +(defn- extract-response + [ctx] + (with-meta (::i/response ctx) + {:ctx ctx})) + (defn- exec "Given an XRPC request, execute it against the specified session. The mechanism for returning results is specified via a :channel, :callback, or @@ -133,14 +138,14 @@ channel #?(:cljs (if (empty? opts) (a/chan) channel) :default channel) cb (cond - channel #?(:clj #(a/>!! channel (::i/response %)) - :cljs #(a/go (a/>! channel (::i/response %))) + channel #?(:clj #(a/>!! channel (extract-response %)) + :cljs #(a/go (a/>! channel (extract-response %))) :cljd #(throw (ex-info "core.async not supported" {}))) - promise #?(:clj #(deliver promise (::i/response %)) + promise #?(:clj #(deliver promise (extract-response %)) :default #(throw (ex-info "JVM promises not supported" {}))) - callback #(callback (::i/response %)))] + callback #(callback (extract-response %)))] (i/execute (assoc session ::i/queue (:interceptors session) ::i/request request) cb) diff --git a/src/net/gosha/atproto/impl/jvm.clj b/src/net/gosha/atproto/impl/jvm.clj index 5f1197e..126c637 100644 --- a/src/net/gosha/atproto/impl/jvm.clj +++ b/src/net/gosha/atproto/impl/jvm.clj @@ -36,4 +36,5 @@ {:error (.getName (.getClass error)) :message (.getMessage error) :exception error})) - (i/continue (assoc ctx ::i/response resp))))))}) + (i/continue (assoc ctx ::i/response resp + ::response resp))))))}) From b393931bd2a91ee418693913a837f7a7c79d6353 Mon Sep 17 00:00:00 2001 From: Luke VanderHart Date: Wed, 5 Feb 2025 11:55:18 -0500 Subject: [PATCH 4/8] decouple async logic --- src/net/gosha/atproto/client.cljc | 48 ++++++++------------------ src/net/gosha/atproto/interceptor.cljc | 35 +++++++++++++++---- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/net/gosha/atproto/client.cljc b/src/net/gosha/atproto/client.cljc index 0ffc37c..910e8bf 100644 --- a/src/net/gosha/atproto/client.cljc +++ b/src/net/gosha/atproto/client.cljc @@ -115,55 +115,35 @@ - `:identifier` + `:password` - Password-based authentication" [endpoint & {:keys [:identifier :password] :as opts}] - (let [interceptors (concat - [xrpc-interceptor] - (when identifier - [(password-auth-interceptor identifier password)]) - (impl-interceptors))] - {:endpoint endpoint - :interceptors interceptors})) - -(defn- extract-response - [ctx] - (with-meta (::i/response ctx) - {:ctx ctx})) - -(defn- exec + {:endpoint endpoint + :http-interceptors (impl-interceptors) + :auth-interceptors (when password + [(password-auth-interceptor identifier password)])}) + +(defn- exec-xrpc "Given an XRPC request, execute it against the specified session. The mechanism for returning results is specified via a :channel, :callback, or :promise keyword arg, defaulting to a platform-appropriate type." - [session request & {:keys [:channel :callback :promise] :as opts}] - (let [promise #?(:clj (if (empty? opts) (clojure.core/promise) promise) - :default promise) - channel #?(:cljs (if (empty? opts) (a/chan) channel) - :default channel) - cb (cond - channel #?(:clj #(a/>!! channel (extract-response %)) - :cljs #(a/go (a/>! channel (extract-response %))) - :cljd #(throw (ex-info - "core.async not supported" {}))) - promise #?(:clj #(deliver promise (extract-response %)) - :default #(throw (ex-info - "JVM promises not supported" {}))) - callback #(callback (extract-response %)))] - (i/execute (assoc session - ::i/queue (:interceptors session) - ::i/request request) cb) - (or channel promise))) + [session request & {:as opts}] + (i/execute (assoc session + ::i/queue (concat [xrpc-interceptor] + (:auth-interceptors session) + (:http-interceptors session)) + ::i/request request) opts)) (defn query "Query using the provided NSID and parameters. The mechanism for returning results is specified via a :channel, :callback, or :promise keyword arg, which is returned. Defaults to a platform-appropriate type." [session nsid parameters & {:as opts}] - (exec session {:nsid nsid :parameters parameters} opts)) + (exec-xrpc session {:nsid nsid :parameters parameters} opts)) (defn procedure "Execute a procedure using the provided NSID and parameters. The mechanism for returning results is specified via a :channel, :callback, or :promise keyword arg which is returned. Defaults to a platform-appropriate type." [session nsid input & {:as opts}] - (exec session {:nsid nsid :input input} opts)) + (exec-xrpc session {:nsid nsid :input input} opts)) ;; TODO: Validate by reading Lexicon files, and converting to schema/spec/other (defn validate diff --git a/src/net/gosha/atproto/interceptor.cljc b/src/net/gosha/atproto/interceptor.cljc index 609c674..4e69194 100644 --- a/src/net/gosha/atproto/interceptor.cljc +++ b/src/net/gosha/atproto/interceptor.cljc @@ -40,7 +40,8 @@ - ::response - The response object (present only for `leave` phase.) Errors are represented as response objects with an `:error` key indicating - the error type, and a `:message` key with a human-readable error message.") + the error type, and a `:message` key with a human-readable error message." + (:require #?@(:cljd [] :default [[clojure.core.async :as a]]))) (declare continue) @@ -111,10 +112,32 @@ [{:keys [::response] :as ctx}] (if response (leave ctx) (enter ctx))) +(defn- extract-response + [ctx] + (with-meta (::response ctx) + {::ctx ctx})) + (defn execute - "Execute an interceptor chain starting with the context, passing the - final context to the provided callback." - [ctx callback] - (let [final {::name ::execute ::leave callback} + "Execute an interceptor chain, returning the final response using the + requested async mechanism as specified in `opts`. Defaults to a + platform-appropriate mechanism. + + The final context is added to the response as metadata, for debug purposes." + [ctx & {:keys [:channel :callback :promise] :as opts}] + (let [promise #?(:clj (if (empty? opts) (clojure.core/promise) promise) + :default promise) + channel #?(:cljs (if (empty? opts) (a/chan) channel) + :default channel) + cb (cond + channel #?(:clj #(a/>!! channel (extract-response %)) + :cljs #(a/go (a/>! channel (extract-response %))) + :cljd #(throw (ex-info + "core.async not supported" {}))) + promise #?(:clj #(deliver promise (extract-response %)) + :default #(throw (ex-info + "JVM promises not supported" {}))) + callback #(callback (extract-response %))) + final {::name ::execute ::leave cb} ctx (update ctx ::queue #(cons final %))] - (enter ctx))) + (enter ctx) + (or channel promise))) From 0a6aee8cb7c19e5ff5d1e05c0a58c145b8cc5ca5 Mon Sep 17 00:00:00 2001 From: Luke VanderHart Date: Fri, 7 Feb 2025 13:03:03 -0500 Subject: [PATCH 5/8] more refactoring --- src/net/gosha/atproto/client.cljc | 224 +++++++++++++++++-------- src/net/gosha/atproto/impl/jvm.clj | 3 +- src/net/gosha/atproto/interceptor.cljc | 76 ++++++--- 3 files changed, 202 insertions(+), 101 deletions(-) diff --git a/src/net/gosha/atproto/client.cljc b/src/net/gosha/atproto/client.cljc index 910e8bf..5a67cdb 100644 --- a/src/net/gosha/atproto/client.cljc +++ b/src/net/gosha/atproto/client.cljc @@ -1,6 +1,16 @@ (ns net.gosha.atproto.client - "Cross-platform API" + "Cross-platform API. + + All functions are async, and follow the same pattern for specifying how + results are returned. Each function takes keyword argument options, and + callers can specify a :channel, :callback, or :promise which will recieve + the results. + + Not all mechanisms are supported on all platforms. If no result mechanism is + specified, a platform-appropriate deferred value (e.g. promise or core.async + channel will be returned.)" (:require [net.gosha.atproto.interceptor :as i] + [clojure.string :as str] #?@(:cljd [] :default [[clojure.core.async :as a]]) #?(:clj [net.gosha.atproto.impl.jvm :as jvm]))) @@ -27,55 +37,32 @@ (declare procedure) -(defn- authenticate - [ctx identifier password auth-atom] - (procedure (-> ctx - (assoc :skip-auth true) - (dissoc ::i/response)) - :com.atproto.server.createSession - {:identifier identifier :password password} - :callback - (fn auth-result [resp] - (if (:error resp) - (i/continue (assoc ctx ::i/response resp)) - (do - (reset! auth-atom resp) - (i/continue - (add-auth-header ctx (:accessJwt resp)))))))) - -(defn- refresh - [ctx identifier password auth-atom] - (procedure (-> ctx - (assoc :skip-auth true) - (dissoc ::i/response) - (add-auth-header (:refreshJwt @auth-atom))) - :com.atproto.server.refreshSession - :callback - (fn reauth-result [resp] - (if (:error resp) - ;; Try a full reauthenticate if refresh fails - (authenticate ctx identifier password auth-atom) - (do - (reset! auth-atom resp) - (i/continue - (add-auth-header ctx (:accessJwt resp)))))))) - -(defn- password-auth-interceptor - "Construct an interceptor for password-based authentication" - [identifier password] - (let [auth (atom nil)] - {::i/name ::password-auth - ::i/enter (fn enter-app-password [ctx] - (if (:skip-auth ctx) - ctx - (if-let [{token :accessJwt} @auth] - (add-auth-header ctx token) - (authenticate ctx identifier password auth)))) - ::i/leave (fn leave-app-password [{:keys [::i/response] :as ctx}] +(def auth-header-interceptor + "Interceptor to add auth headers" + {::i/name ::auth-headers + ::i/enter (fn enter-auth-headers [{:keys [auth refresh?] :as ctx}] + (println "adding auth header: refresh=" refresh?) + (assoc-in ctx [::i/request :headers "Authorization"] + (str "Bearer " (if refresh? + (:refreshJwt @auth) + (:accessJwt @auth)))))}) + +(def refresh-token-interceptor + "Interceptor that refreshes and retries expired tokens" + {::i/name ::refresh-tokens + ::i/leave (fn leave-refresh-tokens [{:keys [::i/response] :as ctx}] + (if-not (= "ExpiredToken" (-> response :body :error)) ctx - (if-not (= "ExpiredToken" (-> response :body :error)) - ctx - (refresh ctx identifier password auth)))})) + (procedure (assoc ctx :refresh? true) + :com.atproto.server.refreshSession + {} + :callback (fn [resp] + (if (:error resp) + (i/continue (assoc ctx ::i/response resp)) + (do + (reset! (:auth ctx) resp) + (i/continue + (dissoc ctx ::i/response))))))))}) (def xrpc-interceptor @@ -88,10 +75,12 @@ {::i/name ::xrpc ::i/enter (fn xrpc-enter [{:keys [::i/request :endpoint] :as ctx}] (let [url (str endpoint "/xrpc/" (name (:nsid request))) - req (if (:input request) - {:method :post - :body (:input request) - :headers {"content-type" "application/json"}} + req (if-let [input (:input request)] + (if (empty? input) + {:method :post} + {:method :post + :body (:input request) + :headers {"content-type" "application/json"}}) {:method :get :query-params (:parameters request)})] (assoc ctx ::i/request (assoc req :url url)))) @@ -109,42 +98,131 @@ :else (assoc ctx ::i/response (http-error-map response))))}) -(defn init - "Initialize an ATProto session using the given endpoint and options. Valid - options are: +(def default-public-endpoint "https://public.api.bsky.app") + +(declare pd-server) + +(def ^:private cfg-endpoint + "Config interceptor that resolves an endpoint if not provided" + {::i/name ::resolve-endpoint + ::i/enter (fn [{:keys [:endpoint :identifier] :as ctx}] + (if endpoint + ctx + (if identifier + (pd-server identifier :callback + #(i/continue + (assoc ctx :endpoint %))) + (assoc ctx :endpoint default-public-endpoint))))}) + + +(def ^:private cfg-auth-password + "Config interceptor that performs password-based authentication" + {::i/name ::cfg-auth-password + ::i/enter (fn [{:keys [identifier password] :as ctx}] + (if-not password + ctx + (procedure ctx :com.atproto.server.createSession + {:identifier identifier :password password} + :callback + (fn [resp] + (if (:error resp) + (i/continue (assoc ctx ::i/response resp)) + (i/continue (-> ctx + (assoc :auth (atom resp)) + (update :interceptors concat + [auth-header-interceptor + refresh-token-interceptor]))))))))}) + +(def ^:private cfg-session + "Config interceptor to clean up and return the current context as a session" + {::i/enter (fn [ctx] + (assoc ctx ::i/response (dissoc ctx ::i/queue ::i/stack)))}) - - `:identifier` + `:password` - Password-based authentication" - [endpoint & {:keys [:identifier :password] :as opts}] - {:endpoint endpoint - :http-interceptors (impl-interceptors) - :auth-interceptors (when password - [(password-auth-interceptor identifier password)])}) +(defn init + "Initialize an ATProto session using the given configuration options. + Valid config options are: + + - `:endpoint` - Specify a server to connect to. If not specified, will + resolve and use the PDS server associated with :identifier`, + or else https://public.api.bsky.app. + - `:identifier` - A user's DID or handle. + - `:password` - Password for app-password based authentication. + - `:interceptors` - Custom user-supplied interceptors to modify http requests. + + " + [& {:as opts}] + (let [cfg-interceptors [cfg-endpoint + cfg-auth-password + cfg-session]] + (i/execute (-> opts + (assoc ::i/queue cfg-interceptors) + (dissoc :promise :callback :channel)) + (select-keys opts [:promise :callback :channel])))) (defn- exec-xrpc "Given an XRPC request, execute it against the specified session. The mechanism for returning results is specified via a :channel, :callback, or :promise keyword arg, defaulting to a platform-appropriate type." [session request & {:as opts}] - (i/execute (assoc session - ::i/queue (concat [xrpc-interceptor] - (:auth-interceptors session) - (:http-interceptors session)) - ::i/request request) opts)) + (i/execute (-> session + (assoc ::i/queue (concat + [xrpc-interceptor] + (:interceptors session) + (impl-interceptors))) + (assoc ::i/request request) + (dissoc ::i/response)) + opts)) (defn query - "Query using the provided NSID and parameters. The mechanism for returning - results is specified via a :channel, :callback, or :promise keyword arg, - which is returned. Defaults to a platform-appropriate type." + "Query using the provided NSID and parameters." [session nsid parameters & {:as opts}] (exec-xrpc session {:nsid nsid :parameters parameters} opts)) (defn procedure - "Execute a procedure using the provided NSID and parameters. The mechanism - for returning results is specified via a :channel, :callback, or :promise - keyword arg which is returned. Defaults to a platform-appropriate type." + "Execute a procedure using the provided NSID and parameters." [session nsid input & {:as opts}] (exec-xrpc session {:nsid nsid :input input} opts)) +(defn resolve-handle + "Resolve the given handle to a DID." + [handle & {:keys [endpoint] :as opts + :or {endpoint default-public-endpoint}}] + (query {:endpoint endpoint} :com.atproto.identity.resolveHandle + {:handle handle} + opts)) + +(defn id-doc + "Retrieve the identity document for a handle or DID" + [handle-or-did & {:keys [endpoint] + :as opts + :or {endpoint default-public-endpoint}}] + (let [req (fn [did] {:method :get + :url (str "https://plc.directory/" did)}) + interceptor {::i/enter (fn [ctx] + (if (str/starts-with? handle-or-did "did:") + (assoc ctx ::i/request (req handle-or-did)) + (resolve-handle handle-or-did :callback + (fn [{:keys [:did]}] + (i/continue (assoc ctx ::i/request + (req did))))))) + ::i/leave (fn [ctx] + (update ctx ::i/response :body))}] + (i/execute {:endpoint endpoint + ::i/queue (cons interceptor (impl-interceptors))} + opts))) + +(defn pd-server + [handle-or-did & {:as opts}] + (let [[cb val] (i/platform-async opts)] + (id-doc handle-or-did :endpoint (:endpoint opts) + :callback (fn [id-doc] + (cb (->> (:service id-doc) + (filter #(= (:type %) "AtprotoPersonalDataServer")) + (first) + (:serviceEndpoint))))) + val)) + + ;; TODO: Validate by reading Lexicon files, and converting to schema/spec/other (defn validate [nsid parameters-or-input] diff --git a/src/net/gosha/atproto/impl/jvm.clj b/src/net/gosha/atproto/impl/jvm.clj index 126c637..7bce62d 100644 --- a/src/net/gosha/atproto/impl/jvm.clj +++ b/src/net/gosha/atproto/impl/jvm.clj @@ -8,7 +8,8 @@ "Test if a request or response should be interpreted as json" [req-or-resp] (when-let [ct (:content-type (:headers req-or-resp))] - (.startsWith ct "application/json"))) + (or (.startsWith ct "application/json") + (.startsWith ct "application/did+ld+json")))) (def json-interceptor "Interceptor for JSON request and response bodies" diff --git a/src/net/gosha/atproto/interceptor.cljc b/src/net/gosha/atproto/interceptor.cljc index 4e69194..e6171a6 100644 --- a/src/net/gosha/atproto/interceptor.cljc +++ b/src/net/gosha/atproto/interceptor.cljc @@ -54,7 +54,9 @@ (if (not f) ctx (let [ret (f ctx)] - (if (and ret (not (and (map? ret) (contains? ret ::request)))) + (if (and ret (not (and (map? ret) + (or (contains? ret ::queue) + (contains? ret ::stack))))) {::request ::missing ::response {:error "Invalid Interceptor Context" :message (str "Phase " phase " of " (::name i) @@ -75,7 +77,7 @@ (if (empty? stack) ctx (let [current (first stack) - ctx' (assoc ctx ::stack (rest stack)) + ctx' (assoc ctx ::stack (rest stack) ::queue (cons current queue)) ctx'' (try-invoke current ::leave ctx')] (when ctx'' (continue ctx''))))) @@ -83,7 +85,9 @@ "Execute the enter phase of an interceptor context." [{:keys [::queue ::stack] :as ctx}] (if (empty? queue) - (leave ctx) + (leave (assoc ctx ::response + {:error "NoResponseInterceptor" + :message "No interceptor returned a response"})) (let [current (first queue) ctx' (assoc ctx ::stack (cons current stack) ::queue (rest queue)) ctx'' (try-invoke current ::enter ctx')] @@ -112,32 +116,50 @@ [{:keys [::response] :as ctx}] (if response (leave ctx) (enter ctx))) -(defn- extract-response +(defn response + "Given a context, return the response (with the context as metadata, + if possible.)" [ctx] - (with-meta (::response ctx) - {::ctx ctx})) + (let [r (::response ctx)] + (if (coll? r) (with-meta r (merge (meta r) ctx)) r))) + +(defn platform-async + "Given an options map, return a tuple of [cb async-val]. `cb` is a function + that should be called to deliver an asyncronous response, and `async-val` + is the deferred value (as specified by the options.) + + If no options are provided, returns a platform-appropriate deferred type." + [& {:keys [:channel :callback :promise] :as opts}] + (let [promise #?(:clj (if (empty? opts) + (clojure.core/promise) + (:promise opts)) + :default (:promise opts)) + channel #?(:cljs (if (empty? opts) + (a/chan) + (:channel opts)) + :default (:channel opts)) + callback (cond + channel #?(:clj #(a/>!! channel %) + :cljs #(a/go (a/>! channel %)) + :cljd #(throw (ex-info + "core.async not supported" + {}))) + promise #?(:clj #(deliver promise %) + :default #(throw (ex-info + "JVM promises not supported" + {}))) + (:callback opts) (:callback opts))] + [callback (or channel promise)])) (defn execute - "Execute an interceptor chain, returning the final response using the - requested async mechanism as specified in `opts`. Defaults to a - platform-appropriate mechanism. - - The final context is added to the response as metadata, for debug purposes." - [ctx & {:keys [:channel :callback :promise] :as opts}] - (let [promise #?(:clj (if (empty? opts) (clojure.core/promise) promise) - :default promise) - channel #?(:cljs (if (empty? opts) (a/chan) channel) - :default channel) - cb (cond - channel #?(:clj #(a/>!! channel (extract-response %)) - :cljs #(a/go (a/>! channel (extract-response %))) - :cljd #(throw (ex-info - "core.async not supported" {}))) - promise #?(:clj #(deliver promise (extract-response %)) - :default #(throw (ex-info - "JVM promises not supported" {}))) - callback #(callback (extract-response %))) - final {::name ::execute ::leave cb} + "Execute an interceptor chain, returning the final result using the requested + async mechanism as specified in `opts`. + + Defaults to a platform-appropriate async mechanism." + [ctx & {:as opts}] + (let [resp-fn (or (:resp-fn opts) response) + [cb val] (platform-async (dissoc opts :resp-fn)) + final {::name ::execute ::leave #(cb (resp-fn %))} ctx (update ctx ::queue #(cons final %))] (enter ctx) - (or channel promise))) + val)) From 360415169adb4c7235152fc5ae4f72811f4343bb Mon Sep 17 00:00:00 2001 From: Luke VanderHart Date: Fri, 7 Feb 2025 13:10:46 -0500 Subject: [PATCH 6/8] update readme --- README.md | 20 +++++++++++++------- src/net/gosha/atproto/client.cljc | 5 ----- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e868596..e5bfb38 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,9 @@ The workflow for utilizing the client is to: A session is a thread-safe, stateful object containing the information required to make ATProto HTTP requests. -All calls use the "NSID" of the query or procedure, and a Clojure map of parameters/input. All queries are asynchronous, and return immediately. The return value depends on platform: +`query` and `procedure` calls use the "NSID" of the query or procedure, and a Clojure map of parameters/input. + +All calls (including the call to `init`) are asynchronous, and return immediately. The return value depends on platform: - Clojure: a Clojure promise. - ClojureScript: a core.async channel. @@ -46,16 +48,20 @@ All calls use the "NSID" of the query or procedure, and a Clojure map of paramet You can also provide a `:channel`, `:callback` or `:promise` keyword option to recieve the return value. Not all options are supported on all platforms. + ```clojure (require '[net.gosha.atproto.client :as at]) -;; Unauthenticated client -(def session (at/init "https://public.api.bsky.app")) +;; Unauthenticated client to default endpoint +(def session @(at/init)) + +;; Unauthenticated client to a particular server +(def session @(at/init :endpoint "https://public.api.bsky.app")) ;; Password-based authenticated client -(def session (at/init "https://bsky.social" - :identifier "me.bsky.social" - :password "SECRET")) +;; Defaults to looking up and using the identifier's PD server +(def session @(at/init :identifier "me.bsky.social" + :password "SECRET")) ;; Bluesky endpoints and their query params can be found here: ;; https://docs.bsky.app/docs/category/http-reference @@ -68,7 +74,7 @@ You can also provide a `:channel`, `:callback` or `:promise` keyword option to r ;; Using core.async (def result (async/chan)) -(at/query sess :app.bsky.actor.getProfile {:actor "gosha.net"} :channel result) +(at/query session :app.bsky.actor.getProfile {:actor "gosha.net"} :channel result) (async/ Date: Fri, 7 Feb 2025 13:19:41 -0500 Subject: [PATCH 7/8] remove println --- src/net/gosha/atproto/client.cljc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/net/gosha/atproto/client.cljc b/src/net/gosha/atproto/client.cljc index 6a5b28f..a2c2e63 100644 --- a/src/net/gosha/atproto/client.cljc +++ b/src/net/gosha/atproto/client.cljc @@ -36,7 +36,6 @@ "Interceptor to add auth headers" {::i/name ::auth-headers ::i/enter (fn enter-auth-headers [{:keys [auth refresh?] :as ctx}] - (println "adding auth header: refresh=" refresh?) (assoc-in ctx [::i/request :headers "Authorization"] (str "Bearer " (if refresh? (:refreshJwt @auth) From 716a44eb1cc5643c0f211e65b1153f4e0bff2eb1 Mon Sep 17 00:00:00 2001 From: Luke VanderHart Date: Fri, 14 Feb 2025 10:41:21 -0500 Subject: [PATCH 8/8] fixes --- src/net/gosha/atproto/client.cljc | 71 +++++++++++++++----------- src/net/gosha/atproto/impl/jvm.clj | 28 ++++++---- src/net/gosha/atproto/interceptor.cljc | 6 +-- src/net/gosha/atproto/tid.cljc | 63 +++++++++++++++++++++++ 4 files changed, 125 insertions(+), 43 deletions(-) create mode 100644 src/net/gosha/atproto/tid.cljc diff --git a/src/net/gosha/atproto/client.cljc b/src/net/gosha/atproto/client.cljc index a2c2e63..5dd9705 100644 --- a/src/net/gosha/atproto/client.cljc +++ b/src/net/gosha/atproto/client.cljc @@ -45,39 +45,27 @@ "Interceptor that refreshes and retries expired tokens" {::i/name ::refresh-tokens ::i/leave (fn leave-refresh-tokens [{:keys [::i/response] :as ctx}] - (if-not (= "ExpiredToken" (-> response :body :error)) - ctx + (if (and (= "ExpiredToken" (-> response :body :error)) + (not (:refresh? ctx))) (procedure (assoc ctx :refresh? true) :com.atproto.server.refreshSession {} :callback (fn [resp] (if (:error resp) - (i/continue (assoc ctx ::i/response resp)) + (i/continue + (assoc ctx ::i/response resp)) (do (reset! (:auth ctx) resp) (i/continue - (dissoc ctx ::i/response))))))))}) - + (dissoc ctx + ::i/response + :refresh?)))))) ctx))}) -(def xrpc-interceptor - "Construct an interceptor that converts XRPC requests to HTTP requests against - the provided endpoint. - An XRPC request has a :nsid and either :parameters or :input. :parameters - indicates an XRPC 'query' (GET request) while `input` indicates a 'procedure' - (POST request)." +(def xrpc-response + "Response interceptor that extracts the content of an XRPC response into an + appropriate map." {::i/name ::xrpc - ::i/enter (fn xrpc-enter [{:keys [::i/request :endpoint] :as ctx}] - (let [url (str endpoint "/xrpc/" (name (:nsid request))) - req (if-let [input (:input request)] - (if (empty? input) - {:method :post} - {:method :post - :body (:input request) - :headers {"content-type" "application/json"}}) - {:method :get - :query-params (:parameters request)})] - (assoc ctx ::i/request (assoc req :url url)))) ::i/leave (fn xrpc-leave [{:keys [::i/response] :as ctx}] (cond (:error response) @@ -154,28 +142,49 @@ (select-keys opts [:promise :callback :channel])))) (defn- exec-xrpc - "Given an XRPC request, execute it against the specified session. The - mechanism for returning results is specified via a :channel, :callback, or - :promise keyword arg, defaulting to a platform-appropriate type." + "Given an XRPC request, execute it against the specified session." [session request & {:as opts}] (i/execute (-> session (assoc ::i/queue (concat - [xrpc-interceptor] + [xrpc-response] (:interceptors session) (impl-interceptors))) (assoc ::i/request request) (dissoc ::i/response)) opts)) +(defn- url + "Construct a URL given a session and a NSID" + [{:keys [endpoint]} nsid] + (str endpoint "/xrpc/" (name nsid))) + (defn query "Query using the provided NSID and parameters." - [session nsid parameters & {:as opts}] - (exec-xrpc session {:nsid nsid :parameters parameters} opts)) + [session nsid parameters & {:keys [headers] :as opts}] + (exec-xrpc session {:url (url session nsid) + :method :get + :query-params parameters + :headers headers} opts)) (defn procedure - "Execute a procedure using the provided NSID and parameters." - [session nsid input & {:as opts}] - (exec-xrpc session {:nsid nsid :input input} opts)) + "Execute a procedure using the provided NSID and input. + + Input can be a map or arbitrary binary data. If binary data, a `:content-type` + header should be provided." + [session nsid input & {:keys [headers] :as opts}] + (let [body (if (and (coll? input) (empty? input)) + nil + input) + headers (cond + (:content-type headers) headers + (not body) headers + (coll? body) (assoc headers :content-type "application/json") + :else (throw (ex-info "Must supply content-type header" {})))] + (exec-xrpc session (cond-> {:url (url session nsid) + :method :post} + body (assoc :body body) + headers (assoc :headers headers)) + opts))) (defn resolve-handle "Resolve the given handle to a DID." diff --git a/src/net/gosha/atproto/impl/jvm.clj b/src/net/gosha/atproto/impl/jvm.clj index 7bce62d..85ed4e4 100644 --- a/src/net/gosha/atproto/impl/jvm.clj +++ b/src/net/gosha/atproto/impl/jvm.clj @@ -26,16 +26,26 @@ #(json/read-json % :key-fn keyword)) ctx))}) +(defn- normalize-headers + "Convert keyword keys to strings for a header" + [headers] + (when headers + (zipmap + (map name (keys headers)) + (vals headers)))) + (def httpkit-handler "Interceptor to handle HTTP requests using httpkit" {::i/name ::http ::i/enter (fn enter-httpkit [ctx] - (http/request (::i/request ctx) - (fn [{:keys [error] :as resp}] - (if error - (i/continue (assoc ctx ::i/response - {:error (.getName (.getClass error)) - :message (.getMessage error) - :exception error})) - (i/continue (assoc ctx ::i/response resp - ::response resp))))))}) + (let [ctx (update-in ctx [::i/request :headers] + normalize-headers)] + (http/request (::i/request ctx) + (fn [{:keys [error] :as resp}] + (if error + (i/continue (assoc ctx ::i/response + {:error (.getName (.getClass error)) + :message (.getMessage error) + :exception error})) + (i/continue (assoc ctx ::i/response resp + ::response resp)))))))}) diff --git a/src/net/gosha/atproto/interceptor.cljc b/src/net/gosha/atproto/interceptor.cljc index e6171a6..06c4f04 100644 --- a/src/net/gosha/atproto/interceptor.cljc +++ b/src/net/gosha/atproto/interceptor.cljc @@ -157,9 +157,9 @@ Defaults to a platform-appropriate async mechanism." [ctx & {:as opts}] - (let [resp-fn (or (:resp-fn opts) response) - [cb val] (platform-async (dissoc opts :resp-fn)) - final {::name ::execute ::leave #(cb (resp-fn %))} + (let [opts (select-keys opts [:channel :callback :promise]) + [cb val] (platform-async opts) + final {::name ::execute ::leave #(cb (response %))} ctx (update ctx ::queue #(cons final %))] (enter ctx) val)) diff --git a/src/net/gosha/atproto/tid.cljc b/src/net/gosha/atproto/tid.cljc new file mode 100644 index 0000000..851b89c --- /dev/null +++ b/src/net/gosha/atproto/tid.cljc @@ -0,0 +1,63 @@ +(ns net.gosha.atproto.tid + (:require [clojure.string :as str])) + +(def base32-sortable "234567abcdefghijklmnopqrstuvwxyz") + +(defn encode + "Encode a 64-bit integer to a base32-sortable string" + [i] + (str/join (map #(nth base32-sortable + (bit-and 31 (bit-shift-right i (* % 5)))) + (range 12 -1 -1)))) + +#?(:clj + (let [last (volatile! 0)] + (defn clj-tid [] + (locking last + (let [nt (System/nanoTime) + us (^[long long] Math/floorDiv nt 1000) + tid (bit-or (bit-shift-left us 10) + (bit-and nt 1023))] + (if (= @last tid) + (do (println "bounce") (Thread/sleep 0 1) (clj-tid)) + (do (vreset! last tid) tid))))))) + +(defn tid + "Return a ATProto timestamp identifier using the spec defined at + https://atproto.com/specs/tid" + [] + (encode + #?(:clj (clj-tid)))) + +(def example "3lhmaghq7vs2k") + +(defn extract-ts + [] nil + ) + + + +;; 1000 ms in a s +;; 1000 microseconds in a ms +;; 1000 nanoseconds in a us + +(java.lang.Long/toString + (/ (Math/pow 2 53) 1000) 10) + + + +(comment + + (range 13) + (range 12 -1 -1) + + (encode 123456) + (encode2 123456) + + (java.lang.Integer/toBinaryString 31) + (java.lang.Integer/toBinaryString 1234) + (Long/parseLong "10010" 2) + + (bit-and 31 (bit-shift-right 1234 60)) + + )