//// ## PDS (Personal Data Server) //// //// A PDS, or Personal Data Server, is a server that hosts a user, it will //// always store the user's data repo and signing keys. It may also assign //// the user a handle and a DID. Many PDSes will host multiple users. //// //// A PDS doesn't typically run any applications itself, though it will have //// general account management interfaces such as the OAuth login screen. //// PDSes actively sync their data repos with Relays. //// //// ## Lexicon //// //// Lexicon is a schema language. It's used in the Atmosphere to describe data //// records and HTTP APIs. Functionally it's very similar to JSON-Schema and OpenAPI. //// Lexicon's sole purpose is to help developers build compatible software. //// //// ## Record Keys //// //// A record key (sometimes shortened to "rkey") is used to name and reference an individual //// record within the same collection of an atproto repository. //// It ends up as a segment in AT URIs, and in the repo MST path. //// //// ## CID (Content ID) //// //// CIDs, or Content Identifiers, are cryptographic hashes of records. //// They are used to track specific versions of records. //// //// CIDs include a metadata code which indicates whether it links to a node //// or arbitrary binary data. In atproto, object nodes often include a string //// field `$type` that specifies their Lexicon schema. import gleam/bool import gleam/http import gleam/http/request import gleam/int import gleam/json import gleam/option import possum/did import possum/handle import possum/nsid /// Documentation: [/xrpc/com.atproto.repo](https://endpoints.bsky.app/#bluesky-app/tag/comatprotorepo) const com_atproto_repo = "xrpc/com.atproto.repo" /// Documentation: [/xrpc/com.atproto.identity](https://endpoints.bsky.app/#bluesky-app/tag/comatprotoidentity) const com_atproto_identity = "xrpc/com.atproto.identity" /// Documentation: [xrpc/com.atproto.server](https://endpoints.bsky.app/#bluesky-app/tag/comatprotoserver) const com_atproto_server = "xrpc/com.atproto.server" /// Documentation: [xrpc/com.atproto.sync](https://endpoints.bsky.app/#bluesky-app/tag/comatprotosync) const com_atproto_sync = "xrpc/com.atproto.sync" /// Get current PLC Data for the indicated DID, /// this returns information about a Decentralized Identifier (DID), /// including their handle and service endpoint. /// /// PLC is a persistent global identifier system, stands for /// "Public Ledger of Credentials". /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum/did /// import possum /// /// let assert Ok(did) = did.parse("did:plc:z72i7hdynmk6r22z27h6tvur") /// let request = possum.get_plc_data(did) /// /// // You can use this decoder to extract the server endpoint /// let decoder = decode.at(["services", "atproto_pds", "endpoint"], decode.string) /// ``` /// /// ## Response /// /// ```json /// { /// "did": "string", /// "verificationMethods": { /// "atproto": "string" /// }, /// "rotationKeys": [ /// "string", /// "string" /// ], /// "alsoKnownAs": [ /// "string" /// ], /// "services": { /// "atproto_pds": { /// "type": "string", /// "endpoint": "string" /// } /// } /// ``` /// /// Documentation: [DID PLC Directory](https://web.plc.directory/) pub fn get_plc_data(did: did.Did) -> request.Request(String) { request.Request( method: http.Get, headers: [#("accept", "application/json")], body: "", scheme: http.Https, host: "plc.directory", port: option.None, path: did.to_string(did) <> "/data", query: option.None, ) } /// Resolves a handle to a DID using a HTTPS `/.well-known/` URL path, /// it looks for a file called "atproto-did" that contains the user DID. /// /// ## Examples /// /// ```gleam /// import possum /// /// let request = possum.resolve_well_known_did(host: "gleam.run") /// ``` /// /// ## Response /// /// ```txt /// did:plc:k5vecqzf4d5mdvkcu3mx6l5g /// ``` /// /// Documentation: [HTTPS well-known Method](https://atproto.com/specs/handle#https-well-known-method) pub fn resolve_well_known_did(host host: String) -> request.Request(String) { request.Request( method: http.Get, headers: [], body: "", scheme: http.Https, host:, port: option.None, path: ".well-known/atproto-did", query: option.None, ) } /// Resolves an atproto handle (hostname) to a DID. /// Does not necessarily bi-directionally verify against the the DID document. /// /// You can use projects like [Slingshot](https://slingshot.microcosm.blue) /// for easy access to cached data. /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum/handle /// import possum /// /// let assert Ok(handle) = handle.parse("gleam.run") /// let request = possum.resolve_handle(handle, pds: "slingshot.microcosm.blue") /// ``` /// /// ## Response /// /// ```json /// { /// "did": "string" /// } /// ``` /// /// Endpoint Docs: [/xrpc/com.atproto.identity.resolveHandle](https://endpoints.bsky.app/#bluesky-app/tag/comatprotoidentity/GET/xrpc/com.atproto.identity.resolveHandle) pub fn resolve_handle( handle: handle.Handle, pds host: String, ) -> request.Request(String) { request.Request( method: http.Get, headers: [#("accept", "application/json")], body: "", scheme: http.Https, host:, port: option.None, path: com_atproto_identity <> ".resolveHandle", query: option.None, ) |> request.set_query([#("handle", handle.to_string(handle))]) } /// Create an authentication session. /// An **App Password** can be generated in your Bluesky settings. /// /// ## Body /// /// - **password**: App password /// - **allowTakendown**: When true, instead of throwing error for takendown accounts, /// a valid response with a narrow scoped token will be returned /// - **authFactorToken**: Used during the authentication process to handle /// Two-Factor Authentication /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum/did /// import possum /// /// let assert Ok(did) = did.parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz") /// /// let request = /// possum.create_session( /// did, /// pds: "personal.data-server.pds", /// app_password: "bsky-app-password", /// allow_takendown: option.None, /// auth_factor_token: option.None /// ) /// ``` /// /// ## Response /// /// ```json /// { /// "did": "string", /// "email": "string", /// "active": true, /// "didDoc": null, /// "handle": "string", /// "status": "takendown", /// "accessJwt": "string", /// "refreshJwt": "string", /// "emailConfirmed": true, /// "emailAuthFactor": true /// } /// ``` /// /// Endpoint Docs: [/xrpc/com.atproto.server.createSession](https://endpoints.bsky.app/#bluesky-app/tag/comatprotoserver/POST/xrpc/com.atproto.server.createSession) pub fn create_session( did: did.Did, pds host: String, app_password app_password: String, allow_takendown allow_takendown: option.Option(Bool), auth_factor_token auth_factor_token: option.Option(String), ) -> request.Request(String) { let values = [ #("identifier", json.string(did.to_string(did))), #("password", json.string(app_password)), ] let values = case allow_takendown { option.Some(value) -> [#("allowTakendown", json.bool(value)), ..values] option.None -> values } let values = case auth_factor_token { option.Some(value) -> [#("authFactorToken", json.string(value)), ..values] option.None -> values } request.Request( method: http.Post, headers: [ #("accept", "application/json"), #("content-type", "application/json"), ], body: json.to_string(json.object(values)), scheme: http.Https, host:, port: option.None, path: com_atproto_server <> ".createSession", query: option.None, ) } /// Refresh an authentication session. /// Requires auth using the 'refreshJwt' (not the 'accessJwt'). /// /// ## Examples /// /// ```gleam /// import possum /// /// let request = /// possum.refresh_session("refresh-token", pds: "personal.data-server.pds") /// ``` /// /// ## Response /// /// ```json /// { /// "did": "string", /// "email": "string", /// "active": true, /// "didDoc": null, /// "handle": "string", /// "status": "takendown", /// "accessJwt": "string", /// "refreshJwt": "string", /// "emailConfirmed": true, /// "emailAuthFactor": true /// } /// ``` /// /// Endpoint Docs: [/xrpc/com.atproto.server.refreshSession](https://endpoints.bsky.app/#bluesky-app/tag/comatprotoserver/POST/xrpc/com.atproto.server.refreshSession) pub fn refresh_session( token: String, pds host: String, ) -> request.Request(String) { request.Request( method: http.Post, headers: [#("accept", "application/json")], body: "", scheme: http.Https, host:, port: option.None, path: com_atproto_server <> ".refreshSession", query: option.None, ) |> authorized(token) } /// Get information about the current auth session. /// Requires auth. /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum /// /// let request = /// possum.get_session_info(pds: "personal.data-server.pds") /// |> possum.authorized("access-token") /// ``` /// /// ## Response /// /// ```json /// { /// "did": "string", /// "email": "string", /// "active": true, /// "didDoc": null, /// "handle": "string", /// "status": "takendown", /// "emailConfirmed": true, /// "emailAuthFactor": true /// } /// ``` /// /// Endpoint Docs: [/xrpc/com.atproto.server.getSession](https://endpoints.bsky.app/#bluesky-app/tag/comatprotoserver/POST/xrpc/com.atproto.server.getSession) pub fn get_session_info(pds host: String) -> request.Request(String) { request.Request( method: http.Get, headers: [#("accept", "application/json")], body: "", scheme: http.Https, host:, port: option.None, path: com_atproto_server <> ".getSession", query: option.None, ) } /// Include a access token in the request's headers. /// Tokens can be acquired with `create_session`. /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum /// /// let request = /// request.new() /// |> possum.authorized(access_token: "access-token") /// /// assert request.headers != [] /// ``` /// pub fn authorized( request: request.Request(String), access_token token: String, ) -> request.Request(String) { request.prepend_header(request, "authorization", "bearer " <> token) } /// Get information about an account and repository, including the list of collections. /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum/did /// import possum /// /// let assert Ok(did) = did.parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz") /// let request = /// possum.describe_repository(did, pds: "personal.data-server.pds") /// ``` /// /// ## Response /// /// ```json /// { /// "did": "string", /// "didDoc": null, /// "handle": "string", /// "collections": [ /// "string" /// ], /// "handleIsCorrect": true /// } /// ``` /// /// Endpoint Docs: [/xrpc/com.atproto.repo.describeRepo](https://endpoints.bsky.app/#bluesky-app/tag/comatprotorepo/GET/xrpc/com.atproto.repo.describeRepo) pub fn describe_repository( did: did.Did, pds host: String, ) -> request.Request(String) { request.Request( method: http.Get, headers: [#("accept", "application/json")], body: "", scheme: http.Https, host:, port: option.None, path: com_atproto_repo <> ".describeRepo", query: option.None, ) |> request.set_query([#("repo", did.to_string(did))]) } /// Create a single new repository record. /// Requires auth, implemented by PDS. /// /// ## Body /// /// - **collection**: The NSID of the record collection. /// - **repo**: The handle or DID of the repo (aka, current account). /// - **rkey**: The Record Key. /// - **swapCommit**: Compare and swap with the previous commit by CID. /// - **validate**: Can be set to `option.Some(False)` to skip Lexicon schema validation /// of record data, `option.Some(True)` to require it, or leave unset to validate only /// for known Lexicons. /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum/nsid /// import possum/did /// import possum /// /// let assert Ok(did) = did.parse("did:plc:k5vecqzf4d5mdvkcu3mx6l5g") /// let assert Ok(collection) = nsid.parse("wibble.wobble.woo") /// /// let request = /// possum.create_record( /// did, /// pds: "personal.data-server.pds", /// collection:, /// rkey: "wibble", /// record: [], /// swap_commit: option.None, /// validate: option.None, /// ) /// |> possum.authorized("access-token") /// ``` /// /// ## Response /// /// ```json /// { /// "cid": "string", /// "uri": "string", /// "commit": { /// "cid": "string", /// "rev": "string" /// }, /// "validationStatus": "valid" /// } /// ``` /// /// Endpoint Docs: [/xrpc/com.atproto.repo.createRecord](https://endpoints.bsky.app/#bluesky-app/tag/comatprotorepo/POST/xrpc/com.atproto.repo.createRecord) pub fn create_record( did: did.Did, pds host: String, collection nsid: nsid.Nsid, rkey rkey: option.Option(String), record record: List(#(String, json.Json)), swap_commit swap_commit: option.Option(String), validate validate: option.Option(Bool), ) -> request.Request(String) { let body = [ #("collection", json.string(nsid.to_string(nsid))), #("record", json.object(record)), #("repo", json.string(did.to_string(did))), ] let body = case rkey { option.Some(value) -> [#("rkey", json.string(value)), ..body] option.None -> body } let body = case swap_commit { option.Some(value) -> [#("swapCommit", json.string(value)), ..body] option.None -> body } let body = case validate { option.Some(value) -> [#("validate", json.bool(value)), ..body] option.None -> body } request.Request( method: http.Post, headers: [ #("accept", "application/json"), #("content-type", "application/json"), ], body: json.to_string(json.object(body)), scheme: http.Https, host:, port: option.None, path: com_atproto_repo <> ".createRecord", query: option.None, ) } /// Get a single record from a repository. Does not require auth. /// /// You can use projects like [Slingshot](https://slingshot.microcosm.blue) /// for easy access to cached data. /// /// ```gleam /// import gleam/http/request /// import possum/did /// import possum/nsid /// import possum /// /// let assert Ok(did) = did.parse("did:plc:k5vecqzf4d5mdvkcu3mx6l5g") /// let assert Ok(collection) = nsid.parse("site.standard.document") /// /// let request = /// possum.get_record( /// did, /// pds: "slingshot.microcosm.blue", /// collection:, /// rkey: "mn6n3lvibc2b", /// cursor: option.None, /// cid: option.None, /// ) /// ``` /// /// ## Response /// /// ```json /// { /// "cid": "string", /// "uri": "string", /// "value": null /// } /// ``` /// /// Endpoint Docs: [/xrpc/com.atproto.repo.getRecord](https://endpoints.bsky.app/#bluesky-app/tag/comatprotorepo/GET/xrpc/com.atproto.repo.getRecord) /// Documentation: [atproto.com](https://atproto.com/specs/record-key) pub fn get_record( did: did.Did, pds host: String, collection nsid: nsid.Nsid, rkey rkey: String, cid cid: option.Option(String), ) -> request.Request(String) { let query = [ #("repo", did.to_string(did)), #("rkey", rkey), #("collection", nsid.to_string(nsid)), ] let query = case cid { option.Some(cid) -> [#("cid", cid), ..query] option.None -> query } request.Request( method: http.Get, headers: [#("accept", "application/json")], body: "", scheme: http.Https, host:, port: option.None, path: com_atproto_repo <> ".getRecord", query: option.None, ) |> request.set_query(query) } /// List a range of records in a repository, matching a specific collection. /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum/did /// import possum/nsid /// import possum /// /// let assert Ok(did) = did.parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz") /// let assert Ok(collection) = nsid.parse("site.standard.document") /// /// let request = /// possum.list_records( /// did, /// pds: "personal.data-server.pds", /// collection:, /// limit: option.Some(3), /// cursor: option.None, /// reverse: option.None, /// ) /// ``` /// /// ## Response /// /// ```json /// { /// "cursor": "string", /// "records": [ /// { /// "cid": "string", /// "uri": "string", /// "value": null /// } /// ] /// } /// ``` /// /// Endpoint Docs: [/xrpc/com.atproto.repo.listRecords](https://endpoints.bsky.app/#bluesky-app/tag/comatprotorepo/GET/xrpc/com.atproto.repo.listRecords) pub fn list_records( did: did.Did, pds host: String, collection nsid: nsid.Nsid, limit limit: option.Option(Int), cursor cursor: option.Option(String), reverse reverse: option.Option(Bool), ) -> request.Request(String) { let query = [ #("repo", did.to_string(did)), #("collection", nsid.to_string(nsid)), ] let query = case limit { option.Some(value) -> [#("limit", int.to_string(value)), ..query] option.None -> query } let query = case cursor { option.Some(value) -> [#("cursor", value), ..query] option.None -> query } let query = case reverse { option.Some(value) -> [#("reverse", bool.to_string(value)), ..query] option.None -> query } request.Request( method: http.Get, headers: [#("accept", "application/json")], body: "", scheme: http.Https, host:, port: option.None, path: com_atproto_repo <> ".listRecords", query: option.None, ) |> request.set_query(query) } /// Delete a repository record, or ensure it doesn't exist. /// Requires auth, implemented by PDS. /// /// - **collection**: The NSID of the record collection. /// - **repo**: The handle or DID of the repo (aka, current account). /// - **rkey**: The Record Key. /// - **swapCommit**: Compare and swap with the previous commit by CID. /// - **swapRecord**: Compare and swap with the previous record by CID. /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum/did /// import possum/nsid /// import possum /// /// let assert Ok(did) = did.parse("did:plc:k5vecqzf4d5mdvkcu3mx6l5g") /// let assert Ok(collection) = nsid.parse("wibble.wobble.woo") /// /// let request = /// possum.delete_record( /// did, /// pds: "personal.data-server.pds" /// collection:, /// rkey: "wibble-wobble", /// swap_commit: option.None, /// swap_record: option.None, /// ) /// |> possum.authorized("access-token") /// ``` /// /// ## Response /// /// ```json /// { /// "commit": { /// "cid": "string", /// "rev": "string" /// } /// } /// ``` /// /// Endpoint Docs: [/xrpc/com.atproto.repo.deleteRecord](https://endpoints.bsky.app/#bluesky-app/tag/comatprotorepo/POST/xrpc/com.atproto.repo.deleteRecord) pub fn delete_record( did: did.Did, pds host: String, collection nsid: nsid.Nsid, rkey rkey: String, swap_commit swap_commit: option.Option(String), swap_record swap_record: option.Option(String), ) -> request.Request(String) { let body = [ #("repo", json.string(did.to_string(did))), #("collection", json.string(nsid.to_string(nsid))), #("rkey", json.string(rkey)), ] let body = case swap_commit { option.Some(value) -> [#("swapCommit", json.string(value)), ..body] option.None -> body } let body = case swap_record { option.Some(value) -> [#("swapRecord", json.string(value)), ..body] option.None -> body } request.Request( method: http.Post, headers: [ #("accept", "application/json"), #("content-type", "application/json"), ], body: json.to_string(json.object(body)), scheme: http.Https, host:, port: option.None, path: com_atproto_repo <> ".deleteRecord", query: option.None, ) } /// Write a repository record, creating or updating it as needed. /// Requires auth, implemented by PDS. /// /// ## Body /// /// - **collection**: The NSID of the record collection. /// - **repo**: The handle or DID of the repo (aka, current account). /// - **rkey**: The Record Key. /// - **swapCommit**: Compare and swap with the previous commit by CID. /// - **swapRecord**: Compare and swap with the previous record by CID. /// - **validate**: Can be set to `option.Some(False)` to skip Lexicon schema validation /// of record data, `option.Some(True)` to require it, or leave unset to validate only /// for known Lexicons. /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum/did /// import possum/nsid /// import possum /// /// let assert Ok(did) = did.parse("did:plc:k5vecqzf4d5mdvkcu3mx6l5g") /// let assert Ok(collection) = nsid.parse("target.collection.data") /// /// let request = /// possum.put_record( /// did, /// pds: "personal.data-server.pds", /// collection:, /// rkey: "wibble-wobble", /// record: [], /// swap_commit: option.None, /// swap_record: option.None, /// validate: option.None, /// ) /// |> possum.authorized("access-token") /// ``` /// /// ## Response /// /// ```json /// { /// "cid": "string", /// "uri": "string", /// "commit": { /// "cid": "string", /// "rev": "string" /// }, /// "validationStatus": "valid" /// } /// ``` /// /// Endpoint Docs: [/xrpc/com.atproto.repo.putRecord](https://endpoints.bsky.app/#bluesky-app/tag/comatprotorepo/POST/xrpc/com.atproto.repo.putRecord) pub fn put_record( did: did.Did, pds host: String, collection nsid: nsid.Nsid, rkey rkey: String, record record: List(#(String, json.Json)), swap_commit swap_commit: option.Option(String), swap_record swap_record: option.Option(String), validate validate: option.Option(Bool), ) -> request.Request(String) { let body = [ #("collection", json.string(nsid.to_string(nsid))), #("record", json.object(record)), #("repo", json.string(did.to_string(did))), #("rkey", json.string(rkey)), ] let body = case swap_commit { option.Some(value) -> [#("swapCommit", json.string(value)), ..body] option.None -> body } let body = case swap_record { option.Some(value) -> [#("swapRecord", json.string(value)), ..body] option.None -> body } let body = case validate { option.Some(value) -> [#("validate", json.bool(value)), ..body] option.None -> body } request.Request( method: http.Post, headers: [ #("accept", "application/json"), #("content-type", "application/json"), ], body: json.to_string(json.object(body)), scheme: http.Https, host:, port: option.None, path: com_atproto_repo <> ".putRecord", query: option.None, ) } /// Get a blob associated with a given account. /// Returns the full blob as originally uploaded. /// Does not require auth; implemented by PDS. /// /// Endpoint Docs: [/xrpc/com.atproto.sync.getBlob](https://endpoints.bsky.app/#bluesky-app/tag/comatprotosync/GET/xrpc/com.atproto.sync.getBlob) pub fn get_blob( did: did.Did, cid: String, pds host: String, ) -> request.Request(BitArray) { let query = [#("did", did.to_string(did)), #("cid", cid)] request.Request( method: http.Get, headers: [#("accept", "*/*")], body: <<>>, scheme: http.Https, host:, port: option.None, path: com_atproto_sync <> ".getBlob", query: option.None, ) |> request.set_query(query) }