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