# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# 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
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]
_ -> []
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,
+ false <- Visibility.is_local_public?(activity) do
Pleroma.Web.Federator.publish(activity)
end
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"])
"cc" => cc,
"context" => object.data["context"]
}
- |> maybe_put("id", activity_id)
+ |> Maps.put_if_present("id", activity_id)
end
def make_emoji_reaction_data(user, object, emoji, activity_id) 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
|> 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
"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
|> 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)
#### 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
"cc" => [],
"context" => object.data["context"]
}
- |> maybe_put("id", activity_id)
+ |> Maps.put_if_present("id", activity_id)
end
def make_announce_data(
"cc" => [Pleroma.Constants.as_public()],
"context" => object.data["context"]
}
- |> maybe_put("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
- ) 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)
- 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
def make_undo_data(
"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()) ::
"to" => [followed.ap_id],
"object" => follow_activity.data
}
- |> maybe_put("id", activity_id)
+ |> Maps.put_if_present("id", activity_id)
end
#### Block-related helpers
"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
def make_flag_data(_, _), do: %{}
- defp build_flag_object(%{account: account, statuses: statuses} = _) do
- [account.ap_id] ++ build_flag_object(%{statuses: statuses})
+ defp build_flag_object(%{account: account, statuses: statuses}) do
+ [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(%Activity{data: %{"id" => id}, object: %{data: data}}) do
+ activity_actor = User.get_by_ap_id(data["actor"])
+
+ %{
+ "type" => "Note",
+ "id" => id,
+ "content" => data["content"],
+ "published" => data["published"],
+ "actor" =>
+ AccountView.render(
+ "show.json",
+ %{user: activity_actor, skip_visibility_check: true}
+ )
+ }
+ end
+
defp build_flag_object(act) when is_map(act) or is_binary(act) do
id =
case act do
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: User.get_by_ap_id(activity.object.data["actor"])
- })
- }
-
- _ ->
- %{"id" => id, "deleted" => true}
+ build_flag_object(activity)
+
+ nil ->
+ if activity = Activity.get_by_object_ap_id_with_object(id) do
+ build_flag_object(activity)
+ else
+ %{"id" => id, "deleted" => true}
+ end
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
- end
- end
-
#### Report-related helpers
def get_reports(params, page, page_size) do
params =
params
- |> 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)
+ |> 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
- def parse_report_group(activity) do
- 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)
- [%{data: %{"object" => [account_id | _]}} | _] = reports
-
- account =
- AccountView.render("show.json", %{
- user: User.get_by_ap_id(account_id)
- })
-
- status = get_status_data(activity)
-
- %{
- date: max_date.data["published"],
- account: account,
- status: status,
- actors: Enum.uniq(actors),
- reports: reports
- }
- end
-
- defp get_status_data(status) do
- case status["deleted"] do
- true ->
- %{
- "id" => status["id"],
- "deleted" => true
- }
-
- _ ->
- Activity.get_by_ap_id(status["id"])
- end
- 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}]),
- or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id])
- )
- |> Activity.with_preloaded_user_actor()
- |> Repo.all()
- end
-
- @spec get_reports_grouped_by_status([String.t()]) :: %{
- required(:groups) => [
- %{
- required(:date) => String.t(),
- required(:account) => %{},
- required(:status) => %{},
- required(:actors) => [%User{}],
- required(:reports) => [%Activity{}]
- }
- ]
- }
- def get_reports_grouped_by_status(activity_ids) do
- parsed_groups =
- activity_ids
- |> Enum.map(fn id ->
- id
- |> build_flag_object()
- |> parse_report_group()
- end)
-
- %{
- groups: parsed_groups
- }
- end
-
- @spec get_reported_activities() :: [
- %{
- required(:activity) => String.t(),
- required(:date) => String.t()
- }
- ]
- def get_reported_activities do
- reported_activities_query =
- from(a in Activity,
- where: fragment("(?)->>'type' = 'Flag'", a.data),
- select: %{
- activity: fragment("jsonb_array_elements((? #- '{object,0}')->'object')", a.data)
- },
- group_by: fragment("activity")
- )
-
- from(a in subquery(reported_activities_query),
- distinct: true,
- select: %{
- id: fragment("COALESCE(?->>'id'::text, ? #>> '{}')", a.activity, a.activity)
- }
- )
- |> Repo.all()
- |> Enum.map(& &1.id)
- end
-
def update_report_state(%Activity{} = activity, state)
when state in @strip_status_report_states do
{:ok, stripped_activity} = strip_report_status_data(activity)
|> 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