added tests for ActivityPub.like\unlike

This commit is contained in:
Maksim Pechnikov 2019-08-27 16:21:03 +03:00
parent ba5e14be05
commit 00abe099cd
8 changed files with 304 additions and 127 deletions

View File

@ -0,0 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -150,8 +150,6 @@ defmodule Pleroma.Object do
def update_and_set_cache(changeset) do def update_and_set_cache(changeset) do
with {:ok, object} <- Repo.update(changeset) do with {:ok, object} <- Repo.update(changeset) do
set_cache(object) set_cache(object)
else
e -> e
end end
end end

View File

@ -139,7 +139,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# Splice in the child object if we have one. # Splice in the child object if we have one.
activity = activity =
if !is_nil(object) do if not is_nil(object) do
Map.put(activity, :object, object) Map.put(activity, :object, object)
else else
activity activity
@ -331,12 +331,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end end
end end
def unlike( def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object), with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
unlike_data <- make_unlike_data(actor, like_activity, activity_id), unlike_data <- make_unlike_data(actor, like_activity, activity_id),
{:ok, unlike_activity} <- insert(unlike_data, local), {:ok, unlike_activity} <- insert(unlike_data, local),

View File

@ -309,42 +309,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end end
def update_outbox( def update_outbox(
%{assigns: %{user: user}} = conn, %{assigns: %{user: %User{nickname: user_nickname} = user}} = conn,
%{"nickname" => nickname} = params %{"nickname" => nickname} = params
) do )
if nickname == user.nickname do when user_nickname == nickname do
actor = user.ap_id() actor = user.ap_id()
params = params =
params params
|> Map.drop(["id"]) |> Map.drop(["id"])
|> Map.put("actor", actor) |> Map.put("actor", actor)
|> Transmogrifier.fix_addressing() |> 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
)
with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
conn conn
|> put_status(:forbidden) |> put_status(:created)
|> json(err) |> put_resp_header("location", activity.data["id"])
|> json(activity.data)
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(message)
end end
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 def errors(conn, {:error, :not_found}) do
conn conn
|> put_status(:not_found) |> put_status(:not_found)

View File

@ -166,6 +166,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """ @doc """
Enqueues an activity for federation if it's local Enqueues an activity for federation if it's local
""" """
@spec maybe_federate(any()) :: :ok
def maybe_federate(%Activity{local: true} = activity) do def maybe_federate(%Activity{local: true} = activity) do
if Pleroma.Config.get!([:instance, :federating]) do if Pleroma.Config.get!([:instance, :federating]) do
priority = priority =
@ -256,46 +257,27 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """ @doc """
Returns an existing like if a user already liked an object 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 def get_existing_like(actor, %{data: %{"id" => id}}) do
query = actor
from( |> Activity.Queries.by_actor()
activity in Activity, |> Activity.Queries.by_object_id(id)
where: fragment("(?)->>'actor' = ?", activity.data, ^actor), |> Activity.Queries.by_type("Like")
# this is to use the index |> Activity.Queries.limit(1)
where: |> Repo.one()
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^id
),
where: fragment("(?)->>'type' = 'Like'", activity.data)
)
Repo.one(query)
end end
@doc """ @doc """
Returns like activities targeting an object Returns like activities targeting an object
""" """
def get_object_likes(%{data: %{"id" => id}}) do def get_object_likes(%{data: %{"id" => id}}) do
query = id
from( |> Activity.Queries.by_object_id()
activity in Activity, |> Activity.Queries.by_type("Like")
# this is to use the index |> Repo.all()
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^id
),
where: fragment("(?)->>'type' = 'Like'", activity.data)
)
Repo.all(query)
end end
@spec make_like_data(User.t(), map(), String.t()) :: map()
def make_like_data( def make_like_data(
%User{ap_id: ap_id} = actor, %User{ap_id: ap_id} = actor,
%{data: %{"actor" => object_actor_id, "id" => id}} = object, %{data: %{"actor" => object_actor_id, "id" => id}} = object,
@ -315,7 +297,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> List.delete(actor.ap_id) |> List.delete(actor.ap_id)
|> List.delete(object_actor.follower_address) |> List.delete(object_actor.follower_address)
data = %{ %{
"type" => "Like", "type" => "Like",
"actor" => ap_id, "actor" => ap_id,
"object" => id, "object" => id,
@ -323,38 +305,49 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"cc" => cc, "cc" => cc,
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end 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 def update_element_in_object(property, element, object) do
with new_data <- data =
object.data Map.merge(
|> Map.put("#{property}_count", length(element)) object.data,
|> Map.put("#{property}s", element), %{"#{property}_count" => length(element), "#{property}s" => element}
changeset <- Changeset.change(object, data: new_data), )
{:ok, object} <- Object.update_and_set_cache(changeset) do
{:ok, object} object
end |> Changeset.change(data: data)
|> Object.update_and_set_cache()
end 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) update_element_in_object("like", likes, object)
end end
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do defp fetch_likes(object) do
likes = if is_list(object.data["likes"]), do: object.data["likes"], else: [] if is_list(object.data["likes"]) do
object.data["likes"]
with likes <- [actor | likes] |> Enum.uniq() do else
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)
end end
end end
@ -405,7 +398,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
%User{ap_id: followed_id} = _followed, %User{ap_id: followed_id} = _followed,
activity_id activity_id
) do ) do
data = %{ %{
"type" => "Follow", "type" => "Follow",
"actor" => follower_id, "actor" => follower_id,
"to" => [followed_id], "to" => [followed_id],
@ -413,10 +406,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"object" => followed_id, "object" => followed_id,
"state" => "pending" "state" => "pending"
} }
|> maybe_put("id", activity_id)
data = if activity_id, do: Map.put(data, "id", activity_id), else: data
data
end end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
@ -478,7 +468,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
activity_id, activity_id,
false false
) do ) do
data = %{ %{
"type" => "Announce", "type" => "Announce",
"actor" => ap_id, "actor" => ap_id,
"object" => id, "object" => id,
@ -486,8 +476,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"cc" => [], "cc" => [],
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
def make_announce_data( def make_announce_data(
@ -496,7 +485,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
activity_id, activity_id,
true true
) do ) do
data = %{ %{
"type" => "Announce", "type" => "Announce",
"actor" => ap_id, "actor" => ap_id,
"object" => id, "object" => id,
@ -504,8 +493,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
@doc """ @doc """
@ -516,7 +504,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
%Activity{data: %{"context" => context}} = activity, %Activity{data: %{"context" => context}} = activity,
activity_id activity_id
) do ) do
data = %{ %{
"type" => "Undo", "type" => "Undo",
"actor" => ap_id, "actor" => ap_id,
"object" => activity.data, "object" => activity.data,
@ -524,8 +512,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => context "context" => context
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
def make_unlike_data( def make_unlike_data(
@ -533,7 +520,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
%Activity{data: %{"context" => context}} = activity, %Activity{data: %{"context" => context}} = activity,
activity_id activity_id
) do ) do
data = %{ %{
"type" => "Undo", "type" => "Undo",
"actor" => ap_id, "actor" => ap_id,
"object" => activity.data, "object" => activity.data,
@ -541,8 +528,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => context "context" => context
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
def add_announce_to_object( def add_announce_to_object(
@ -573,14 +559,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do
#### Unfollow-related helpers #### Unfollow-related helpers
def make_unfollow_data(follower, followed, follow_activity, activity_id) do def make_unfollow_data(follower, followed, follow_activity, activity_id) do
data = %{ %{
"type" => "Undo", "type" => "Undo",
"actor" => follower.ap_id, "actor" => follower.ap_id,
"to" => [followed.ap_id], "to" => [followed.ap_id],
"object" => follow_activity.data "object" => follow_activity.data
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
#### Block-related helpers #### Block-related helpers
@ -610,25 +595,23 @@ defmodule Pleroma.Web.ActivityPub.Utils do
end end
def make_block_data(blocker, blocked, activity_id) do def make_block_data(blocker, blocked, activity_id) do
data = %{ %{
"type" => "Block", "type" => "Block",
"actor" => blocker.ap_id, "actor" => blocker.ap_id,
"to" => [blocked.ap_id], "to" => [blocked.ap_id],
"object" => blocked.ap_id "object" => blocked.ap_id
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
def make_unblock_data(blocker, blocked, block_activity, activity_id) do def make_unblock_data(blocker, blocked, block_activity, activity_id) do
data = %{ %{
"type" => "Undo", "type" => "Undo",
"actor" => blocker.ap_id, "actor" => blocker.ap_id,
"to" => [blocked.ap_id], "to" => [blocked.ap_id],
"object" => block_activity.data "object" => block_activity.data
} }
|> maybe_put("id", activity_id)
if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
#### Create-related helpers #### Create-related helpers
@ -799,4 +782,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Repo.all(query) Repo.all(query)
end end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end end

View File

@ -207,13 +207,15 @@ defmodule Pleroma.Factory do
object = Object.normalize(note_activity) object = Object.normalize(note_activity)
user = insert(:user) user = insert(:user)
data = %{ data =
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), %{
"actor" => user.ap_id, "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Like", "actor" => user.ap_id,
"object" => object.data["id"], "type" => "Like",
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601() "object" => object.data["id"],
} "published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
|> Map.merge(attrs[:data_attrs] || %{})
%Pleroma.Activity{ %Pleroma.Activity{
data: data data: data

View File

@ -21,6 +21,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
:ok :ok
end end
clear_config([:instance, :federating])
describe "streaming out participations" do describe "streaming out participations" do
test "it streams them out" do test "it streams them out" do
user = insert(:user) user = insert(:user)
@ -676,6 +678,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end end
describe "like an object" do 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 test "adds a like activity to the db" do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity) assert object = Object.normalize(note_activity)
@ -706,6 +731,25 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end end
describe "unliking" do 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 test "unliking a previously liked object" do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
object = Object.normalize(note_activity) object = Object.normalize(note_activity)

View File

@ -14,6 +14,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
import Pleroma.Factory import Pleroma.Factory
require Pleroma.Constants
describe "fetch the latest Follow" do describe "fetch the latest Follow" do
test "fetches the latest Follow activity" do test "fetches the latest Follow activity" do
%Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
@ -87,6 +89,32 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
end end
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 describe "make_like_data" do
setup do setup do
user = insert(:user) user = insert(:user)
@ -299,4 +327,78 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject" assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
end end
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 end