@@ -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 |
@@ -150,8 +150,6 @@ defmodule Pleroma.Object do | |||
def update_and_set_cache(changeset) do | |||
with {:ok, object} <- Repo.update(changeset) do | |||
set_cache(object) | |||
else | |||
e -> e | |||
end | |||
end | |||
@@ -139,7 +139,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
# 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
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), | |||
@@ -309,42 +309,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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() | |||
params = | |||
params | |||
|> Map.drop(["id"]) | |||
|> Map.put("actor", actor) | |||
|> Transmogrifier.fix_addressing() | |||
) | |||
when user_nickname == nickname do | |||
actor = user.ap_id() | |||
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) | |||
@@ -166,6 +166,7 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
@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 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
|> List.delete(actor.ap_id) | |||
|> List.delete(object_actor.follower_address) | |||
data = %{ | |||
%{ | |||
"type" => "Like", | |||
"actor" => ap_id, | |||
"object" => id, | |||
@@ -323,38 +305,49 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"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 | |||
end | |||
data = | |||
Map.merge( | |||
object.data, | |||
%{"#{property}_count" => length(element), "#{property}s" => element} | |||
) | |||
def update_likes_in_object(likes, object) do | |||
update_element_in_object("like", likes, object) | |||
object | |||
|> Changeset.change(data: data) | |||
|> Object.update_and_set_cache() | |||
end | |||
@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 | |||
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 | |||
[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 | |||
likes = if is_list(object.data["likes"]), do: object.data["likes"], else: [] | |||
object | |||
|> fetch_likes() | |||
|> List.delete(actor) | |||
|> update_likes_in_object(object) | |||
end | |||
with likes <- likes |> List.delete(actor) do | |||
update_likes_in_object(likes, object) | |||
defp update_likes_in_object(likes, object) do | |||
update_element_in_object("like", likes, object) | |||
end | |||
defp fetch_likes(object) do | |||
if is_list(object.data["likes"]) do | |||
object.data["likes"] | |||
else | |||
[] | |||
end | |||
end | |||
@@ -405,7 +398,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
%User{ap_id: followed_id} = _followed, | |||
activity_id | |||
) do | |||
data = %{ | |||
%{ | |||
"type" => "Follow", | |||
"actor" => follower_id, | |||
"to" => [followed_id], | |||
@@ -413,10 +406,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"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 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
activity_id, | |||
false | |||
) do | |||
data = %{ | |||
%{ | |||
"type" => "Announce", | |||
"actor" => ap_id, | |||
"object" => id, | |||
@@ -486,8 +476,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"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 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
activity_id, | |||
true | |||
) do | |||
data = %{ | |||
%{ | |||
"type" => "Announce", | |||
"actor" => ap_id, | |||
"object" => id, | |||
@@ -504,8 +493,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"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 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
%Activity{data: %{"context" => context}} = activity, | |||
activity_id | |||
) do | |||
data = %{ | |||
%{ | |||
"type" => "Undo", | |||
"actor" => ap_id, | |||
"object" => activity.data, | |||
@@ -524,8 +512,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"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 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
%Activity{data: %{"context" => context}} = activity, | |||
activity_id | |||
) do | |||
data = %{ | |||
%{ | |||
"type" => "Undo", | |||
"actor" => ap_id, | |||
"object" => activity.data, | |||
@@ -541,8 +528,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 |
@@ -207,13 +207,15 @@ defmodule Pleroma.Factory 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 | |||
@@ -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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest 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) | |||
@@ -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 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest 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 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest 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 |