First
[anni] / lib / pleroma / web / activity_pub / mrf / keyword_policy.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
6   require Pleroma.Constants
7
8   @moduledoc "Reject or Word-Replace messages with a keyword or regex"
9
10   @behaviour Pleroma.Web.ActivityPub.MRF.Policy
11   defp string_matches?(string, _) when not is_binary(string) do
12     false
13   end
14
15   defp string_matches?(string, pattern) when is_binary(pattern) do
16     String.contains?(string, pattern)
17   end
18
19   defp string_matches?(string, pattern) do
20     String.match?(string, pattern)
21   end
22
23   defp object_payload(%{} = object) do
24     [object["content"], object["summary"], object["name"]]
25     |> Enum.filter(& &1)
26     |> Enum.join("\n")
27   end
28
29   defp check_reject(%{"object" => %{} = object} = message) do
30     with {:ok, _new_object} <-
31            Pleroma.Object.Updater.do_with_history(object, fn object ->
32              payload = object_payload(object)
33
34              if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
35                   string_matches?(payload, pattern)
36                 end) do
37                {:reject, "[KeywordPolicy] Matches with rejected keyword"}
38              else
39                {:ok, message}
40              end
41            end) do
42       {:ok, message}
43     else
44       e -> e
45     end
46   end
47
48   defp check_ftl_removal(%{"type" => "Create", "to" => to, "object" => %{} = object} = message) do
49     check_keyword = fn object ->
50       payload = object_payload(object)
51
52       if Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
53            string_matches?(payload, pattern)
54          end) do
55         {:should_delist, nil}
56       else
57         {:ok, %{}}
58       end
59     end
60
61     should_delist? = fn object ->
62       with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check_keyword) do
63         false
64       else
65         _ -> true
66       end
67     end
68
69     if Pleroma.Constants.as_public() in to and should_delist?.(object) do
70       to = List.delete(to, Pleroma.Constants.as_public())
71       cc = [Pleroma.Constants.as_public() | message["cc"] || []]
72
73       message =
74         message
75         |> Map.put("to", to)
76         |> Map.put("cc", cc)
77
78       {:ok, message}
79     else
80       {:ok, message}
81     end
82   end
83
84   defp check_ftl_removal(message) do
85     {:ok, message}
86   end
87
88   defp check_replace(%{"object" => %{} = object} = message) do
89     replace_kw = fn object ->
90       ["content", "name", "summary"]
91       |> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)
92       |> Enum.reduce(object, fn field, object ->
93         data =
94           Enum.reduce(
95             Pleroma.Config.get([:mrf_keyword, :replace]),
96             object[field],
97             fn {pat, repl}, acc -> String.replace(acc, pat, repl) end
98           )
99
100         Map.put(object, field, data)
101       end)
102       |> (fn object -> {:ok, object} end).()
103     end
104
105     {:ok, object} = Pleroma.Object.Updater.do_with_history(object, replace_kw)
106
107     message = Map.put(message, "object", object)
108
109     {:ok, message}
110   end
111
112   @impl true
113   def filter(%{"type" => type, "object" => %{"content" => _content}} = message)
114       when type in ["Create", "Update"] do
115     with {:ok, message} <- check_reject(message),
116          {:ok, message} <- check_ftl_removal(message),
117          {:ok, message} <- check_replace(message) do
118       {:ok, message}
119     else
120       {:reject, nil} -> {:reject, "[KeywordPolicy] "}
121       {:reject, _} = e -> e
122       _e -> {:reject, "[KeywordPolicy] "}
123     end
124   end
125
126   @impl true
127   def filter(message), do: {:ok, message}
128
129   @impl true
130   def describe do
131     # This horror is needed to convert regex sigils to strings
132     mrf_keyword =
133       Pleroma.Config.get(:mrf_keyword, [])
134       |> Enum.map(fn {key, value} ->
135         {key,
136          Enum.map(value, fn
137            {pattern, replacement} ->
138              %{
139                "pattern" =>
140                  if not is_binary(pattern) do
141                    inspect(pattern)
142                  else
143                    pattern
144                  end,
145                "replacement" => replacement
146              }
147
148            pattern ->
149              if not is_binary(pattern) do
150                inspect(pattern)
151              else
152                pattern
153              end
154          end)}
155       end)
156       |> Enum.into(%{})
157
158     {:ok, %{mrf_keyword: mrf_keyword}}
159   end
160
161   @impl true
162   def config_description do
163     %{
164       key: :mrf_keyword,
165       related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy",
166       label: "MRF Keyword",
167       description:
168         "Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
169       children: [
170         %{
171           key: :reject,
172           type: {:list, :string},
173           description: """
174             A list of patterns which result in message being rejected.
175
176             Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
177           """,
178           suggestions: ["foo", ~r/foo/iu]
179         },
180         %{
181           key: :federated_timeline_removal,
182           type: {:list, :string},
183           description: """
184             A list of patterns which result in message being removed from federated timelines (a.k.a unlisted).
185
186             Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
187           """,
188           suggestions: ["foo", ~r/foo/iu]
189         },
190         %{
191           key: :replace,
192           type: {:list, :tuple},
193           key_placeholder: "instance",
194           value_placeholder: "reason",
195           description: """
196             **Pattern**: a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
197
198             **Replacement**: a string. Leaving the field empty is permitted.
199           """
200         }
201       ]
202     }
203   end
204 end