defmodule ActivityPubClient.Client do
@moduledoc """
Interface for handling `%OAuth2.Client{}` structs (see `OAuth2.Client`).
"""
# @TODO
# set config :oauth2, debug: true
# for dev and test env
alias ActivityPubClient.App
alias ActivityPubClient.User
@repo Application.fetch_env!(:activity_pub_client, :ecto_repos) |> List.first()
@default_scopes "read write follow push"
@doc """
Gets a valid `%OAuth2.Client{}` struct.
Expects the URL of the server and an ActivityPub ID (most of the time it
corresponds to the user profile URL).
"""
def client(site, ap_id) do
with {:ok, client} <- ensure_app_registered(site),
{:ok, client} <- ensure_access_token(client, ap_id) do
{:ok, client}
end
end
@doc """
Sets the OAuth2 code as returned to the user after authorizing the application.
"""
def set_code(ap_id, code) do
# %User{ap_id: ap_id, code: code}
@repo.insert!(
%User{ap_id: ap_id, code: code},
on_conflict: [set: [code: code]],
conflict_target: :ap_id
)
end
defp ensure_app_registered(site) do
case get_app(site) do
nil -> register_app(site)
%App{} = app -> {:ok, new_client(app)}
end
end
defp register_app(site) do
url = site <> "/api/v1/apps"
headers = [{"content-type", "application/x-www-form-urlencoded"}]
body =
%{
client_name: "Kazarma",
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
scopes: @default_scopes,
website: "https://gitlab.com/technostructures/kazarma/kazarma"
}
|> URI.encode_query()
req_opts = [with_body: true]
with {:ok, 200, _headers, body} <-
:hackney.post(url, headers, body, req_opts),
{:ok, resp} <- Jason.decode(body),
{:ok, app} <- create_app(site, resp) do
{:ok, new_client(app)}
else
{:ok, 400, _headers, body} ->
{:error, Jason.decode!(body) |> Map.fetch!("error")}
{:ok, 422, _headers, body} ->
{:error, Jason.decode!(body) |> Map.fetch!("error")}
{:error, reason} ->
{:error, reason}
end
end
defp ensure_access_token(client, ap_id) do
case get_user(ap_id) do
nil ->
{:needs_authorization, client, OAuth2.Client.authorize_url!(client)}
%User{code: nil} ->
{:needs_authorization, client, OAuth2.Client.authorize_url!(client)}
%User{code: code, token: nil} = user ->
client = OAuth2.Client.get_token!(client, code: code)
update_user_token(user, client.token)
{:ok, client}
%User{code: _code, token: token} = user ->
token = %OAuth2.AccessToken{
access_token: token.access_token,
refresh_token: token.refresh_token,
expires_at: token.expires_at
}
client = %{client | token: token}
if OAuth2.AccessToken.expired?(token) do
client = OAuth2.Client.refresh_token!(client)
update_user_token(user, client.token)
{:ok, client}
else
{:ok, client}
end
end
end
defp new_client(app) do
OAuth2.Client.new(
strategy: OAuth2.Strategy.AuthCode,
client_id: app.client_id,
client_secret: app.client_secret,
site: app.site,
redirect_uri: "urn:ietf:wg:oauth:2.0:oob"
)
|> OAuth2.Client.put_serializer("application/json", Jason)
end
defp create_app(site, attrs) do
attrs = Map.put(attrs, "site", site)
%App{}
|> App.changeset(attrs)
|> @repo.insert()
end
defp get_app(site) do
@repo.get_by(App, site: site)
end
defp get_user(ap_id) do
@repo.get_by(User, ap_id: ap_id)
end
defp update_user_token(user, token) do
user
|> User.changeset(%{"token" => Map.from_struct(token)})
|> @repo.update()
end
end