diff --git a/changelog.d/emoji-policy.add b/changelog.d/emoji-policy.add
new file mode 100644
index 000000000..45510c4f6
--- /dev/null
+++ b/changelog.d/emoji-policy.add
@@ -0,0 +1 @@
+Implement MRF policy to reject or delist according to emojis
diff --git a/config/config.exs b/config/config.exs
index a92ab574b..ebcbf8b49 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -408,6 +408,12 @@
federated_timeline_removal: [],
replace: []
+config :pleroma, :mrf_emoji,
+ remove_url: [],
+ remove_shortcode: [],
+ federated_timeline_removal_url: [],
+ federated_timeline_removal_shortcode: []
+
config :pleroma, :mrf_hashtag,
sensitive: ["nsfw"],
reject: [],
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 1e49a79d0..f43cde114 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -261,6 +261,11 @@ Notes:
* `follower_nickname`: The name of the bot account to use for following newly discovered users. Using `followbot` or similar is strongly suggested.
+#### :mrf_emoji
+* `remove_url`: 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 a [regular expression](https://hexdocs.pm/elixir/Regex.html).
+* `remove_shortcode`: 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 a [regular expression](https://hexdocs.pm/elixir/Regex.html).
+* `federated_timeline_removal_url`: 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 a [regular expression](https://hexdocs.pm/elixir/Regex.html).
+* `federated_timeline_removal_shortcode`: 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 a [regular expression](https://hexdocs.pm/elixir/Regex.html).
### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index cfb405218..7b4fd03b6 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -42,6 +42,18 @@ defmodule Pleroma.Constants do
]
)
+ const(status_object_types,
+ do: [
+ "Note",
+ "Question",
+ "Audio",
+ "Video",
+ "Event",
+ "Article",
+ "Page"
+ ]
+ )
+
const(updatable_object_types,
do: [
"Note",
diff --git a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex
new file mode 100644
index 000000000..f884962b9
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex
@@ -0,0 +1,281 @@
+# 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
diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
index 687ec6c2f..874fe9ab9 100644
--- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
@@ -5,6 +5,8 @@
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
require Pleroma.Constants
+ alias Pleroma.Web.ActivityPub.MRF.Utils
+
@moduledoc "Reject or Word-Replace messages with a keyword or regex"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
@@ -128,7 +130,6 @@ def filter(message), do: {:ok, message}
@impl true
def describe do
- # This horror is needed to convert regex sigils to strings
mrf_keyword =
Pleroma.Config.get(:mrf_keyword, [])
|> Enum.map(fn {key, value} ->
@@ -136,21 +137,12 @@ def describe do
Enum.map(value, fn
{pattern, replacement} ->
%{
- "pattern" =>
- if not is_binary(pattern) do
- inspect(pattern)
- else
- pattern
- end,
+ "pattern" => Utils.describe_regex_or_string(pattern),
"replacement" => replacement
}
pattern ->
- if not is_binary(pattern) do
- inspect(pattern)
- else
- pattern
- end
+ Utils.describe_regex_or_string(pattern)
end)}
end)
|> Enum.into(%{})
diff --git a/lib/pleroma/web/activity_pub/mrf/utils.ex b/lib/pleroma/web/activity_pub/mrf/utils.ex
new file mode 100644
index 000000000..f2dc9eea9
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/utils.ex
@@ -0,0 +1,15 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.Utils do
+ @spec describe_regex_or_string(String.t() | Regex.t()) :: String.t()
+ def describe_regex_or_string(pattern) do
+ # This horror is needed to convert regex sigils to strings
+ if not is_binary(pattern) do
+ inspect(pattern)
+ else
+ pattern
+ end
+ end
+end
diff --git a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs
new file mode 100644
index 000000000..7350800f0
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs
@@ -0,0 +1,425 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicyTest do
+ use Pleroma.DataCase
+
+ require Pleroma.Constants
+
+ alias Pleroma.Web.ActivityPub.MRF
+ alias Pleroma.Web.ActivityPub.MRF.EmojiPolicy
+
+ setup do: clear_config(:mrf_emoji)
+
+ setup do
+ clear_config([:mrf_emoji], %{
+ remove_url: [],
+ remove_shortcode: [],
+ federated_timeline_removal_url: [],
+ federated_timeline_removal_shortcode: []
+ })
+ end
+
+ @emoji_tags [
+ %{
+ "icon" => %{
+ "type" => "Image",
+ "url" => "https://example.org/emoji/biribiri/mikoto_smile2.png"
+ },
+ "id" => "https://example.org/emoji/biribiri/mikoto_smile2.png",
+ "name" => ":mikoto_smile2:",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z"
+ },
+ %{
+ "icon" => %{
+ "type" => "Image",
+ "url" => "https://example.org/emoji/biribiri/mikoto_smile3.png"
+ },
+ "id" => "https://example.org/emoji/biribiri/mikoto_smile3.png",
+ "name" => ":mikoto_smile3:",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z"
+ },
+ %{
+ "icon" => %{
+ "type" => "Image",
+ "url" => "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ },
+ "id" => "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png",
+ "name" => ":nekomimi_girl_emoji_007:",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z"
+ },
+ %{
+ "icon" => %{
+ "type" => "Image",
+ "url" => "https://example.org/test.png"
+ },
+ "id" => "https://example.org/test.png",
+ "name" => ":test:",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z"
+ }
+ ]
+
+ @misc_tags [%{"type" => "Placeholder"}]
+
+ @user_data %{
+ "type" => "Person",
+ "id" => "https://example.org/placeholder",
+ "name" => "lol",
+ "tag" => @emoji_tags ++ @misc_tags
+ }
+
+ @status_data %{
+ "type" => "Create",
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://example.org/placeholder",
+ "content" => "lol",
+ "tag" => @emoji_tags ++ @misc_tags,
+ "emoji" => %{
+ "mikoto_smile2" => "https://example.org/emoji/biribiri/mikoto_smile2.png",
+ "mikoto_smile3" => "https://example.org/emoji/biribiri/mikoto_smile3.png",
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png",
+ "test" => "https://example.org/test.png"
+ },
+ "to" => ["https://example.org/self", Pleroma.Constants.as_public()],
+ "cc" => ["https://example.org/someone"]
+ },
+ "to" => ["https://example.org/self", Pleroma.Constants.as_public()],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ @status_data_with_history %{
+ "type" => "Create",
+ "object" =>
+ @status_data["object"]
+ |> Map.merge(%{
+ "formerRepresentations" => %{
+ "type" => "OrderedCollection",
+ "orderedItems" => [@status_data["object"] |> Map.put("content", "older")],
+ "totalItems" => 1
+ }
+ }),
+ "to" => ["https://example.org/self", Pleroma.Constants.as_public()],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ @emoji_react_data %{
+ "type" => "EmojiReact",
+ "tag" => [@emoji_tags |> Enum.at(3)],
+ "object" => "https://example.org/someobject",
+ "to" => ["https://example.org/self"],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ @emoji_react_data_matching_regex %{
+ "type" => "EmojiReact",
+ "tag" => [@emoji_tags |> Enum.at(1)],
+ "object" => "https://example.org/someobject",
+ "to" => ["https://example.org/self"],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ @emoji_react_data_matching_nothing %{
+ "type" => "EmojiReact",
+ "tag" => [@emoji_tags |> Enum.at(2)],
+ "object" => "https://example.org/someobject",
+ "to" => ["https://example.org/self"],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ @emoji_react_data_unicode %{
+ "type" => "EmojiReact",
+ "content" => "😍",
+ "object" => "https://example.org/someobject",
+ "to" => ["https://example.org/self"],
+ "cc" => ["https://example.org/someone"]
+ }
+
+ describe "remove_url" do
+ setup do
+ clear_config([:mrf_emoji, :remove_url], [
+ "https://example.org/test.png",
+ ~r{/biribiri/mikoto_smile[23]\.png},
+ "nekomimi_girl_emoji"
+ ])
+
+ :ok
+ end
+
+ test "processes user" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @user_data)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ assert %{"tag" => ^expected_tags} = filtered
+ end
+
+ test "processes status" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{"object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}} = filtered
+ end
+
+ test "processes status with history" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data_with_history)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{
+ "object" => %{
+ "tag" => ^expected_tags,
+ "emoji" => ^expected_emoji,
+ "formerRepresentations" => %{"orderedItems" => [item]}
+ }
+ } = filtered
+
+ assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item
+ end
+
+ test "processes updates" do
+ {:ok, filtered} =
+ MRF.filter_one(EmojiPolicy, @status_data_with_history |> Map.put("type", "Update"))
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{
+ "object" => %{
+ "tag" => ^expected_tags,
+ "emoji" => ^expected_emoji,
+ "formerRepresentations" => %{"orderedItems" => [item]}
+ }
+ } = filtered
+
+ assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item
+ end
+
+ test "processes EmojiReact" do
+ assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data)
+
+ assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_regex)
+
+ assert {:ok, @emoji_react_data_matching_nothing} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_nothing)
+
+ assert {:ok, @emoji_react_data_unicode} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_unicode)
+ end
+ end
+
+ describe "remove_shortcode" do
+ setup do
+ clear_config([:mrf_emoji, :remove_shortcode], [
+ "test",
+ ~r{mikoto_s},
+ "nekomimi_girl_emoji"
+ ])
+
+ :ok
+ end
+
+ test "processes user" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @user_data)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ assert %{"tag" => ^expected_tags} = filtered
+ end
+
+ test "processes status" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{"object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}} = filtered
+ end
+
+ test "processes status with history" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data_with_history)
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{
+ "object" => %{
+ "tag" => ^expected_tags,
+ "emoji" => ^expected_emoji,
+ "formerRepresentations" => %{"orderedItems" => [item]}
+ }
+ } = filtered
+
+ assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item
+ end
+
+ test "processes updates" do
+ {:ok, filtered} =
+ MRF.filter_one(EmojiPolicy, @status_data_with_history |> Map.put("type", "Update"))
+
+ expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags
+
+ expected_emoji = %{
+ "nekomimi_girl_emoji_007" =>
+ "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png"
+ }
+
+ assert %{
+ "object" => %{
+ "tag" => ^expected_tags,
+ "emoji" => ^expected_emoji,
+ "formerRepresentations" => %{"orderedItems" => [item]}
+ }
+ } = filtered
+
+ assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item
+ end
+
+ test "processes EmojiReact" do
+ assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data)
+
+ assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_regex)
+
+ assert {:ok, @emoji_react_data_matching_nothing} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_nothing)
+
+ assert {:ok, @emoji_react_data_unicode} ==
+ MRF.filter_one(EmojiPolicy, @emoji_react_data_unicode)
+ end
+ end
+
+ describe "federated_timeline_removal_url" do
+ setup do
+ clear_config([:mrf_emoji, :federated_timeline_removal_url], [
+ "https://example.org/test.png",
+ ~r{/biribiri/mikoto_smile[23]\.png},
+ "nekomimi_girl_emoji"
+ ])
+
+ :ok
+ end
+
+ test "processes status" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data)
+
+ expected_tags = @status_data["object"]["tag"]
+ expected_emoji = @status_data["object"]["emoji"]
+
+ expected_to = ["https://example.org/self"]
+ expected_cc = [Pleroma.Constants.as_public(), "https://example.org/someone"]
+
+ assert %{
+ "to" => ^expected_to,
+ "cc" => ^expected_cc,
+ "object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}
+ } = filtered
+ end
+
+ test "ignore updates" do
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data |> Map.put("type", "Update"))
+
+ expected_tags = @status_data["object"]["tag"]
+ expected_emoji = @status_data["object"]["emoji"]
+
+ expected_to = ["https://example.org/self", Pleroma.Constants.as_public()]
+ expected_cc = ["https://example.org/someone"]
+
+ assert %{
+ "to" => ^expected_to,
+ "cc" => ^expected_cc,
+ "object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}
+ } = filtered
+ end
+
+ test "processes status with history" do
+ status =
+ @status_data_with_history
+ |> put_in(["object", "tag"], @misc_tags)
+ |> put_in(["object", "emoji"], %{})
+
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, status)
+
+ expected_tags = @status_data["object"]["tag"]
+ expected_emoji = @status_data["object"]["emoji"]
+
+ expected_to = ["https://example.org/self"]
+ expected_cc = [Pleroma.Constants.as_public(), "https://example.org/someone"]
+
+ assert %{
+ "to" => ^expected_to,
+ "cc" => ^expected_cc,
+ "object" => %{
+ "formerRepresentations" => %{
+ "orderedItems" => [%{"tag" => ^expected_tags, "emoji" => ^expected_emoji}]
+ }
+ }
+ } = filtered
+ end
+ end
+
+ describe "edge cases" do
+ setup do
+ clear_config([:mrf_emoji, :remove_url], [
+ "https://example.org/test.png",
+ ~r{/biribiri/mikoto_smile[23]\.png},
+ "nekomimi_girl_emoji"
+ ])
+
+ :ok
+ end
+
+ test "non-statuses" do
+ answer = @status_data |> put_in(["object", "type"], "Answer")
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, answer)
+
+ assert filtered == answer
+ end
+
+ test "without tag" do
+ status = @status_data |> Map.put("object", Map.drop(@status_data["object"], ["tag"]))
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, status)
+
+ refute Map.has_key?(filtered["object"], "tag")
+ end
+
+ test "without emoji" do
+ status = @status_data |> Map.put("object", Map.drop(@status_data["object"], ["emoji"]))
+ {:ok, filtered} = MRF.filter_one(EmojiPolicy, status)
+
+ refute Map.has_key?(filtered["object"], "emoji")
+ end
+ end
+end
diff --git a/test/pleroma/web/activity_pub/mrf/utils_test.exs b/test/pleroma/web/activity_pub/mrf/utils_test.exs
new file mode 100644
index 000000000..3bbc2cfd3
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/utils_test.exs
@@ -0,0 +1,19 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.UtilsTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Web.ActivityPub.MRF.Utils
+
+ describe "describe_regex_or_string/1" do
+ test "describes regex" do
+ assert "~r/foo/i" == Utils.describe_regex_or_string(~r/foo/i)
+ end
+
+ test "returns string as-is" do
+ assert "foo" == Utils.describe_regex_or_string("foo")
+ end
+ end
+end