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 defp check_accept(%{host: actor_host} = _actor_info, object) do
18 instance_list(:accept)
19 |> MRF.subdomains_regex()
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"}
29 defp check_reject(%{host: actor_host} = _actor_info, object) 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 check_object(%{"object" => object} = activity) do
182 with {:ok, _object} <- filter(object) do
187 defp check_object(object), do: {:ok, object}
189 defp instance_list(config_key) do
190 Config.get([:mrf_simple, config_key])
191 |> MRF.instance_list_from_tuples()
195 def filter(%{"type" => "Delete", "actor" => actor} = object) do
196 %{host: actor_host} = URI.parse(actor)
199 instance_list(:reject_deletes)
200 |> MRF.subdomains_regex()
202 if MRF.subdomain_match?(reject_deletes, actor_host) do
203 {:reject, "[SimplePolicy] host in reject_deletes list"}
210 def filter(%{"actor" => actor} = object) do
211 actor_info = URI.parse(actor)
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
223 {:reject, nil} -> {:reject, "[SimplePolicy]"}
224 {:reject, _} = e -> e
225 _ -> {:reject, "[SimplePolicy]"}
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)
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
239 {:reject, nil} -> {:reject, "[SimplePolicy]"}
240 {:reject, _} = e -> e
241 _ -> {:reject, "[SimplePolicy]"}
245 def filter(object) when is_binary(object) do
246 uri = URI.parse(object)
248 with {:ok, object} <- check_accept(uri, object),
249 {:ok, object} <- check_reject(uri, object) do
252 {:reject, nil} -> {:reject, "[SimplePolicy]"}
253 {:reject, _} = e -> e
254 _ -> {:reject, "[SimplePolicy]"}
258 def filter(object), do: {:ok, object}
260 defp obfuscate(string) when is_binary(string) do
269 if 3 <= index && index < String.length(string) - 3, do: ?*, else: char
274 defp maybe_obfuscate(host, obfuscations) do
275 if MRF.subdomain_match?(obfuscations, host) do
284 exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples()
287 Config.get([:mrf, :transparency_obfuscate_domains], []) |> MRF.subdomains_regex()
289 mrf_simple_excluded =
290 Config.get(:mrf_simple)
291 |> Enum.map(fn {rule, instances} ->
292 {rule, Enum.reject(instances, fn {host, _} -> host in exclusions end)}
297 |> Enum.map(fn {rule, instances} ->
298 {rule, Enum.map(instances, fn {host, _} -> maybe_obfuscate(host, obfuscations) end)}
302 # This is for backwards compatibility. We originally didn't sent
303 # extra info like a reason why an instance was rejected/quarantined/etc.
304 # Because we didn't want to break backwards compatibility it was decided
305 # to add an extra "info" key.
308 |> Enum.map(fn {rule, instances} ->
309 {rule, Enum.reject(instances, fn {_, reason} -> reason == "" end)}
311 |> Enum.reject(fn {_, instances} -> instances == [] end)
312 |> Enum.map(fn {rule, instances} ->
315 |> Enum.map(fn {host, reason} ->
316 {maybe_obfuscate(host, obfuscations), %{"reason" => reason}}
324 {:ok, %{mrf_simple: mrf_simple, mrf_simple_info: mrf_simple_info}}
328 def config_description do
331 related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
333 description: "Simple ingress policies",
339 "List of instances to strip media attachments from and the reason for doing so"
345 "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
348 key: :federated_timeline_removal,
350 "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
355 "List of instances to reject activities from (except deletes) and the reason for doing so"
360 "List of instances to only accept activities from (except deletes) and the reason for doing so"
363 key: :followers_only,
365 "Force posts from the given instances to be visible by followers only and the reason for doing so"
368 key: :report_removal,
369 description: "List of instances to reject reports from and the reason for doing so"
372 key: :avatar_removal,
373 description: "List of instances to strip avatars from and the reason for doing so"
376 key: :banner_removal,
377 description: "List of instances to strip banners from and the reason for doing so"
380 key: :reject_deletes,
381 description: "List of instances to reject deletions from and the reason for doing so"
384 |> Enum.map(fn setting ->
388 type: {:list, :tuple},
389 key_placeholder: "instance",
390 value_placeholder: "reason",
391 suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}]