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