remove public post quarantine exception (#114)
[akkoma] / lib / pleroma / web / activity_pub / publisher.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.Publisher do
6 alias Pleroma.Activity
7 alias Pleroma.Config
8 alias Pleroma.Delivery
9 alias Pleroma.HTTP
10 alias Pleroma.Instances
11 alias Pleroma.Object
12 alias Pleroma.Repo
13 alias Pleroma.User
14 alias Pleroma.Web.ActivityPub.Relay
15 alias Pleroma.Web.ActivityPub.Transmogrifier
16
17 require Pleroma.Constants
18
19 import Pleroma.Web.ActivityPub.Visibility
20
21 @behaviour Pleroma.Web.Federator.Publisher
22
23 require Logger
24
25 @moduledoc """
26 ActivityPub outgoing federation module.
27 """
28
29 @doc """
30 Determine if an activity can be represented by running it through Transmogrifier.
31 """
32 def is_representable?(%Activity{} = activity) do
33 with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
34 true
35 else
36 _e ->
37 false
38 end
39 end
40
41 @doc """
42 Publish a single message to a peer. Takes a struct with the following
43 parameters set:
44
45 * `inbox`: the inbox to publish to
46 * `json`: the JSON message body representing the ActivityPub message
47 * `actor`: the actor which is signing the message
48 * `id`: the ActivityStreams URI of the message
49 """
50 def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
51 Logger.debug("Federating #{id} to #{inbox}")
52 uri = %{path: path} = URI.parse(inbox)
53 digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
54
55 date = Pleroma.Signature.signed_date()
56
57 signature =
58 Pleroma.Signature.sign(actor, %{
59 "(request-target)": "post #{path}",
60 host: signature_host(uri),
61 "content-length": byte_size(json),
62 digest: digest,
63 date: date
64 })
65
66 with {:ok, %{status: code}} = result when code in 200..299 <-
67 HTTP.post(
68 inbox,
69 json,
70 [
71 {"Content-Type", "application/activity+json"},
72 {"Date", date},
73 {"signature", signature},
74 {"digest", digest}
75 ]
76 ) do
77 if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
78 Instances.set_reachable(inbox)
79 end
80
81 result
82 else
83 {_post_result, response} ->
84 unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
85 {:error, response}
86 end
87 end
88
89 def publish_one(%{actor_id: actor_id} = params) do
90 actor = User.get_cached_by_id(actor_id)
91
92 params
93 |> Map.delete(:actor_id)
94 |> Map.put(:actor, actor)
95 |> publish_one()
96 end
97
98 defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
99 if port == URI.default_port(scheme) do
100 host
101 else
102 "#{host}:#{port}"
103 end
104 end
105
106 defp should_federate?(inbox) do
107 %{host: host} = URI.parse(inbox)
108
109 quarantined_instances =
110 Config.get([:instance, :quarantined_instances], [])
111 |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
112 |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
113
114 !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
115 end
116
117 @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
118 defp recipients(actor, activity) do
119 followers =
120 if actor.follower_address in activity.recipients do
121 User.get_external_followers(actor)
122 else
123 []
124 end
125
126 fetchers =
127 with %Activity{data: %{"type" => "Delete"}} <- activity,
128 %Object{id: object_id} <- Object.normalize(activity, fetch: false),
129 fetchers <- User.get_delivered_users_by_object_id(object_id),
130 _ <- Delivery.delete_all_by_object_id(object_id) do
131 fetchers
132 else
133 _ ->
134 []
135 end
136
137 Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
138 end
139
140 defp get_cc_ap_ids(ap_id, recipients) do
141 host = Map.get(URI.parse(ap_id), :host)
142
143 recipients
144 |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
145 |> Enum.map(& &1.ap_id)
146 end
147
148 defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
149 defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
150
151 @doc """
152 Determine a user inbox to use based on heuristics. These heuristics
153 are based on an approximation of the ``sharedInbox`` rules in the
154 [ActivityPub specification][ap-sharedinbox].
155
156 Please do not edit this function (or its children) without reading
157 the spec, as editing the code is likely to introduce some breakage
158 without some familiarity.
159
160 [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
161 """
162 def determine_inbox(
163 %Activity{data: activity_data},
164 %User{inbox: inbox} = user
165 ) do
166 to = activity_data["to"] || []
167 cc = activity_data["cc"] || []
168 type = activity_data["type"]
169
170 cond do
171 type == "Delete" ->
172 maybe_use_sharedinbox(user)
173
174 Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
175 maybe_use_sharedinbox(user)
176
177 length(to) + length(cc) > 1 ->
178 maybe_use_sharedinbox(user)
179
180 true ->
181 inbox
182 end
183 end
184
185 @doc """
186 Publishes an activity with BCC to all relevant peers.
187 """
188
189 def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
190 when is_list(bcc) and bcc != [] do
191 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
192
193 recipients = recipients(actor, activity)
194
195 inboxes =
196 recipients
197 |> Enum.filter(&User.ap_enabled?/1)
198 |> Enum.map(fn actor -> actor.inbox end)
199 |> Enum.filter(fn inbox -> should_federate?(inbox) end)
200 |> Instances.filter_reachable()
201
202 Repo.checkout(fn ->
203 Enum.each(inboxes, fn {inbox, unreachable_since} ->
204 %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
205
206 # Get all the recipients on the same host and add them to cc. Otherwise, a remote
207 # instance would only accept a first message for the first recipient and ignore the rest.
208 cc = get_cc_ap_ids(ap_id, recipients)
209
210 json =
211 data
212 |> Map.put("cc", cc)
213 |> Jason.encode!()
214
215 Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
216 inbox: inbox,
217 json: json,
218 actor_id: actor.id,
219 id: activity.data["id"],
220 unreachable_since: unreachable_since
221 })
222 end)
223 end)
224 end
225
226 # Publishes an activity to all relevant peers.
227 def publish(%User{} = actor, %Activity{} = activity) do
228 public = is_public?(activity)
229
230 if public && Config.get([:instance, :allow_relay]) do
231 Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)
232 Relay.publish(activity)
233 end
234
235 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
236 json = Jason.encode!(data)
237
238 recipients(actor, activity)
239 |> Enum.filter(fn user -> User.ap_enabled?(user) end)
240 |> Enum.map(fn %User{} = user ->
241 determine_inbox(activity, user)
242 end)
243 |> Enum.uniq()
244 |> Enum.filter(fn inbox -> should_federate?(inbox) end)
245 |> Instances.filter_reachable()
246 |> Enum.each(fn {inbox, unreachable_since} ->
247 Pleroma.Web.Federator.Publisher.enqueue_one(
248 __MODULE__,
249 %{
250 inbox: inbox,
251 json: json,
252 actor_id: actor.id,
253 id: activity.data["id"],
254 unreachable_since: unreachable_since
255 }
256 )
257 end)
258 end
259
260 def gather_webfinger_links(%User{} = user) do
261 [
262 %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
263 %{
264 "rel" => "self",
265 "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
266 "href" => user.ap_id
267 },
268 %{
269 "rel" => "http://ostatus.org/schema/1.0/subscribe",
270 "template" => "#{Pleroma.Web.Endpoint.url()}/ostatus_subscribe?acct={uri}"
271 }
272 ]
273 end
274
275 def gather_nodeinfo_protocol_names, do: ["activitypub"]
276 end