# Pleroma: A lightweight social networking server # Copyright © 2017-2023 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do require Pleroma.Constants alias Pleroma.Object.Updater alias Pleroma.Web.ActivityPub.MRF.Utils @moduledoc "Reject or force-unlisted emojis with certain URLs or names" @behaviour Pleroma.Web.ActivityPub.MRF.Policy defp config_remove_url do Pleroma.Config.get([:mrf_emoji, :remove_url], []) end defp config_remove_shortcode do Pleroma.Config.get([:mrf_emoji, :remove_shortcode], []) end defp config_unlist_url do Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_url], []) end defp config_unlist_shortcode do Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_shortcode], []) end @impl Pleroma.Web.ActivityPub.MRF.Policy def history_awareness, do: :manual @impl Pleroma.Web.ActivityPub.MRF.Policy def filter(%{"type" => type, "object" => %{"type" => objtype} = object} = message) when type in ["Create", "Update"] and objtype in Pleroma.Constants.status_object_types() do with {:ok, object} <- Updater.do_with_history(object, fn object -> {:ok, process_remove(object, :url, config_remove_url())} end), {:ok, object} <- Updater.do_with_history(object, fn object -> {:ok, process_remove(object, :shortcode, config_remove_shortcode())} end), activity <- Map.put(message, "object", object), activity <- maybe_delist(activity) do {:ok, activity} end end @impl Pleroma.Web.ActivityPub.MRF.Policy def filter(%{"type" => type} = object) when type in Pleroma.Constants.actor_types() do with object <- process_remove(object, :url, config_remove_url()), object <- process_remove(object, :shortcode, config_remove_shortcode()) do {:ok, object} end end @impl Pleroma.Web.ActivityPub.MRF.Policy def filter(%{"type" => "EmojiReact"} = object) do with {:ok, _} <- matched_emoji_checker(config_remove_url(), config_remove_shortcode()).(object) do {:ok, object} else _ -> {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} end end @impl Pleroma.Web.ActivityPub.MRF.Policy def filter(message) do {:ok, message} end defp match_string?(string, pattern) when is_binary(pattern) do string == pattern end defp match_string?(string, %Regex{} = pattern) do String.match?(string, pattern) end defp match_any?(string, patterns) do Enum.any?(patterns, &match_string?(string, &1)) end defp url_from_tag(%{"icon" => %{"url" => url}}), do: url defp url_from_tag(_), do: nil defp url_from_emoji({_name, url}), do: url defp shortcode_from_tag(%{"name" => name}) when is_binary(name), do: String.trim(name, ":") defp shortcode_from_tag(_), do: nil defp shortcode_from_emoji({name, _url}), do: name defp process_remove(object, :url, patterns) do process_remove_impl(object, &url_from_tag/1, &url_from_emoji/1, patterns) end defp process_remove(object, :shortcode, patterns) do process_remove_impl(object, &shortcode_from_tag/1, &shortcode_from_emoji/1, patterns) end defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) do object = if object["tag"] do Map.put( object, "tag", Enum.filter( object["tag"], fn %{"type" => "Emoji"} = tag -> str = extract_from_tag.(tag) if is_binary(str) do not match_any?(str, patterns) else true end _ -> true end ) ) else object end object = if object["emoji"] do Map.put( object, "emoji", object["emoji"] |> Enum.reduce(%{}, fn {name, url} = emoji, acc -> if not match_any?(extract_from_emoji.(emoji), patterns) do Map.put(acc, name, url) else acc end end) ) else object end object end defp matched_emoji_checker(urls, shortcodes) do fn object -> if any_emoji_match?(object, &url_from_tag/1, &url_from_emoji/1, urls) or any_emoji_match?( object, &shortcode_from_tag/1, &shortcode_from_emoji/1, shortcodes ) do {:matched, nil} else {:ok, %{}} end end end defp maybe_delist(%{"object" => object, "to" => to, "type" => "Create"} = activity) do check = matched_emoji_checker(config_unlist_url(), config_unlist_shortcode()) should_delist? = fn object -> with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check) do false else _ -> true end end if Pleroma.Constants.as_public() in to and should_delist?.(object) do to = List.delete(to, Pleroma.Constants.as_public()) cc = [Pleroma.Constants.as_public() | activity["cc"] || []] activity |> Map.put("to", to) |> Map.put("cc", cc) else activity end end defp maybe_delist(activity), do: activity defp any_emoji_match?(object, extract_from_tag, extract_from_emoji, patterns) do Kernel.||( Enum.any?( object["tag"] || [], fn %{"type" => "Emoji"} = tag -> str = extract_from_tag.(tag) if is_binary(str) do match_any?(str, patterns) else false end _ -> false end ), (object["emoji"] || []) |> Enum.any?(fn emoji -> match_any?(extract_from_emoji.(emoji), patterns) end) ) end @impl Pleroma.Web.ActivityPub.MRF.Policy def describe do mrf_emoji = Pleroma.Config.get(:mrf_emoji, []) |> Enum.map(fn {key, value} -> {key, Enum.map(value, &Utils.describe_regex_or_string/1)} end) |> Enum.into(%{}) {:ok, %{mrf_emoji: mrf_emoji}} end @impl Pleroma.Web.ActivityPub.MRF.Policy def config_description do %{ key: :mrf_emoji, related_policy: "Pleroma.Web.ActivityPub.MRF.EmojiPolicy", label: "MRF Emoji", description: "Reject or force-unlisted emojis whose URLs or names match a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).", children: [ %{ key: :remove_url, type: {:list, :string}, description: """ A list of patterns which result in emoji whose URL matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles. Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. """, suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu] }, %{ key: :remove_shortcode, type: {:list, :string}, description: """ A list of patterns which result in emoji whose shortcode matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles. Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. """, suggestions: ["foo", ~r/foo/iu] }, %{ key: :federated_timeline_removal_url, type: {:list, :string}, description: """ A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. """, suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu] }, %{ key: :federated_timeline_removal_shortcode, type: {:list, :string}, description: """ A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. """, suggestions: ["foo", ~r/foo/iu] } ] } end end