1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
6 @moduledoc "Filter activities depending on their origin instance"
7 @behaviour Pleroma.Web.ActivityPub.MRF.Policy
10 alias Pleroma.FollowingRelationship
12 alias Pleroma.Web.ActivityPub.MRF
14 require Pleroma.Constants
16 def check_accept(%{host: actor_host} = _actor_info) do
18 instance_list(:accept)
19 |> MRF.subdomains_regex()
22 accepts == [] -> {:ok, nil}
23 actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, nil}
24 MRF.subdomain_match?(accepts, actor_host) -> {:ok, nil}
25 true -> {:reject, "[SimplePolicy] host not in accept list"}
29 def check_reject(%{host: actor_host} = _actor_info) do
31 instance_list(:reject)
32 |> MRF.subdomains_regex()
34 if MRF.subdomain_match?(rejects, actor_host) do
35 {:reject, "[SimplePolicy] host in reject list"}
41 defp check_media_removal(
42 %{host: actor_host} = _actor_info,
43 %{"type" => type, "object" => %{"attachment" => child_attachment}} = object
45 when type in ["Create", "Update"] and length(child_attachment) > 0 do
47 instance_list(:media_removal)
48 |> MRF.subdomains_regex()
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)
61 defp check_media_removal(_actor_info, object), do: {:ok, object}
63 defp check_media_nsfw(
64 %{host: actor_host} = _actor_info,
67 "object" => %{} = _child_object
70 when type in ["Create", "Update"] do
72 instance_list(:media_nsfw)
73 |> MRF.subdomains_regex()
76 if MRF.subdomain_match?(media_nsfw, actor_host) do
77 Kernel.put_in(object, ["object", "sensitive"], true)
85 defp check_media_nsfw(_actor_info, object), do: {:ok, object}
87 defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
89 instance_list(:federated_timeline_removal)
90 |> MRF.subdomains_regex()
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]
98 cc = List.delete(object["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
110 defp intersection(list1, list2) do
111 list1 -- list1 -- list2
114 defp check_followers_only(%{host: actor_host} = _actor_info, object) do
116 instance_list(:followers_only)
117 |> MRF.subdomains_regex()
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"] || []
126 to = FollowingRelationship.followers_ap_ids(user, fixed_to)
127 cc = FollowingRelationship.followers_ap_ids(user, fixed_cc)
130 |> Map.put("to", intersection([user.follower_address | to], fixed_to))
131 |> Map.put("cc", intersection([user.follower_address | cc], fixed_cc))
139 defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do
141 instance_list(:report_removal)
142 |> MRF.subdomains_regex()
144 if MRF.subdomain_match?(report_removal, actor_host) do
145 {:reject, "[SimplePolicy] host in report_removal list"}
151 defp check_report_removal(_actor_info, object), do: {:ok, object}
153 defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do
155 instance_list(:avatar_removal)
156 |> MRF.subdomains_regex()
158 if MRF.subdomain_match?(avatar_removal, actor_host) do
159 {:ok, Map.delete(object, "icon")}
165 defp check_avatar_removal(_actor_info, object), do: {:ok, object}
167 defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do
169 instance_list(:banner_removal)
170 |> MRF.subdomains_regex()
172 if MRF.subdomain_match?(banner_removal, actor_host) do
173 {:ok, Map.delete(object, "image")}
179 defp check_banner_removal(_actor_info, object), do: {:ok, object}
181 defp extract_context_uri(%{"conversation" => "tag:" <> rest}) do
183 |> String.split(",", parts: 2, trim: true)
187 hostname -> URI.parse("//" <> hostname)
191 defp extract_context_uri(%{"context" => "http" <> _ = context}), do: URI.parse(context)
193 defp extract_context_uri(_), do: nil
195 defp check_context(activity) do
196 uri = extract_context_uri(activity)
198 with {:uri, true} <- {:uri, Kernel.match?(%URI{}, uri)},
199 {:ok, _} <- check_accept(uri),
200 {:ok, _} <- check_reject(uri) do
204 {:uri, false} -> {:ok, activity}
205 {:reject, nil} -> {:reject, "[SimplePolicy]"}
206 {:reject, _} = e -> e
207 _ -> {:reject, "[SimplePolicy]"}
211 defp check_reply_to(%{"object" => %{"inReplyTo" => in_reply_to}} = activity) do
212 with {:ok, _} <- filter(in_reply_to) do
217 defp check_reply_to(activity), do: {:ok, activity}
219 defp maybe_check_thread(activity) do
220 if Config.get([:mrf_simple, :handle_threads], true) do
221 with {:ok, _} <- check_context(activity),
222 {:ok, _} <- check_reply_to(activity) do
230 defp check_object(%{"object" => object} = activity) do
231 with {:ok, _object} <- filter(object) do
236 defp check_object(object), do: {:ok, object}
238 defp instance_list(config_key) do
239 Config.get([:mrf_simple, config_key])
240 |> MRF.instance_list_from_tuples()
244 def filter(%{"type" => "Delete", "actor" => actor} = object) do
245 %{host: actor_host} = URI.parse(actor)
248 instance_list(:reject_deletes)
249 |> MRF.subdomains_regex()
251 if MRF.subdomain_match?(reject_deletes, actor_host) do
252 {:reject, "[SimplePolicy] host in reject_deletes list"}
259 def filter(%{"actor" => actor} = object) do
260 actor_info = URI.parse(actor)
262 with {:ok, _} <- check_accept(actor_info),
263 {:ok, _} <- check_reject(actor_info),
264 {:ok, object} <- check_media_removal(actor_info, object),
265 {:ok, object} <- check_media_nsfw(actor_info, object),
266 {:ok, object} <- check_ftl_removal(actor_info, object),
267 {:ok, object} <- check_followers_only(actor_info, object),
268 {:ok, object} <- check_report_removal(actor_info, object),
269 {:ok, object} <- maybe_check_thread(object),
270 {:ok, object} <- check_object(object) do
273 {:reject, nil} -> {:reject, "[SimplePolicy]"}
274 {:reject, _} = e -> e
275 _ -> {:reject, "[SimplePolicy]"}
279 def filter(%{"id" => actor, "type" => obj_type} = object)
280 when obj_type in ["Application", "Group", "Organization", "Person", "Service"] do
281 actor_info = URI.parse(actor)
283 with {:ok, _} <- check_accept(actor_info),
284 {:ok, _} <- check_reject(actor_info),
285 {:ok, object} <- check_avatar_removal(actor_info, object),
286 {:ok, object} <- check_banner_removal(actor_info, object) do
289 {:reject, nil} -> {:reject, "[SimplePolicy]"}
290 {:reject, _} = e -> e
291 _ -> {:reject, "[SimplePolicy]"}
295 def filter(%{"id" => id} = object) do
296 with {:ok, _} <- filter(id) do
301 def filter(object) when is_binary(object) do
302 uri = URI.parse(object)
304 with {:ok, _} <- check_accept(uri),
305 {:ok, _} <- check_reject(uri) do
308 {:reject, nil} -> {:reject, "[SimplePolicy]"}
309 {:reject, _} = e -> e
310 _ -> {:reject, "[SimplePolicy]"}
314 def filter(object), do: {:ok, object}
316 defp obfuscate(string) when is_binary(string) do
325 if 3 <= index && index < String.length(string) - 3, do: ?*, else: char
330 defp maybe_obfuscate(host, obfuscations) do
331 if MRF.subdomain_match?(obfuscations, host) do
340 exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples()
343 Config.get([:mrf, :transparency_obfuscate_domains], []) |> MRF.subdomains_regex()
345 mrf_simple_excluded =
346 Config.get(:mrf_simple)
347 |> Enum.filter(fn {_, v} -> is_list(v) end)
348 |> Enum.map(fn {rule, instances} ->
349 {rule, Enum.reject(instances, fn {host, _} -> host in exclusions end)}
354 |> Enum.map(fn {rule, instances} ->
355 {rule, Enum.map(instances, fn {host, _} -> maybe_obfuscate(host, obfuscations) end)}
359 # This is for backwards compatibility. We originally didn't sent
360 # extra info like a reason why an instance was rejected/quarantined/etc.
361 # Because we didn't want to break backwards compatibility it was decided
362 # to add an extra "info" key.
365 |> Enum.map(fn {rule, instances} ->
366 {rule, Enum.reject(instances, fn {_, reason} -> reason == "" end)}
368 |> Enum.reject(fn {_, instances} -> instances == [] end)
369 |> Enum.map(fn {rule, instances} ->
372 |> Enum.map(fn {host, reason} ->
373 {maybe_obfuscate(host, obfuscations), %{"reason" => reason}}
381 {:ok, %{mrf_simple: mrf_simple, mrf_simple_info: mrf_simple_info}}
385 def config_description do
388 related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
390 description: "Simple ingress policies",
396 "List of instances to strip media attachments from and the reason for doing so"
402 "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
405 key: :federated_timeline_removal,
407 "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
412 "List of instances to reject activities from (except deletes) and the reason for doing so"
417 "List of instances to only accept activities from (except deletes) and the reason for doing so"
420 key: :followers_only,
422 "Force posts from the given instances to be visible by followers only and the reason for doing so"
425 key: :report_removal,
426 description: "List of instances to reject reports from and the reason for doing so"
429 key: :avatar_removal,
430 description: "List of instances to strip avatars from and the reason for doing so"
433 key: :banner_removal,
434 description: "List of instances to strip banners from and the reason for doing so"
437 key: :reject_deletes,
438 description: "List of instances to reject deletions from and the reason for doing so"
441 |> Enum.map(fn setting ->
445 type: {:list, :tuple},
446 key_placeholder: "instance",
447 value_placeholder: "reason",
449 {"example.com", "Some reason"},
450 {"*.example.com", "Another reason"}
457 key: :handle_threads,
458 label: "Apply to entire threads",
461 "Enable to filter replies to threads based from their originating instance, using the reject and accept rules"