fix test
[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 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 Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers
113 end
114
115 defp get_cc_ap_ids(ap_id, recipients) do
116 host = Map.get(URI.parse(ap_id), :host)
117
118 recipients
119 |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
120 |> Enum.map(& &1.ap_id)
121 end
122
123 defp maybe_use_sharedinbox(%User{info: %{source_data: data}}),
124 do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
125
126 @doc """
127 Determine a user inbox to use based on heuristics. These heuristics
128 are based on an approximation of the ``sharedInbox`` rules in the
129 [ActivityPub specification][ap-sharedinbox].
130
131 Please do not edit this function (or its children) without reading
132 the spec, as editing the code is likely to introduce some breakage
133 without some familiarity.
134
135 [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
136 """
137 def determine_inbox(
138 %Activity{data: activity_data},
139 %User{info: %{source_data: data}} = user
140 ) do
141 to = activity_data["to"] || []
142 cc = activity_data["cc"] || []
143 type = activity_data["type"]
144
145 cond do
146 type == "Delete" ->
147 maybe_use_sharedinbox(user)
148
149 Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
150 maybe_use_sharedinbox(user)
151
152 length(to) + length(cc) > 1 ->
153 maybe_use_sharedinbox(user)
154
155 true ->
156 data["inbox"]
157 end
158 end
159
160 @doc """
161 Publishes an activity with BCC to all relevant peers.
162 """
163
164 def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do
165 public = is_public?(activity)
166 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
167
168 recipients = recipients(actor, activity)
169
170 recipients
171 |> Enum.filter(&User.ap_enabled?/1)
172 |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end)
173 |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
174 |> Instances.filter_reachable()
175 |> Enum.each(fn {inbox, unreachable_since} ->
176 %User{ap_id: ap_id} =
177 Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end)
178
179 # Get all the recipients on the same host and add them to cc. Otherwise, a remote
180 # instance would only accept a first message for the first recipient and ignore the rest.
181 cc = get_cc_ap_ids(ap_id, recipients)
182
183 json =
184 data
185 |> Map.put("cc", cc)
186 |> Jason.encode!()
187
188 Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
189 inbox: inbox,
190 json: json,
191 actor: actor,
192 id: activity.data["id"],
193 unreachable_since: unreachable_since
194 })
195 end)
196 end
197
198 @doc """
199 Publishes an activity to all relevant peers.
200 """
201 def publish(%User{} = actor, %Activity{} = activity) do
202 public = is_public?(activity)
203
204 if public && Config.get([:instance, :allow_relay]) do
205 Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
206 Relay.publish(activity)
207 end
208
209 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
210 json = Jason.encode!(data)
211
212 recipients(actor, activity)
213 |> Enum.filter(fn user -> User.ap_enabled?(user) end)
214 |> Enum.map(fn %User{} = user ->
215 determine_inbox(activity, user)
216 end)
217 |> Enum.uniq()
218 |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
219 |> Instances.filter_reachable()
220 |> Enum.each(fn {inbox, unreachable_since} ->
221 Pleroma.Web.Federator.Publisher.enqueue_one(
222 __MODULE__,
223 %{
224 inbox: inbox,
225 json: json,
226 actor: actor,
227 id: activity.data["id"],
228 unreachable_since: unreachable_since
229 }
230 )
231 end)
232 end
233
234 def gather_webfinger_links(%User{} = user) do
235 [
236 %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
237 %{
238 "rel" => "self",
239 "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
240 "href" => user.ap_id
241 }
242 ]
243 end
244
245 def gather_nodeinfo_protocol_names, do: ["activitypub"]
246 end