X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fweb%2Factivity_pub%2Futils.ex;h=dfae602dfea61a6c01496f54c51fe289cb40ce05;hb=167812a3f2c1470012cb161f3c5ba4c021fbad97;hp=5a51b7884115098fde0cc1928b0a44c5cf773053;hpb=f171095960d172d54015b28e8da302b5745dca86;p=akkoma diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 5a51b7884..dfae602df 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -1,11 +1,13 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.Utils do alias Ecto.Changeset alias Ecto.UUID alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.Maps alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -22,7 +24,16 @@ defmodule Pleroma.Web.ActivityPub.Utils do require Logger require Pleroma.Constants - @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer", "Audio"] + @supported_object_types [ + "Article", + "Note", + "Event", + "Video", + "Page", + "Question", + "Answer", + "Audio" + ] @strip_status_report_states ~w(closed resolved) @supported_report_states ~w(open closed resolved) @valid_visibilities ~w(public unlisted private direct) @@ -36,8 +47,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do Map.put(params, "actor", get_ap_id(params["actor"])) end - @spec determine_explicit_mentions(map()) :: map() - def determine_explicit_mentions(%{"tag" => tag} = _) when is_list(tag) do + @spec determine_explicit_mentions(map()) :: [any] + def determine_explicit_mentions(%{"tag" => tag}) when is_list(tag) do Enum.flat_map(tag, fn %{"type" => "Mention", "href" => href} -> [href] _ -> [] @@ -160,8 +171,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do 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 + def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do + outgoing_blocks = Config.get([:activitypub, :outgoing_blocks]) + + with true <- Config.get!([:instance, :federating]), + true <- type != "Block" || outgoing_blocks do Pleroma.Web.Federator.publish(activity) end @@ -231,7 +245,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do Inserts a full object if it is contained in an activity. """ def insert_full_object(%{"object" => %{"type" => type} = object_data} = map) - when is_map(object_data) and type in @supported_object_types do + when type in @supported_object_types do with {:ok, object} <- Object.create(object_data) do map = Map.put(map, "object", object.data["id"]) @@ -256,6 +270,16 @@ defmodule Pleroma.Web.ActivityPub.Utils do |> Repo.one() end + @doc """ + Returns like activities targeting an object + """ + def get_object_likes(%{data: %{"id" => id}}) do + 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, @@ -284,16 +308,26 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => cc, "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end - @spec update_element_in_object(String.t(), list(any), Object.t()) :: + def make_emoji_reaction_data(user, object, emoji, activity_id) do + make_like_data(user, object, activity_id) + |> Map.put("type", "EmojiReact") + |> Map.put("content", emoji) + end + + @spec update_element_in_object(String.t(), list(any), Object.t(), integer() | nil) :: {:ok, Object.t()} | {:error, Ecto.Changeset.t()} - def update_element_in_object(property, element, object) do + def update_element_in_object(property, element, object, count \\ nil) do + length = + count || + length(element) + data = Map.merge( object.data, - %{"#{property}_count" => length(element), "#{property}s" => element} + %{"#{property}_count" => length, "#{property}s" => element} ) object @@ -301,6 +335,69 @@ defmodule Pleroma.Web.ActivityPub.Utils do |> Object.update_and_set_cache() end + @spec add_emoji_reaction_to_object(Activity.t(), Object.t()) :: + {:ok, Object.t()} | {:error, Ecto.Changeset.t()} + + def add_emoji_reaction_to_object( + %Activity{data: %{"content" => emoji, "actor" => actor}}, + object + ) do + reactions = get_cached_emoji_reactions(object) + + new_reactions = + case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do + nil -> + reactions ++ [[emoji, [actor]]] + + index -> + List.update_at( + reactions, + index, + fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end + ) + end + + count = emoji_count(new_reactions) + + update_element_in_object("reaction", new_reactions, object, count) + end + + def emoji_count(reactions_list) do + Enum.reduce(reactions_list, 0, fn [_, users], acc -> acc + length(users) end) + end + + def remove_emoji_reaction_from_object( + %Activity{data: %{"content" => emoji, "actor" => actor}}, + object + ) do + reactions = get_cached_emoji_reactions(object) + + new_reactions = + case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do + nil -> + reactions + + index -> + List.update_at( + reactions, + index, + fn [emoji, users] -> [emoji, List.delete(users, actor)] end + ) + |> Enum.reject(fn [_, users] -> Enum.empty?(users) end) + end + + count = emoji_count(new_reactions) + update_element_in_object("reaction", new_reactions, object, count) + end + + def get_cached_emoji_reactions(object) do + if is_list(object.data["reactions"]) do + object.data["reactions"] + else + [] + end + 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 @@ -335,7 +432,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do @doc """ Updates a follow activity's state (for locked accounts). """ - @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity} | {:error, any()} + @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil} def update_follow_state_for_all( %Activity{data: %{"actor" => actor, "object" => object}} = activity, state @@ -348,22 +445,19 @@ defmodule Pleroma.Web.ActivityPub.Utils do |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)]) |> Repo.update_all([]) - User.set_follow_state_cache(actor, object, state) - activity = Activity.get_by_id(activity.id) {:ok, activity} end def update_follow_state( - %Activity{data: %{"actor" => actor, "object" => object}} = activity, + %Activity{} = activity, state ) do new_data = Map.put(activity.data, "state", state) changeset = Changeset.change(activity, data: new_data) with {:ok, activity} <- Repo.update(changeset) do - User.set_follow_state_cache(actor, object, state) {:ok, activity} end end @@ -384,7 +478,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "object" => followed_id, "state" => "pending" } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do @@ -398,10 +492,32 @@ defmodule Pleroma.Web.ActivityPub.Utils do |> Repo.one() end + def fetch_latest_undo(%User{ap_id: ap_id}) do + "Undo" + |> Activity.Queries.by_type() + |> where(actor: ^ap_id) + |> order_by([activity], fragment("? desc nulls last", activity.id)) + |> limit(1) + |> Repo.one() + end + + def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do + %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id) + + "EmojiReact" + |> Activity.Queries.by_type() + |> where(actor: ^ap_id) + |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji)) + |> Activity.Queries.by_object_id(object_ap_id) + |> order_by([activity], fragment("? desc nulls last", activity.id)) + |> limit(1) + |> Repo.one() + end + #### Announce-related helpers @doc """ - Retruns an existing announce activity if the notice has already been announced + Returns an existing announce activity if the notice has already been announced """ @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do @@ -431,7 +547,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => [], "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def make_announce_data( @@ -448,46 +564,26 @@ defmodule Pleroma.Web.ActivityPub.Utils do "cc" => [Pleroma.Constants.as_public()], "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end - @doc """ - Make unannounce activity data for the given actor and object - """ - def make_unannounce_data( - %User{ap_id: ap_id} = user, - %Activity{data: %{"context" => context, "object" => object}} = activity, - activity_id + def make_undo_data( + %User{ap_id: actor, follower_address: follower_address}, + %Activity{ + data: %{"id" => undone_activity_id, "context" => context}, + actor: undone_activity_actor + }, + activity_id \\ nil ) do - object = Object.normalize(object) - %{ "type" => "Undo", - "actor" => ap_id, - "object" => activity.data, - "to" => [user.follower_address, object.data["actor"]], + "actor" => actor, + "object" => undone_activity_id, + "to" => [follower_address, undone_activity_actor], "cc" => [Pleroma.Constants.as_public()], "context" => context } - |> maybe_put("id", activity_id) - end - - def make_unlike_data( - %User{ap_id: ap_id} = user, - %Activity{data: %{"context" => context, "object" => object}} = activity, - activity_id - ) do - object = Object.normalize(object) - - %{ - "type" => "Undo", - "actor" => ap_id, - "object" => activity.data, - "to" => [user.follower_address, object.data["actor"]], - "cc" => [Pleroma.Constants.as_public()], - "context" => context - } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end @spec add_announce_to_object(Activity.t(), Object.t()) :: @@ -532,7 +628,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "to" => [followed.ap_id], "object" => follow_activity.data } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end #### Block-related helpers @@ -555,17 +651,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "to" => [blocked.ap_id], "object" => blocked.ap_id } - |> maybe_put("id", activity_id) - end - - def make_unblock_data(blocker, blocked, block_activity, activity_id) do - %{ - "type" => "Undo", - "actor" => blocker.ap_id, - "to" => [blocked.ap_id], - "object" => block_activity.data - } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end #### Create-related helpers @@ -616,150 +702,55 @@ defmodule Pleroma.Web.ActivityPub.Utils do def make_flag_data(_, _), do: %{} defp build_flag_object(%{account: account, statuses: statuses} = _) do - [account.ap_id] ++ - Enum.map(statuses || [], fn act -> - id = - case act do - %Activity{} = act -> act.data["id"] - act when is_map(act) -> act["id"] - act when is_binary(act) -> act - end - - activity = Activity.get_by_ap_id_with_object(id) - actor = User.get_by_ap_id(activity.object.data["actor"]) + [account.ap_id] ++ build_flag_object(%{statuses: statuses}) + end + defp build_flag_object(%{statuses: statuses}) do + Enum.map(statuses || [], &build_flag_object/1) + end + + defp build_flag_object(act) when is_map(act) or is_binary(act) do + id = + case act do + %Activity{} = act -> act.data["id"] + act when is_map(act) -> act["id"] + act when is_binary(act) -> act + end + + case Activity.get_by_ap_id_with_object(id) do + %Activity{} = activity -> %{ "type" => "Note", "id" => activity.data["id"], "content" => activity.object.data["content"], "published" => activity.object.data["published"], - "actor" => AccountView.render("show.json", %{user: actor}) + "actor" => + AccountView.render("show.json", %{ + user: User.get_by_ap_id(activity.object.data["actor"]) + }) } - end) - end - - defp build_flag_object(_), do: [] - @doc """ - Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after - the first one to `pages_left` pages. - If the amount of pages is higher than the collection has, it returns whatever was there. - """ - def fetch_ordered_collection(from, pages_left, acc \\ []) do - with {:ok, response} <- Tesla.get(from), - {:ok, collection} <- Jason.decode(response.body) do - case collection["type"] do - "OrderedCollection" -> - # If we've encountered the OrderedCollection and not the page, - # just call the same function on the page address - fetch_ordered_collection(collection["first"], pages_left) - - "OrderedCollectionPage" -> - if pages_left > 0 do - # There are still more pages - if Map.has_key?(collection, "next") do - # There are still more pages, go deeper saving what we have into the accumulator - fetch_ordered_collection( - collection["next"], - pages_left - 1, - acc ++ collection["orderedItems"] - ) - else - # No more pages left, just return whatever we already have - acc ++ collection["orderedItems"] - end - else - # Got the amount of pages needed, add them all to the accumulator - acc ++ collection["orderedItems"] - end - - _ -> - {:error, "Not an OrderedCollection or OrderedCollectionPage"} - end + _ -> + %{"id" => id, "deleted" => true} end end + defp build_flag_object(_), do: [] + #### Report-related helpers def get_reports(params, page, page_size) do params = params - |> Map.put("type", "Flag") - |> Map.put("skip_preload", true) - |> Map.put("total", true) - |> Map.put("limit", page_size) - |> Map.put("offset", (page - 1) * page_size) + |> Map.put(:type, "Flag") + |> Map.put(:skip_preload, true) + |> Map.put(:preload_report_notes, true) + |> Map.put(:total, true) + |> Map.put(:limit, page_size) + |> Map.put(:offset, (page - 1) * page_size) ActivityPub.fetch_activities([], params, :offset) end - @spec get_reports_grouped_by_status() :: %{ - required(:groups) => [ - %{ - required(:date) => String.t(), - required(:account) => %{}, - required(:status) => %{}, - required(:actors) => [%User{}], - required(:reports) => [%Activity{}] - } - ], - required(:total) => integer - } - def get_reports_grouped_by_status do - groups = - get_reported_status_ids() - |> Enum.map(fn entry -> - activity = Jason.decode!(entry.activity) - reports = get_reports_by_status_id(activity["id"]) - max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"])) - actors = Enum.map(reports, & &1.user_actor) - - %{ - date: max_date.data["published"], - account: activity["actor"], - status: %{ - id: activity["id"], - content: activity["content"], - published: activity["published"] - }, - actors: Enum.uniq(actors), - reports: reports - } - end) - - %{ - groups: groups - } - end - - def get_reports_by_status_id(ap_id) do - from(a in Activity, - where: fragment("(?)->>'type' = 'Flag'", a.data), - where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]) - ) - |> Activity.with_preloaded_user_actor() - |> Repo.all() - end - - @spec get_reported_status_ids() :: [ - %{ - required(:activity) => String.t(), - required(:date) => String.t() - } - ] - def get_reported_status_ids do - from(a in Activity, - where: fragment("(?)->>'type' = 'Flag'", a.data), - select: %{ - date: fragment("max(?->>'published') date", a.data), - activity: - fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity", a.data) - }, - group_by: fragment("activity"), - order_by: fragment("date DESC") - ) - |> Repo.all() - end - def update_report_state(%Activity{} = activity, state) when state in @strip_status_report_states do {:ok, stripped_activity} = strip_report_status_data(activity) @@ -798,7 +789,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do def strip_report_status_data(activity) do [actor | reported_activities] = activity.data["object"] - stripped_activities = Enum.map(reported_activities, & &1["id"]) + + stripped_activities = + Enum.map(reported_activities, fn + act when is_map(act) -> act["id"] + act when is_binary(act) -> act + end) + new_data = put_in(activity.data, ["object"], [actor | stripped_activities]) {:ok, %{activity | data: new_data}} @@ -874,7 +871,4 @@ defmodule Pleroma.Web.ActivityPub.Utils do |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) |> Repo.all() end - - def maybe_put(map, _key, nil), do: map - def maybe_put(map, key, value), do: Map.put(map, key, value) end