Compare commits

...

2 commits

Author SHA1 Message Date
Ro
ac462cfe14
feat: Unfollow 2024-11-26 19:08:51 -06:00
Ro
e89cc78274
feat: Support unfollowing 2024-11-26 18:04:08 -06:00
11 changed files with 136 additions and 15 deletions

View file

@ -21,7 +21,7 @@
- [x] Sending follow request
- [x] View following list
- [x] Withdrawing follow request
- [ ] Unfollowing
- [x] Unfollowing
- [ ] Proactively check the outbox of newly-accepted follows
## Being Followed
@ -73,7 +73,7 @@
## Protocol Support
- [x] Check that signature header (digest) matches digest of body contents
- [ ] Check the domain of the public key against the domain of the object being CRUDed
- [x] Check the domain of the public key against the domain of the object being CRUDed
## Testing

View file

@ -38,9 +38,12 @@ defmodule ActivityPub do
|> Req.Request.put_header("accept", "application/json")
case Req.get(request) do
{:ok, result} ->
{:ok, %{status: 200} = result} ->
{:ok, result.body}
{:ok, %{status: 404}} ->
nil
error ->
error
end

View file

@ -77,6 +77,9 @@ defmodule ActivityPub.Headers do
{:ok, public_key} ->
:public_key.verify(to_verify, :sha256, signature, public_key)
nil ->
false
error ->
error
end

View file

@ -1,14 +1,18 @@
defmodule Postland.Actors do
def inbox(actor_id) do
Map.get(actor(actor_id), "inbox")
Map.get(actor(actor_id) || %{}, "inbox")
end
def actor(actor_id) do
result =
Cachex.fetch(:main_cache, "actor:#{actor_id}", fn _key ->
{:ok, actor} = ActivityPub.fetch_actor(actor_id)
case ActivityPub.fetch_actor(actor_id) do
{:ok, actor} ->
{:commit, actor, expire: :timer.seconds(300)}
{:commit, actor, expire: :timer.seconds(300)}
nil ->
{:commit, nil, expire: :timer.seconds(300)}
end
end)
case result do

View file

@ -52,14 +52,30 @@ defmodule Postland.Follows do
def record_and_send_withdrawl(request) do
Multi.new()
|> Multi.delete(:delete, request)
|> Multi.run(:send_acceptance, fn _data, _repo ->
|> Multi.run(:send_withdrawl, fn _data, _repo ->
send_withdrawl(request)
end)
|> Repo.transaction()
end
def record_and_unfollow(follow) do
Multi.new()
|> Multi.delete(:delete, follow)
|> Multi.run(:send_delete_follow, fn _data, _repo ->
send_delete_follow(follow)
end)
|> Repo.transaction()
end
def send_withdrawl(request) do
{:ok, actor} = ActivityPub.fetch_actor(request.followee)
request.followee
|> ActivityPub.fetch_actor()
|> do_send_withdrawl(request)
end
defp do_send_withdrawl(nil, _), do: {:ok, nil}
defp do_send_withdrawl({:ok, actor}, request) do
inbox = Map.get(actor, "inbox")
body =
@ -84,7 +100,14 @@ defmodule Postland.Follows do
end
def send_acceptance(request) do
{:ok, actor} = ActivityPub.fetch_actor(request.follower)
request.follower
|> ActivityPub.fetch_actor()
|> do_send_acceptance(request)
end
defp do_send_acceptance(nil, _), do: {:ok, nil}
defp do_send_acceptance({:ok, actor}, request) do
inbox = Map.get(actor, "inbox")
case get_follow_activity(request.follower) do
@ -180,6 +203,34 @@ defmodule Postland.Follows do
Req.post(inbox, headers: headers, body: follow_request)
end
def send_delete_follow(follow) do
encoded_followee = Base.url_encode64(follow.followee, padding: false)
encoded_follower = Base.url_encode64(follow.follower, padding: false)
{:ok, actor} = ActivityPub.fetch_actor(follow.followee)
inbox = Map.get(actor, "inbox")
follow_request =
%{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Delete",
"actor" => Postland.my_actor_id(),
"object" => url(~p"/follows/#{encoded_followee}/#{encoded_follower}")
}
|> 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()

View file

@ -26,7 +26,7 @@ defmodule PostlandWeb.CoreComponents do
assigns = Map.put(assigns, :host, host)
~H"""
<div class="flex items-start space-x-4">
<div class="flex items-start space-x-4" data-status={@status}>
<div class="flex-shrink-0 relative">
<img
class="h-16 w-16 rounded-full drop-shadow-lg"
@ -52,6 +52,14 @@ defmodule PostlandWeb.CoreComponents do
</p>
</div>
<div class="flex-grow text-right">
<.button
:if={@status == :follower_pending}
phx-click="confirm"
phx-value-dom-id={@dom_id}
phx-value-id={@account["id"]}
>
Accept Request
</.button>
<.button
:if={@status == :following_pending}
phx-click="withdraw"
@ -60,6 +68,14 @@ defmodule PostlandWeb.CoreComponents do
>
Withdraw Request
</.button>
<.button
:if={@status == :following_confirmed}
phx-click="unfollow"
phx-value-dom-id={@dom_id}
phx-value-id={@account["id"]}
>
Unfollow
</.button>
</div>
</div>
</div>
@ -177,7 +193,7 @@ defmodule PostlandWeb.CoreComponents do
end
def post_card(assigns) do
author = Postland.Timeline.attribution(assigns.post) || %{}
author = Postland.Timeline.attribution(assigns.post)
%{host: host} =
case author do
@ -201,7 +217,7 @@ defmodule PostlandWeb.CoreComponents do
|> Map.put(:cw, cw)
~H"""
<div class="flex items-start space-x-4 w-full">
<div :if={@author} class="flex items-start space-x-4 w-full">
<div class="flex-shrink-0">
<img
class="inline-block h-16 w-16 rounded-full drop-shadow-lg"

View file

@ -22,5 +22,6 @@ defmodule PostlandWeb.InboxController do
send_resp(conn, 403, "forbidden")
end
|> halt()
end
end

View file

@ -36,9 +36,34 @@ defmodule PostlandWeb.FollowersLive do
account: Actors.actor(follow.follower)
}
end)
|> Enum.reject(&is_nil(&1.account))
account_count = Enum.count(followers)
{:ok, socket |> stream(:accounts, followers) |> assign(count: account_count)}
end
def handle_event("confirm", %{"id" => id, "dom-id" => dom_id}, socket) do
request = Follows.get(id, Postland.my_actor_id())
socket =
case Follows.record_and_send_acceptance(request) do
{:ok, _} ->
request = Follows.get(Postland.my_actor_id(), id)
acct = %{
id: request.follower,
confirmed: !!request.onfirmed_at,
account: Actors.actor(request.follower)
}
socket
|> stream_insert(:accounts, acct)
_ ->
put_flash(socket, :error, "An unexpected error occurred.")
end
{:noreply, socket}
end
end

View file

@ -33,6 +33,7 @@ defmodule PostlandWeb.FollowingLive do
account: Actors.actor(follow.followee)
}
end)
|> Enum.reject(&is_nil(&1.account))
account_count = Enum.count(follows)
@ -60,4 +61,21 @@ defmodule PostlandWeb.FollowingLive do
{:noreply, socket}
end
def handle_event("unfollow", %{"id" => id, "dom-id" => dom_id}, socket) do
request = Follows.get(Postland.my_actor_id(), id)
socket =
case Follows.record_and_unfollow(request) do
{:ok, _} ->
socket
|> stream_delete_by_dom_id(:accounts, dom_id)
|> assign(:count, socket.assigns.count - 1)
_ ->
put_flash(socket, :error, "An unexpected error occurred.")
end
{:noreply, socket}
end
end

View file

@ -57,7 +57,7 @@ defmodule Postland.MixProject do
{:gettext, "~> 0.20"},
{:jason, "~> 1.2"},
{:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.2"},
{:bandit, "~> 1.6.0"},
{:req, "~> 0.5.6"},
{:stream_data, "~> 1.1.1", only: [:test]},
{:earmark, "~> 1.4.47"},

View file

@ -1,5 +1,5 @@
%{
"bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
"bandit": {:hex, :bandit, "1.6.0", "9cb6c67c27cecab2d0c93968cb957fa8decccb7275193c8bf33f97397b3ac25d", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "fd2491e564a7c5e11ff8496ebf530c342c742452c59de17ac0fb1f814a0ab01a"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
"cachex": {:hex, :cachex, "4.0.2", "120f9c27b0a453c7cb3319d9dc6c61c050a480e5299fc1f8bded1e2e334992ab", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "4f4890122bddd979f6c217d5e300d0c0d3eb858a976cbe1f65a94e6322bc5825"},
"castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
@ -48,7 +48,7 @@
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
"thousand_island": {:hex, :thousand_island, "1.3.6", "835a626a8a6f6a1e681b63e1132a8427e87ce443aaf4888fbf63b2df77539b97", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0ed8798084c8c49a223840b20598b022e4eb8c9f390fb6701864c307fc9aa2cd"},
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},