diff --git a/lib/clacks/activitypub.ex b/lib/clacks/activitypub.ex index 88a35cf..de96d2b 100644 --- a/lib/clacks/activitypub.ex +++ b/lib/clacks/activitypub.ex @@ -132,6 +132,19 @@ defmodule Clacks.ActivityPub do } end + @spec undo_follow(actor :: String.t(), follow_activity :: map()) :: map() + def undo_follow(actor, %{"object" => followee} = follow_activity) do + %{ + "@context" => @context, + "type" => "Undo", + "id" => activity_id(Ecto.UUID.generate()), + "actor" => actor, + "object" => follow_activity, + "to" => [followee], + "cc" => [] + } + end + @spec object_id(id :: String.t()) :: String.t() def object_id(id) do url = Application.get_env(:clacks, ClacksWeb.Endpoint)[:url] diff --git a/lib/clacks/activitypub/federator.ex b/lib/clacks/activitypub/federator.ex index 15a71d7..1a0cbb3 100644 --- a/lib/clacks/activitypub/federator.ex +++ b/lib/clacks/activitypub/federator.ex @@ -67,7 +67,7 @@ defmodule Clacks.ActivityPub.Federator do @public in activity["to"] or @public in activity["cc"] -> shared_inbox_for(actor) - actor.data["followers"] in activity["to"] or actor.data["followers"] in activity["cc"] -> + actor["followers"] in activity["to"] or actor["followers"] in activity["cc"] -> shared_inbox_for(actor) true -> diff --git a/lib/clacks/inbox.ex b/lib/clacks/inbox.ex index 25e788d..a85272c 100644 --- a/lib/clacks/inbox.ex +++ b/lib/clacks/inbox.ex @@ -73,7 +73,6 @@ defmodule Clacks.Inbox do } = activity ) do followee = Actor.get_by_ap_id(followee_id) - follower = Actor.get_by_ap_id(follower_id) store_activity(activity) @@ -106,6 +105,8 @@ defmodule Clacks.Inbox do # as a fallback, just store the activity def handle(activity) do + Logger.debug("Unhandled activity: #{inspect(activity)}") + case store_activity(activity) do {:error, changeset} -> Logger.error("Could not store activity: #{inspect(changeset)}") diff --git a/lib/clacks/object.ex b/lib/clacks/object.ex index a47c8a9..7e452f2 100644 --- a/lib/clacks/object.ex +++ b/lib/clacks/object.ex @@ -54,7 +54,7 @@ defmodule Clacks.Object do end @spec fetch(ap_id :: String.t(), synthesize_create :: boolean(), return :: :object | :activity) :: - t() | nil + t() | Activity.t() | nil def fetch(ap_id, synthesize_create \\ true, return \\ :object) do case Clacks.ActivityPub.Fetcher.fetch_object(ap_id) do nil -> diff --git a/lib/clacks_web/controllers/frontend_controller.ex b/lib/clacks_web/controllers/frontend_controller.ex index fd8c1db..00d356e 100644 --- a/lib/clacks_web/controllers/frontend_controller.ex +++ b/lib/clacks_web/controllers/frontend_controller.ex @@ -174,6 +174,40 @@ defmodule ClacksWeb.FrontendController do end end + def unfollow(conn, %{"id" => id}) do + current_user = conn.assigns[:user] |> Repo.preload(:actor) + + case Repo.get(Actor, id) do + nil -> + resp(conn, 404, "Not Found") + + followee -> + unless current_user.actor.ap_id in followee.followers do + redirect(conn, to: ClacksWeb.FrontendView.local_actor_link(followee)) + else + new_followers = List.delete(followee.followers, current_user.actor.ap_id) + changeset = Actor.changeset(followee, %{followers: new_followers}) + {:ok, followee} = Repo.update(changeset) + + follow_activity = follow_activity(current_user.actor, followee) + + changeset = + Activity.changeset(follow_activity, %{ + data: %{follow_activity.data | "state" => "unfollowed"} + }) + + {:ok, follow_activity} = Repo.update(changeset) + + undo_follow = ActivityPub.undo_follow(current_user.actor.ap_id, follow_activity.data) + ActivityPub.Helper.save_and_federate(undo_follow, current_user.actor) + + conn + |> put_flash(:info, "Unfollowed") + |> redirect(to: ClacksWeb.FrontendView.local_actor_link(followee)) + end + end + end + def search(conn, %{"q" => q}) when is_binary(q) do current_user = conn.assigns[:user] @@ -258,23 +292,31 @@ defmodule ClacksWeb.FrontendController do ActivityPub.note(current_user.actor.ap_id, content) end - @spec following_state(follower :: Actor.t(), followee :: Actor.t()) :: boolean() - defp following_state(follower, followee) do + @spec follow_activity(follower :: Actor.t(), followee :: Actor.t()) :: map() + def follow_activity(follower, followee) do + # todo: get latest query = Activity |> where([a], fragment("?->>'type'", a.data) == "Follow") |> where([a], fragment("?->>'actor'", a.data) == ^follower.ap_id) |> where([a], fragment("?->>'object'", a.data) == ^followee.ap_id) + |> order_by(desc: :inserted_at) + |> limit(1) - case Repo.one(query) do - nil -> - :not_following + Repo.one(query) + end + @spec following_state(follower :: Actor.t(), followee :: Actor.t()) :: boolean() + defp following_state(follower, followee) do + case follow_activity(follower, followee) do %Activity{data: %{"state" => "pending"}} -> :pending %Activity{data: %{"state" => "accepted"}} -> :following + + _ -> + :not_following end end end