defmodule NullaWeb.InboxController do use NullaWeb, :controller alias Nulla.ActivityPub alias Nulla.Snowflake alias Nulla.HTTPSignature alias Nulla.Utils alias Nulla.Models.User alias Nulla.Models.Actor alias Nulla.Models.Relation alias Nulla.Models.Activity def inbox(conn, %{ "id" => follow_id, "type" => "Follow", "actor" => actor_uri, "object" => target_uri }) do accept_id = Snowflake.next_id() with local_actor <- Actor.get_actor(ap_id: target_uri), {:ok, remote_actor_json} <- Utils.fetch_remote_actor(actor_uri), :ok <- HTTPSignature.verify(conn, remote_actor_json["publicKey"]["publicKeyPem"]), {:ok, remote_actor} <- get_or_create_actor(remote_actor_json), {:ok, follow_activity} <- Activity.create_activity(%{ ap_id: follow_id, type: "Follow", actor: remote_actor.ap_id, object: target_uri }), {:ok, accept_activity} <- Activity.create_activity(%{ id: accept_id, ap_id: "https://#{local_actor.domain}/activities/accept/#{accept_id}", type: "Accept", actor: local_actor.ap_id, object: Jason.encode!(follow_activity) }), {:ok, _relation} <- get_or_create_relation(local_actor.id, remote_actor.id), :ok <- deliver_accept(accept_activity, remote_actor_json["inbox"], local_actor) do send_resp(conn, 200, "") else error -> IO.inspect(error, label: "Follow error") json(conn, %{"error" => "Failed to process Follow"}) end end defp get_or_create_actor(remote_actor_json) do ap_id = remote_actor_json["id"] case Actor.get_actor(ap_id: ap_id) do nil -> params = remote_actor_json |> Map.put("ap_id", ap_id) |> Map.delete("id") |> Map.put("domain", URI.parse(ap_id).host) case Actor.create_actor(params) do {:ok, actor} -> {:ok, actor} {:error, changeset} -> {:error, {:actor_creation_failed, changeset}} end actor -> {:ok, actor} end end defp get_or_create_relation(local_actor_id, remote_actor_id) do case Relation.get_relation(local_actor_id: local_actor_id, remote_actor_id: remote_actor_id) do nil -> Relation.create_relation(%{ followed_by: true, local_actor_id: local_actor_id, remote_actor_id: remote_actor_id }) relation -> {:ok, relation} end end defp deliver_accept(accept_activity, inbox_url, local_actor) do accept_activity = %Activity{accept_activity | object: Jason.decode!(accept_activity.object)} body = Jason.encode!(ActivityPub.activity(accept_activity)) digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64()) date = DateTime.utc_now() |> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT") uri = URI.parse(inbox_url) signature_string = """ (request-target): post #{uri.path} host: #{uri.host} date: #{date} digest: #{digest} """ user = User.get_user(id: local_actor.id) private_key = case :public_key.pem_decode(user.privateKeyPem) do [entry] -> :public_key.pem_entry_decode(entry) _ -> raise "Invalid PEM format" end signature = :public_key.sign(signature_string, :sha256, private_key) |> Base.encode64() signature_header = """ keyId="#{local_actor.publicKey["id"]}", algorithm="rsa-sha256", headers="(request-target) host date digest", signature="#{signature}" """ |> String.replace("\n", "") |> String.trim() headers = [ {"Content-Type", "application/activity+json"}, {"Date", date}, {"Digest", digest}, {"Signature", signature_header} ] request = Finch.build(:post, inbox_url, headers, body) case Finch.request(request, Nulla.Finch) do {:ok, %Finch.Response{status: code}} when code in 200..299 -> IO.puts("Accept delivered successfully") :ok {:ok, %Finch.Response{status: code, body: resp}} -> IO.inspect({:error, code, resp}, label: "Failed to deliver Accept") {:error, {:http_error, code}} {:error, reason} -> IO.inspect(reason, label: "Finch delivery failed") {:error, reason} end end def inbox(conn, %{ "id" => accept_id, "type" => "Accept", "actor" => actor_uri, "object" => object }) do with {:ok, remote_actor_json} <- Utils.fetch_remote_actor(actor_uri), :ok <- HTTPSignature.verify(conn, remote_actor_json), {:ok, remote_actor} <- get_or_create_actor(remote_actor_json), {:ok, follow_activity} <- decode_follow_activity(object), {:ok, local_actor} <- Actor.get_actor(ap_id: follow_activity["object"]), {:ok, _relation} <- Relation.mark_follow_accepted(local_actor.id, remote_actor.id) do send_resp(conn, 200, "") else error -> IO.inspect(error, label: "Accept error") json(conn, %{"error" => "Failed to process Accept"}) end end defp decode_follow_activity(object) when is_map(object), do: {:ok, object} defp decode_follow_activity(object) when is_binary(object), do: Jason.decode(object) def inbox(conn, %{ "id" => accept_id, "type" => "Undo", "actor" => actor_uri, "object" => object }) do send_resp(conn, 200, "") end def inbox(conn, params) do IO.inspect(params) send_resp(conn, 400, "") end end