29f3221d1a8d1c9718d68578fea5675e0d57d3eb
[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 = URI.parse(inbox).host
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 host: host,
60 "content-length": byte_size(json),
61 digest: digest,
62 date: date
63 })
64
65 with {:ok, %{status: code}} when code in 200..299 <-
66 result =
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 !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
78 do: Instances.set_reachable(inbox)
79
80 result
81 else
82 {_post_result, response} ->
83 unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
84 {:error, response}
85 end
86 end
87
88 def publish_one(%{actor_id: actor_id} = params) do
89 actor = User.get_by_id(actor_id)
90
91 params
92 |> Map.delete(:actor_id)
93 |> Map.put(:actor, actor)
94 |> publish_one()
95 end
96
97 defp should_federate?(inbox, public) do
98 if public do
99 true
100 else
101 %{host: host} = URI.parse(inbox)
102
103 quarantined_instances =
104 Config.get([:instance, :quarantined_instances], [])
105 |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
106
107 !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
108 end
109 end
110
111 @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
112 defp recipients(actor, activity) do
113 {:ok, followers} =
114 if actor.follower_address in activity.recipients do
115 User.get_external_followers(actor)
116 else
117 {:ok, []}
118 end
119
120 Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers
121 end
122
123 defp get_cc_ap_ids(ap_id, recipients) do
124 host = Map.get(URI.parse(ap_id), :host)
125
126 recipients
127 |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
128 |> Enum.map(& &1.ap_id)
129 end
130
131 defp maybe_use_sharedinbox(%User{info: %{source_data: data}}),
132 do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
133
134 @doc """
135 Determine a user inbox to use based on heuristics. These heuristics
136 are based on an approximation of the ``sharedInbox`` rules in the
137 [ActivityPub specification][ap-sharedinbox].
138
139 Please do not edit this function (or its children) without reading
140 the spec, as editing the code is likely to introduce some breakage
141 without some familiarity.
142
143 [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
144 """
145 def determine_inbox(
146 %Activity{data: activity_data},
147 %User{info: %{source_data: data}} = user
148 ) do
149 to = activity_data["to"] || []
150 cc = activity_data["cc"] || []
151 type = activity_data["type"]
152
153 cond do
154 type == "Delete" ->
155 maybe_use_sharedinbox(user)
156
157 Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
158 maybe_use_sharedinbox(user)
159
160 length(to) + length(cc) > 1 ->
161 maybe_use_sharedinbox(user)
162
163 true ->
164 data["inbox"]
165 end
166 end
167
168 @doc """
169 Publishes an activity with BCC to all relevant peers.
170 """
171
172 def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
173 when is_list(bcc) and bcc != [] do
174 public = is_public?(activity)
175 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
176
177 recipients = recipients(actor, activity)
178
179 recipients
180 |> Enum.filter(&User.ap_enabled?/1)
181 |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end)
182 |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
183 |> Instances.filter_reachable()
184 |> Enum.each(fn {inbox, unreachable_since} ->
185 %User{ap_id: ap_id} =
186 Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end)
187
188 # Get all the recipients on the same host and add them to cc. Otherwise, a remote
189 # instance would only accept a first message for the first recipient and ignore the rest.
190 cc = get_cc_ap_ids(ap_id, recipients)
191
192 json =
193 data
194 |> Map.put("cc", cc)
195 |> Jason.encode!()
196
197 Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
198 inbox: inbox,
199 json: json,
200 actor_id: actor.id,
201 id: activity.data["id"],
202 unreachable_since: unreachable_since
203 })
204 end)
205 end
206
207 @doc """
208 Publishes an activity to all relevant peers.
209 """
210 def publish(%User{} = actor, %Activity{} = activity) do
211 public = is_public?(activity)
212
213 if public && Config.get([:instance, :allow_relay]) do
214 Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
215 Relay.publish(activity)
216 end
217
218 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
219 json = Jason.encode!(data)
220
221 recipients(actor, activity)
222 |> Enum.filter(fn user -> User.ap_enabled?(user) end)
223 |> Enum.map(fn %User{} = user ->
224 determine_inbox(activity, user)
225 end)
226 |> Enum.uniq()
227 |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
228 |> Instances.filter_reachable()
229 |> Enum.each(fn {inbox, unreachable_since} ->
230 Pleroma.Web.Federator.Publisher.enqueue_one(
231 __MODULE__,
232 %{
233 inbox: inbox,
234 json: json,
235 actor_id: actor.id,
236 id: activity.data["id"],
237 unreachable_since: unreachable_since
238 }
239 )
240 end)
241 end
242
243 def gather_webfinger_links(%User{} = user) do
244 [
245 %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
246 %{
247 "rel" => "self",
248 "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
249 "href" => user.ap_id
250 }
251 ]
252 end
253
254 def gather_nodeinfo_protocol_names, do: ["activitypub"]
255 end