From 1259d3202dc8293e9060ca8d4fd1638fb85d4296 Mon Sep 17 00:00:00 2001 From: Ro Date: Wed, 9 Oct 2024 22:45:28 +0000 Subject: [PATCH 1/7] feat: Handle follows --- .vscode/extensions.json | 13 ++++++++ lib/activity_pub.ex | 2 ++ lib/postland.ex | 6 ++++ lib/postland/activities.ex | 27 ++++++++++++++++ lib/postland/follow.ex | 29 +++++++++++++++++ lib/postland/follows.ex | 31 +++++++++++++++++++ .../controllers/inbox_controller.ex | 2 +- mix.lock | 2 +- .../migrations/20241009214840_add_follows.exs | 11 +++++++ 9 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 lib/activity_pub.ex create mode 100644 lib/postland/follow.ex create mode 100644 lib/postland/follows.ex create mode 100644 priv/repo/migrations/20241009214840_add_follows.exs diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..d1987c7 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,13 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [ + + ] +} \ No newline at end of file diff --git a/lib/activity_pub.ex b/lib/activity_pub.ex new file mode 100644 index 0000000..79d8e8d --- /dev/null +++ b/lib/activity_pub.ex @@ -0,0 +1,2 @@ +defmodule ActivityPub do +end diff --git a/lib/postland.ex b/lib/postland.ex index e5571fe..56a62fd 100644 --- a/lib/postland.ex +++ b/lib/postland.ex @@ -7,6 +7,8 @@ defmodule Postland do if it comes from the database, an external API or others. """ + use Phoenix.VerifiedRoutes, endpoint: PostlandWeb.Endpoint, router: PostlandWeb.Router + alias Postland.Accounts def temporary_password() do @@ -51,4 +53,8 @@ defmodule Postland do def setup?() do Accounts.solo_user() != nil end + + def my_actor_id do + url(~p"/actor") + end end diff --git a/lib/postland/activities.ex b/lib/postland/activities.ex index 0b216be..5dbbb0b 100644 --- a/lib/postland/activities.ex +++ b/lib/postland/activities.ex @@ -1,10 +1,37 @@ defmodule Postland.Activities do + use Phoenix.VerifiedRoutes, endpoint: PostlandWeb.Endpoint, router: PostlandWeb.Router + alias Postland.Activity alias Postland.Repo + def process_activity(params) do + case record_activity(params) do + {:ok, activity} -> + cause_effects(activity) + + other -> + other + end + end + def record_activity(params) do params |> Activity.changeset() |> Repo.insert() end + + def cause_effects(%Activity{actor_id: actor_id, type: "Follow", data: %{"object" => object}} = activity) do + if object == Postland.my_actor_id() do + case Postland.Follows.record_inbound_request(actor_id) do + {:ok, _follow} -> + {:ok, activity} + other -> + other + end + else + {:ok, activity} + end + end + + def cause_effects(activity), do: activity end diff --git a/lib/postland/follow.ex b/lib/postland/follow.ex new file mode 100644 index 0000000..da31a9d --- /dev/null +++ b/lib/postland/follow.ex @@ -0,0 +1,29 @@ +defmodule Postland.Follow do + use Ecto.Schema + + import Ecto.Changeset + + schema "follows" do + field :follower, :string, primary_key: true + field :followee, :string, primary_key: true + field :confirmed_at, :naive_datetime + end + + def changeset(follower, followee, confirmed \\ false) do + attrs = %{ + follower: follower, + followee: followee, + confirmed_at: (if confirmed, do: NaiveDateTime.utc_now()) + } + + %__MODULE__{} + |> cast(attrs, [:follower, :followee, :confirmed_at]) + |> validate_required(:followee) + |> validate_required(:follower) + end + + def confirm_changeset(request) do + request + |> cast(%{confirmed_at: NaiveDateTime.utc_now()}, [:confirmed_at]) + end +end diff --git a/lib/postland/follows.ex b/lib/postland/follows.ex new file mode 100644 index 0000000..19abe8a --- /dev/null +++ b/lib/postland/follows.ex @@ -0,0 +1,31 @@ +defmodule Postland.Follows do + import Ecto.Query, warn: false + + alias Postland.Follow + alias Postland.Repo + + def pending_inbound_requests() do + my_actor_id = Postland.my_actor_id() + + from(f in Follow, where: f.followee == ^my_actor_id, where: is_nil(f.confirmed_at)) + |> Repo.all() + end + + def record_outbound_request(to_actor_id) do + Postland.my_actor_id() + |> Follow.changeset(to_actor_id) + |> Repo.insert(conflict_target: [:followee, :follower], on_conflict: :nothing) + end + + def record_inbound_request(from_actor_id) do + from_actor_id + |> Follow.changeset(Postland.my_actor_id()) + |> Repo.insert(conflict_target: [:followee, :follower], on_conflict: :nothing) + end + + def confirm_request(request) do + request + |> Follow.confirm_changeset() + |> Repo.update() + end +end diff --git a/lib/postland_web/controllers/inbox_controller.ex b/lib/postland_web/controllers/inbox_controller.ex index f43525a..393cc74 100644 --- a/lib/postland_web/controllers/inbox_controller.ex +++ b/lib/postland_web/controllers/inbox_controller.ex @@ -8,7 +8,7 @@ defmodule PostlandWeb.InboxController do def post(conn, params) do if Headers.verify(conn.method, conn.request_path, conn.req_headers) do - case Activities.record_activity(params) do + case Activities.process_activity(params) do {:ok, _activity} -> render(conn, :ok) error -> diff --git a/mix.lock b/mix.lock index 6753c6c..e115060 100644 --- a/mix.lock +++ b/mix.lock @@ -18,7 +18,7 @@ "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"}, - "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]}, + "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, diff --git a/priv/repo/migrations/20241009214840_add_follows.exs b/priv/repo/migrations/20241009214840_add_follows.exs new file mode 100644 index 0000000..639c728 --- /dev/null +++ b/priv/repo/migrations/20241009214840_add_follows.exs @@ -0,0 +1,11 @@ +defmodule Postland.Repo.Migrations.AddActors do + use Ecto.Migration + + def change do + create table("follows", primary_key: false) do + add :follower, :string, primary_key: true + add :followee, :string, primary_key: true + add :confirmed_at, :naive_datetime + end + end +end From 90a158c380c9e4b536fdf8608933f0a886578ff6 Mon Sep 17 00:00:00 2001 From: Ro Date: Thu, 10 Oct 2024 00:01:44 +0000 Subject: [PATCH 2/7] feat: Send follow requests --- lib/activity_pub.ex | 13 +++++++++++++ lib/activity_pub/headers.ex | 10 +++------- lib/postland/activities.ex | 17 ++++++++++++++++ lib/postland/follow.ex | 1 + lib/postland/follows.ex | 39 +++++++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/lib/activity_pub.ex b/lib/activity_pub.ex index 79d8e8d..ef64556 100644 --- a/lib/activity_pub.ex +++ b/lib/activity_pub.ex @@ -1,2 +1,15 @@ defmodule ActivityPub do + def fetch_actor(actor_id) do + request = + Req.new(url: actor_id) + |> Req.Request.put_header("accept", "application/json") + + case Req.get(request) do + {:ok, result} -> + {:ok, result.body} + + error -> + error + end + end end diff --git a/lib/activity_pub/headers.ex b/lib/activity_pub/headers.ex index 24025a9..bde3cdb 100644 --- a/lib/activity_pub/headers.ex +++ b/lib/activity_pub/headers.ex @@ -85,13 +85,9 @@ defmodule ActivityPub.Headers do end def fetch_actor_key(key_id) do - request = - Req.new(url: key_id) - |> Req.Request.put_header("accept", "application/json") - - case Req.get(request) do - {:ok, result} -> - key_map = result.body["publicKey"] + case ActivityPub.fetch_actor(key_id) do + {:ok, body} -> + key_map = body["publicKey"] if key_map["id"] == key_id do [public_key | _] = diff --git a/lib/postland/activities.ex b/lib/postland/activities.ex index 5dbbb0b..14ede6c 100644 --- a/lib/postland/activities.ex +++ b/lib/postland/activities.ex @@ -1,8 +1,11 @@ defmodule Postland.Activities do use Phoenix.VerifiedRoutes, endpoint: PostlandWeb.Endpoint, router: PostlandWeb.Router + require Logger + alias Postland.Activity alias Postland.Repo + alias Postland.Follows def process_activity(params) do case record_activity(params) do @@ -20,6 +23,20 @@ defmodule Postland.Activities do |> Repo.insert() end + def cause_effects(%Activity{actor_id: actor_id, type: "Accept", data: %{"object" => %{"type" => "Follow"}}} = activity) do + case Follows.get(Postland.my_actor_id(), actor_id) do + nil -> + Logger.warning("Got accept for a follow we don't have in the db: #{actor_id}") + + {:ok, activity} + + request -> + Follows.confirm_request(request) + end + + {:ok, activity} + end + def cause_effects(%Activity{actor_id: actor_id, type: "Follow", data: %{"object" => object}} = activity) do if object == Postland.my_actor_id() do case Postland.Follows.record_inbound_request(actor_id) do diff --git a/lib/postland/follow.ex b/lib/postland/follow.ex index da31a9d..cc5f6bc 100644 --- a/lib/postland/follow.ex +++ b/lib/postland/follow.ex @@ -3,6 +3,7 @@ defmodule Postland.Follow do import Ecto.Changeset + @primary_key false schema "follows" do field :follower, :string, primary_key: true field :followee, :string, primary_key: true diff --git a/lib/postland/follows.ex b/lib/postland/follows.ex index 19abe8a..4bdf3fd 100644 --- a/lib/postland/follows.ex +++ b/lib/postland/follows.ex @@ -1,8 +1,47 @@ defmodule Postland.Follows do + use Phoenix.VerifiedRoutes, endpoint: PostlandWeb.Endpoint, router: PostlandWeb.Router + import Ecto.Query, warn: false + alias Postland.Accounts alias Postland.Follow alias Postland.Repo + alias ActivityPub.Headers + + alias Ecto.Multi + + def record_and_send_follow_request(to_actor_id) do + Multi.new() + |> Multi.run(:follow_record, fn _data, _repo -> record_outbound_request(to_actor_id) end) + |> Multi.run(:send_request, fn _data, _repo -> + send_follow_request(to_actor_id) + end) + |> Repo.transaction() + end + + def send_follow_request(to_actor_id) do + encoded_followee = Base.url_encode64(to_actor_id) + encoded_follower = Base.url_encode64(Postland.my_actor_id()) + + actor = ActivityPub.fetch_actor(to_actor_id) + inbox = Map.get(actor, "inbox") + follow_request = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => url(~p"/follows/#{encoded_followee}/#{encoded_follower}"), + "type" => "Follow", + "actor" => Postland.my_actor_id(), + "object" => to_actor_id + } + |> Jason.encode!() + + headers = Headers.signing_headers("POST", inbox, follow_request, Postland.my_actor_id(), Accounts.solo_user().private_key) + + Req.post(inbox, headers: headers, body: follow_request) + end + + def get(follower, followee) do + from(f in Follow, where: f.followee == ^followee, where: f.follower == ^follower) |> Repo.one() + end def pending_inbound_requests() do my_actor_id = Postland.my_actor_id() From 54bc877f0a2515565ec563915b96315675acfd22 Mon Sep 17 00:00:00 2001 From: Ro Date: Thu, 10 Oct 2024 02:01:30 +0000 Subject: [PATCH 3/7] fix: Fix various bugs with following --- lib/activity_pub/headers.ex | 26 +++++++++++++++++-- lib/postland/activities.ex | 4 ++- lib/postland/follows.ex | 2 +- .../controllers/actor_controller.ex | 4 ++- .../controllers/inbox_controller.ex | 6 ++--- lib/postland_web/router.ex | 2 +- 6 files changed, 35 insertions(+), 9 deletions(-) diff --git a/lib/activity_pub/headers.ex b/lib/activity_pub/headers.ex index bde3cdb..5ab22d9 100644 --- a/lib/activity_pub/headers.ex +++ b/lib/activity_pub/headers.ex @@ -10,6 +10,23 @@ defmodule ActivityPub.Headers do ] def signing_headers(method, url, body, actor_url, private_key) do + url = case url do + url when is_struct(url, URI) -> + url + url when is_binary(url) -> + URI.parse(url) + end + + private_key = case private_key do + "-----BEGIN" <> _ = key_pem -> + key_pem + |> :public_key.pem_decode() + |> hd() + |> :public_key.pem_entry_decode() + private_key -> + private_key + end + method = String.downcase("#{method}") date = DateTime.utc_now() |> Calendar.strftime(@http_date_format) host = url.host @@ -35,9 +52,11 @@ defmodule ActivityPub.Headers do end def verify(method, path, headers, actor_fetcher \\ &fetch_actor_key/1) do + dbg(headers) + {_key, signature_header} = Enum.find(headers, fn {key, _} -> key == "signature" end) - signature_kv = SignatureSplitter.split(signature_header) + signature_kv = SignatureSplitter.split(signature_header) |> dbg() key_id = find_value(signature_kv, "keyId") signature = signature_kv |> find_value("signature") |> Base.decode64!() signing_text_headers = signature_kv |> find_value("headers") |> String.split(" ") @@ -65,6 +84,7 @@ defmodule ActivityPub.Headers do value = find_value(header_pairs, header_name) "#{header_name}: #{value}\n" end) + |> String.trim() end def split_signature_header(signature_header) do @@ -79,6 +99,8 @@ defmodule ActivityPub.Headers do end defp find_value(headers, key) do + key = String.downcase(key) + Enum.find_value(headers, fn {key_candidate, value} -> String.downcase(key_candidate) == key && value end) @@ -90,7 +112,7 @@ defmodule ActivityPub.Headers do key_map = body["publicKey"] if key_map["id"] == key_id do - [public_key | _] = + public_key = :public_key.pem_decode(key_map["publicKeyPem"]) |> hd() |> :public_key.pem_entry_decode() diff --git a/lib/postland/activities.ex b/lib/postland/activities.ex index 14ede6c..2f9d8bd 100644 --- a/lib/postland/activities.ex +++ b/lib/postland/activities.ex @@ -26,6 +26,8 @@ defmodule Postland.Activities do def cause_effects(%Activity{actor_id: actor_id, type: "Accept", data: %{"object" => %{"type" => "Follow"}}} = activity) do case Follows.get(Postland.my_actor_id(), actor_id) do nil -> + # TODO: Need to handle the scenario where the we're following has an alias (/@foobar becomes /users/foobar by the time + # they Accept) Logger.warning("Got accept for a follow we don't have in the db: #{actor_id}") {:ok, activity} @@ -50,5 +52,5 @@ defmodule Postland.Activities do end end - def cause_effects(activity), do: activity + def cause_effects(activity), do: {:ok, activity} end diff --git a/lib/postland/follows.ex b/lib/postland/follows.ex index 4bdf3fd..a1ed31c 100644 --- a/lib/postland/follows.ex +++ b/lib/postland/follows.ex @@ -23,7 +23,7 @@ defmodule Postland.Follows do encoded_followee = Base.url_encode64(to_actor_id) encoded_follower = Base.url_encode64(Postland.my_actor_id()) - actor = ActivityPub.fetch_actor(to_actor_id) + {:ok, actor} = ActivityPub.fetch_actor(to_actor_id) inbox = Map.get(actor, "inbox") follow_request = %{ "@context" => "https://www.w3.org/ns/activitystreams", diff --git a/lib/postland_web/controllers/actor_controller.ex b/lib/postland_web/controllers/actor_controller.ex index 1404f7f..77d0743 100644 --- a/lib/postland_web/controllers/actor_controller.ex +++ b/lib/postland_web/controllers/actor_controller.ex @@ -2,6 +2,8 @@ defmodule PostlandWeb.ActorController do use PostlandWeb, :controller def get(conn, _params) do - render(conn, :actor, %{}) + conn + |> Plug.Conn.put_resp_header("content-type", "application/activity+json") + |> render(:actor, %{}) end end diff --git a/lib/postland_web/controllers/inbox_controller.ex b/lib/postland_web/controllers/inbox_controller.ex index 393cc74..75f5578 100644 --- a/lib/postland_web/controllers/inbox_controller.ex +++ b/lib/postland_web/controllers/inbox_controller.ex @@ -10,13 +10,13 @@ defmodule PostlandWeb.InboxController do if Headers.verify(conn.method, conn.request_path, conn.req_headers) do case Activities.process_activity(params) do {:ok, _activity} -> - render(conn, :ok) + Plug.Conn.send_resp(conn, 200, Jason.encode!(params)) error -> Logger.error(error) - render(conn, :unprocessable_entity) + Plug.Conn.send_resp(conn, 422, "unprocessable entity") end else - render(conn, :forbidden) + Plug.Conn.send_resp(conn, 403, "forbidden") end end end diff --git a/lib/postland_web/router.ex b/lib/postland_web/router.ex index 60a9c50..0d1d73c 100644 --- a/lib/postland_web/router.ex +++ b/lib/postland_web/router.ex @@ -27,7 +27,7 @@ defmodule PostlandWeb.Router do get "/.well-known/webfinger", WebfingerController, :get get "/actor", ActorController, :get - get "/inbox", InboxController, :post + post "/inbox", InboxController, :post get "/outbox", OutboxController, :get end From caf6e0b166c5c7a206655cc9966a0c3c8502a100 Mon Sep 17 00:00:00 2001 From: Ro Date: Fri, 11 Oct 2024 01:17:08 +0000 Subject: [PATCH 4/7] fix: Process Accept activities --- fly.toml | 4 ++-- lib/postland/activities.ex | 15 ++++++++++++--- lib/postland/follows.ex | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/fly.toml b/fly.toml index 78693f1..e0ca97f 100644 --- a/fly.toml +++ b/fly.toml @@ -21,9 +21,9 @@ kill_signal = 'SIGTERM' [http_service] internal_port = 8080 force_https = true - auto_stop_machines = 'stop' + auto_stop_machines = false auto_start_machines = true - min_machines_running = 0 + min_machines_running = 1 processes = ['app'] [http_service.concurrency] diff --git a/lib/postland/activities.ex b/lib/postland/activities.ex index 2f9d8bd..2929a79 100644 --- a/lib/postland/activities.ex +++ b/lib/postland/activities.ex @@ -23,11 +23,20 @@ defmodule Postland.Activities do |> Repo.insert() end - def cause_effects(%Activity{actor_id: actor_id, type: "Accept", data: %{"object" => %{"type" => "Follow"}}} = activity) do + def cause_effects(%Activity{type: "Accept", data: %{"object" => %{"type" => "Follow", "id" => follow_id}}} = activity) do + pattern = ~r|/follows/([^/]+)| + + actor_id = case Regex.run(pattern, follow_id) do + [_, encoded_actor_id] -> + dbg(encoded_actor_id) + Base.url_decode64!(encoded_actor_id, padding: false) + + _other -> + nil + end + case Follows.get(Postland.my_actor_id(), actor_id) do nil -> - # TODO: Need to handle the scenario where the we're following has an alias (/@foobar becomes /users/foobar by the time - # they Accept) Logger.warning("Got accept for a follow we don't have in the db: #{actor_id}") {:ok, activity} diff --git a/lib/postland/follows.ex b/lib/postland/follows.ex index a1ed31c..8daa6d4 100644 --- a/lib/postland/follows.ex +++ b/lib/postland/follows.ex @@ -20,8 +20,8 @@ defmodule Postland.Follows do end def send_follow_request(to_actor_id) do - encoded_followee = Base.url_encode64(to_actor_id) - encoded_follower = Base.url_encode64(Postland.my_actor_id()) + encoded_followee = Base.url_encode64(to_actor_id, padding: false) + encoded_follower = Base.url_encode64(Postland.my_actor_id(), padding: false) {:ok, actor} = ActivityPub.fetch_actor(to_actor_id) inbox = Map.get(actor, "inbox") From 4bb55c08a5c93429d3d1b56cb4627fd2de3dfac5 Mon Sep 17 00:00:00 2001 From: Ro Date: Fri, 11 Oct 2024 01:19:29 +0000 Subject: [PATCH 5/7] chore: Update TODOs --- README.md | 14 ++++++++------ lib/postland_web/controllers/webfinger_json.ex | 1 - 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c11688d..aa6f0a0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Postland -- Posting -- Deleting posts -- Following -- Unfollowing -- Liking -- Unliking +- [ ] Posting +- [ ] Timeline +- [ ] Deleting posts +- [x] Following +- [ ] Unfollowing +- [ ] Being followed +- [ ] Liking +- [ ] Unliking diff --git a/lib/postland_web/controllers/webfinger_json.ex b/lib/postland_web/controllers/webfinger_json.ex index f6e7c6e..f295dfd 100644 --- a/lib/postland_web/controllers/webfinger_json.ex +++ b/lib/postland_web/controllers/webfinger_json.ex @@ -6,7 +6,6 @@ defmodule PostlandWeb.WebfingerJSON do def render("webfinger.json", _assigns) do user = Accounts.solo_user() - # TODO: Check that the host here is correct after deploy %{ subject: "acct:#{user.username}@#{PostlandWeb.Endpoint.host()}", links: [ From 7824edbec0bb61dd7a8b7d468553d8d75e42c66b Mon Sep 17 00:00:00 2001 From: Ro Date: Fri, 11 Oct 2024 02:44:55 +0000 Subject: [PATCH 6/7] feat: Accept follow requests --- README.md | 17 +++++++- lib/activity_pub/headers.ex | 4 +- lib/postland/activities.ex | 1 - lib/postland/activity.ex | 4 +- lib/postland/follows.ex | 43 +++++++++++++++++++ ...011023003_add_timestamps_to_activities.exs | 9 ++++ 6 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 priv/repo/migrations/20241011023003_add_timestamps_to_activities.exs diff --git a/README.md b/README.md index aa6f0a0..b0fdf94 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,25 @@ # Postland +## Backend + - [ ] Posting - [ ] Timeline - [ ] Deleting posts - [x] Following - [ ] Unfollowing -- [ ] Being followed +- [x] Being followed +- [x] Accepting follows +- [ ] Liking +- [ ] Unliking + +## UX + +- [ ] Posting +- [ ] Timeline +- [ ] Deleting posts +- [x] Following +- [ ] Unfollowing +- [x] Being followed +- [ ] Accepting follows - [ ] Liking - [ ] Unliking diff --git a/lib/activity_pub/headers.ex b/lib/activity_pub/headers.ex index 5ab22d9..fd7ef0c 100644 --- a/lib/activity_pub/headers.ex +++ b/lib/activity_pub/headers.ex @@ -52,11 +52,9 @@ defmodule ActivityPub.Headers do end def verify(method, path, headers, actor_fetcher \\ &fetch_actor_key/1) do - dbg(headers) - {_key, signature_header} = Enum.find(headers, fn {key, _} -> key == "signature" end) - signature_kv = SignatureSplitter.split(signature_header) |> dbg() + signature_kv = SignatureSplitter.split(signature_header) key_id = find_value(signature_kv, "keyId") signature = signature_kv |> find_value("signature") |> Base.decode64!() signing_text_headers = signature_kv |> find_value("headers") |> String.split(" ") diff --git a/lib/postland/activities.ex b/lib/postland/activities.ex index 2929a79..e97b4b3 100644 --- a/lib/postland/activities.ex +++ b/lib/postland/activities.ex @@ -28,7 +28,6 @@ defmodule Postland.Activities do actor_id = case Regex.run(pattern, follow_id) do [_, encoded_actor_id] -> - dbg(encoded_actor_id) Base.url_decode64!(encoded_actor_id, padding: false) _other -> diff --git a/lib/postland/activity.ex b/lib/postland/activity.ex index 2da246a..08fcbff 100644 --- a/lib/postland/activity.ex +++ b/lib/postland/activity.ex @@ -12,13 +12,15 @@ defmodule Postland.Activity do def changeset(attrs) do attrs = %{ + "id" => Map.get(attrs, "id", Ecto.UUID.autogenerate()), "actor_id" => Map.get(attrs, "actor"), "type" => Map.get(attrs, "type"), "data" => attrs } %__MODULE__{} - |> cast(attrs, [:actor_id, :type, :data]) + |> cast(attrs, [:id, :actor_id, :type, :data]) + |> validate_required(:id) |> validate_required(:data) |> validate_required(:type) end diff --git a/lib/postland/follows.ex b/lib/postland/follows.ex index 8daa6d4..6903534 100644 --- a/lib/postland/follows.ex +++ b/lib/postland/follows.ex @@ -4,12 +4,55 @@ defmodule Postland.Follows do import Ecto.Query, warn: false alias Postland.Accounts + alias Postland.Activity alias Postland.Follow alias Postland.Repo alias ActivityPub.Headers alias Ecto.Multi + def record_and_send_acceptance(request) do + Multi.new() + |> Multi.run(:confirm_timestamp, fn _data, _repo -> confirm_request(request) end) + |> Multi.run(:send_acceptance, fn _data, _repo -> + send_acceptance(request) + end) + |> Repo.transaction() + end + + def send_acceptance(request) do + {:ok, actor} = ActivityPub.fetch_actor(request.follower) + inbox = Map.get(actor, "inbox") + + case get_follow_activity(request.follower) do + nil -> + {:error, "could not find Follow activity to Accept"} + + follow_activity -> + body = + %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Accept", + "actor" => Postland.my_actor_id(), + "object" => %{ + "actor" => request.follower, + "id" => Map.get(follow_activity.data, "id"), + "object" => Postland.my_actor_id(), + "type" => "Follow" + } + } + |> Jason.encode!() + + headers = Headers.signing_headers("POST", inbox, body, Postland.my_actor_id(), Accounts.solo_user().private_key) + + Req.post(inbox, headers: headers, body: body) + end + end + + def get_follow_activity(follower) do + from(a in Activity, where: a.type == "Follow", where: a.actor_id == ^follower, limit: 1, order_by: [desc: :inserted_at]) |> Repo.one() + end + def record_and_send_follow_request(to_actor_id) do Multi.new() |> Multi.run(:follow_record, fn _data, _repo -> record_outbound_request(to_actor_id) end) diff --git a/priv/repo/migrations/20241011023003_add_timestamps_to_activities.exs b/priv/repo/migrations/20241011023003_add_timestamps_to_activities.exs new file mode 100644 index 0000000..2b454d1 --- /dev/null +++ b/priv/repo/migrations/20241011023003_add_timestamps_to_activities.exs @@ -0,0 +1,9 @@ +defmodule Postland.Repo.Migrations.AddTimestampsToActivities do + use Ecto.Migration + + def change do + alter table("activities") do + timestamps() + end + end +end From 54fdaec2c9dbed5a1d5708d5e5ee48ed350cb345 Mon Sep 17 00:00:00 2001 From: Ro Date: Fri, 11 Oct 2024 02:48:53 +0000 Subject: [PATCH 7/7] fix: Missing template error in OutboxController --- lib/postland_web/controllers/outbox_controller.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/postland_web/controllers/outbox_controller.ex b/lib/postland_web/controllers/outbox_controller.ex index f8690a9..7a5695a 100644 --- a/lib/postland_web/controllers/outbox_controller.ex +++ b/lib/postland_web/controllers/outbox_controller.ex @@ -13,9 +13,9 @@ defmodule PostlandWeb.OutboxController do } if Headers.verify(conn.method, conn.request_path, conn.req_headers) do - render(conn, json) + Plug.Conn.send_resp(conn, 200, Jason.encode!(json)) else - render(conn, :forbidden) + Plug.Conn.send_resp(conn, 403, "forbidden") end end end