1# AUTOGENERATED: This file was generated using the mix task `lexgen`.
2defmodule Atproto do
3 @default_pds_hostname Application.compile_env!(:comet, :default_pds_hostname)
4
5 @typedoc """
6 A type representing the names of the options that can be passed to `query/3` and `procedure/3`.
7 """
8 @type xrpc_opt :: :pds_hostname | :authorization
9
10 @typedoc """
11 A keyword list of options that can be passed to `query/3` and `procedure/3`.
12 """
13 @type xrpc_opts :: [{xrpc_opt(), any()}]
14
15 @doc """
16 Converts a JSON string, or decoded JSON map, into a struct based on the given module.
17
18 This function uses `String.to_existing_atom/1` to convert the keys of the map to atoms, meaning this will throw an error if the input JSON contains keys which are not already defined as atoms in the existing structs or codebase.
19 """
20 @spec decode_to_struct(module(), binary() | map()) :: map()
21 def decode_to_struct(module, json) when is_binary(json) do
22 decode_to_struct(module, Jason.decode!(json, keys: :atoms!))
23 end
24
25 def decode_to_struct(module, map) when is_map(map) do
26 Map.merge(module.new(), map)
27 end
28
29 @doc """
30 Raises an error if any required parameters are missing from the given map.
31 """
32 @spec ensure_required(map(), [String.t()]) :: map()
33 def ensure_required(params, required) do
34 if Enum.all?(required, fn key -> Map.has_key?(params, key) end) do
35 params
36 else
37 raise ArgumentError, "Missing one or more required parameters: #{Enum.join(required, ", ")}"
38 end
39 end
40
41 @doc """
42 Executes a "GET" HTTP request and returns the response body as a map.
43
44 If the `:pds_hostname` option is not provided, the default PDS hostname as provided in the compile-time configuration will be used.
45 """
46 @spec query(map(), String.t(), xrpc_opts()) :: Req.Request.t()
47 def query(params, target, opts \\ []) do
48 target
49 |> endpoint(opts)
50 |> URI.new!()
51 |> URI.append_query(URI.encode_query(params))
52 |> Req.get(build_req_auth(opts))
53 |> handle_response(opts)
54 end
55
56 @doc """
57 Executes a "POST" HTTP request and returns the response body as a map.
58
59 If the `:pds_hostname` option is not provided, the default PDS hostname as provided in the compile-time configuration will be used.
60 """
61 @spec procedure(map(), String.t(), xrpc_opts()) :: {:ok | :refresh | :error, map()}
62 def procedure(params, target, opts \\ []) do
63 req_opts =
64 opts
65 |> build_req_auth()
66 |> build_req_headers(opts, target)
67 |> build_req_body(params, target)
68
69 target
70 |> endpoint(opts)
71 |> URI.new!()
72 |> Req.post(req_opts)
73 |> handle_response(opts)
74 end
75
76 defp build_req_auth(opts) do
77 case Keyword.get(opts, :access_token) do
78 nil ->
79 case Keyword.get(opts, :admin_token) do
80 nil ->
81 []
82
83 token ->
84 [auth: {:basic, "admin:#{token}"}]
85 end
86
87 token ->
88 [auth: {:bearer, token}]
89 end
90 end
91
92 defp build_req_headers(req_opts, opts, "com.atproto.repo.uploadBlob") do
93 [
94 {:headers,
95 [
96 {"Content-Type", Keyword.fetch!(opts, :content_type)},
97 {"Content-Length", Keyword.fetch!(opts, :content_length)}
98 ]}
99 | req_opts
100 ]
101 end
102
103 defp build_req_headers(req_opts, _opts, _target), do: req_opts
104
105 defp build_req_body(opts, blob, "com.atproto.repo.uploadBlob") do
106 [{:body, blob} | opts]
107 end
108
109 defp build_req_body(opts, %{} = params, _target) when map_size(params) > 0 do
110 [{:json, params} | opts]
111 end
112
113 defp build_req_body(opts, _params, _target), do: opts
114
115 defp endpoint(target, opts) do
116 (Keyword.get(opts, :pds_hostname) || @default_pds_hostname) <> "/xrpc/" <> target
117 end
118
119 defp handle_response({:ok, %Req.Response{} = response}, opts) do
120 case response.status do
121 x when x in 200..299 ->
122 {:ok, response.body}
123
124 _ ->
125 if response.body["error"] == "ExpiredToken" do
126 {:ok, user} =
127 Com.Atproto.Server.RefreshSession.main(%{},
128 access_token: Keyword.get(opts, :refresh_token)
129 )
130
131 {:refresh, user}
132 else
133 {:error, response.body}
134 end
135 end
136 end
137
138 defp handle_response(error, _opts), do: error
139
140 @doc """
141 Converts a "map-like" entity into a standard map. This will also omit any entries that have a `nil` value.
142
143 This is useful for converting structs or schemas into regular maps before sending them over XRPC requests.
144
145 You may optionally pass in an keyword list of options:
146
147 - `:stringify` - `boolean` - If `true`, converts the keys to strings. Otherwise, converts keys to atoms. Default is `false`.
148 - *Note*: When `false`, this feature uses the `to_existing_atom/1` function to avoid reckless conversion of string keys.
149 """
150 @spec to_map(map() | struct()) :: map()
151 def to_map(%{__struct__: _} = m, opts \\ []) do
152 string_keys = Keyword.get(opts, :stringify, false)
153
154 m
155 |> Map.drop([:__struct__, :__meta__])
156 |> Enum.map(fn
157 {_, nil} ->
158 nil
159
160 {k, v} when is_atom(k) ->
161 if string_keys, do: {to_string(k), v}, else: {k, v}
162
163 {k, v} when is_binary(k) ->
164 if string_keys, do: {k, v}, else: {String.to_existing_atom(k), v}
165 end)
166 |> Enum.reject(&is_nil/1)
167 |> Enum.into(%{})
168 end
169end