1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
6 require Pleroma.Constants
8 alias Pleroma.Object.Updater
9 alias Pleroma.Web.ActivityPub.MRF.Utils
11 @moduledoc "Reject or force-unlisted emojis with certain URLs or names"
13 @behaviour Pleroma.Web.ActivityPub.MRF.Policy
15 defp config_remove_url do
16 Pleroma.Config.get([:mrf_emoji, :remove_url], [])
19 defp config_remove_shortcode do
20 Pleroma.Config.get([:mrf_emoji, :remove_shortcode], [])
23 defp config_unlist_url do
24 Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_url], [])
27 defp config_unlist_shortcode do
28 Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_shortcode], [])
31 @impl Pleroma.Web.ActivityPub.MRF.Policy
32 def history_awareness, do: :manual
34 @impl Pleroma.Web.ActivityPub.MRF.Policy
35 def filter(%{"type" => type, "object" => %{"type" => objtype} = object} = message)
36 when type in ["Create", "Update"] and objtype in Pleroma.Constants.status_object_types() do
38 Updater.do_with_history(object, fn object ->
39 {:ok, process_remove(object, :url, config_remove_url())}
42 Updater.do_with_history(object, fn object ->
43 {:ok, process_remove(object, :shortcode, config_remove_shortcode())}
45 activity <- Map.put(message, "object", object),
46 activity <- maybe_delist(activity) do
51 @impl Pleroma.Web.ActivityPub.MRF.Policy
52 def filter(%{"type" => type} = object) when type in Pleroma.Constants.actor_types() do
53 with object <- process_remove(object, :url, config_remove_url()),
54 object <- process_remove(object, :shortcode, config_remove_shortcode()) do
59 @impl Pleroma.Web.ActivityPub.MRF.Policy
60 def filter(%{"type" => "EmojiReact"} = object) do
62 matched_emoji_checker(config_remove_url(), config_remove_shortcode()).(object) do
66 {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"}
70 @impl Pleroma.Web.ActivityPub.MRF.Policy
71 def filter(message) do
75 defp match_string?(string, pattern) when is_binary(pattern) do
79 defp match_string?(string, %Regex{} = pattern) do
80 String.match?(string, pattern)
83 defp match_any?(string, patterns) do
84 Enum.any?(patterns, &match_string?(string, &1))
87 defp url_from_tag(%{"icon" => %{"url" => url}}), do: url
88 defp url_from_tag(_), do: nil
90 defp url_from_emoji({_name, url}), do: url
92 defp shortcode_from_tag(%{"name" => name}) when is_binary(name), do: String.trim(name, ":")
93 defp shortcode_from_tag(_), do: nil
95 defp shortcode_from_emoji({name, _url}), do: name
97 defp process_remove(object, :url, patterns) do
98 process_remove_impl(object, &url_from_tag/1, &url_from_emoji/1, patterns)
101 defp process_remove(object, :shortcode, patterns) do
102 process_remove_impl(object, &shortcode_from_tag/1, &shortcode_from_emoji/1, patterns)
105 defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) do
114 %{"type" => "Emoji"} = tag ->
115 str = extract_from_tag.(tag)
118 not match_any?(str, patterns)
133 if object["emoji"] do
138 |> Enum.reduce(%{}, fn {name, url} = emoji, acc ->
139 if not match_any?(extract_from_emoji.(emoji), patterns) do
140 Map.put(acc, name, url)
153 defp matched_emoji_checker(urls, shortcodes) do
155 if any_emoji_match?(object, &url_from_tag/1, &url_from_emoji/1, urls) or
158 &shortcode_from_tag/1,
159 &shortcode_from_emoji/1,
169 defp maybe_delist(%{"object" => object, "to" => to, "type" => "Create"} = activity) do
170 check = matched_emoji_checker(config_unlist_url(), config_unlist_shortcode())
172 should_delist? = fn object ->
173 with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check) do
180 if Pleroma.Constants.as_public() in to and should_delist?.(object) do
181 to = List.delete(to, Pleroma.Constants.as_public())
182 cc = [Pleroma.Constants.as_public() | activity["cc"] || []]
192 defp maybe_delist(activity), do: activity
194 defp any_emoji_match?(object, extract_from_tag, extract_from_emoji, patterns) do
199 %{"type" => "Emoji"} = tag ->
200 str = extract_from_tag.(tag)
203 match_any?(str, patterns)
212 (object["emoji"] || [])
213 |> Enum.any?(fn emoji -> match_any?(extract_from_emoji.(emoji), patterns) end)
217 @impl Pleroma.Web.ActivityPub.MRF.Policy
220 Pleroma.Config.get(:mrf_emoji, [])
221 |> Enum.map(fn {key, value} ->
222 {key, Enum.map(value, &Utils.describe_regex_or_string/1)}
226 {:ok, %{mrf_emoji: mrf_emoji}}
229 @impl Pleroma.Web.ActivityPub.MRF.Policy
230 def config_description do
233 related_policy: "Pleroma.Web.ActivityPub.MRF.EmojiPolicy",
236 "Reject or force-unlisted emojis whose URLs or names match a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
240 type: {:list, :string},
242 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.
244 Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
246 suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu]
249 key: :remove_shortcode,
250 type: {:list, :string},
252 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.
254 Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
256 suggestions: ["foo", ~r/foo/iu]
259 key: :federated_timeline_removal_url,
260 type: {:list, :string},
262 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.
264 Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
266 suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu]
269 key: :federated_timeline_removal_shortcode,
270 type: {:list, :string},
272 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.
274 Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
276 suggestions: ["foo", ~r/foo/iu]