total rebase
[anni] / lib / pleroma / web / activity_pub / mrf / emoji_policy.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
6   require Pleroma.Constants
7
8   alias Pleroma.Object.Updater
9   alias Pleroma.Web.ActivityPub.MRF.Utils
10
11   @moduledoc "Reject or force-unlisted emojis with certain URLs or names"
12
13   @behaviour Pleroma.Web.ActivityPub.MRF.Policy
14
15   defp config_remove_url do
16     Pleroma.Config.get([:mrf_emoji, :remove_url], [])
17   end
18
19   defp config_remove_shortcode do
20     Pleroma.Config.get([:mrf_emoji, :remove_shortcode], [])
21   end
22
23   defp config_unlist_url do
24     Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_url], [])
25   end
26
27   defp config_unlist_shortcode do
28     Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_shortcode], [])
29   end
30
31   @impl Pleroma.Web.ActivityPub.MRF.Policy
32   def history_awareness, do: :manual
33
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
37     with {:ok, object} <-
38            Updater.do_with_history(object, fn object ->
39              {:ok, process_remove(object, :url, config_remove_url())}
40            end),
41          {:ok, object} <-
42            Updater.do_with_history(object, fn object ->
43              {:ok, process_remove(object, :shortcode, config_remove_shortcode())}
44            end),
45          activity <- Map.put(message, "object", object),
46          activity <- maybe_delist(activity) do
47       {:ok, activity}
48     end
49   end
50
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
55       {:ok, object}
56     end
57   end
58
59   @impl Pleroma.Web.ActivityPub.MRF.Policy
60   def filter(%{"type" => "EmojiReact"} = object) do
61     with {:ok, _} <-
62            matched_emoji_checker(config_remove_url(), config_remove_shortcode()).(object) do
63       {:ok, object}
64     else
65       _ ->
66         {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"}
67     end
68   end
69
70   @impl Pleroma.Web.ActivityPub.MRF.Policy
71   def filter(message) do
72     {:ok, message}
73   end
74
75   defp match_string?(string, pattern) when is_binary(pattern) do
76     string == pattern
77   end
78
79   defp match_string?(string, %Regex{} = pattern) do
80     String.match?(string, pattern)
81   end
82
83   defp match_any?(string, patterns) do
84     Enum.any?(patterns, &match_string?(string, &1))
85   end
86
87   defp url_from_tag(%{"icon" => %{"url" => url}}), do: url
88   defp url_from_tag(_), do: nil
89
90   defp url_from_emoji({_name, url}), do: url
91
92   defp shortcode_from_tag(%{"name" => name}) when is_binary(name), do: String.trim(name, ":")
93   defp shortcode_from_tag(_), do: nil
94
95   defp shortcode_from_emoji({name, _url}), do: name
96
97   defp process_remove(object, :url, patterns) do
98     process_remove_impl(object, &url_from_tag/1, &url_from_emoji/1, patterns)
99   end
100
101   defp process_remove(object, :shortcode, patterns) do
102     process_remove_impl(object, &shortcode_from_tag/1, &shortcode_from_emoji/1, patterns)
103   end
104
105   defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) do
106     object =
107       if object["tag"] do
108         Map.put(
109           object,
110           "tag",
111           Enum.filter(
112             object["tag"],
113             fn
114               %{"type" => "Emoji"} = tag ->
115                 str = extract_from_tag.(tag)
116
117                 if is_binary(str) do
118                   not match_any?(str, patterns)
119                 else
120                   true
121                 end
122
123               _ ->
124                 true
125             end
126           )
127         )
128       else
129         object
130       end
131
132     object =
133       if object["emoji"] do
134         Map.put(
135           object,
136           "emoji",
137           object["emoji"]
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)
141             else
142               acc
143             end
144           end)
145         )
146       else
147         object
148       end
149
150     object
151   end
152
153   defp matched_emoji_checker(urls, shortcodes) do
154     fn object ->
155       if any_emoji_match?(object, &url_from_tag/1, &url_from_emoji/1, urls) or
156            any_emoji_match?(
157              object,
158              &shortcode_from_tag/1,
159              &shortcode_from_emoji/1,
160              shortcodes
161            ) do
162         {:matched, nil}
163       else
164         {:ok, %{}}
165       end
166     end
167   end
168
169   defp maybe_delist(%{"object" => object, "to" => to, "type" => "Create"} = activity) do
170     check = matched_emoji_checker(config_unlist_url(), config_unlist_shortcode())
171
172     should_delist? = fn object ->
173       with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check) do
174         false
175       else
176         _ -> true
177       end
178     end
179
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"] || []]
183
184       activity
185       |> Map.put("to", to)
186       |> Map.put("cc", cc)
187     else
188       activity
189     end
190   end
191
192   defp maybe_delist(activity), do: activity
193
194   defp any_emoji_match?(object, extract_from_tag, extract_from_emoji, patterns) do
195     Kernel.||(
196       Enum.any?(
197         object["tag"] || [],
198         fn
199           %{"type" => "Emoji"} = tag ->
200             str = extract_from_tag.(tag)
201
202             if is_binary(str) do
203               match_any?(str, patterns)
204             else
205               false
206             end
207
208           _ ->
209             false
210         end
211       ),
212       (object["emoji"] || [])
213       |> Enum.any?(fn emoji -> match_any?(extract_from_emoji.(emoji), patterns) end)
214     )
215   end
216
217   @impl Pleroma.Web.ActivityPub.MRF.Policy
218   def describe do
219     mrf_emoji =
220       Pleroma.Config.get(:mrf_emoji, [])
221       |> Enum.map(fn {key, value} ->
222         {key, Enum.map(value, &Utils.describe_regex_or_string/1)}
223       end)
224       |> Enum.into(%{})
225
226     {:ok, %{mrf_emoji: mrf_emoji}}
227   end
228
229   @impl Pleroma.Web.ActivityPub.MRF.Policy
230   def config_description do
231     %{
232       key: :mrf_emoji,
233       related_policy: "Pleroma.Web.ActivityPub.MRF.EmojiPolicy",
234       label: "MRF Emoji",
235       description:
236         "Reject or force-unlisted emojis whose URLs or names match a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
237       children: [
238         %{
239           key: :remove_url,
240           type: {:list, :string},
241           description: """
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.
243
244             Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
245           """,
246           suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu]
247         },
248         %{
249           key: :remove_shortcode,
250           type: {:list, :string},
251           description: """
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.
253
254             Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
255           """,
256           suggestions: ["foo", ~r/foo/iu]
257         },
258         %{
259           key: :federated_timeline_removal_url,
260           type: {:list, :string},
261           description: """
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.
263
264             Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
265           """,
266           suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu]
267         },
268         %{
269           key: :federated_timeline_removal_shortcode,
270           type: {:list, :string},
271           description: """
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.
273
274             Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
275           """,
276           suggestions: ["foo", ~r/foo/iu]
277         }
278       ]
279     }
280   end
281 end