Merge branch 'develop' into issue/1354
[akkoma] / lib / pleroma / web / activity_pub / publisher.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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 %{host: host, path: path} = URI.parse(inbox)
53
54 digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
55
56 date = Pleroma.Signature.signed_date()
57
58 signature =
59 Pleroma.Signature.sign(actor, %{
60 "(request-target)": "post #{path}",
61 host: host,
62 "content-length": byte_size(json),
63 digest: digest,
64 date: date
65 })
66
67 with {:ok, %{status: code}} when code in 200..299 <-
68 result =
69 HTTP.post(
70 inbox,
71 json,
72 [
73 {"Content-Type", "application/activity+json"},
74 {"Date", date},
75 {"signature", signature},
76 {"digest", digest}
77 ]
78 ) do
79 if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
80 do: Instances.set_reachable(inbox)
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 should_federate?(inbox, public) do
100 if public do
101 true
102 else
103 %{host: host} = URI.parse(inbox)
104
105 quarantined_instances =
106 Config.get([:instance, :quarantined_instances], [])
107 |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
108
109 !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
110 end
111 end
112
113 @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
114 defp recipients(actor, activity) do
115 followers =
116 if actor.follower_address in activity.recipients do
117 User.get_external_followers(actor)
118 else
119 []
120 end
121
122 fetchers =
123 with %Activity{data: %{"type" => "Delete"}} <- activity,
124 %Object{id: object_id} <- Object.normalize(activity),
125 fetchers <- User.get_delivered_users_by_object_id(object_id),
126 _ <- Delivery.delete_all_by_object_id(object_id) do
127 fetchers
128 else
129 _ ->
130 []
131 end
132
133 Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
134 end
135
136 defp get_cc_ap_ids(ap_id, recipients) do
137 host = Map.get(URI.parse(ap_id), :host)
138
139 recipients
140 |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
141 |> Enum.map(& &1.ap_id)
142 end
143
144 defp maybe_use_sharedinbox(%User{source_data: data}),
145 do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
146
147 @doc """
148 Determine a user inbox to use based on heuristics. These heuristics
149 are based on an approximation of the ``sharedInbox`` rules in the
150 [ActivityPub specification][ap-sharedinbox].
151
152 Please do not edit this function (or its children) without reading
153 the spec, as editing the code is likely to introduce some breakage
154 without some familiarity.
155
156 [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
157 """
158 def determine_inbox(
159 %Activity{data: activity_data},
160 %User{source_data: data} = user
161 ) do
162 to = activity_data["to"] || []
163 cc = activity_data["cc"] || []
164 type = activity_data["type"]
165
166 cond do
167 type == "Delete" ->
168 maybe_use_sharedinbox(user)
169
170 Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
171 maybe_use_sharedinbox(user)
172
173 length(to) + length(cc) > 1 ->
174 maybe_use_sharedinbox(user)
175
176 true ->
177 data["inbox"]
178 end
179 end
180
181 @doc """
182 Publishes an activity with BCC to all relevant peers.
183 """
184
185 def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
186 when is_list(bcc) and bcc != [] do
187 public = is_public?(activity)
188 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
189
190 recipients = recipients(actor, activity)
191
192 inboxes =
193 recipients
194 |> Enum.filter(&User.ap_enabled?/1)
195 |> Enum.map(fn %{source_data: data} -> data["inbox"] end)
196 |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
197 |> Instances.filter_reachable()
198
199 Repo.checkout(fn ->
200 Enum.each(inboxes, fn {inbox, unreachable_since} ->
201 %User{ap_id: ap_id} =
202 Enum.find(recipients, fn %{source_data: data} -> data["inbox"] == inbox end)
203
204 # Get all the recipients on the same host and add them to cc. Otherwise, a remote
205 # instance would only accept a first message for the first recipient and ignore the rest.
206 cc = get_cc_ap_ids(ap_id, recipients)
207
208 json =
209 data
210 |> Map.put("cc", cc)
211 |> Jason.encode!()
212
213 Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
214 inbox: inbox,
215 json: json,
216 actor_id: actor.id,
217 id: activity.data["id"],
218 unreachable_since: unreachable_since
219 })
220 end)
221 end)
222 end
223
224 @doc """
225 Publishes an activity to all relevant peers.
226 """
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, public) 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.base_url()}/ostatus_subscribe?acct={uri}"
271 }
272 ]
273 end
274
275 def gather_nodeinfo_protocol_names, do: ["activitypub"]
276 end