move to 2.5.5
[anni] / lib / pleroma / web / activity_pub / mrf / simple_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.SimplePolicy do
6   @moduledoc "Filter activities depending on their origin instance"
7   @behaviour Pleroma.Web.ActivityPub.MRF.Policy
8
9   alias Pleroma.Config
10   alias Pleroma.FollowingRelationship
11   alias Pleroma.User
12   alias Pleroma.Web.ActivityPub.MRF
13
14   require Pleroma.Constants
15
16   defp check_accept(%{host: actor_host} = _actor_info, object) do
17     accepts =
18       instance_list(:accept)
19       |> MRF.subdomains_regex()
20
21     cond do
22       accepts == [] -> {:ok, object}
23       actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
24       MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
25       true -> {:reject, "[SimplePolicy] host not in accept list"}
26     end
27   end
28
29   defp check_reject(%{host: actor_host} = _actor_info, object) do
30     rejects =
31       instance_list(:reject)
32       |> MRF.subdomains_regex()
33
34     if MRF.subdomain_match?(rejects, actor_host) do
35       {:reject, "[SimplePolicy] host in reject list"}
36     else
37       {:ok, object}
38     end
39   end
40
41   defp check_media_removal(
42          %{host: actor_host} = _actor_info,
43          %{"type" => type, "object" => %{"attachment" => child_attachment}} = object
44        )
45        when length(child_attachment) > 0 and type in ["Create", "Update"] do
46     media_removal =
47       instance_list(:media_removal)
48       |> MRF.subdomains_regex()
49
50     object =
51       if MRF.subdomain_match?(media_removal, actor_host) do
52         child_object = Map.delete(object["object"], "attachment")
53         Map.put(object, "object", child_object)
54       else
55         object
56       end
57
58     {:ok, object}
59   end
60
61   defp check_media_removal(_actor_info, object), do: {:ok, object}
62
63   defp check_media_nsfw(
64          %{host: actor_host} = _actor_info,
65          %{
66            "type" => type,
67            "object" => %{} = _child_object
68          } = object
69        )
70        when type in ["Create", "Update"] do
71     media_nsfw =
72       instance_list(:media_nsfw)
73       |> MRF.subdomains_regex()
74
75     object =
76       if MRF.subdomain_match?(media_nsfw, actor_host) do
77         Kernel.put_in(object, ["object", "sensitive"], true)
78       else
79         object
80       end
81
82     {:ok, object}
83   end
84
85   defp check_media_nsfw(_actor_info, object), do: {:ok, object}
86
87   defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
88     timeline_removal =
89       instance_list(:federated_timeline_removal)
90       |> MRF.subdomains_regex()
91
92     object =
93       with true <- MRF.subdomain_match?(timeline_removal, actor_host),
94            user <- User.get_cached_by_ap_id(object["actor"]),
95            true <- Pleroma.Constants.as_public() in object["to"] do
96         to = List.delete(object["to"], Pleroma.Constants.as_public()) ++ [user.follower_address]
97
98         cc = List.delete(object["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
99
100         object
101         |> Map.put("to", to)
102         |> Map.put("cc", cc)
103       else
104         _ -> object
105       end
106
107     {:ok, object}
108   end
109
110   defp intersection(list1, list2) do
111     list1 -- list1 -- list2
112   end
113
114   defp check_followers_only(%{host: actor_host} = _actor_info, object) do
115     followers_only =
116       instance_list(:followers_only)
117       |> MRF.subdomains_regex()
118
119     object =
120       with true <- MRF.subdomain_match?(followers_only, actor_host),
121            user <- User.get_cached_by_ap_id(object["actor"]) do
122         # Don't use Map.get/3 intentionally, these must not be nil
123         fixed_to = object["to"] || []
124         fixed_cc = object["cc"] || []
125
126         to = FollowingRelationship.followers_ap_ids(user, fixed_to)
127         cc = FollowingRelationship.followers_ap_ids(user, fixed_cc)
128
129         object
130         |> Map.put("to", intersection([user.follower_address | to], fixed_to))
131         |> Map.put("cc", intersection([user.follower_address | cc], fixed_cc))
132       else
133         _ -> object
134       end
135
136     {:ok, object}
137   end
138
139   defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do
140     report_removal =
141       instance_list(:report_removal)
142       |> MRF.subdomains_regex()
143
144     if MRF.subdomain_match?(report_removal, actor_host) do
145       {:reject, "[SimplePolicy] host in report_removal list"}
146     else
147       {:ok, object}
148     end
149   end
150
151   defp check_report_removal(_actor_info, object), do: {:ok, object}
152
153   defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do
154     avatar_removal =
155       instance_list(:avatar_removal)
156       |> MRF.subdomains_regex()
157
158     if MRF.subdomain_match?(avatar_removal, actor_host) do
159       {:ok, Map.delete(object, "icon")}
160     else
161       {:ok, object}
162     end
163   end
164
165   defp check_avatar_removal(_actor_info, object), do: {:ok, object}
166
167   defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do
168     banner_removal =
169       instance_list(:banner_removal)
170       |> MRF.subdomains_regex()
171
172     if MRF.subdomain_match?(banner_removal, actor_host) do
173       {:ok, Map.delete(object, "image")}
174     else
175       {:ok, object}
176     end
177   end
178
179   defp check_banner_removal(_actor_info, object), do: {:ok, object}
180
181   defp check_object(%{"object" => object} = activity) do
182     with {:ok, _object} <- filter(object) do
183       {:ok, activity}
184     end
185   end
186
187   defp check_object(object), do: {:ok, object}
188
189   defp instance_list(config_key) do
190     Config.get([:mrf_simple, config_key])
191     |> MRF.instance_list_from_tuples()
192   end
193
194   @impl true
195   def filter(%{"type" => "Delete", "actor" => actor} = object) do
196     %{host: actor_host} = URI.parse(actor)
197
198     reject_deletes =
199       instance_list(:reject_deletes)
200       |> MRF.subdomains_regex()
201
202     if MRF.subdomain_match?(reject_deletes, actor_host) do
203       {:reject, "[SimplePolicy] host in reject_deletes list"}
204     else
205       {:ok, object}
206     end
207   end
208
209   @impl true
210   def filter(%{"actor" => actor} = object) do
211     actor_info = URI.parse(actor)
212
213     with {:ok, object} <- check_accept(actor_info, object),
214          {:ok, object} <- check_reject(actor_info, object),
215          {:ok, object} <- check_media_removal(actor_info, object),
216          {:ok, object} <- check_media_nsfw(actor_info, object),
217          {:ok, object} <- check_ftl_removal(actor_info, object),
218          {:ok, object} <- check_followers_only(actor_info, object),
219          {:ok, object} <- check_report_removal(actor_info, object),
220          {:ok, object} <- check_object(object) do
221       {:ok, object}
222     else
223       {:reject, nil} -> {:reject, "[SimplePolicy]"}
224       {:reject, _} = e -> e
225       _ -> {:reject, "[SimplePolicy]"}
226     end
227   end
228
229   def filter(%{"id" => actor, "type" => obj_type} = object)
230       when obj_type in ["Application", "Group", "Organization", "Person", "Service"] do
231     actor_info = URI.parse(actor)
232
233     with {:ok, object} <- check_accept(actor_info, object),
234          {:ok, object} <- check_reject(actor_info, object),
235          {:ok, object} <- check_avatar_removal(actor_info, object),
236          {:ok, object} <- check_banner_removal(actor_info, object) do
237       {:ok, object}
238     else
239       {:reject, nil} -> {:reject, "[SimplePolicy]"}
240       {:reject, _} = e -> e
241       _ -> {:reject, "[SimplePolicy]"}
242     end
243   end
244
245   def filter(object) when is_binary(object) do
246     uri = URI.parse(object)
247
248     with {:ok, object} <- check_accept(uri, object),
249          {:ok, object} <- check_reject(uri, object) do
250       {:ok, object}
251     else
252       {:reject, nil} -> {:reject, "[SimplePolicy]"}
253       {:reject, _} = e -> e
254       _ -> {:reject, "[SimplePolicy]"}
255     end
256   end
257
258   def filter(object), do: {:ok, object}
259
260   @impl true
261   def describe do
262     exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples()
263
264     mrf_simple_excluded =
265       Config.get(:mrf_simple)
266       |> Enum.map(fn {rule, instances} ->
267         {rule, Enum.reject(instances, fn {host, _} -> host in exclusions end)}
268       end)
269
270     mrf_simple =
271       mrf_simple_excluded
272       |> Enum.map(fn {rule, instances} ->
273         {rule, Enum.map(instances, fn {host, _} -> host end)}
274       end)
275       |> Map.new()
276
277     # This is for backwards compatibility. We originally didn't sent
278     # extra info like a reason why an instance was rejected/quarantined/etc.
279     # Because we didn't want to break backwards compatibility it was decided
280     # to add an extra "info" key.
281     mrf_simple_info =
282       mrf_simple_excluded
283       |> Enum.map(fn {rule, instances} ->
284         {rule, Enum.reject(instances, fn {_, reason} -> reason == "" end)}
285       end)
286       |> Enum.reject(fn {_, instances} -> instances == [] end)
287       |> Enum.map(fn {rule, instances} ->
288         instances =
289           instances
290           |> Enum.map(fn {host, reason} -> {host, %{"reason" => reason}} end)
291           |> Map.new()
292
293         {rule, instances}
294       end)
295       |> Map.new()
296
297     {:ok, %{mrf_simple: mrf_simple, mrf_simple_info: mrf_simple_info}}
298   end
299
300   @impl true
301   def config_description do
302     %{
303       key: :mrf_simple,
304       related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
305       label: "MRF Simple",
306       description: "Simple ingress policies",
307       children:
308         [
309           %{
310             key: :media_removal,
311             description:
312               "List of instances to strip media attachments from and the reason for doing so"
313           },
314           %{
315             key: :media_nsfw,
316             label: "Media NSFW",
317             description:
318               "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
319           },
320           %{
321             key: :federated_timeline_removal,
322             description:
323               "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
324           },
325           %{
326             key: :reject,
327             description:
328               "List of instances to reject activities from (except deletes) and the reason for doing so"
329           },
330           %{
331             key: :accept,
332             description:
333               "List of instances to only accept activities from (except deletes) and the reason for doing so"
334           },
335           %{
336             key: :followers_only,
337             description:
338               "Force posts from the given instances to be visible by followers only and the reason for doing so"
339           },
340           %{
341             key: :report_removal,
342             description: "List of instances to reject reports from and the reason for doing so"
343           },
344           %{
345             key: :avatar_removal,
346             description: "List of instances to strip avatars from and the reason for doing so"
347           },
348           %{
349             key: :banner_removal,
350             description: "List of instances to strip banners from and the reason for doing so"
351           },
352           %{
353             key: :reject_deletes,
354             description: "List of instances to reject deletions from and the reason for doing so"
355           }
356         ]
357         |> Enum.map(fn setting ->
358           Map.merge(
359             setting,
360             %{
361               type: {:list, :tuple},
362               key_placeholder: "instance",
363               value_placeholder: "reason",
364               suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}]
365             }
366           )
367         end)
368     }
369   end
370 end