defmodule Immich.API do
@moduledoc """
Public facade for the most common Immich API workflows.
This module combines:
- OAuth bootstrap (`authorize/2`, `callback/3`)
- Authenticated user lookup (`current_user/2`)
- Sync stream consumption (`sync_stream/3`)
- Sync acknowledgement posting (`sync_ack/3`)
Most request functions accept `shared_opts` so callers can inject a custom
client implementation (for example in tests) while keeping the same API.
## Typical usage
1. Call `authorize/2` and open the returned login URL.
2. After redirect, call `callback/3` to build an authenticated `Session`.
3. Use that `Session` with `current_user/2`, `sync_stream/3`, and `sync_ack/3`.
"""
alias Immich.API.Client
alias Immich.API.OAuth
alias Immich.API.Session
@typedoc """
Standard API error shape returned by the configured client.
This includes transport failures, authentication/authorization failures,
and non-success HTTP responses mapped by `Immich.API.Client`.
"""
@type api_error :: Client.api_error()
@typedoc """
Options shared by authenticated API functions.
Supported options:
- `:client` - module implementing `Immich.API.Client.Behaviour`
(defaults to `Immich.API.Client`)
"""
@type shared_opts :: [
client: Client.t()
]
@doc """
Initiates the OAuth flow by requesting PKCE parameters and an authorization URL.
Returns the login URL and OAuth context required by `callback/3`.
This delegates to `Immich.API.OAuth.authorize/2`.
See `Immich.API.OAuth.authorize/2` for details.
"""
@spec authorize(OAuth.redirect_uri(), OAuth.shared_opts()) ::
{:ok, OAuth.login_url(), OAuth.oauth_context()} | {:error, OAuth.oauth_error()}
defdelegate authorize(redirect_uri, opts), to: OAuth
@doc """
Completes OAuth callback exchange and returns an authenticated session.
The provided `oauth_context` must be the exact context returned by `authorize/2`.
This delegates to `Immich.API.OAuth.callback/3`.
See `Immich.API.OAuth.callback/3` for details.
"""
@spec callback(OAuth.callback_uri(), OAuth.oauth_context(), OAuth.shared_opts()) ::
{:ok, Session.t()} | {:error, OAuth.oauth_error()}
defdelegate callback(callback_uri, oauth_context, shared_opts), to: OAuth
@doc """
Fetches the currently authenticated user for a session.
Uses `session.base_url` to build `/api/users/me` and attaches a bearer token
from `session.access_token`.
"""
@spec current_user(Session.t(), shared_opts()) :: {:ok, map()} | {:error, api_error()}
def current_user(session, opts \\ []) do
api_get(session, "/api/users/me", opts)
end
@typedoc """
Sync type identifier sent to Immich sync APIs.
Values are expected to match server-recognized type names (for example
`"AssetsV1"`, `"StacksV1"`).
"""
@type sync_type :: String.t()
@typedoc """
Options for `sync_stream/3`.
Supported options:
- all `shared_opts`
- `:reset?` - when `true`, requests a reset sync from the server
(defaults to `false`)
"""
@type sync_stream_opts :: [
{:reset?, boolean()} | {:client, Client.t()}
]
@doc """
Opens the Immich sync stream and returns decoded NDJSON events.
The request payload is forwarded as:
- `"types"` from `sync_types`
- `"reset"` from `opts[:reset?]` (defaults to `false`)
Returned events are yielded as a lazy enumerable of decoded maps.
"""
@spec sync_stream(Session.t(), [sync_type()], sync_stream_opts()) ::
{:ok, Enumerable.t(map())} | {:error, api_error()}
def sync_stream(session, sync_types, opts \\ []) do
reset? = Keyword.get(opts, :reset?, false)
request = %{"reset" => reset?, "types" => sync_types}
api_ndjson_stream(session, "/api/sync/stream", request, opts)
end
@typedoc """
Sync acknowledgement identifier sent back to the server.
Each acknowledgement corresponds to an event previously received from
`sync_stream/3`.
"""
@type sync_ack :: String.t()
@doc """
Posts sync acknowledgements.
Sends acknowledgements to `/api/sync/ack` using the authenticated session.
"""
@spec sync_ack(Session.t(), [sync_ack()], shared_opts()) ::
{:ok, map() | term()} | {:error, api_error()}
def sync_ack(session, acks, opts \\ []) do
request = %{"acks" => acks}
api_post(session, "/api/sync/ack", request, opts)
end
defp api_get(session, path, opts) do
client(opts).get(build_url(session, path), authorization_headers(session))
end
defp api_post(session, path, request, opts) do
client(opts).post(build_url(session, path), request, authorization_headers(session))
end
defp api_ndjson_stream(session, path, request, opts) do
client(opts).ndjson_stream(
build_url(session, path),
request,
authorization_headers(session)
)
end
defp client(opts) do
Keyword.get(opts, :client, Client)
end
defp build_url(session, path) do
URI.merge(session.base_url, path)
end
defp authorization_headers(session),
do: [{"authorization", "Bearer #{session.access_token}"}]
end