//// ## 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" /// 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 = /// request.new() /// |> 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( request: request.Request(_), did: did.Did, ) -> request.Request(_) { request |> request.prepend_header("accept", "application/json") |> request.set_host("plc.directory") |> request.set_method(http.Get) |> request.set_path(did.to_string(did) <> "/data") } /// 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 gleam/http/request /// import possum/handle /// import possum /// /// let assert Ok(handle) = handle.parse("gleam.run") /// /// request.new() /// |> possum.resolve_well_known_did(handle) /// ``` /// /// ## 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( request: request.Request(_), host: String, ) -> request.Request(_) { request |> request.set_host(host) |> request.set_method(http.Get) |> request.set_path(".well-known/atproto-did") } /// 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 = /// request.new() /// |> request.set_host("slingshot.microcosm.blue") /// |> possum.resolve_handle(handle) /// ``` /// /// ## 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( request: request.Request(_), handle: handle.Handle, ) -> request.Request(_) { request |> request.prepend_header("accept", "application/json") |> request.set_method(http.Get) |> request.set_path(com_atproto_identity <> ".resolveHandle") |> 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 = /// request.new() /// |> request.set_host("personal.data-server.pds") /// |> possum.create_session( /// did, /// 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( request: request.Request(_), did: did.Did, app_password app_password: String, allow_takendown allow_takendown: option.Option(Bool), auth_factor_token auth_factor_token: option.Option(String), ) -> request.Request(_) { 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.prepend_header("accept", "application/json") |> request.prepend_header("content-type", "application/json") |> request.set_body(json.to_string(json.object(values))) |> request.set_method(http.Post) |> request.set_path(com_atproto_server <> ".createSession") } /// Refresh an authentication session. /// Requires auth using the 'refreshJwt' (not the 'accessJwt'). /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum /// /// let request = /// request.new() /// |> request.set_host("personal.data-server.pds") /// |> possum.refresh_session("refresh-token") /// ``` /// /// ## 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( request: request.Request(_), refresh token: String, ) -> request.Request(_) { request |> authorized(token) |> request.prepend_header("accept", "application/json") |> request.set_method(http.Post) |> request.set_path(com_atproto_server <> ".refreshSession") } /// Get information about the current auth session. /// Requires auth. /// /// ## Examples /// /// ```gleam /// import gleam/http/request /// import possum /// /// let request = /// request.new() /// |> request.set_host("personal.data-server.pds") /// |> possum.authorized("access-token") /// |> possum.get_session_info /// ``` /// /// ## 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(request: request.Request(_)) -> request.Request(_) { request |> request.prepend_header("accept", "application/json") |> request.set_method(http.Get) |> request.set_path(com_atproto_server <> ".getSession") } /// 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() /// |> request.set_host("personal.data-server.pds") /// |> possum.authorized("access-token") /// ``` /// pub fn authorized( request: request.Request(_), access_token token: String, ) -> request.Request(_) { 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 = /// request.new() /// |> request.set_host("personal.data-server.pds") /// |> possum.describe_repository(did) /// ``` /// /// ## 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( request: request.Request(_), did: did.Did, ) -> request.Request(_) { request |> request.prepend_header("accept", "application/json") |> request.set_method(http.Get) |> request.set_path(com_atproto_repo <> ".describeRepo") |> 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 'false' to skip Lexicon schema validation /// of record data, '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 = /// request.new() /// |> request.set_host("personal.data-server.pds") /// |> possum.authorized("access-token") /// |> possum.create_record( /// did, /// collection:, /// rkey: "wibble", /// record: [], /// swap_commit: option.None, /// validate: option.None, /// ) /// ``` /// /// ## 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( request: request.Request(_), did: did.Did, 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(_) { 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.prepend_header("accept", "application/json") |> request.prepend_header("content-type", "application/json") |> request.set_body(json.to_string(json.object(body))) |> request.set_method(http.Post) |> request.set_path(com_atproto_repo <> ".createRecord") } /// Get a single record from a repository. Does not require auth. /// /// A **record key** (rkey) is used to name and reference an individual record /// withing the same collection of an repository. /// /// 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 = /// request.new() /// |> request.set_host("slingshot.microcosm.blue") /// |> possum.get_record( /// did, /// 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( request: request.Request(_), did: did.Did, collection nsid: nsid.Nsid, rkey rkey: String, cid cid: option.Option(String), ) -> request.Request(_) { 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.prepend_header("accept", "application/json") |> request.set_method(http.Get) |> request.set_path(com_atproto_repo <> ".getRecord") |> 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 = /// request.new() /// |> request.set_host("personal.data-server.pds") /// |> possum.list_records( /// did, /// 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( request: request.Request(_), did: did.Did, collection nsid: nsid.Nsid, limit limit: option.Option(Int), cursor cursor: option.Option(String), reverse reverse: option.Option(Bool), ) -> request.Request(_) { 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.prepend_header("accept", "application/json") |> request.set_method(http.Get) |> request.set_path(com_atproto_repo <> ".listRecords") |> 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 = /// request.new() /// |> request.set_host("personal.data-server.pds") /// |> possum.authorized("access-token") /// |> possum.delete_record( /// did, /// collection:, /// rkey: "wibble-wobble", /// swap_commit: option.None, /// swap_record: option.None, /// ) /// ``` /// /// ## 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( request: request.Request(_), did: did.Did, collection nsid: nsid.Nsid, rkey rkey: String, swap_commit swap_commit: option.Option(String), swap_record swap_record: option.Option(String), ) -> request.Request(_) { 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.prepend_header("accept", "application/json") |> request.prepend_header("content-type", "application/json") |> request.set_body(json.to_string(json.object(body))) |> request.set_method(http.Post) |> request.set_path(com_atproto_repo <> ".deleteRecord") } /// Write a repository record, creating or updating it as needed. /// Requires auth, implemented by PDS. /// /// ## Body /// /// - **collection**: The NSID of the record collection. /// - **record** /// - **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. /// WARNING: nullable and optional field; /// - **validate**: Can be set to `false` to skip Lexicon schema validation /// of record data, 'true' to require it, or leave unset to validate only /// for known Lexicons. /// /// ## 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 = /// request.new() /// |> request.set_host("personal.data-server.pds") /// |> possum.authorized("access-token") /// |> possum.put_record( /// did, /// collection:, /// rkey: "wibble-wobble", /// record: [], /// swap_commit: option.None, /// swap_record: option.None, /// validate: option.None, /// ) /// ``` /// /// ## 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( request: request.Request(_), did: did.Did, 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(_) { 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.prepend_header("accept", "application/json") |> request.prepend_header("content-type", "application/json") |> request.set_body(json.to_string(json.object(body))) |> request.set_method(http.Post) |> request.set_path(com_atproto_repo <> ".putRecord") }