1defmodule Atex.XRPC.Client do
2 @doc """
3 Struct to store client information for ATProto XRPC.
4 """
5
6 alias Atex.XRPC
7 use TypedStruct
8
9 typedstruct do
10 field :endpoint, String.t(), enforce: true
11 field :access_token, String.t() | nil
12 field :refresh_token, String.t() | nil
13 end
14
15 @doc """
16 Create a new `Atex.XRPC.Client` from an endpoint, and optionally an
17 access/refresh token.
18
19 Endpoint should be the base URL of a PDS, or an AppView in the case of
20 unauthenticated requests (like Bluesky's public API), e.g.
21 `https://bsky.social`.
22 """
23 @spec new(String.t()) :: t()
24 @spec new(String.t(), String.t() | nil) :: t()
25 @spec new(String.t(), String.t() | nil, String.t() | nil) :: t()
26 def new(endpoint, access_token \\ nil, refresh_token \\ nil) do
27 %__MODULE__{endpoint: endpoint, access_token: access_token, refresh_token: refresh_token}
28 end
29
30 @doc """
31 Create a new `Atex.XRPC.Client` by logging in with an `identifier` and
32 `password` to fetch an initial pair of access & refresh tokens.
33
34 Uses `com.atproto.server.createSession` under the hood, so `identifier` can be
35 either a handle or a DID.
36
37 ## Examples
38
39 iex> Atex.XRPC.Client.login("https://bsky.social", "example.com", "password123")
40 {:ok, %Atex.XRPC.Client{...}}
41 """
42 @spec login(String.t(), String.t(), String.t()) :: {:ok, t()} | XRPC.Adapter.error()
43 @spec login(String.t(), String.t(), String.t(), String.t() | nil) ::
44 {:ok, t()} | XRPC.Adapter.error()
45 def login(endpoint, identifier, password, auth_factor_token \\ nil) do
46 json =
47 %{identifier: identifier, password: password}
48 |> then(
49 &if auth_factor_token do
50 Map.merge(&1, %{authFactorToken: auth_factor_token})
51 else
52 &1
53 end
54 )
55
56 response = XRPC.unauthed_post(endpoint, "com.atproto.server.createSession", json: json)
57
58 case response do
59 {:ok, %{"accessJwt" => access_token, "refreshJwt" => refresh_token}} ->
60 {:ok, new(endpoint, access_token, refresh_token)}
61
62 err ->
63 err
64 end
65 end
66
67 @doc """
68 Request a new `refresh_token` for the given client.
69 """
70 @spec refresh(t()) :: {:ok, t()} | XRPC.Adapter.error()
71 def refresh(%__MODULE__{endpoint: endpoint, refresh_token: refresh_token} = client) do
72 response =
73 XRPC.unauthed_post(
74 endpoint,
75 "com.atproto.server.refreshSession",
76 XRPC.put_auth([], refresh_token)
77 )
78
79 case response do
80 {:ok, %{"accessJwt" => access_token, "refreshJwt" => refresh_token}} ->
81 %{client | access_token: access_token, refresh_token: refresh_token}
82
83 err ->
84 err
85 end
86 end
87end