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