b187d3a48040cee9c5b1b182c4ea28051fcef093
[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 blocked_instances do
107 Config.get([:instance, :quarantined_instances], []) ++
108 Config.get([:mrf_simple, :reject], [])
109 end
110
111 def should_federate?(url) do
112 %{host: host} = URI.parse(url)
113
114 quarantined_instances =
115 blocked_instances()
116 |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
117 |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
118
119 !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
120 end
121
122 @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
123 defp recipients(actor, activity) do
124 followers =
125 if actor.follower_address in activity.recipients do
126 User.get_external_followers(actor)
127 else
128 []
129 end
130
131 fetchers =
132 with %Activity{data: %{"type" => "Delete"}} <- activity,
133 %Object{id: object_id} <- Object.normalize(activity, fetch: false),
134 fetchers <- User.get_delivered_users_by_object_id(object_id),
135 _ <- Delivery.delete_all_by_object_id(object_id) do
136 fetchers
137 else
138 _ ->
139 []
140 end
141
142 Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
143 end
144
145 defp get_cc_ap_ids(ap_id, recipients) do
146 host = Map.get(URI.parse(ap_id), :host)
147
148 recipients
149 |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
150 |> Enum.map(& &1.ap_id)
151 end
152
153 defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
154 defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
155
156 @doc """
157 Determine a user inbox to use based on heuristics. These heuristics
158 are based on an approximation of the ``sharedInbox`` rules in the
159 [ActivityPub specification][ap-sharedinbox].
160
161 Please do not edit this function (or its children) without reading
162 the spec, as editing the code is likely to introduce some breakage
163 without some familiarity.
164
165 [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
166 """
167 def determine_inbox(
168 %Activity{data: activity_data},
169 %User{inbox: inbox} = user
170 ) do
171 to = activity_data["to"] || []
172 cc = activity_data["cc"] || []
173 type = activity_data["type"]
174
175 cond do
176 type == "Delete" ->
177 maybe_use_sharedinbox(user)
178
179 Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
180 maybe_use_sharedinbox(user)
181
182 length(to) + length(cc) > 1 ->
183 maybe_use_sharedinbox(user)
184
185 true ->
186 inbox
187 end
188 end
189
190 @doc """
191 Publishes an activity with BCC to all relevant peers.
192 """
193
194 def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
195 when is_list(bcc) and bcc != [] do
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) 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) 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