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" => "Create", "object" => %{"attachment" => child_attachment}} = object
45 when 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
71 instance_list(:media_nsfw)
72 |> MRF.subdomains_regex()
75 if MRF.subdomain_match?(media_nsfw, actor_host) do
76 Kernel.put_in(object, ["object", "sensitive"], true)
84 defp check_media_nsfw(_actor_info, object), do: {:ok, object}
86 defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
88 instance_list(:federated_timeline_removal)
89 |> MRF.subdomains_regex()
92 with true <- MRF.subdomain_match?(timeline_removal, actor_host),
93 user <- User.get_cached_by_ap_id(object["actor"]),
94 true <- Pleroma.Constants.as_public() in object["to"] do
95 to = List.delete(object["to"], Pleroma.Constants.as_public()) ++ [user.follower_address]
97 cc = List.delete(object["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
109 defp intersection(list1, list2) do
110 list1 -- list1 -- list2
113 defp check_followers_only(%{host: actor_host} = _actor_info, object) do
115 instance_list(:followers_only)
116 |> MRF.subdomains_regex()
119 with true <- MRF.subdomain_match?(followers_only, actor_host),
120 user <- User.get_cached_by_ap_id(object["actor"]) do
121 # Don't use Map.get/3 intentionally, these must not be nil
122 fixed_to = object["to"] || []
123 fixed_cc = object["cc"] || []
125 to = FollowingRelationship.followers_ap_ids(user, fixed_to)
126 cc = FollowingRelationship.followers_ap_ids(user, fixed_cc)
129 |> Map.put("to", intersection([user.follower_address | to], fixed_to))
130 |> Map.put("cc", intersection([user.follower_address | cc], fixed_cc))
138 defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do
140 instance_list(:report_removal)
141 |> MRF.subdomains_regex()
143 if MRF.subdomain_match?(report_removal, actor_host) do
144 {:reject, "[SimplePolicy] host in report_removal list"}
150 defp check_report_removal(_actor_info, object), do: {:ok, object}
152 defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do
154 instance_list(:avatar_removal)
155 |> MRF.subdomains_regex()
157 if MRF.subdomain_match?(avatar_removal, actor_host) do
158 {:ok, Map.delete(object, "icon")}
164 defp check_avatar_removal(_actor_info, object), do: {:ok, object}
166 defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do
168 instance_list(:banner_removal)
169 |> MRF.subdomains_regex()
171 if MRF.subdomain_match?(banner_removal, actor_host) do
172 {:ok, Map.delete(object, "image")}
178 defp check_banner_removal(_actor_info, object), do: {:ok, object}
180 defp check_object(%{"object" => object} = activity) do
181 with {:ok, _object} <- filter(object) do
186 defp check_object(object), do: {:ok, object}
188 defp instance_list(config_key) do
189 Config.get([:mrf_simple, config_key])
190 |> MRF.instance_list_from_tuples()
194 def filter(%{"type" => "Delete", "actor" => actor} = object) do
195 %{host: actor_host} = URI.parse(actor)
198 instance_list(:reject_deletes)
199 |> MRF.subdomains_regex()
201 if MRF.subdomain_match?(reject_deletes, actor_host) do
202 {:reject, "[SimplePolicy] host in reject_deletes list"}
209 def filter(%{"actor" => actor} = object) do
210 actor_info = URI.parse(actor)
212 with {:ok, object} <- check_accept(actor_info, object),
213 {:ok, object} <- check_reject(actor_info, object),
214 {:ok, object} <- check_media_removal(actor_info, object),
215 {:ok, object} <- check_media_nsfw(actor_info, object),
216 {:ok, object} <- check_ftl_removal(actor_info, object),
217 {:ok, object} <- check_followers_only(actor_info, object),
218 {:ok, object} <- check_report_removal(actor_info, object),
219 {:ok, object} <- check_object(object) do
222 {:reject, nil} -> {:reject, "[SimplePolicy]"}
223 {:reject, _} = e -> e
224 _ -> {:reject, "[SimplePolicy]"}
228 def filter(%{"id" => actor, "type" => obj_type} = object)
229 when obj_type in ["Application", "Group", "Organization", "Person", "Service"] do
230 actor_info = URI.parse(actor)
232 with {:ok, object} <- check_accept(actor_info, object),
233 {:ok, object} <- check_reject(actor_info, object),
234 {:ok, object} <- check_avatar_removal(actor_info, object),
235 {:ok, object} <- check_banner_removal(actor_info, object) do
238 {:reject, nil} -> {:reject, "[SimplePolicy]"}
239 {:reject, _} = e -> e
240 _ -> {:reject, "[SimplePolicy]"}
244 def filter(object) when is_binary(object) do
245 uri = URI.parse(object)
247 with {:ok, object} <- check_accept(uri, object),
248 {:ok, object} <- check_reject(uri, object) do
251 {:reject, nil} -> {:reject, "[SimplePolicy]"}
252 {:reject, _} = e -> e
253 _ -> {:reject, "[SimplePolicy]"}
257 def filter(object), do: {:ok, object}
259 defp obfuscate(string) when is_binary(string) do
268 if 3 <= index && index < String.length(string) - 3, do: ?*, else: char
273 defp maybe_obfuscate(host, obfuscations) do
274 if MRF.subdomain_match?(obfuscations, host) do
283 exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples()
286 Config.get([:mrf, :transparency_obfuscate_domains], []) |> MRF.subdomains_regex()
288 mrf_simple_excluded =
289 Config.get(:mrf_simple)
290 |> Enum.map(fn {rule, instances} ->
291 {rule, Enum.reject(instances, fn {host, _} -> host in exclusions end)}
296 |> Enum.map(fn {rule, instances} ->
297 {rule, Enum.map(instances, fn {host, _} -> maybe_obfuscate(host, obfuscations) end)}
301 # This is for backwards compatibility. We originally didn't sent
302 # extra info like a reason why an instance was rejected/quarantined/etc.
303 # Because we didn't want to break backwards compatibility it was decided
304 # to add an extra "info" key.
307 |> Enum.map(fn {rule, instances} ->
308 {rule, Enum.reject(instances, fn {_, reason} -> reason == "" end)}
310 |> Enum.reject(fn {_, instances} -> instances == [] end)
311 |> Enum.map(fn {rule, instances} ->
314 |> Enum.map(fn {host, reason} ->
315 {maybe_obfuscate(host, obfuscations), %{"reason" => reason}}
323 {:ok, %{mrf_simple: mrf_simple, mrf_simple_info: mrf_simple_info}}
327 def config_description do
330 related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
332 description: "Simple ingress policies",
338 "List of instances to strip media attachments from and the reason for doing so"
344 "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
347 key: :federated_timeline_removal,
349 "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
354 "List of instances to reject activities from (except deletes) and the reason for doing so"
359 "List of instances to only accept activities from (except deletes) and the reason for doing so"
362 key: :followers_only,
364 "Force posts from the given instances to be visible by followers only and the reason for doing so"
367 key: :report_removal,
368 description: "List of instances to reject reports from and the reason for doing so"
371 key: :avatar_removal,
372 description: "List of instances to strip avatars from and the reason for doing so"
375 key: :banner_removal,
376 description: "List of instances to strip banners from and the reason for doing so"
379 key: :reject_deletes,
380 description: "List of instances to reject deletions from and the reason for doing so"
383 |> Enum.map(fn setting ->
387 type: {:list, :tuple},
388 key_placeholder: "instance",
389 value_placeholder: "reason",
390 suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}]