···1616### Added
17171818- `deflexicon` now emits structs for records, objects, queries, and procedures.
1919+- `Atex.XRPC.get/3` and `Atex.XRPC.post/3` now support having a lexicon struct
2020+ as the second argument instead of the method's name, making it easier to have
2121+ properly checked API calls.
19222023## [0.5.0] - 2025-10-11
2124
+60
examples/oauth.ex
···11+defmodule ExampleOAuthPlug do
22+ use Plug.Router
33+ alias Atex.OAuth
44+ alias Atex.XRPC
55+66+ plug :put_secret_key_base
77+88+ plug Plug.Session,
99+ store: :cookie,
1010+ key: "atex-oauth",
1111+ signing_salt: "signing-salt"
1212+1313+ plug :match
1414+ plug :dispatch
1515+1616+ forward "/oauth", to: Atex.OAuth.Plug
1717+1818+ get "/whoami" do
1919+ conn = fetch_session(conn)
2020+2121+ with {:ok, client} <- XRPC.OAuthClient.from_conn(conn),
2222+ {:ok, response, client} <-
2323+ XRPC.post(client, %Com.Atproto.Repo.CreateRecord{
2424+ input: %Com.Atproto.Repo.CreateRecord.Input{
2525+ repo: client.did,
2626+ collection: "app.bsky.feed.post",
2727+ rkey: Atex.TID.now() |> to_string(),
2828+ record: %App.Bsky.Feed.Post{
2929+ text: "Hello world from atex!",
3030+ createdAt: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
3131+ }
3232+ }
3333+ }) do
3434+ IO.inspect(response, label: "output")
3535+3636+ conn
3737+ |> XRPC.OAuthClient.update_plug(client)
3838+ |> send_resp(200, response.uri)
3939+ else
4040+ :error ->
4141+ send_resp(conn, 401, "Unauthorized")
4242+4343+ err ->
4444+ IO.inspect(err, label: "xrpc failed")
4545+ send_resp(conn, 500, "xrpc failed")
4646+ end
4747+ end
4848+4949+ match _ do
5050+ send_resp(conn, 404, "oops")
5151+ end
5252+5353+ def put_secret_key_base(conn, _) do
5454+ put_in(
5555+ conn.secret_key_base,
5656+ # Don't use this in production
5757+ "5ef1078e1617463a3eb3feb9b152e76587a75a6809e0485a125b6bb7ae468f086680771f700d77ff61dfdc8d8ee8a5c7848024a41cf5ad4b6eb3115f74ce6e46"
5858+ )
5959+ end
6060+end
+142-13
lib/atex/xrpc.ex
···1212 {:ok, client} = Atex.XRPC.LoginClient.login("https://bsky.social", "user.bsky.social", "password")
1313 {:ok, response, client} = Atex.XRPC.get(client, "app.bsky.actor.getProfile", params: [actor: "user.bsky.social"])
14141515- # OAuth-based client (coming next)
1616- oauth_client = Atex.XRPC.OAuthClient.new_from_oauth_tokens(endpoint, access_token, refresh_token, dpop_key)
1515+ # OAuth-based client
1616+ {:ok, oauth_client} = Atex.XRPC.OAuthClient.from_conn(conn)
1717 {:ok, response, oauth_client} = Atex.XRPC.get(oauth_client, "app.bsky.actor.getProfile", params: [actor: "user.bsky.social"])
18181919 ## Unauthenticated requests
···2424 {:ok, response} = Atex.XRPC.unauthed_get("https://bsky.social", "com.atproto.sync.getHead", params: [did: "did:plc:..."])
2525 """
26262727+ alias Atex.XRPC.Client
2828+2729 @doc """
2830 Perform a HTTP GET on a XRPC resource. Called a "query" in lexicons.
29313032 Accepts any client that implements `Atex.XRPC.Client` and returns
3133 both the response and the (potentially updated) client.
3434+3535+ Can be called either with the XRPC operation name as a string, or with a lexicon
3636+ struct (generated via `deflexicon`) for type safety and automatic parameter/response handling.
3737+3838+ When using a lexicon struct, the response body will be automatically converted to the
3939+ corresponding type if an Output struct exists for the lexicon.
4040+4141+ ## Examples
4242+4343+ # Using string XRPC name
4444+ {:ok, response, client} =
4545+ Atex.XRPC.get(client, "app.bsky.actor.getProfile", params: [actor: "ovyerus.com"])
4646+4747+ # Using lexicon struct with typed construction
4848+ {:ok, response, client} =
4949+ Atex.XRPC.get(client, %App.Bsky.Actor.GetProfile{
5050+ params: %App.Bsky.Actor.GetProfile.Params{actor: "ovyerus.com"}
5151+ })
3252 """
3333- @spec get(Atex.XRPC.Client.client(), String.t(), keyword()) ::
3434- {:ok, Req.Response.t(), Atex.XRPC.Client.client()}
3535- | {:error, any(), Atex.XRPC.Client.client()}
3636- def get(client, name, opts \\ []) do
5353+ @spec get(Client.client(), String.t() | struct(), keyword()) ::
5454+ {:ok, Req.Response.t(), Client.client()}
5555+ | {:error, any(), Client.client()}
5656+ def get(client, name, opts \\ [])
5757+5858+ def get(client, name, opts) when is_binary(name) do
3759 client.__struct__.get(client, name, opts)
3860 end
39616262+ def get(client, %{__struct__: module} = query, opts) do
6363+ opts = if Map.get(query, :params), do: Keyword.put(opts, :params, query.params), else: opts
6464+ output_struct = Module.concat(module, Output)
6565+ output_exists = Code.ensure_loaded?(output_struct)
6666+6767+ case client.__struct__.get(client, module.id(), opts) do
6868+ {:ok, %{status: 200} = response, client} ->
6969+ if output_exists do
7070+ case output_struct.from_json(response.body) do
7171+ {:ok, output} ->
7272+ {:ok, %{response | body: output}, client}
7373+7474+ err ->
7575+ err
7676+ end
7777+ else
7878+ {:ok, response, client}
7979+ end
8080+8181+ {:ok, _, _} = ok ->
8282+ ok
8383+8484+ err ->
8585+ err
8686+ end
8787+ end
8888+4089 @doc """
4141- Perform a HTTP POST on a XRPC resource. Called a "prodecure" in lexicons.
9090+ Perform a HTTP POST on a XRPC resource. Called a "procedure" in lexicons.
9191+9292+ Accepts any client that implements `Atex.XRPC.Client` and returns both the
9393+ response and the (potentially updated) client.
9494+9595+ Can be called either with the XRPC operation name as a string, or with a
9696+ lexicon struct (generated via `deflexicon`) for type safety and automatic
9797+ input/parameter mapping.
9898+9999+ When using a lexicon struct, the response body will be automatically converted
100100+ to the corresponding type if an Output struct exists for the lexicon.
101101+102102+ ## Examples
103103+104104+ # Using string XRPC name
105105+ {:ok, response, client} =
106106+ Atex.XRPC.post(
107107+ client,
108108+ "com.atproto.repo.createRecord",
109109+ json: %{
110110+ repo: "did:plc:...",
111111+ collection: "app.bsky.feed.post",
112112+ rkey: Atex.TID.now() |> to_string(),
113113+ record: %{
114114+ text: "Hello World",
115115+ createdAt: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
116116+ }
117117+ }
118118+ )
421194343- Accepts any client that implements `Atex.XRPC.Client` and returns
4444- both the response and the (potentially updated) client.
120120+ # Using lexicon struct with typed construction
121121+ {:ok, response, client} =
122122+ Atex.XRPC.post(client, %Com.Atproto.Repo.CreateRecord{
123123+ input: %Com.Atproto.Repo.CreateRecord.Input{
124124+ repo: "did:plc:...",
125125+ collection: "app.bsky.feed.post",
126126+ rkey: Atex.TID.now() |> to_string(),
127127+ record: %App.Bsky.Feed.Post{
128128+ text: "Hello World!",
129129+ createdAt: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
130130+ }
131131+ }
132132+ })
45133 """
4646- @spec post(Atex.XRPC.Client.client(), String.t(), keyword()) ::
4747- {:ok, Req.Response.t(), Atex.XRPC.Client.client()}
4848- | {:error, any(), Atex.XRPC.Client.client()}
4949- def post(client, name, opts \\ []) do
134134+ @spec post(Client.client(), String.t() | struct(), keyword()) ::
135135+ {:ok, Req.Response.t(), Client.client()}
136136+ | {:error, any(), Client.client()}
137137+ def post(client, name, opts \\ [])
138138+139139+ def post(client, name, opts) when is_binary(name) do
50140 client.__struct__.post(client, name, opts)
141141+ end
142142+143143+ def post(client, %{__struct__: module} = procedure, opts) do
144144+ opts =
145145+ opts
146146+ |> then(
147147+ &if Map.get(procedure, :params), do: Keyword.put(&1, :params, procedure.params), else: &1
148148+ )
149149+ |> then(
150150+ &cond do
151151+ Map.get(procedure, :input) -> Keyword.put(&1, :json, procedure.input)
152152+ Map.get(procedure, :raw_input) -> Keyword.put(&1, :body, procedure.raw_input)
153153+ true -> &1
154154+ end
155155+ )
156156+157157+ output_struct = Module.concat(module, Output)
158158+ output_exists = Code.ensure_loaded?(output_struct)
159159+160160+ case client.__struct__.post(client, module.id(), opts) do
161161+ {:ok, %{status: 200} = response, client} ->
162162+ if output_exists do
163163+ case output_struct.from_json(response.body) do
164164+ {:ok, output} ->
165165+ {:ok, %{response | body: output}, client}
166166+167167+ err ->
168168+ err
169169+ end
170170+ else
171171+ {:ok, response, client}
172172+ end
173173+174174+ {:ok, _, _} = ok ->
175175+ ok
176176+177177+ err ->
178178+ err
179179+ end
51180 end
5218153182 @doc """