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