2023-02-28 15:40:24 +00:00
|
|
|
# Pleroma: A lightweight social networking server
|
|
|
|
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
|
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
|
|
defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|
|
|
require Pleroma.Constants
|
|
|
|
|
2023-02-28 16:47:53 +00:00
|
|
|
alias Pleroma.Object.Updater
|
|
|
|
|
2023-02-28 15:40:24 +00:00
|
|
|
@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
|
|
|
|
|
2023-02-28 15:51:56 +00:00
|
|
|
defp config_remove_shortcode do
|
|
|
|
Pleroma.Config.get([:mrf_emoji, :remove_shortcode], [])
|
|
|
|
end
|
|
|
|
|
2023-02-28 16:47:53 +00:00
|
|
|
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
|
|
|
|
|
2023-02-28 15:40:24 +00:00
|
|
|
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
|
|
|
def filter(%{"type" => type, "object" => %{} = object} = message)
|
|
|
|
when type in ["Create", "Update"] do
|
2023-02-28 16:47:53 +00:00
|
|
|
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}
|
2023-02-28 15:40:24 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
|
|
|
def filter(%{"type" => type} = object) when type in Pleroma.Constants.actor_types() do
|
2023-02-28 15:51:56 +00:00
|
|
|
with object <- process_remove(object, :url, config_remove_url()),
|
|
|
|
object <- process_remove(object, :shortcode, config_remove_shortcode()) do
|
2023-02-28 15:40:24 +00:00
|
|
|
{: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
|
|
|
|
|
2023-02-28 16:47:53 +00:00
|
|
|
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
|
|
|
|
|
2023-02-28 15:40:24 +00:00
|
|
|
defp process_remove(object, :url, patterns) do
|
2023-02-28 16:47:53 +00:00
|
|
|
process_remove_impl(object, &url_from_tag/1, &url_from_emoji/1, patterns)
|
2023-02-28 15:51:56 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
defp process_remove(object, :shortcode, patterns) do
|
2023-02-28 16:47:53 +00:00
|
|
|
process_remove_impl(object, &shortcode_from_tag/1, &shortcode_from_emoji/1, patterns)
|
2023-02-28 15:51:56 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) do
|
2023-02-28 15:40:24 +00:00
|
|
|
processed_tag =
|
|
|
|
Enum.filter(
|
|
|
|
object["tag"],
|
|
|
|
fn
|
2023-02-28 15:51:56 +00:00
|
|
|
%{"type" => "Emoji"} = tag ->
|
|
|
|
str = extract_from_tag.(tag)
|
|
|
|
|
|
|
|
if is_binary(str) do
|
|
|
|
not match_any?(str, patterns)
|
|
|
|
else
|
|
|
|
true
|
|
|
|
end
|
2023-02-28 15:40:24 +00:00
|
|
|
|
|
|
|
_ ->
|
|
|
|
true
|
|
|
|
end
|
|
|
|
)
|
|
|
|
|
|
|
|
processed_emoji =
|
|
|
|
if object["emoji"] do
|
|
|
|
object["emoji"]
|
2023-02-28 15:51:56 +00:00
|
|
|
|> Enum.reduce(%{}, fn {name, url} = emoji, acc ->
|
|
|
|
if not match_any?(extract_from_emoji.(emoji), patterns) do
|
2023-02-28 15:40:24 +00:00
|
|
|
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
|
|
|
|
|
2023-02-28 16:47:53 +00:00
|
|
|
defp maybe_delist(%{"object" => object, "to" => to, "type" => "Create"} = activity) do
|
|
|
|
check = fn object ->
|
|
|
|
if any_emoji_match?(object, &url_from_tag/1, &url_from_emoji/1, config_unlist_url()) or
|
|
|
|
any_emoji_match?(
|
|
|
|
object,
|
|
|
|
&shortcode_from_tag/1,
|
|
|
|
&shortcode_from_emoji/1,
|
|
|
|
config_unlist_shortcode()
|
|
|
|
) do
|
|
|
|
{:should_delist, nil}
|
|
|
|
else
|
|
|
|
{:ok, %{}}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2023-02-28 15:40:24 +00:00
|
|
|
@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/`.
|
|
|
|
""",
|
2023-02-28 17:14:48 +00:00
|
|
|
suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu]
|
2023-02-28 15:40:24 +00:00
|
|
|
},
|
|
|
|
%{
|
|
|
|
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/`.
|
|
|
|
""",
|
2023-02-28 17:14:48 +00:00
|
|
|
suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu]
|
2023-02-28 15:40:24 +00:00
|
|
|
},
|
|
|
|
%{
|
|
|
|
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
|