aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/activity
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/activity')
-rw-r--r--lib/pleroma/activity/html.ex81
-rw-r--r--lib/pleroma/activity/ir/topics.ex99
-rw-r--r--lib/pleroma/activity/queries.ex108
-rw-r--r--lib/pleroma/activity/search.ex162
4 files changed, 450 insertions, 0 deletions
diff --git a/lib/pleroma/activity/html.ex b/lib/pleroma/activity/html.ex
new file mode 100644
index 0000000..706b2d3
--- /dev/null
+++ b/lib/pleroma/activity/html.ex
@@ -0,0 +1,81 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Activity.HTML do
+ alias Pleroma.HTML
+ alias Pleroma.Object
+
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
+ # We store a list of cache keys related to an activity in a
+ # separate cache, scrubber_management_cache. It has the same
+ # size as scrubber_cache (see application.ex). Every time we add
+ # a cache to scrubber_cache, we update scrubber_management_cache.
+ #
+ # The most recent write of a certain key in the management cache
+ # is the same as the most recent write of any record related to that
+ # key in the main cache.
+ # Assuming LRW ( https://hexdocs.pm/cachex/Cachex.Policy.LRW.html ),
+ # this means when the management cache is evicted by cachex, all
+ # related records in the main cache will also have been evicted.
+
+ defp get_cache_keys_for(activity_id) do
+ with {:ok, list} when is_list(list) <- @cachex.get(:scrubber_management_cache, activity_id) do
+ list
+ else
+ _ -> []
+ end
+ end
+
+ defp add_cache_key_for(activity_id, additional_key) do
+ current = get_cache_keys_for(activity_id)
+
+ unless additional_key in current do
+ @cachex.put(:scrubber_management_cache, activity_id, [additional_key | current])
+ end
+ end
+
+ def invalidate_cache_for(activity_id) do
+ keys = get_cache_keys_for(activity_id)
+ Enum.map(keys, &@cachex.del(:scrubber_cache, &1))
+ @cachex.del(:scrubber_management_cache, activity_id)
+ end
+
+ def get_cached_scrubbed_html_for_activity(
+ content,
+ scrubbers,
+ activity,
+ key \\ "",
+ callback \\ fn x -> x end
+ ) do
+ key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
+
+ @cachex.fetch!(:scrubber_cache, key, fn _key ->
+ object = Object.normalize(activity, fetch: false)
+
+ add_cache_key_for(activity.id, key)
+ HTML.ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
+ end)
+ end
+
+ def get_cached_stripped_html_for_activity(content, activity, key) do
+ get_cached_scrubbed_html_for_activity(
+ content,
+ FastSanitize.Sanitizer.StripTags,
+ activity,
+ key,
+ &HtmlEntities.decode/1
+ )
+ end
+
+ defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do
+ generate_scrubber_signature([scrubber])
+ end
+
+ defp generate_scrubber_signature(scrubbers) do
+ Enum.reduce(scrubbers, "", fn scrubber, signature ->
+ "#{signature}#{to_string(scrubber)}"
+ end)
+ end
+end
diff --git a/lib/pleroma/activity/ir/topics.ex b/lib/pleroma/activity/ir/topics.ex
new file mode 100644
index 0000000..8249cbe
--- /dev/null
+++ b/lib/pleroma/activity/ir/topics.ex
@@ -0,0 +1,99 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Activity.Ir.Topics do
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.Visibility
+
+ def get_activity_topics(activity) do
+ activity
+ |> Object.normalize(fetch: false)
+ |> generate_topics(activity)
+ |> List.flatten()
+ end
+
+ defp generate_topics(%{data: %{"type" => "ChatMessage"}}, %{data: %{"type" => "Delete"}}) do
+ ["user", "user:pleroma_chat"]
+ end
+
+ defp generate_topics(%{data: %{"type" => "ChatMessage"}}, %{data: %{"type" => "Create"}}) do
+ []
+ end
+
+ defp generate_topics(%{data: %{"type" => "Answer"}}, _) do
+ []
+ end
+
+ defp generate_topics(object, activity) do
+ ["user", "list"] ++ visibility_tags(object, activity)
+ end
+
+ defp visibility_tags(object, %{data: %{"type" => type}} = activity) when type != "Announce" do
+ case Visibility.get_visibility(activity) do
+ "public" ->
+ if activity.local do
+ ["public", "public:local"]
+ else
+ ["public"]
+ end
+ |> item_creation_tags(object, activity)
+
+ "local" ->
+ ["public:local"]
+ |> item_creation_tags(object, activity)
+
+ "direct" ->
+ ["direct"]
+
+ _ ->
+ []
+ end
+ end
+
+ defp visibility_tags(_object, _activity) do
+ []
+ end
+
+ defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do
+ tags ++
+ remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity)
+ end
+
+ defp item_creation_tags(tags, _, _) do
+ tags
+ end
+
+ defp hashtags_to_topics(object) do
+ object
+ |> Object.hashtags()
+ |> Enum.map(fn hashtag -> "hashtag:" <> hashtag end)
+ end
+
+ defp remote_topics(%{local: true}), do: []
+
+ defp remote_topics(%{actor: actor}) when is_binary(actor),
+ do: ["public:remote:" <> URI.parse(actor).host]
+
+ defp remote_topics(_), do: []
+
+ defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: []
+
+ defp attachment_topics(_object, %{local: true} = activity) do
+ case Visibility.get_visibility(activity) do
+ "public" ->
+ ["public:media", "public:local:media"]
+
+ "local" ->
+ ["public:local:media"]
+
+ _ ->
+ []
+ end
+ end
+
+ defp attachment_topics(_object, %{actor: actor}) when is_binary(actor),
+ do: ["public:media", "public:remote:media:" <> URI.parse(actor).host]
+
+ defp attachment_topics(_object, _act), do: ["public:media"]
+end
diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex
new file mode 100644
index 0000000..81c44ac
--- /dev/null
+++ b/lib/pleroma/activity/queries.ex
@@ -0,0 +1,108 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 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, where: 3]
+
+ @type query :: Ecto.Queryable.t() | Activity.t()
+
+ alias Pleroma.Activity
+ alias Pleroma.User
+
+ @spec by_id(query(), String.t()) :: query()
+ def by_id(query \\ Activity, id) do
+ from(a in query, where: a.id == ^id)
+ end
+
+ @spec by_ap_id(query, String.t()) :: query
+ def by_ap_id(query \\ Activity, ap_id) do
+ from(
+ activity in query,
+ where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id))
+ )
+ end
+
+ @spec by_actor(query, String.t()) :: query
+ def by_actor(query \\ Activity, actor) do
+ from(a in query, where: a.actor == ^actor)
+ end
+
+ @spec by_author(query, User.t()) :: query
+ def by_author(query \\ Activity, %User{ap_id: ap_id}) do
+ from(a in query, where: a.actor == ^ap_id)
+ end
+
+ def find_by_object_ap_id(activities, object_ap_id) do
+ Enum.find(
+ activities,
+ &(object_ap_id in [is_map(&1.data["object"]) && &1.data["object"]["id"], &1.data["object"]])
+ )
+ end
+
+ @spec by_object_id(query, String.t() | [String.t()]) :: query
+ def by_object_id(query \\ Activity, object_id)
+
+ def by_object_id(query, object_ids) when is_list(object_ids) do
+ from(
+ activity in query,
+ where:
+ fragment(
+ "associated_object_id((?)) = ANY(?)",
+ activity.data,
+ ^object_ids
+ )
+ )
+ end
+
+ def by_object_id(query, object_id) when is_binary(object_id) do
+ from(activity in query,
+ where:
+ fragment(
+ "associated_object_id((?)) = ?",
+ activity.data,
+ ^object_id
+ )
+ )
+ end
+
+ @spec by_object_in_reply_to_id(query, String.t(), keyword()) :: query
+ def by_object_in_reply_to_id(query, in_reply_to_id, opts \\ []) do
+ query =
+ if opts[:skip_preloading] do
+ Activity.with_joined_object(query)
+ else
+ Activity.with_preloaded_object(query)
+ end
+
+ where(
+ query,
+ [activity, object: o],
+ fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(in_reply_to_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 exclude_type(query, String.t()) :: query
+ def exclude_type(query \\ Activity, activity_type) do
+ from(
+ activity in query,
+ where: fragment("(?)->>'type' != ?", activity.data, ^activity_type)
+ )
+ end
+
+ def exclude_authors(query \\ Activity, actors) do
+ from(activity in query, where: activity.actor not in ^actors)
+ end
+end
diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex
new file mode 100644
index 0000000..0b9b24a
--- /dev/null
+++ b/lib/pleroma/activity/search.ex
@@ -0,0 +1,162 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Activity.Search do
+ alias Pleroma.Activity
+ alias Pleroma.Object.Fetcher
+ alias Pleroma.Pagination
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Visibility
+
+ require Pleroma.Constants
+
+ import Ecto.Query
+
+ def search(user, search_query, options \\ []) do
+ index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin
+ limit = Enum.min([Keyword.get(options, :limit), 40])
+ offset = Keyword.get(options, :offset, 0)
+ author = Keyword.get(options, :author)
+
+ search_function =
+ if :persistent_term.get({Pleroma.Repo, :postgres_version}) >= 11 do
+ :websearch
+ else
+ :plain
+ end
+
+ try do
+ Activity
+ |> Activity.with_preloaded_object()
+ |> Activity.restrict_deactivated_users()
+ |> restrict_public(user)
+ |> query_with(index_type, search_query, search_function)
+ |> maybe_restrict_local(user)
+ |> maybe_restrict_author(author)
+ |> maybe_restrict_blocked(user)
+ |> Pagination.fetch_paginated(
+ %{"offset" => offset, "limit" => limit, "skip_order" => index_type == :rum},
+ :offset
+ )
+ |> maybe_fetch(user, search_query)
+ rescue
+ _ -> maybe_fetch([], user, search_query)
+ end
+ end
+
+ def maybe_restrict_author(query, %User{} = author) do
+ Activity.Queries.by_author(query, author)
+ end
+
+ def maybe_restrict_author(query, _), do: query
+
+ def maybe_restrict_blocked(query, %User{} = user) do
+ Activity.Queries.exclude_authors(query, User.blocked_users_ap_ids(user))
+ end
+
+ def maybe_restrict_blocked(query, _), do: query
+
+ defp restrict_public(q, user) when not is_nil(user) do
+ intended_recipients = [
+ Pleroma.Constants.as_public(),
+ Pleroma.Web.ActivityPub.Utils.as_local_public()
+ ]
+
+ from([a, o] in q,
+ where: fragment("?->>'type' = 'Create'", a.data),
+ where: fragment("? && ?", ^intended_recipients, a.recipients)
+ )
+ end
+
+ defp restrict_public(q, _user) do
+ from([a, o] in q,
+ where: fragment("?->>'type' = 'Create'", a.data),
+ where: ^Pleroma.Constants.as_public() in a.recipients
+ )
+ end
+
+ defp query_with(q, :gin, search_query, :plain) do
+ %{rows: [[tsc]]} =
+ Ecto.Adapters.SQL.query!(
+ Pleroma.Repo,
+ "select current_setting('default_text_search_config')::regconfig::oid;"
+ )
+
+ from([a, o] in q,
+ where:
+ fragment(
+ "to_tsvector(?::oid::regconfig, ?->>'content') @@ plainto_tsquery(?)",
+ ^tsc,
+ o.data,
+ ^search_query
+ )
+ )
+ end
+
+ defp query_with(q, :gin, search_query, :websearch) do
+ %{rows: [[tsc]]} =
+ Ecto.Adapters.SQL.query!(
+ Pleroma.Repo,
+ "select current_setting('default_text_search_config')::regconfig::oid;"
+ )
+
+ from([a, o] in q,
+ where:
+ fragment(
+ "to_tsvector(?::oid::regconfig, ?->>'content') @@ websearch_to_tsquery(?)",
+ ^tsc,
+ o.data,
+ ^search_query
+ )
+ )
+ end
+
+ defp query_with(q, :rum, search_query, :plain) do
+ from([a, o] in q,
+ where:
+ fragment(
+ "? @@ plainto_tsquery(?)",
+ o.fts_content,
+ ^search_query
+ ),
+ order_by: [fragment("? <=> now()::date", o.inserted_at)]
+ )
+ end
+
+ defp query_with(q, :rum, search_query, :websearch) do
+ from([a, o] in q,
+ where:
+ fragment(
+ "? @@ websearch_to_tsquery(?)",
+ o.fts_content,
+ ^search_query
+ ),
+ order_by: [fragment("? <=> now()::date", o.inserted_at)]
+ )
+ end
+
+ defp maybe_restrict_local(q, user) do
+ limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
+
+ case {limit, user} do
+ {:all, _} -> restrict_local(q)
+ {:unauthenticated, %User{}} -> q
+ {:unauthenticated, _} -> restrict_local(q)
+ {false, _} -> q
+ end
+ end
+
+ defp restrict_local(q), do: where(q, local: true)
+
+ defp maybe_fetch(activities, user, search_query) do
+ with true <- Regex.match?(~r/https?:/, search_query),
+ {:ok, object} <- Fetcher.fetch_object_from_id(search_query),
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
+ true <- Visibility.visible_for_user?(activity, user) do
+ [activity | activities]
+ else
+ _ -> activities
+ end
+ end
+end