diff --git a/lib/vonbraun/activity_pub/handler/accept.ex b/lib/vonbraun/activity_pub/handler/accept.ex index 10da07a..bbf23d3 100644 --- a/lib/vonbraun/activity_pub/handler/accept.ex +++ b/lib/vonbraun/activity_pub/handler/accept.ex @@ -2,8 +2,8 @@ defmodule Vonbraun.ActivityPub.Handler.Accept do @behaviour Vonbraun.ActivityPub.HandlerBehaviour require Logger + alias Vonbraun.Util alias Vonbraun.Ecto.Schema.Actor - alias Vonbraun.ActivityPub.Object @verb "Accept" @@ -41,7 +41,7 @@ defmodule Vonbraun.ActivityPub.Handler.Accept do %{} ) do with {:actor, {:ok, follow_actor_id}} <- {:actor, extract_follow_object_actor(object)}, - {:match, true} <- {:match, follow_actor_id == Object.my_id()}, + {:match, true} <- {:match, follow_actor_id == Util.my_id()}, {:asked, {:ok, %Actor{:blocked => nil, :following_state => "accepted"}}} <- {:asked, Actor.mark_pending_follow(actor_id, "accepted", force: true)} do Logger.info("Now following: #{actor_id}") diff --git a/lib/vonbraun/activity_pub/handler/follow.ex b/lib/vonbraun/activity_pub/handler/follow.ex index 161bddc..fc4ad99 100644 --- a/lib/vonbraun/activity_pub/handler/follow.ex +++ b/lib/vonbraun/activity_pub/handler/follow.ex @@ -5,6 +5,7 @@ defmodule Vonbraun.ActivityPub.Handler.Follow do alias Vonbraun.ActivityPubReq alias Vonbraun.Ecto.Schema.Actor alias Vonbraun.ActivityPub.Object + alias Vonbraun.Util @verb "Follow" @@ -20,7 +21,7 @@ defmodule Vonbraun.ActivityPub.Handler.Follow do actor = %{} ) when is_binary(follow_requester_id) and is_binary(follow_target) and is_binary(activity_id) do - with {:valid_target, true} <- {:valid_target, Object.my_id() == follow_target}, + with {:valid_target, true} <- {:valid_target, Util.my_id() == follow_target}, {:add, {:ok, %Actor{:blocked => nil, :follows_me_state => follows_me_state}}} when not is_nil(follows_me_state) <- {:add, Actor.maybe_add_follower(follow_requester_id)} do diff --git a/lib/vonbraun/activity_pub/handler/reject.ex b/lib/vonbraun/activity_pub/handler/reject.ex index 2356780..c236c41 100644 --- a/lib/vonbraun/activity_pub/handler/reject.ex +++ b/lib/vonbraun/activity_pub/handler/reject.ex @@ -3,7 +3,7 @@ defmodule Vonbraun.ActivityPub.Handler.Reject do require Logger alias Vonbraun.Ecto.Schema.Actor - alias Vonbraun.ActivityPub.Object + alias Vonbraun.Util import Vonbraun.ActivityPub.Handler.Accept, only: [extract_follow_object_actor: 1] @verb "Reject" @@ -16,7 +16,7 @@ defmodule Vonbraun.ActivityPub.Handler.Reject do "object" => object = %{"type" => "Follow"} }) do with {:actor, {:ok, follow_actor_id}} <- {:actor, extract_follow_object_actor(object)}, - {:match, true} <- {:match, follow_actor_id == Object.my_id()}, + {:match, true} <- {:match, follow_actor_id == Util.my_id()}, {:asked, {:ok, %Actor{:blocked => nil, :following_state => "accepted"}}} <- {:asked, Actor.mark_pending_follow(actor_id, "rejected", force: true)} do Logger.info("Now following: #{actor_id}") diff --git a/lib/vonbraun/activity_pub/handler/undo.ex b/lib/vonbraun/activity_pub/handler/undo.ex index 6e4f591..a56f2e4 100644 --- a/lib/vonbraun/activity_pub/handler/undo.ex +++ b/lib/vonbraun/activity_pub/handler/undo.ex @@ -3,7 +3,7 @@ defmodule Vonbraun.ActivityPub.Handler.Undo do require Logger alias Vonbraun.Ecto.Schema.Actor - alias Vonbraun.ActivityPub.Object + alias Vonbraun.Util import Vonbraun.ActivityPub.Handler.Accept, only: [extract_follow_object_actor: 1] @verb "Undo" @@ -14,7 +14,7 @@ defmodule Vonbraun.ActivityPub.Handler.Undo do def handle(%{"type" => @verb, "actor" => actor_id, "object" => object = %{"type" => "Follow"}}) do with {:actor, {:ok, follow_actor_id}} <- {:actor, extract_follow_object_actor(object)}, - {:match, true} <- {:match, follow_actor_id == Object.my_id()}, + {:match, true} <- {:match, follow_actor_id == Util.my_id()}, {:asked, {:ok, %Actor{:following_state => nil}}} <- {:asked, Actor.remove_follower(actor_id)} do {:ok, :removed_follower} diff --git a/lib/vonbraun/activity_pub/object.ex b/lib/vonbraun/activity_pub/object.ex index 52ab918..96c5738 100644 --- a/lib/vonbraun/activity_pub/object.ex +++ b/lib/vonbraun/activity_pub/object.ex @@ -2,17 +2,7 @@ defmodule Vonbraun.ActivityPub.Object do @context "https://www.w3.org/ns/activitystreams" @public_to "https://www.w3.org/ns/activitystreams#Public" - @spec my_id() :: String.t() - def my_id() do - domain = Application.fetch_env!(:vonbraun, :domain) - nickname = Application.fetch_env!(:vonbraun, :nickname) - "https://#{domain}/users/#{nickname}" - end - - @spec my_key_id() :: String.t() - def my_key_id() do - "#{my_id()}#main-key" - end + import Vonbraun.Util, only: [my_id: 0] def add_context(object = %{"@context" => context}) when is_list(context) or is_binary(context) do @@ -112,6 +102,9 @@ defmodule Vonbraun.ActivityPub.Object do to: to_follow_id ) + @spec accept_follow_activity(binary(), binary(), :accept | :reject) :: %{ + optional(<<_::16, _::_*8>>) => any() + } def accept_follow_activity(followee_id, activity_id, type \\ :accept) when is_binary(followee_id) and is_binary(activity_id) and type in [:accept, :reject] do activity_type = @@ -132,4 +125,102 @@ defmodule Vonbraun.ActivityPub.Object do activity(activity_type, accept_activity_id, object, to: followee_id) end + + def is_maybe_activitypub?(%{"@context" => @context}), do: true + + def is_maybe_activitypub?(%{"@context" => context_list}) when is_list(context_list) do + Enum.reduce_while(context_list, false, fn + @context, _ -> + {:halt, true} + + context = %{}, _ -> + if Enum.find(context, fn item -> elem(item, 1) == @context end) do + {:halt, true} + else + {:cont, false} + end + + _, _ -> + {:cont, false} + end) + end + + def is_maybe_activitypub?(_), do: false + + @spec fix_ld_properties(any()) :: + {:error, :child_id | :child_type | :id | :list | :type | :unknown} + | {:ok, list(list() | map()) | map()} + @doc """ + If an object has type or id convert them to "@type" and "@id" respectively. + This is so that AP objects conform to JSON-LD for our purposes even though + they don't have to in the outside world. If the properties don't exist, they + are not added. Also fixes the properties of an "object" value, if present. + """ + def fix_ld_properties(object = %{}) do + has_unadorned_id? = Map.has_key?(object, "id") + has_id? = Map.has_key?(object, "@id") + has_unadored_type? = Map.has_key?(object, "type") + has_type? = Map.has_key?(object, "@type") + + cond do + has_unadorned_id? && has_id? -> + {:error, :id} + + has_unadored_type? && has_type? -> + {:error, :type} + + true -> + object = + if has_unadorned_id? do + id = Map.get(object, "id") + object |> Map.delete("id") |> Map.put("@id", id) + else + object + end + + if has_unadored_type? do + type = Map.get(object, "type") + object |> Map.delete("type") |> Map.put("@type", type) + else + object + end + + # I am just assuming here the object is a JSON-LD object, not sure yet + # if this is a valid assumption. + with {:object, child_object = %{}} <- {:object, Map.get(object, "object")}, + {:valid, {:ok, child_object}} <- {:valid, fix_ld_properties(child_object)} do + {:ok, Map.put(object, "object", child_object)} + else + {:object, child_object} when is_nil(child_object) or is_binary(child_object) -> + {:ok, Map.put(object, "object", child_object)} + + {:valid, {:error, :type}} -> + {:error, :child_type} + + {:valid, {:error, :id}} -> + {:error, :child_id} + end + end + end + + # I forgot why I wrote this lol. + def fix_ld_properties(list) when is_list(list) do + fixed_objects = + Enum.reduce_while(list, {:ok, []}, fn object, objects -> + case fix_ld_properties(object) do + {:ok, fixed_object} -> {:cont, [fixed_object | objects]} + {:error, _error} -> {:halt, :error} + end + end) + + case fixed_objects do + :error -> + {:error, :list} + + _ -> + {:ok, Enum.reverse(fixed_objects)} + end + end + + def fix_ld_properties(_), do: {:error, :unknown} end diff --git a/lib/vonbraun/activitypub_req.ex b/lib/vonbraun/activitypub_req.ex index 2595e6f..176f592 100644 --- a/lib/vonbraun/activitypub_req.ex +++ b/lib/vonbraun/activitypub_req.ex @@ -1,8 +1,8 @@ defmodule Vonbraun.ActivityPubReq do require Logger alias Vonbraun.Cache + alias Vonbraun.Util alias Vonbraun.HTTPSignature - alias Vonbraun.ActivityPub.Object @ttl :timer.minutes(1) @@ -19,7 +19,7 @@ defmodule Vonbraun.ActivityPubReq do path end - headers = HTTPSignature.add_get_signature(headers, Object.my_key_id(), target) + headers = HTTPSignature.add_get_signature(headers, Util.my_key_id(), target) Req.get(url, headers: headers) end @@ -39,7 +39,7 @@ defmodule Vonbraun.ActivityPubReq do path end - headers = HTTPSignature.add_post_signature(headers, Object.my_key_id(), target, body) + headers = HTTPSignature.add_post_signature(headers, Util.my_key_id(), target, body) Logger.debug("POST payload is: `#{body}`") diff --git a/lib/vonbraun/util.ex b/lib/vonbraun/util.ex new file mode 100644 index 0000000..f40eccc --- /dev/null +++ b/lib/vonbraun/util.ex @@ -0,0 +1,13 @@ +defmodule Vonbraun.Util do + @spec my_id() :: String.t() + def my_id() do + domain = Application.fetch_env!(:vonbraun, :domain) + nickname = Application.fetch_env!(:vonbraun, :nickname) + "https://#{domain}/users/#{nickname}" + end + + @spec my_key_id() :: String.t() + def my_key_id() do + "#{my_id()}#main-key" + end +end