Add sender

This commit is contained in:
Mirai Kumiko 2025-06-30 13:18:06 +02:00
parent 20a3ed9e71
commit fa350aa551
Signed by: miraikumiko
GPG key ID: 3F178B1B5E0CB278
5 changed files with 85 additions and 25 deletions

View file

@ -1,8 +1,7 @@
defmodule Nulla.HTTPSignature do defmodule Nulla.HTTPSignature do
import Plug.Conn 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()) digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64())
date = DateTime.utc_now() |> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT") date = DateTime.utc_now() |> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT")
uri = URI.parse(inbox_url) uri = URI.parse(inbox_url)
@ -13,10 +12,8 @@ defmodule Nulla.HTTPSignature do
"date: #{date}\n" <> "date: #{date}\n" <>
"digest: #{digest}" "digest: #{digest}"
user = User.get_user(id: actor.id)
private_key = private_key =
case :public_key.pem_decode(user.privateKeyPem) do case :public_key.pem_decode(privateKeyPem) do
[entry] -> :public_key.pem_entry_decode(entry) [entry] -> :public_key.pem_entry_decode(entry)
_ -> raise "Invalid PEM format" _ -> raise "Invalid PEM format"
end end
@ -27,7 +24,7 @@ defmodule Nulla.HTTPSignature do
signature_header = signature_header =
""" """
keyId="#{actor.publicKey["id"]}", keyId="#{publicKeyId}",
algorithm="rsa-sha256", algorithm="rsa-sha256",
headers="(request-target) host date digest", headers="(request-target) host date digest",
signature="#{signature}" signature="#{signature}"

View file

@ -3,6 +3,7 @@ defmodule Nulla.Models.Activity do
import Ecto.Changeset import Ecto.Changeset
alias Nulla.Repo alias Nulla.Repo
alias Nulla.Snowflake alias Nulla.Snowflake
alias Nulla.Types.StringOrJson
@derive {Jason.Encoder, only: [:ap_id, :type, :actor, :object]} @derive {Jason.Encoder, only: [:ap_id, :type, :actor, :object]}
@primary_key {:id, :integer, autogenerate: false} @primary_key {:id, :integer, autogenerate: false}
@ -10,7 +11,7 @@ defmodule Nulla.Models.Activity do
field :ap_id, :string field :ap_id, :string
field :type, :string field :type, :string
field :actor, :string field :actor, :string
field :object, :string field :object, StringOrJson
field :to, {:array, :string} field :to, {:array, :string}
field :cc, {:array, :string} field :cc, {:array, :string}

24
lib/nulla/sender.ex Normal file
View file

@ -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

View file

@ -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

View file

@ -1,9 +1,10 @@
defmodule NullaWeb.InboxController do defmodule NullaWeb.InboxController do
use NullaWeb, :controller use NullaWeb, :controller
alias Nulla.ActivityPub
alias Nulla.Snowflake alias Nulla.Snowflake
alias Nulla.HTTPSignature alias Nulla.HTTPSignature
alias Nulla.Sender
alias Nulla.Utils alias Nulla.Utils
alias Nulla.Models.User
alias Nulla.Models.Actor alias Nulla.Models.Actor
alias Nulla.Models.Relation alias Nulla.Models.Relation
alias Nulla.Models.Activity alias Nulla.Models.Activity
@ -109,24 +110,15 @@ defmodule NullaWeb.InboxController do
}), }),
{:ok, _relation} <- {:ok, _relation} <-
Relation.get_or_create_relation(local_actor.id, remote_actor.id, followed_by: true) do 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)} user = User.get_user(id: local_actor.id)
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)
case Finch.request(request, Nulla.Finch) do Sender.send_activity(
{:ok, %Finch.Response{status: code}} when code in 200..299 -> :post,
IO.puts("Accept delivered successfully") remote_actor.inbox,
:ok accept_activity,
local_actor.publicKey["id"],
{:ok, %Finch.Response{status: code, body: resp}} -> user.privateKeyPem
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
send_resp(conn, 200, "") send_resp(conn, 200, "")
else else