From 7894049b6cdf8f2fcc2e1fb141e06ee414edb59d Mon Sep 17 00:00:00 2001 From: Ro Date: Sun, 13 Oct 2024 20:02:59 -0500 Subject: [PATCH] feat: Add rudimentary follow UI --- lib/activity_pub.ex | 34 +++++--- lib/activity_pub/webfinger.ex | 22 ++++++ lib/postland/follows.ex | 54 +++++++++++-- .../controllers/export_controller.ex | 2 +- lib/postland_web/live/other_profile_live.ex | 77 +++++++++++++++++++ lib/postland_web/router.ex | 1 + 6 files changed, 173 insertions(+), 17 deletions(-) create mode 100644 lib/activity_pub/webfinger.ex create mode 100644 lib/postland_web/live/other_profile_live.ex diff --git a/lib/activity_pub.ex b/lib/activity_pub.ex index ef64556..d3b7639 100644 --- a/lib/activity_pub.ex +++ b/lib/activity_pub.ex @@ -1,15 +1,31 @@ defmodule ActivityPub do + def get(url, actor_url, private_key) do + headers = ActivityPub.Headers.signing_headers("GET", url, "", actor_url, private_key) + + Req.new(url: url) + |> Req.Request.put_header("accept", "application/activity+json") + |> Req.Request.put_headers(headers) + |> Req.get() + |> case do + {:ok, response} -> + {:ok, response.body} + + other -> + other + end + end + def fetch_actor(actor_id) do - request = - Req.new(url: actor_id) - |> Req.Request.put_header("accept", "application/json") + request = + Req.new(url: actor_id) + |> Req.Request.put_header("accept", "application/json") - case Req.get(request) do - {:ok, result} -> - {:ok, result.body} + case Req.get(request) do + {:ok, result} -> + {:ok, result.body} - error -> - error - end + error -> + error + end end end diff --git a/lib/activity_pub/webfinger.ex b/lib/activity_pub/webfinger.ex new file mode 100644 index 0000000..613a49a --- /dev/null +++ b/lib/activity_pub/webfinger.ex @@ -0,0 +1,22 @@ +defmodule ActivityPub.Webfinger do + def lookup_resource(acct_handle) do + [_handle, domain] = String.split(acct_handle, "@") + + uri = %URI{ + scheme: "https", + authority: domain, + host: domain, + port: 443, + path: "/.well-known/webfinger", + query: "resource=acct:#{acct_handle}" + } + + case Req.get(uri) do + {:ok, response} -> + {:ok, response.body} + + other -> + other + end + end +end diff --git a/lib/postland/follows.ex b/lib/postland/follows.ex index 6903534..404539b 100644 --- a/lib/postland/follows.ex +++ b/lib/postland/follows.ex @@ -43,14 +43,27 @@ defmodule Postland.Follows do } |> Jason.encode!() - headers = Headers.signing_headers("POST", inbox, body, Postland.my_actor_id(), Accounts.solo_user().private_key) + 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() + 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 @@ -60,6 +73,19 @@ defmodule Postland.Follows do send_follow_request(to_actor_id) end) |> Repo.transaction() + |> case do + {:ok, %{follow_record: follow_record}} = result -> + Phoenix.PubSub.broadcast( + Postland.PubSub, + "follows:#{to_actor_id}", + {:update, follow_record} + ) + + result + + other -> + other + end end def send_follow_request(to_actor_id) do @@ -68,22 +94,32 @@ defmodule Postland.Follows do {:ok, actor} = ActivityPub.fetch_actor(to_actor_id) inbox = Map.get(actor, "inbox") - follow_request = %{ + + 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!() + } + |> Jason.encode!() - headers = Headers.signing_headers("POST", inbox, follow_request, Postland.my_actor_id(), Accounts.solo_user().private_key) + 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() + from(f in Follow, where: f.followee == ^followee, where: f.follower == ^follower) + |> Repo.one() end def pending_inbound_requests() do @@ -109,5 +145,9 @@ defmodule Postland.Follows do request |> Follow.confirm_changeset() |> Repo.update() + |> case do + {:ok, follow} -> + Phoenix.PubSub.broadcast(Postland.PubSub, "follows:#{follow.followee}", {:update, follow}) + end end end diff --git a/lib/postland_web/controllers/export_controller.ex b/lib/postland_web/controllers/export_controller.ex index 1ab1acb..4552c86 100644 --- a/lib/postland_web/controllers/export_controller.ex +++ b/lib/postland_web/controllers/export_controller.ex @@ -9,7 +9,7 @@ defmodule PostlandWeb.ExportController do files = [filename, "#{filename}-shm", "#{filename}-wal"] |> Enum.map(&String.to_charlist/1) zip_path = Path.join([File.cwd!(), "priv", "tmp", "db-export.zip"]) - :zip.create(zip_path, files, cwd: dirname) + :zip.create(String.to_charlist(zip_path), files, cwd: dirname) send_download(conn, {:file, zip_path}, filename: "db-export.zip") end diff --git a/lib/postland_web/live/other_profile_live.ex b/lib/postland_web/live/other_profile_live.ex new file mode 100644 index 0000000..e4b8b10 --- /dev/null +++ b/lib/postland_web/live/other_profile_live.ex @@ -0,0 +1,77 @@ +defmodule PostlandWeb.OtherProfileLive do + use PostlandWeb, :live_view + + alias Postland.Follows + + def render(assigns) do + ~H""" +

<%= Map.get(@profile, "name") %> (<%= @acct %>)

+ <%= case @follow do %> + <% nil -> %> + <.button phx-click="follow">Follow + <% %{confirmed_at: nil} -> %> + <.button disabled>Pending + <% _ -> %> + <.button phx-click="unfollow">Unfollow + <% end %> + <.list> + <:item :for={attachment <- Map.get(@profile, "attachment")} title={attachment["name"]}> + <%= attachment["value"] %> + + + """ + end + + def mount(params, _session, socket) do + {:ok, resource} = ActivityPub.Webfinger.lookup_resource(Map.get(params, "acct")) + + %{"href" => json_profile} = + resource + |> Map.get("links") + |> Enum.find(fn %{"rel" => rel} = link -> + rel == "self" && String.contains?(Map.get(link, "type"), "json") + end) + + {:ok, profile} = + ActivityPub.get( + json_profile, + Postland.my_actor_id(), + Postland.Accounts.solo_user().private_key + ) + + follow = Follows.get(Postland.my_actor_id(), json_profile) + + if Phoenix.LiveView.connected?(socket) do + Phoenix.PubSub.subscribe(Postland.PubSub, "follows:#{json_profile}") + end + + {:ok, + assign(socket, + acct: Map.get(params, "acct"), + actor_id: json_profile, + follow: follow, + profile: profile + )} + end + + def handle_event("follow", _unsigned_params, socket) do + actor_id = socket.assigns.actor_id + + case Follows.record_and_send_follow_request(actor_id) do + {:ok, _} -> + {:noreply, + socket + |> put_flash(:success, "Follow request sent.") + |> assign(follow: Follows.get(Postland.my_actor_id(), actor_id))} + + _other -> + {:noreply, + socket + |> put_flash(:error, "An unexpected error has occurred.")} + end + end + + def handle_info({:update, follow}, socket) do + {:noreply, assign(socket, follow: follow)} + end +end diff --git a/lib/postland_web/router.ex b/lib/postland_web/router.ex index 4829799..8acd2f9 100644 --- a/lib/postland_web/router.ex +++ b/lib/postland_web/router.ex @@ -80,6 +80,7 @@ defmodule PostlandWeb.Router do live_session :require_authenticated_user, on_mount: [{PostlandWeb.UserAuth, :ensure_authenticated}] do live "/users/settings", UserSettingsLive, :edit + live "/@:acct", OtherProfileLive, :show end end