Merge branch 'feature/1087-wildcard-option-for-blocks' into 'develop'
[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 import Pleroma.Web.ActivityPub.Visibility
15
16 @behaviour Pleroma.Web.Federator.Publisher
17
18 require Logger
19
20 @moduledoc """
21 ActivityPub outgoing federation module.
22 """
23
24 @doc """
25 Determine if an activity can be represented by running it through Transmogrifier.
26 """
27 def is_representable?(%Activity{} = activity) do
28 with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
29 true
30 else
31 _e ->
32 false
33 end
34 end
35
36 @doc """
37 Publish a single message to a peer. Takes a struct with the following
38 parameters set:
39
40 * `inbox`: the inbox to publish to
41 * `json`: the JSON message body representing the ActivityPub message
42 * `actor`: the actor which is signing the message
43 * `id`: the ActivityStreams URI of the message
44 """
45 def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
46 Logger.info("Federating #{id} to #{inbox}")
47 host = URI.parse(inbox).host
48
49 digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
50
51 date =
52 NaiveDateTime.utc_now()
53 |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
54
55 signature =
56 Pleroma.Signature.sign(actor, %{
57 host: host,
58 "content-length": byte_size(json),
59 digest: digest,
60 date: date
61 })
62
63 with {:ok, %{status: code}} when code in 200..299 <-
64 result =
65 HTTP.post(
66 inbox,
67 json,
68 [
69 {"Content-Type", "application/activity+json"},
70 {"Date", date},
71 {"signature", signature},
72 {"digest", digest}
73 ]
74 ) do
75 if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
76 do: Instances.set_reachable(inbox)
77
78 result
79 else
80 {_post_result, response} ->
81 unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
82 {:error, response}
83 end
84 end
85
86 defp should_federate?(inbox, public) do
87 if public do
88 true
89 else
90 %{host: host} = URI.parse(inbox)
91
92 quarantined_instances =
93 Config.get([:instance, :quarantined_instances], [])
94 |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
95
96 !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
97 end
98 end
99
100 defp recipients(actor, activity) do
101 followers =
102 if actor.follower_address in activity.recipients do
103 {:ok, followers} = User.get_followers(actor)
104 Enum.filter(followers, &(!&1.local))
105 else
106 []
107 end
108
109 Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers
110 end
111
112 defp get_cc_ap_ids(ap_id, recipients) do
113 host = Map.get(URI.parse(ap_id), :host)
114
115 recipients
116 |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
117 |> Enum.map(& &1.ap_id)
118 end
119
120 @as_public "https://www.w3.org/ns/activitystreams#Public"
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 @as_public in to || @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