alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger
alias Pleroma.Workers.BackgroundWorker
def increase_poll_votes_if_vote(_create_data), do: :noop
+ @spec common_pipeline(map(), keyword()) :: {:ok, Activity.t(), keyword()} | {:error, any()}
+ def common_pipeline(object, meta) do
+ with {_, {:ok, validated_object, meta}} <-
+ {:validate_object, ObjectValidator.validate(object, meta)},
+ {_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
+ {_, {:ok, %Activity{} = activity, meta}} <-
+ {:persist_object, persist(mrfd_object, meta)},
+ {_, {:ok, %Activity{} = activity, meta}} <-
+ {:execute_side_effects, SideEffects.handle(activity, meta)} do
+ {:ok, activity, meta}
+ else
+ e -> {:error, e}
+ end
+ end
+
+ # TODO rewrite in with style
+ @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
+ def persist(object, meta) do
+ local = Keyword.get(meta, :local)
+ {recipients, _, _} = get_recipients(object)
+
+ {:ok, activity} =
+ Repo.insert(%Activity{
+ data: object,
+ local: local,
+ recipients: recipients,
+ actor: object["actor"]
+ })
+
+ {:ok, activity, meta}
+ end
+
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake),
{_, true} <- {:remote_limit_error, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map),
+ # ???
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
:ok <- Containment.contain_child(map),
{:ok, map, object} <- insert_full_object(map) do
--- /dev/null
+defmodule Pleroma.Web.ActivityPub.Builder do
+ @moduledoc """
+ This module builds the objects. Meant to be used for creating local objects.
+
+ This module encodes our addressing policies and general shape of our objects.
+ """
+
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.User
+ alias Pleroma.Object
+
+ @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
+ def like(actor, object) do
+ object_actor = User.get_cached_by_ap_id(object.data["actor"])
+
+ # Address the actor of the object, and our actor's follower collection if the post is public.
+ to =
+ if Visibility.is_public?(object) do
+ [actor.follower_address, object.data["actor"]]
+ else
+ [object.data["actor"]]
+ end
+
+ # CC everyone who's been addressed in the object, except ourself and the object actor's
+ # follower collection
+ cc =
+ (object.data["to"] ++ (object.data["cc"] || []))
+ |> List.delete(actor.ap_id)
+ |> List.delete(object_actor.follower_address)
+
+ {:ok,
+ %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => actor.ap_id,
+ "type" => "Like",
+ "object" => object.data["id"],
+ "to" => to,
+ "cc" => cc,
+ "context" => object.data["context"]
+ }, []}
+ end
+end
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidator do
+ @moduledoc """
+ This module is responsible for validating an object (which can be an activity)
+ and checking if it is both well formed and also compatible with our view of
+ the system.
+ """
+
+ alias Pleroma.User
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.Utils
+
+ def validate_id(object, meta) do
+ with {_, true} <- {:id_presence, Map.has_key?(object, "id")} do
+ {:ok, object, meta}
+ else
+ e -> {:error, e}
+ end
+ end
+
+ def validate_actor(object, meta) do
+ with {_, %User{}} <- {:actor_validation, User.get_cached_by_ap_id(object["actor"])} do
+ {:ok, object, meta}
+ else
+ e -> {:error, e}
+ end
+ end
+
+ def common_validations(object, meta) do
+ with {_, {:ok, object, meta}} <- {:validate_id, validate_id(object, meta)},
+ {_, {:ok, object, meta}} <- {:validate_actor, validate_actor(object, meta)} do
+ {:ok, object, meta}
+ else
+ e -> {:error, e}
+ end
+ end
+
+ @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
+ def validate(object, meta)
+
+ def validate(%{"type" => "Like"} = object, meta) do
+ with {:ok, object, meta} <- common_validations(object, meta),
+ {_, %Object{} = liked_object} <- {:find_liked_object, Object.normalize(object["object"])},
+ {_, nil} <- {:existing_like, Utils.get_existing_like(object["actor"], liked_object)} do
+ {:ok, object, meta}
+ else
+ e -> {:error, e}
+ end
+ end
+
+ def validate(object, meta) do
+ common_validations(object, meta)
+ end
+end
--- /dev/null
+defmodule Pleroma.Web.ActivityPub.SideEffects do
+ @moduledoc """
+ This module looks at an inserted object and executes the side effects that it
+ implies. For example, a `Like` activity will increase the like count on the
+ liked object, a `Follow` activity will add the user to the follower
+ collection, and so on.
+ """
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Object
+ alias Pleroma.Notification
+
+ def handle(object, meta \\ [])
+
+ # Tasks this handles:
+ # - Add like to object
+ # - Set up notification
+ def handle(%{data: %{"type" => "Like"}} = object, meta) do
+ liked_object = Object.get_by_ap_id(object.data["object"])
+ Utils.add_like_to_object(object, liked_object)
+ Notification.create_notifications(object)
+ {:ok, object, meta}
+ end
+
+ # Nothing to do
+ def handle(object, meta) do
+ {:ok, object, meta}
+ end
+end
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
import Pleroma.Web.CommonAPI.Utils
require Pleroma.Constants
+ require Logger
def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
end
end
- def favorite(id_or_ap_id, user) do
- with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- object <- Object.normalize(activity),
- nil <- Utils.get_existing_like(user.ap_id, object) do
- ActivityPub.like(user, object)
+ @spec favorite(User.t(), binary()) :: {:ok, Activity.t()} | {:error, any()}
+ def favorite(%User{} = user, id) do
+ with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
+ {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
+ {_, {:ok, %Activity{} = activity, _meta}} <-
+ {:common_pipeline,
+ ActivityPub.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
+ {:ok, activity}
else
- _ -> {:error, dgettext("errors", "Could not favorite")}
+ e ->
+ Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
+ {:error, dgettext("errors", "Could not favorite")}
end
end
+ # def favorite(id_or_ap_id, user) do
+ # with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
+ # object <- Object.normalize(activity),
+ # nil <- Utils.get_existing_like(user.ap_id, object) do
+ # ActivityPub.like(user, object)
+ # else
+ # _ -> {:error, dgettext("errors", "Could not favorite")}
+ # end
+ # end
+
def unfavorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
object = Object.normalize(activity)
end
@doc "POST /api/v1/statuses/:id/favourite"
- def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
+ with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
+ %Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
"status" => "hey @#{other_user.nickname}!"
})
- {:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, third_user)
+ {:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id)
assert other_user not in Notification.get_notified_from_activity(activity_two)
end
assert Enum.empty?(Notification.for_user(user))
- {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+ {:ok, _} = CommonAPI.favorite(other_user, activity.id)
assert length(Notification.for_user(user)) == 1
assert Enum.empty?(Notification.for_user(user))
- {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+ {:ok, _} = CommonAPI.favorite(other_user, activity.id)
assert length(Notification.for_user(user)) == 1
assert Enum.empty?(Notification.for_user(user))
- {:error, _} = CommonAPI.favorite(activity.id, other_user)
+ {:error, _} = CommonAPI.favorite(other_user, activity.id)
assert Enum.empty?(Notification.for_user(user))
end
user = insert(:user)
activity = Activity.get_create_by_object_ap_id(object.data["id"])
- {:ok, _activity, object} = CommonAPI.favorite(activity.id, user)
+ {:ok, activity} = CommonAPI.favorite(user, activity.id)
+ object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 1
{:ok, %{id: id, object: object}} = CommonAPI.post(user, %{"status" => "test"})
{:ok, %{object: object2}} = CommonAPI.post(user, %{"status" => "test test"})
- CommonAPI.favorite(id, user2)
+ CommonAPI.favorite(user2, id)
likes = %{
"first" =>
object_two = insert(:note, user: follower)
activity_two = insert(:note_activity, user: follower, note: object_two)
- {:ok, like, _} = CommonAPI.favorite(activity_two.id, user)
- {:ok, like_two, _} = CommonAPI.favorite(activity.id, follower)
+ {:ok, like} = CommonAPI.favorite(user, activity_two.id)
+ {:ok, like_two} = CommonAPI.favorite(follower, activity.id)
{:ok, repeat, _} = CommonAPI.repeat(activity_two.id, user)
{:ok, job} = User.delete(user)
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+
+ describe "likes" do
+ test "it is well formed" do
+ _required_fields = [
+ "id",
+ "actor",
+ "object"
+ ]
+
+ _user = insert(:user)
+ end
+ end
+end
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
+ use Pleroma.DataCase
+ alias Pleroma.Object
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.SideEffects
+
+ import Pleroma.Factory
+ describe "like objects" do
+ setup do
+ user = insert(:user)
+ {:ok, post} = CommonAPI.post(user, %{"status" => "hey"})
+
+ {:ok, like_data, _meta} = Builder.like(user, post.object)
+ {:ok, like, _meta} = ActivityPub.persist(like_data, [])
+
+ %{like: like, user: user}
+ end
+
+ test "add the like to the original object", %{like: like, user: user} do
+ {:ok, like, _} = SideEffects.handle(like)
+ object = Object.get_by_ap_id(like.data["object"])
+ assert object.data["like_count"] == 1
+ assert user.ap_id in object.data["likes"]
+ end
+ end
+end
user = insert(:user)
- {:ok, activity, _} = CommonAPI.favorite(referent_activity.id, user)
+ {:ok, activity} = CommonAPI.favorite(user, referent_activity.id)
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"] == "http://gs.example.org:4040/index.php/notice/29"
object = Object.normalize(note)
user = insert(:user)
- {:ok, like_activity, _} = CommonAPI.favorite(note.id, user)
+ {:ok, like_activity} = CommonAPI.favorite(user, note.id)
result = ObjectView.render("object.json", %{object: like_activity})
user = insert(:user)
other_user = insert(:user)
- {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+ {:ok, post_activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
- {:ok, %Activity{}, _} = CommonAPI.favorite(activity.id, user)
+ {:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id)
+ assert data["type"] == "Like"
+ assert data["actor"] == user.ap_id
+ assert data["object"] == post_activity.data["object"]
end
test "retweeting a status twice returns an error" do
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
- {:ok, %Activity{}, _object} = CommonAPI.favorite(activity.id, user)
- {:error, _} = CommonAPI.favorite(activity.id, user)
+ {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
+ {:error, _} = CommonAPI.favorite(user, activity.id)
end
end
{:ok, mention_activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"})
{:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
- {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, other_user)
+ {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id)
{:ok, reblog_activity, _} = CommonAPI.repeat(create_activity.id, other_user)
{:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
- CommonAPI.favorite(activity.id, user2)
+ {:ok, _} = CommonAPI.favorite(user2, activity.id)
{:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
{:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
{:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
activity = insert(:note_activity)
user = insert(:user)
- {:ok, _, _} = CommonAPI.favorite(activity.id, user)
+ {:ok, _} = CommonAPI.favorite(user, activity.id)
conn =
conn
test "returns users who have favorited the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
- {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+ {:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
- {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+ {:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
test "does not fail on an unauthenticated request", %{conn: conn, activity: activity} do
other_user = insert(:user)
- {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+ {:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
"visibility" => "direct"
})
- {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+ {:ok, _} = CommonAPI.favorite(other_user, activity.id)
conn
|> assign(:user, nil)
{:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
- {:ok, _, _} = CommonAPI.favorite(activity.id, user)
+ {:ok, _} = CommonAPI.favorite(user, activity.id)
first_conn =
conn
"Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
})
- {:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
+ {:ok, _} = CommonAPI.favorite(user, second_activity.id)
last_like = status["id"]
user = insert(:user)
another_user = insert(:user)
{:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
- {:ok, favorite_activity, _object} = CommonAPI.favorite(create_activity.id, another_user)
+ {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id)
{:ok, [notification]} = Notification.create_notifications(favorite_activity)
create_activity = Activity.get_by_id(create_activity.id)
user = insert(:user)
- {:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user)
+ {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
url = "/notice/#{like_activity.id}"
assert like_activity.data["type"] == "Like"
user = insert(:user)
- {:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user)
+ {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
assert like_activity.data["type"] == "Like"
user: user
} do
[activity | _] = insert_pair(:note_activity)
- CommonAPI.favorite(activity.id, user)
+ CommonAPI.favorite(user, activity.id)
response =
conn
user: user
} do
activity = insert(:note_activity)
- CommonAPI.favorite(activity.id, user)
+ CommonAPI.favorite(user, activity.id)
response =
conn
"visibility" => "direct"
})
- CommonAPI.favorite(direct.id, user)
+ CommonAPI.favorite(user, direct.id)
response =
conn
"visibility" => "direct"
})
- CommonAPI.favorite(direct.id, user)
+ CommonAPI.favorite(user, direct.id)
response =
conn
activities = insert_list(10, :note_activity)
Enum.each(activities, fn activity ->
- CommonAPI.favorite(activity.id, user)
+ CommonAPI.favorite(user, activity.id)
end)
third_activity = Enum.at(activities, 2)
7
|> insert_list(:note_activity)
|> Enum.each(fn activity ->
- CommonAPI.favorite(activity.id, user)
+ CommonAPI.favorite(user, activity.id)
end)
response =
} do
user = insert(:user, %{info: %{hide_favorites: true}})
activity = insert(:note_activity)
- CommonAPI.favorite(activity.id, user)
+ CommonAPI.favorite(user, activity.id)
conn =
conn
test "hides favorites for new users by default", %{conn: conn, current_user: current_user} do
user = insert(:user)
activity = insert(:note_activity)
- CommonAPI.favorite(activity.id, user)
+ CommonAPI.favorite(user, activity.id)
conn =
conn
"<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
})
- {:ok, activity, _} = CommonAPI.favorite(activity.id, user)
+ {:ok, activity} = CommonAPI.favorite(user, activity.id)
object = Object.normalize(activity)
assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post"
)
{:ok, activity} = CommonAPI.post(user, %{"status" => ":("})
- {:ok, notif, _} = CommonAPI.favorite(activity.id, blocked)
+ {:ok, notif} = CommonAPI.favorite(blocked, activity.id)
Streamer.stream("user:notification", notif)
Task.await(task)
{:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
{:ok, activity} = CommonAPI.add_mute(user, activity)
- {:ok, notif, _} = CommonAPI.favorite(activity.id, user2)
+ {:ok, notif} = CommonAPI.favorite(user2, activity.id)
Streamer.stream("user:notification", notif)
Task.await(task)
end
{:ok, user} = User.block_domain(user, "hecking-lewd-place.com")
{:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
- {:ok, notif, _} = CommonAPI.favorite(activity.id, user2)
+ {:ok, notif} = CommonAPI.favorite(user2, activity.id)
Streamer.stream("user:notification", notif)
Task.await(task)