From 28ff828caaada329283a37feb3940b543ef260a3 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 28 Feb 2023 10:40:24 -0500 Subject: [PATCH] Add emoji policy to remove emojis matching certain urls https://git.pleroma.social/pleroma/pleroma/-/issues/2775 --- config/config.exs | 6 + lib/pleroma/web/activity_pub/emoji_policy.ex | 158 ++++++++++++++++++ .../activity_pub/mrf/emoji_policy_test.exs | 123 ++++++++++++++ 3 files changed, 287 insertions(+) create mode 100644 lib/pleroma/web/activity_pub/emoji_policy.ex create mode 100644 test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs 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 @@ config :pleroma, :mrf_keyword, 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/lib/pleroma/web/activity_pub/emoji_policy.ex b/lib/pleroma/web/activity_pub/emoji_policy.ex new file mode 100644 index 000000000..747e720b6 --- /dev/null +++ b/lib/pleroma/web/activity_pub/emoji_policy.ex @@ -0,0 +1,158 @@ +# 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 + + @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 + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def filter(%{"type" => type, "object" => %{} = object} = message) + when type in ["Create", "Update"] do + with object <- process_remove(object, :url, config_remove_url()) do + {:ok, Map.put(message, "object", object)} + 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()) do + {:ok, object} + 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 process_remove(object, :url, patterns) do + processed_tag = + Enum.filter( + object["tag"], + fn + %{"type" => "Emoji", "icon" => %{"url" => url}} when is_binary(url) -> + not match_any?(url, patterns) + + _ -> + true + end + ) + + processed_emoji = + if object["emoji"] do + object["emoji"] + |> Enum.reduce(%{}, fn {name, url}, acc -> + if not match_any?(url, patterns) do + Map.put(acc, name, url) + else + acc + end + end) + else + nil + end + + if processed_emoji do + object + |> Map.put("tag", processed_tag) + |> Map.put("emoji", processed_emoji) + else + object + |> Map.put("tag", processed_tag) + end + end + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def describe do + # This horror is needed to convert regex sigils to strings + mrf_emoji = + Pleroma.Config.get(:mrf_emoji, []) + |> Enum.map(fn {key, value} -> + {key, + Enum.map(value, fn + pattern -> + if not is_binary(pattern) do + inspect(pattern) + else + pattern + end + end)} + 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 both statuses 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: :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 both statuses 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: ["foo", ~r/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/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..fe1fb338e --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs @@ -0,0 +1,123 @@ +# 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 + + 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" + } + } + } + + 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 + end +end