Merge branch 'remove/mastofe' 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.instance_list_from_tuples()
116 |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
117
118 !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
119 end
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 public = is_public?(activity)
197 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
198
199 recipients = recipients(actor, activity)
200
201 inboxes =
202 recipients
203 |> Enum.filter(&User.ap_enabled?/1)
204 |> Enum.map(fn actor -> actor.inbox end)
205 |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
206 |> Instances.filter_reachable()
207
208 Repo.checkout(fn ->
209 Enum.each(inboxes, fn {inbox, unreachable_since} ->
210 %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
211
212 # Get all the recipients on the same host and add them to cc. Otherwise, a remote
213 # instance would only accept a first message for the first recipient and ignore the rest.
214 cc = get_cc_ap_ids(ap_id, recipients)
215
216 json =
217 data
218 |> Map.put("cc", cc)
219 |> Jason.encode!()
220
221 Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
222 inbox: inbox,
223 json: json,
224 actor_id: actor.id,
225 id: activity.data["id"],
226 unreachable_since: unreachable_since
227 })
228 end)
229 end)
230 end
231
232 # Publishes an activity to all relevant peers.
233 def publish(%User{} = actor, %Activity{} = activity) do
234 public = is_public?(activity)
235
236 if public && Config.get([:instance, :allow_relay]) do
237 Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)
238 Relay.publish(activity)
239 end
240
241 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
242 json = Jason.encode!(data)
243
244 recipients(actor, activity)
245 |> Enum.filter(fn user -> User.ap_enabled?(user) end)
246 |> Enum.map(fn %User{} = user ->
247 determine_inbox(activity, user)
248 end)
249 |> Enum.uniq()
250 |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
251 |> Instances.filter_reachable()
252 |> Enum.each(fn {inbox, unreachable_since} ->
253 Pleroma.Web.Federator.Publisher.enqueue_one(
254 __MODULE__,
255 %{
256 inbox: inbox,
257 json: json,
258 actor_id: actor.id,
259 id: activity.data["id"],
260 unreachable_since: unreachable_since
261 }
262 )
263 end)
264 end
265
266 def gather_webfinger_links(%User{} = user) do
267 [
268 %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
269 %{
270 "rel" => "self",
271 "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
272 "href" => user.ap_id
273 },
274 %{
275 "rel" => "http://ostatus.org/schema/1.0/subscribe",
276 "template" => "#{Pleroma.Web.Endpoint.url()}/ostatus_subscribe?acct={uri}"
277 }
278 ]
279 end
280
281 def gather_nodeinfo_protocol_names, do: ["activitypub"]
282 end