nulla/lib/nulla_web/controllers/inbox_controller.ex
2025-06-23 09:16:27 +00:00

165 lines
4.7 KiB
Elixir

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),
{:ok, remote_actor} <- get_or_create_actor(remote_actor_json),
{:ok, follow_activity} <-
create_follow_activity(follow_id, remote_actor.ap_id, target_uri),
{:ok, accept_activity} <-
create_accept_activity(accept_id, local_actor, follow_activity),
{:ok, _relation} <- get_or_create_relation(local_actor.id, remote_actor.id),
:ok <- deliver_accept(accept_activity, remote_actor_json, 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 create_follow_activity(follow_id, actor_id, target_uri) do
Activity.create_activity(%{
ap_id: follow_id,
type: "Follow",
actor: actor_id,
object: target_uri
})
end
defp create_accept_activity(accept_id, local_actor, follow_activity) do
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)
})
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, remote_actor_json, local_actor) do
inbox_url = remote_actor_json["inbox"]
accept_activity = %Activity{accept_activity | object: Jason.decode!(accept_activity.object)}
body = Jason.encode!(ActivityPub.activity(accept_activity))
digest =
:crypto.hash(:sha256, body)
|> Base.encode64()
|> then(&("SHA-256=" <> &1))
date =
DateTime.utc_now()
|> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT")
uri = URI.parse(inbox_url)
request_target = "post #{uri.path}"
signature_string = """
(request-target): #{request_target}
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
[{_type, der, _rest}] ->
:public_key.der_decode(:RSAPrivateKey, der)
_ ->
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 content-type",
signature="#{signature}"
"""
|> String.replace("\n", "")
|> String.trim()
headers = [
{"host", uri.host},
{"date", date},
{"digest", digest},
{"signature", signature_header},
{"content-type", "application/activity+json"}
]
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
end