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