From 00abe099cd85b03b880908eef1e469e656d56365 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Tue, 27 Aug 2019 16:21:03 +0300 Subject: [PATCH 1/2] added tests for ActivityPub.like\unlike --- lib/pleroma/activity/queries.ex | 49 ++++++ lib/pleroma/object.ex | 2 - lib/pleroma/web/activity_pub/activity_pub.ex | 9 +- .../activity_pub/activity_pub_controller.ex | 59 +++---- lib/pleroma/web/activity_pub/utils.ex | 150 ++++++++---------- test/support/factory.ex | 16 +- test/web/activity_pub/activity_pub_test.exs | 44 +++++ test/web/activity_pub/utils_test.exs | 102 ++++++++++++ 8 files changed, 304 insertions(+), 127 deletions(-) create mode 100644 lib/pleroma/activity/queries.ex diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex new file mode 100644 index 000000000..aa5b29566 --- /dev/null +++ b/lib/pleroma/activity/queries.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Activity.Queries do + @moduledoc """ + Contains queries for Activity. + """ + + import Ecto.Query, only: [from: 2] + + @type query :: Ecto.Queryable.t() | Activity.t() + + alias Pleroma.Activity + + @spec by_actor(query, String.t()) :: query + def by_actor(query \\ Activity, actor) do + from( + activity in query, + where: fragment("(?)->>'actor' = ?", activity.data, ^actor) + ) + end + + @spec by_object_id(query, String.t()) :: query + def by_object_id(query \\ Activity, object_id) do + from(activity in query, + where: + fragment( + "coalesce((?)->'object'->>'id', (?)->>'object') = ?", + activity.data, + activity.data, + ^object_id + ) + ) + end + + @spec by_type(query, String.t()) :: query + def by_type(query \\ Activity, activity_type) do + from( + activity in query, + where: fragment("(?)->>'type' = ?", activity.data, ^activity_type) + ) + end + + @spec limit(query, pos_integer()) :: query + def limit(query \\ Activity, limit) do + from(activity in query, limit: ^limit) + end +end diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index c8d339c19..d58eb7f7d 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -150,8 +150,6 @@ def set_cache(%Object{data: %{"id" => ap_id}} = object) do def update_and_set_cache(changeset) do with {:ok, object} <- Repo.update(changeset) do set_cache(object) - else - e -> e end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 172c952d4..eeb826814 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -139,7 +139,7 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when # Splice in the child object if we have one. activity = - if !is_nil(object) do + if not is_nil(object) do Map.put(activity, :object, object) else activity @@ -331,12 +331,7 @@ def like( end end - def unlike( - %User{} = actor, - %Object{} = object, - activity_id \\ nil, - local \\ true - ) do + def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object), unlike_data <- make_unlike_data(actor, like_activity, activity_id), {:ok, unlike_activity} <- insert(unlike_data, local), diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index ed801a7ae..5c73fc9f3 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -309,42 +309,43 @@ def handle_user_activity(_, _) do end def update_outbox( - %{assigns: %{user: user}} = conn, + %{assigns: %{user: %User{nickname: user_nickname} = user}} = conn, %{"nickname" => nickname} = params - ) do - if nickname == user.nickname do - actor = user.ap_id() + ) + when user_nickname == nickname do + actor = user.ap_id() - params = - params - |> Map.drop(["id"]) - |> Map.put("actor", actor) - |> Transmogrifier.fix_addressing() - - with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do - conn - |> put_status(:created) - |> put_resp_header("location", activity.data["id"]) - |> json(activity.data) - else - {:error, message} -> - conn - |> put_status(:bad_request) - |> json(message) - end - else - err = - dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}", - nickname: nickname, - as_nickname: user.nickname - ) + params = + params + |> Map.drop(["id"]) + |> Map.put("actor", actor) + |> Transmogrifier.fix_addressing() + with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do conn - |> put_status(:forbidden) - |> json(err) + |> put_status(:created) + |> put_resp_header("location", activity.data["id"]) + |> json(activity.data) + else + {:error, message} -> + conn + |> put_status(:bad_request) + |> json(message) end end + def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do + err = + dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}", + nickname: nickname, + as_nickname: user.nickname + ) + + conn + |> put_status(:forbidden) + |> json(err) + end + def errors(conn, {:error, :not_found}) do conn |> put_status(:not_found) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 1c3058658..c9c0c3763 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -166,6 +166,7 @@ def create_context(context) do @doc """ Enqueues an activity for federation if it's local """ + @spec maybe_federate(any()) :: :ok def maybe_federate(%Activity{local: true} = activity) do if Pleroma.Config.get!([:instance, :federating]) do priority = @@ -256,46 +257,27 @@ def insert_full_object(map), do: {:ok, map, nil} @doc """ Returns an existing like if a user already liked an object """ + @spec get_existing_like(String.t(), map()) :: Activity.t() | nil def get_existing_like(actor, %{data: %{"id" => id}}) do - query = - from( - activity in Activity, - where: fragment("(?)->>'actor' = ?", activity.data, ^actor), - # this is to use the index - where: - fragment( - "coalesce((?)->'object'->>'id', (?)->>'object') = ?", - activity.data, - activity.data, - ^id - ), - where: fragment("(?)->>'type' = 'Like'", activity.data) - ) - - Repo.one(query) + actor + |> Activity.Queries.by_actor() + |> Activity.Queries.by_object_id(id) + |> Activity.Queries.by_type("Like") + |> Activity.Queries.limit(1) + |> Repo.one() end @doc """ Returns like activities targeting an object """ def get_object_likes(%{data: %{"id" => id}}) do - query = - from( - activity in Activity, - # this is to use the index - where: - fragment( - "coalesce((?)->'object'->>'id', (?)->>'object') = ?", - activity.data, - activity.data, - ^id - ), - where: fragment("(?)->>'type' = 'Like'", activity.data) - ) - - Repo.all(query) + id + |> Activity.Queries.by_object_id() + |> Activity.Queries.by_type("Like") + |> Repo.all() end + @spec make_like_data(User.t(), map(), String.t()) :: map() def make_like_data( %User{ap_id: ap_id} = actor, %{data: %{"actor" => object_actor_id, "id" => id}} = object, @@ -315,7 +297,7 @@ def make_like_data( |> List.delete(actor.ap_id) |> List.delete(object_actor.follower_address) - data = %{ + %{ "type" => "Like", "actor" => ap_id, "object" => id, @@ -323,38 +305,49 @@ def make_like_data( "cc" => cc, "context" => object.data["context"] } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end + @spec update_element_in_object(String.t(), list(any), Object.t()) :: + {:ok, Object.t()} | {:error, Ecto.Changeset.t()} def update_element_in_object(property, element, object) do - with new_data <- - object.data - |> Map.put("#{property}_count", length(element)) - |> Map.put("#{property}s", element), - changeset <- Changeset.change(object, data: new_data), - {:ok, object} <- Object.update_and_set_cache(changeset) do - {:ok, object} - end + data = + Map.merge( + object.data, + %{"#{property}_count" => length(element), "#{property}s" => element} + ) + + object + |> Changeset.change(data: data) + |> Object.update_and_set_cache() end - def update_likes_in_object(likes, object) do + @spec add_like_to_object(Activity.t(), Object.t()) :: + {:ok, Object.t()} | {:error, Ecto.Changeset.t()} + def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do + [actor | fetch_likes(object)] + |> Enum.uniq() + |> update_likes_in_object(object) + end + + @spec remove_like_from_object(Activity.t(), Object.t()) :: + {:ok, Object.t()} | {:error, Ecto.Changeset.t()} + def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do + object + |> fetch_likes() + |> List.delete(actor) + |> update_likes_in_object(object) + end + + defp update_likes_in_object(likes, object) do update_element_in_object("like", likes, object) end - def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do - likes = if is_list(object.data["likes"]), do: object.data["likes"], else: [] - - with likes <- [actor | likes] |> Enum.uniq() do - update_likes_in_object(likes, object) - end - end - - def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do - likes = if is_list(object.data["likes"]), do: object.data["likes"], else: [] - - with likes <- likes |> List.delete(actor) do - update_likes_in_object(likes, object) + defp fetch_likes(object) do + if is_list(object.data["likes"]) do + object.data["likes"] + else + [] end end @@ -405,7 +398,7 @@ def make_follow_data( %User{ap_id: followed_id} = _followed, activity_id ) do - data = %{ + %{ "type" => "Follow", "actor" => follower_id, "to" => [followed_id], @@ -413,10 +406,7 @@ def make_follow_data( "object" => followed_id, "state" => "pending" } - - data = if activity_id, do: Map.put(data, "id", activity_id), else: data - - data + |> maybe_put("id", activity_id) end def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do @@ -478,7 +468,7 @@ def make_announce_data( activity_id, false ) do - data = %{ + %{ "type" => "Announce", "actor" => ap_id, "object" => id, @@ -486,8 +476,7 @@ def make_announce_data( "cc" => [], "context" => object.data["context"] } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end def make_announce_data( @@ -496,7 +485,7 @@ def make_announce_data( activity_id, true ) do - data = %{ + %{ "type" => "Announce", "actor" => ap_id, "object" => id, @@ -504,8 +493,7 @@ def make_announce_data( "cc" => [Pleroma.Constants.as_public()], "context" => object.data["context"] } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end @doc """ @@ -516,7 +504,7 @@ def make_unannounce_data( %Activity{data: %{"context" => context}} = activity, activity_id ) do - data = %{ + %{ "type" => "Undo", "actor" => ap_id, "object" => activity.data, @@ -524,8 +512,7 @@ def make_unannounce_data( "cc" => [Pleroma.Constants.as_public()], "context" => context } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end def make_unlike_data( @@ -533,7 +520,7 @@ def make_unlike_data( %Activity{data: %{"context" => context}} = activity, activity_id ) do - data = %{ + %{ "type" => "Undo", "actor" => ap_id, "object" => activity.data, @@ -541,8 +528,7 @@ def make_unlike_data( "cc" => [Pleroma.Constants.as_public()], "context" => context } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end def add_announce_to_object( @@ -573,14 +559,13 @@ def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do #### Unfollow-related helpers def make_unfollow_data(follower, followed, follow_activity, activity_id) do - data = %{ + %{ "type" => "Undo", "actor" => follower.ap_id, "to" => [followed.ap_id], "object" => follow_activity.data } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end #### Block-related helpers @@ -610,25 +595,23 @@ def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do end def make_block_data(blocker, blocked, activity_id) do - data = %{ + %{ "type" => "Block", "actor" => blocker.ap_id, "to" => [blocked.ap_id], "object" => blocked.ap_id } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end def make_unblock_data(blocker, blocked, block_activity, activity_id) do - data = %{ + %{ "type" => "Undo", "actor" => blocker.ap_id, "to" => [blocked.ap_id], "object" => block_activity.data } - - if activity_id, do: Map.put(data, "id", activity_id), else: data + |> maybe_put("id", activity_id) end #### Create-related helpers @@ -799,4 +782,7 @@ def get_existing_votes(actor, %{data: %{"id" => id}}) do Repo.all(query) end + + defp maybe_put(map, _key, nil), do: map + defp maybe_put(map, key, value), do: Map.put(map, key, value) end diff --git a/test/support/factory.ex b/test/support/factory.ex index 62d1de717..719115003 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -207,13 +207,15 @@ def like_activity_factory(attrs \\ %{}) do object = Object.normalize(note_activity) user = insert(:user) - data = %{ - "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), - "actor" => user.ap_id, - "type" => "Like", - "object" => object.data["id"], - "published_at" => DateTime.utc_now() |> DateTime.to_iso8601() - } + data = + %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "actor" => user.ap_id, + "type" => "Like", + "object" => object.data["id"], + "published_at" => DateTime.utc_now() |> DateTime.to_iso8601() + } + |> Map.merge(attrs[:data_attrs] || %{}) %Pleroma.Activity{ data: data diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 1515f4eb6..f72b44aed 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -21,6 +21,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do :ok end + clear_config([:instance, :federating]) + describe "streaming out participations" do test "it streams them out" do user = insert(:user) @@ -676,6 +678,29 @@ test "returns reblogs for users for whom reblogs have not been muted" do end describe "like an object" do + test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do + Pleroma.Config.put([:instance, :federating], true) + note_activity = insert(:note_activity) + assert object_activity = Object.normalize(note_activity) + + user = insert(:user) + + {:ok, like_activity, _object} = ActivityPub.like(user, object_activity) + assert called(Pleroma.Web.Federator.publish(like_activity, 5)) + end + + test "returns exist activity if object already liked" do + note_activity = insert(:note_activity) + assert object_activity = Object.normalize(note_activity) + + user = insert(:user) + + {:ok, like_activity, _object} = ActivityPub.like(user, object_activity) + + {:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity) + assert like_activity == like_activity_exist + end + test "adds a like activity to the db" do note_activity = insert(:note_activity) assert object = Object.normalize(note_activity) @@ -706,6 +731,25 @@ test "adds a like activity to the db" do end describe "unliking" do + test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do + Pleroma.Config.put([:instance, :federating], true) + + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + user = insert(:user) + + {:ok, object} = ActivityPub.unlike(user, object) + refute called(Pleroma.Web.Federator.publish()) + + {:ok, _like_activity, object} = ActivityPub.like(user, object) + assert object.data["like_count"] == 1 + + {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object) + assert object.data["like_count"] == 0 + + assert called(Pleroma.Web.Federator.publish(unlike_activity, 5)) + end + test "unliking a previously liked object" do note_activity = insert(:note_activity) object = Object.normalize(note_activity) diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs index ca5f057a7..eb429b2c4 100644 --- a/test/web/activity_pub/utils_test.exs +++ b/test/web/activity_pub/utils_test.exs @@ -14,6 +14,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do import Pleroma.Factory + require Pleroma.Constants + describe "fetch the latest Follow" do test "fetches the latest Follow activity" do %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) @@ -87,6 +89,32 @@ test "works with an object that has only IR tags" do end end + describe "make_unlike_data/3" do + test "returns data for unlike activity" do + user = insert(:user) + like_activity = insert(:like_activity, data_attrs: %{"context" => "test context"}) + + assert Utils.make_unlike_data(user, like_activity, nil) == %{ + "type" => "Undo", + "actor" => user.ap_id, + "object" => like_activity.data, + "to" => [user.follower_address, like_activity.data["actor"]], + "cc" => [Pleroma.Constants.as_public()], + "context" => like_activity.data["context"] + } + + assert Utils.make_unlike_data(user, like_activity, "9mJEZK0tky1w2xD2vY") == %{ + "type" => "Undo", + "actor" => user.ap_id, + "object" => like_activity.data, + "to" => [user.follower_address, like_activity.data["actor"]], + "cc" => [Pleroma.Constants.as_public()], + "context" => like_activity.data["context"], + "id" => "9mJEZK0tky1w2xD2vY" + } + end + end + describe "make_like_data" do setup do user = insert(:user) @@ -299,4 +327,78 @@ test "updates the state of the given follow activity" do assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject" end end + + describe "update_element_in_object/3" do + test "updates likes" do + user = insert(:user) + activity = insert(:note_activity) + object = Object.normalize(activity) + + assert {:ok, updated_object} = + Utils.update_element_in_object( + "like", + [user.ap_id], + object + ) + + assert updated_object.data["likes"] == [user.ap_id] + assert updated_object.data["like_count"] == 1 + end + end + + describe "add_like_to_object/2" do + test "add actor to likes" do + user = insert(:user) + user2 = insert(:user) + object = insert(:note) + + assert {:ok, updated_object} = + Utils.add_like_to_object( + %Activity{data: %{"actor" => user.ap_id}}, + object + ) + + assert updated_object.data["likes"] == [user.ap_id] + assert updated_object.data["like_count"] == 1 + + assert {:ok, updated_object2} = + Utils.add_like_to_object( + %Activity{data: %{"actor" => user2.ap_id}}, + updated_object + ) + + assert updated_object2.data["likes"] == [user2.ap_id, user.ap_id] + assert updated_object2.data["like_count"] == 2 + end + end + + describe "remove_like_from_object/2" do + test "removes ap_id from likes" do + user = insert(:user) + user2 = insert(:user) + object = insert(:note, data: %{"likes" => [user.ap_id, user2.ap_id], "like_count" => 2}) + + assert {:ok, updated_object} = + Utils.remove_like_from_object( + %Activity{data: %{"actor" => user.ap_id}}, + object + ) + + assert updated_object.data["likes"] == [user2.ap_id] + assert updated_object.data["like_count"] == 1 + end + end + + describe "get_existing_like/2" do + test "fetches existing like" do + note_activity = insert(:note_activity) + assert object = Object.normalize(note_activity) + + user = insert(:user) + refute Utils.get_existing_like(user.ap_id, object) + {:ok, like_activity, _object} = ActivityPub.like(user, object) + + assert ^like_activity = Utils.get_existing_like(user.ap_id, object) + end + end end From ffcd742aa0797b5bb872e58c1e605f22c8652250 Mon Sep 17 00:00:00 2001 From: Maksim Date: Tue, 27 Aug 2019 17:37:19 +0000 Subject: [PATCH 2/2] Apply suggestion to lib/pleroma/web/activity_pub/activity_pub_controller.ex --- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 5c73fc9f3..08bf1c752 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -309,10 +309,9 @@ def handle_user_activity(_, _) do end def update_outbox( - %{assigns: %{user: %User{nickname: user_nickname} = user}} = conn, + %{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{"nickname" => nickname} = params - ) - when user_nickname == nickname do + ) do actor = user.ap_id() params =