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}
261 exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples()
263 mrf_simple_excluded =
264 Config.get(:mrf_simple)
265 |> Enum.map(fn {rule, instances} ->
266 {rule, Enum.reject(instances, fn {host, _} -> host in exclusions end)}
271 |> Enum.map(fn {rule, instances} ->
272 {rule, Enum.map(instances, fn {host, _} -> host end)}
276 # This is for backwards compatibility. We originally didn't sent
277 # extra info like a reason why an instance was rejected/quarantined/etc.
278 # Because we didn't want to break backwards compatibility it was decided
279 # to add an extra "info" key.
282 |> Enum.map(fn {rule, instances} ->
283 {rule, Enum.reject(instances, fn {_, reason} -> reason == "" end)}
285 |> Enum.reject(fn {_, instances} -> instances == [] end)
286 |> Enum.map(fn {rule, instances} ->
289 |> Enum.map(fn {host, reason} -> {host, %{"reason" => reason}} end)
296 {:ok, %{mrf_simple: mrf_simple, mrf_simple_info: mrf_simple_info}}
300 def config_description do
303 related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
305 description: "Simple ingress policies",
311 "List of instances to strip media attachments from and the reason for doing so"
317 "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
320 key: :federated_timeline_removal,
322 "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
327 "List of instances to reject activities from (except deletes) and the reason for doing so"
332 "List of instances to only accept activities from (except deletes) and the reason for doing so"
335 key: :followers_only,
337 "Force posts from the given instances to be visible by followers only and the reason for doing so"
340 key: :report_removal,
341 description: "List of instances to reject reports from and the reason for doing so"
344 key: :avatar_removal,
345 description: "List of instances to strip avatars from and the reason for doing so"
348 key: :banner_removal,
349 description: "List of instances to strip banners from and the reason for doing so"
352 key: :reject_deletes,
353 description: "List of instances to reject deletions from and the reason for doing so"
356 |> Enum.map(fn setting ->
360 type: {:list, :tuple},
361 key_placeholder: "instance",
362 value_placeholder: "reason",
363 suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}]