From fa350aa551cd92da80c093f054c5afb334c68a48 Mon Sep 17 00:00:00 2001 From: miraikumiko Date: Mon, 30 Jun 2025 13:18:06 +0200 Subject: [PATCH] Add sender --- lib/nulla/http_signature.ex | 9 ++-- lib/nulla/models/activity.ex | 3 +- lib/nulla/sender.ex | 24 ++++++++++ lib/nulla/types/string_or_json.ex | 46 +++++++++++++++++++ lib/nulla_web/controllers/inbox_controller.ex | 28 ++++------- 5 files changed, 85 insertions(+), 25 deletions(-) create mode 100644 lib/nulla/sender.ex create mode 100644 lib/nulla/types/string_or_json.ex diff --git a/lib/nulla/http_signature.ex b/lib/nulla/http_signature.ex index 5207c2d..35c3977 100644 --- a/lib/nulla/http_signature.ex +++ b/lib/nulla/http_signature.ex @@ -1,8 +1,7 @@ defmodule Nulla.HTTPSignature do import Plug.Conn - alias Nulla.Models.User - def make_headers(body, inbox_url, actor) do + def make_headers(body, inbox_url, publicKeyId, privateKeyPem) do 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) @@ -13,10 +12,8 @@ defmodule Nulla.HTTPSignature do "date: #{date}\n" <> "digest: #{digest}" - user = User.get_user(id: actor.id) - private_key = - case :public_key.pem_decode(user.privateKeyPem) do + case :public_key.pem_decode(privateKeyPem) do [entry] -> :public_key.pem_entry_decode(entry) _ -> raise "Invalid PEM format" end @@ -27,7 +24,7 @@ defmodule Nulla.HTTPSignature do signature_header = """ - keyId="#{actor.publicKey["id"]}", + keyId="#{publicKeyId}", algorithm="rsa-sha256", headers="(request-target) host date digest", signature="#{signature}" diff --git a/lib/nulla/models/activity.ex b/lib/nulla/models/activity.ex index c69b2af..00add89 100644 --- a/lib/nulla/models/activity.ex +++ b/lib/nulla/models/activity.ex @@ -3,6 +3,7 @@ defmodule Nulla.Models.Activity do import Ecto.Changeset alias Nulla.Repo alias Nulla.Snowflake + alias Nulla.Types.StringOrJson @derive {Jason.Encoder, only: [:ap_id, :type, :actor, :object]} @primary_key {:id, :integer, autogenerate: false} @@ -10,7 +11,7 @@ defmodule Nulla.Models.Activity do field :ap_id, :string field :type, :string field :actor, :string - field :object, :string + field :object, StringOrJson field :to, {:array, :string} field :cc, {:array, :string} diff --git a/lib/nulla/sender.ex b/lib/nulla/sender.ex new file mode 100644 index 0000000..8e3a516 --- /dev/null +++ b/lib/nulla/sender.ex @@ -0,0 +1,24 @@ +defmodule Nulla.Sender do + alias Nulla.ActivityPub + alias Nulla.HTTPSignature + + def send_activity(method, inbox, activity, publicKeyId, privateKeyPem) do + body = Jason.encode!(ActivityPub.activity(activity)) + headers = HTTPSignature.make_headers(body, inbox, publicKeyId, privateKeyPem) + request = Finch.build(method, inbox, headers, body) + + case Finch.request(request, Nulla.Finch) do + {:ok, %Finch.Response{status: code}} when code in 200..299 -> + IO.puts("Activity #{activity.id} delivered successfully") + :ok + + {:ok, %Finch.Response{status: code, body: resp}} -> + IO.inspect({:error, code, resp}, label: "Failed to deliver activity #{activity.id}") + {:error, {:http_error, code}} + + {:error, reason} -> + IO.inspect(reason, label: "Activity #{activity.id} delivery failed") + {:error, reason} + end + end +end diff --git a/lib/nulla/types/string_or_json.ex b/lib/nulla/types/string_or_json.ex new file mode 100644 index 0000000..4463c32 --- /dev/null +++ b/lib/nulla/types/string_or_json.ex @@ -0,0 +1,46 @@ +defmodule Nulla.Types.StringOrJson do + @behaviour Ecto.Type + + @impl true + def type, do: :string + + @impl true + def cast(value) when is_map(value) or is_list(value), do: {:ok, value} + + @impl true + def cast(value) when is_binary(value) do + case Jason.decode(value) do + {:ok, decoded} -> {:ok, decoded} + _ -> {:ok, value} + end + end + + @impl true + def cast(_), do: :error + + @impl true + def dump(value) when is_map(value) or is_list(value), do: Jason.encode(value) + + @impl true + def dump(value) when is_binary(value), do: {:ok, value} + + @impl true + def dump(_), do: :error + + @impl true + def load(value) when is_binary(value) do + case Jason.decode(value) do + {:ok, decoded} -> {:ok, decoded} + _ -> {:ok, value} + end + end + + @impl true + def load(_), do: :error + + @impl true + def embed_as(_format), do: :self + + @impl true + def equal?(term1, term2), do: term1 == term2 +end diff --git a/lib/nulla_web/controllers/inbox_controller.ex b/lib/nulla_web/controllers/inbox_controller.ex index cbec289..4cbbb44 100644 --- a/lib/nulla_web/controllers/inbox_controller.ex +++ b/lib/nulla_web/controllers/inbox_controller.ex @@ -1,9 +1,10 @@ defmodule NullaWeb.InboxController do use NullaWeb, :controller - alias Nulla.ActivityPub alias Nulla.Snowflake alias Nulla.HTTPSignature + alias Nulla.Sender alias Nulla.Utils + alias Nulla.Models.User alias Nulla.Models.Actor alias Nulla.Models.Relation alias Nulla.Models.Activity @@ -109,24 +110,15 @@ defmodule NullaWeb.InboxController do }), {:ok, _relation} <- Relation.get_or_create_relation(local_actor.id, remote_actor.id, followed_by: true) do - activity = %Activity{accept_activity | object: Jason.decode!(accept_activity.object)} - body = Jason.encode!(ActivityPub.activity(activity)) - headers = HTTPSignature.make_headers(body, remote_actor_json["inbox"], local_actor) - request = Finch.build(:post, remote_actor_json["inbox"], headers, body) + user = User.get_user(id: local_actor.id) - 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 + Sender.send_activity( + :post, + remote_actor.inbox, + accept_activity, + local_actor.publicKey["id"], + user.privateKeyPem + ) send_resp(conn, 200, "") else