Merge remote-tracking branch 'remotes/upstream/develop' into 1149-oban-job-queue
[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.HTTP
9 alias Pleroma.Instances
10 alias Pleroma.User
11 alias Pleroma.Web.ActivityPub.Relay
12 alias Pleroma.Web.ActivityPub.Transmogrifier
13
14 require Pleroma.Constants
15
16 import Pleroma.Web.ActivityPub.Visibility
17
18 @behaviour Pleroma.Web.Federator.Publisher
19
20 require Logger
21
22 @moduledoc """
23 ActivityPub outgoing federation module.
24 """
25
26 @doc """
27 Determine if an activity can be represented by running it through Transmogrifier.
28 """
29 def is_representable?(%Activity{} = activity) do
30 with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
31 true
32 else
33 _e ->
34 false
35 end
36 end
37
38 @doc """
39 Publish a single message to a peer. Takes a struct with the following
40 parameters set:
41
42 * `inbox`: the inbox to publish to
43 * `json`: the JSON message body representing the ActivityPub message
44 * `actor`: the actor which is signing the message
45 * `id`: the ActivityStreams URI of the message
46 """
47 def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
48 Logger.info("Federating #{id} to #{inbox}")
49 %{host: host, path: path} = URI.parse(inbox)
50
51 digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
52
53 date =
54 NaiveDateTime.utc_now()
55 |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
56
57 signature =
58 Pleroma.Signature.sign(actor, %{
59 "(request-target)": "post #{path}",
60 host: host,
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 !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
79 do: Instances.set_reachable(inbox)
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_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 should_federate?(inbox, public) do
99 if public do
100 true
101 else
102 %{host: host} = URI.parse(inbox)
103
104 quarantined_instances =
105 Config.get([:instance, :quarantined_instances], [])
106 |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
107
108 !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
109 end
110 end
111
112 @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
113 defp recipients(actor, activity) do
114 {:ok, followers} =
115 if actor.follower_address in activity.recipients do
116 User.get_external_followers(actor)
117 else
118 {:ok, []}
119 end
120
121 Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers
122 end
123
124 defp get_cc_ap_ids(ap_id, recipients) do
125 host = Map.get(URI.parse(ap_id), :host)
126
127 recipients
128 |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
129 |> Enum.map(& &1.ap_id)
130 end
131
132 defp maybe_use_sharedinbox(%User{info: %{source_data: data}}),
133 do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
134
135 @doc """
136 Determine a user inbox to use based on heuristics. These heuristics
137 are based on an approximation of the ``sharedInbox`` rules in the
138 [ActivityPub specification][ap-sharedinbox].
139
140 Please do not edit this function (or its children) without reading
141 the spec, as editing the code is likely to introduce some breakage
142 without some familiarity.
143
144 [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
145 """
146 def determine_inbox(
147 %Activity{data: activity_data},
148 %User{info: %{source_data: data}} = user
149 ) do
150 to = activity_data["to"] || []
151 cc = activity_data["cc"] || []
152 type = activity_data["type"]
153
154 cond do
155 type == "Delete" ->
156 maybe_use_sharedinbox(user)
157
158 Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
159 maybe_use_sharedinbox(user)
160
161 length(to) + length(cc) > 1 ->
162 maybe_use_sharedinbox(user)
163
164 true ->
165 data["inbox"]
166 end
167 end
168
169 @doc """
170 Publishes an activity with BCC to all relevant peers.
171 """
172
173 def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
174 when is_list(bcc) and bcc != [] do
175 public = is_public?(activity)
176 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
177
178 recipients = recipients(actor, activity)
179
180 recipients
181 |> Enum.filter(&User.ap_enabled?/1)
182 |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end)
183 |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
184 |> Instances.filter_reachable()
185 |> Enum.each(fn {inbox, unreachable_since} ->
186 %User{ap_id: ap_id} =
187 Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end)
188
189 # Get all the recipients on the same host and add them to cc. Otherwise, a remote
190 # instance would only accept a first message for the first recipient and ignore the rest.
191 cc = get_cc_ap_ids(ap_id, recipients)
192
193 json =
194 data
195 |> Map.put("cc", cc)
196 |> Jason.encode!()
197
198 Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
199 inbox: inbox,
200 json: json,
201 actor_id: actor.id,
202 id: activity.data["id"],
203 unreachable_since: unreachable_since
204 })
205 end)
206 end
207
208 @doc """
209 Publishes an activity to all relevant peers.
210 """
211 def publish(%User{} = actor, %Activity{} = activity) do
212 public = is_public?(activity)
213
214 if public && Config.get([:instance, :allow_relay]) do
215 Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
216 Relay.publish(activity)
217 end
218
219 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
220 json = Jason.encode!(data)
221
222 recipients(actor, activity)
223 |> Enum.filter(fn user -> User.ap_enabled?(user) end)
224 |> Enum.map(fn %User{} = user ->
225 determine_inbox(activity, user)
226 end)
227 |> Enum.uniq()
228 |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
229 |> Instances.filter_reachable()
230 |> Enum.each(fn {inbox, unreachable_since} ->
231 Pleroma.Web.Federator.Publisher.enqueue_one(
232 __MODULE__,
233 %{
234 inbox: inbox,
235 json: json,
236 actor_id: actor.id,
237 id: activity.data["id"],
238 unreachable_since: unreachable_since
239 }
240 )
241 end)
242 end
243
244 def gather_webfinger_links(%User{} = user) do
245 [
246 %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
247 %{
248 "rel" => "self",
249 "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
250 "href" => user.ap_id
251 }
252 ]
253 end
254
255 def gather_nodeinfo_protocol_names, do: ["activitypub"]
256 end