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