Current section

Files

Jump to
possum src possum.gleam
Raw

src/possum.gleam

//// ## 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")
}