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