a9dfa18e77d5cdc1e7556201e59f15c5100fd344
[akkoma] / lib / pleroma / object / fetcher.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Object.Fetcher do
6 alias Pleroma.HTTP
7 alias Pleroma.Instances
8 alias Pleroma.Maps
9 alias Pleroma.Object
10 alias Pleroma.Object.Containment
11 alias Pleroma.Repo
12 alias Pleroma.Signature
13 alias Pleroma.Web.ActivityPub.InternalFetchActor
14 alias Pleroma.Web.ActivityPub.ObjectValidator
15 alias Pleroma.Web.ActivityPub.Transmogrifier
16 alias Pleroma.Web.Federator
17
18 require Logger
19 require Pleroma.Constants
20
21 defp touch_changeset(changeset) do
22 updated_at =
23 NaiveDateTime.utc_now()
24 |> NaiveDateTime.truncate(:second)
25
26 Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
27 end
28
29 defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do
30 has_history? = fn
31 %{"formerRepresentations" => %{"orderedItems" => list}} when is_list(list) -> true
32 _ -> false
33 end
34
35 internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
36
37 remote_history_exists? = has_history?.(new_data)
38
39 # If the remote history exists, we treat that as the only source of truth.
40 new_data =
41 if has_history?.(old_data) and not remote_history_exists? do
42 Map.put(new_data, "formerRepresentations", old_data["formerRepresentations"])
43 else
44 new_data
45 end
46
47 # If the remote does not have history information, we need to manage it ourselves
48 new_data =
49 if not remote_history_exists? do
50 changed? =
51 Pleroma.Constants.status_updatable_fields()
52 |> Enum.any?(fn field -> Map.get(old_data, field) != Map.get(new_data, field) end)
53
54 %{updated_object: updated_object} =
55 new_data
56 |> Object.Updater.maybe_update_history(old_data,
57 updated: changed?,
58 use_history_in_new_object?: false
59 )
60
61 updated_object
62 else
63 new_data
64 end
65
66 Map.merge(new_data, internal_fields)
67 end
68
69 defp maybe_reinject_internal_fields(_, new_data), do: new_data
70
71 @spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
72 defp reinject_object(%Object{data: %{"type" => "Question"}} = object, new_data) do
73 Logger.debug("Reinjecting object #{new_data["id"]}")
74
75 with data <- maybe_reinject_internal_fields(object, new_data),
76 {:ok, data, _} <- ObjectValidator.validate(data, %{}),
77 changeset <- Object.change(object, %{data: data}),
78 changeset <- touch_changeset(changeset),
79 {:ok, object} <- Repo.insert_or_update(changeset),
80 {:ok, object} <- Object.set_cache(object) do
81 {:ok, object}
82 else
83 e ->
84 Logger.error("Error while processing object: #{inspect(e)}")
85 {:error, e}
86 end
87 end
88
89 defp reinject_object(%Object{} = object, new_data) do
90 Logger.debug("Reinjecting object #{new_data["id"]}")
91
92 with new_data <- Transmogrifier.fix_object(new_data),
93 data <- maybe_reinject_internal_fields(object, new_data),
94 changeset <- Object.change(object, %{data: data}),
95 changeset <- touch_changeset(changeset),
96 {:ok, object} <- Repo.insert_or_update(changeset),
97 {:ok, object} <- Object.set_cache(object) do
98 {:ok, object}
99 else
100 e ->
101 Logger.error("Error while processing object: #{inspect(e)}")
102 {:error, e}
103 end
104 end
105
106 def refetch_object(%Object{data: %{"id" => id}} = object) do
107 with {:local, false} <- {:local, Object.local?(object)},
108 {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
109 {:ok, object} <- reinject_object(object, new_data) do
110 {:ok, object}
111 else
112 {:local, true} -> {:ok, object}
113 e -> {:error, e}
114 end
115 end
116
117 # Note: will create a Create activity, which we need internally at the moment.
118 def fetch_object_from_id(id, options \\ []) do
119 with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
120 {_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
121 {_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
122 {_, nil} <- {:normalize, Object.normalize(data, fetch: false)},
123 params <- prepare_activity_params(data),
124 {_, :ok} <- {:containment, Containment.contain_origin(id, params)},
125 {_, {:ok, activity}} <-
126 {:transmogrifier, Transmogrifier.handle_incoming(params, options)},
127 {_, _data, %Object{} = object} <-
128 {:object, data, Object.normalize(activity, fetch: false)} do
129 {:ok, object}
130 else
131 {:allowed_depth, false} ->
132 {:error, "Max thread distance exceeded."}
133
134 {:containment, _} ->
135 {:error, "Object containment failed."}
136
137 {:transmogrifier, {:error, {:reject, e}}} ->
138 {:reject, e}
139
140 {:transmogrifier, {:reject, e}} ->
141 {:reject, e}
142
143 {:transmogrifier, _} = e ->
144 {:error, e}
145
146 {:object, data, nil} ->
147 reinject_object(%Object{}, data)
148
149 {:normalize, object = %Object{}} ->
150 {:ok, object}
151
152 {:fetch_object, %Object{} = object} ->
153 {:ok, object}
154
155 {:fetch, {:error, error}} ->
156 {:error, error}
157
158 e ->
159 e
160 end
161 end
162
163 defp prepare_activity_params(data) do
164 %{
165 "type" => "Create",
166 # Should we seriously keep this attributedTo thing?
167 "actor" => data["actor"] || data["attributedTo"],
168 "object" => data
169 }
170 |> Maps.put_if_present("to", data["to"])
171 |> Maps.put_if_present("cc", data["cc"])
172 |> Maps.put_if_present("bto", data["bto"])
173 |> Maps.put_if_present("bcc", data["bcc"])
174 end
175
176 def fetch_object_from_id!(id, options \\ []) do
177 with {:ok, object} <- fetch_object_from_id(id, options) do
178 object
179 else
180 {:error, %Tesla.Mock.Error{}} ->
181 nil
182
183 {:error, "Object has been deleted"} ->
184 nil
185
186 {:reject, reason} ->
187 Logger.debug("Rejected #{id} while fetching: #{inspect(reason)}")
188 nil
189
190 e ->
191 Logger.error("Error while fetching #{id}: #{inspect(e)}")
192 nil
193 end
194 end
195
196 defp make_signature(id, date) do
197 uri = URI.parse(id)
198
199 signature =
200 InternalFetchActor.get_actor()
201 |> Signature.sign(%{
202 "(request-target)": "get #{uri.path}",
203 host: uri.host,
204 date: date
205 })
206
207 {"signature", signature}
208 end
209
210 defp sign_fetch(headers, id, date) do
211 if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
212 [make_signature(id, date) | headers]
213 else
214 headers
215 end
216 end
217
218 defp maybe_date_fetch(headers, date) do
219 if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
220 [{"date", date} | headers]
221 else
222 headers
223 end
224 end
225
226 def fetch_and_contain_remote_object_from_id(id)
227
228 def fetch_and_contain_remote_object_from_id(%{"id" => id}),
229 do: fetch_and_contain_remote_object_from_id(id)
230
231 def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
232 Logger.debug("Fetching object #{id} via AP")
233
234 with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
235 {:ok, body} <- get_object(id),
236 {:ok, data} <- safe_json_decode(body),
237 :ok <- Containment.contain_origin_from_id(id, data) do
238 unless Instances.reachable?(id) do
239 Instances.set_reachable(id)
240 end
241
242 {:ok, data}
243 else
244 {:scheme, _} ->
245 {:error, "Unsupported URI scheme"}
246
247 {:error, e} ->
248 {:error, e}
249
250 e ->
251 {:error, e}
252 end
253 end
254
255 def fetch_and_contain_remote_object_from_id(_id),
256 do: {:error, "id must be a string"}
257
258 defp get_object(id) do
259 date = Pleroma.Signature.signed_date()
260
261 headers =
262 [{"accept", "application/activity+json"}]
263 |> maybe_date_fetch(date)
264 |> sign_fetch(id, date)
265
266 case HTTP.get(id, headers) do
267 {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 ->
268 case List.keyfind(headers, "content-type", 0) do
269 {_, content_type} ->
270 case Plug.Conn.Utils.media_type(content_type) do
271 {:ok, "application", "activity+json", _} ->
272 {:ok, body}
273
274 {:ok, "application", "ld+json",
275 %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
276 {:ok, body}
277
278 _ ->
279 {:error, {:content_type, content_type}}
280 end
281
282 _ ->
283 {:error, {:content_type, nil}}
284 end
285
286 {:ok, %{status: code}} when code in [404, 410] ->
287 {:error, "Object has been deleted"}
288
289 {:error, e} ->
290 {:error, e}
291
292 e ->
293 {:error, e}
294 end
295 end
296
297 defp safe_json_decode(nil), do: {:ok, nil}
298 defp safe_json_decode(json), do: Jason.decode(json)
299 end