Add support for AP C2S uploadMedia
[akkoma] / lib / pleroma / web / activity_pub / activity_pub_controller.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.ActivityPubController do
6 use Pleroma.Web, :controller
7
8 alias Pleroma.Activity
9 alias Pleroma.Delivery
10 alias Pleroma.Object
11 alias Pleroma.Object.Fetcher
12 alias Pleroma.User
13 alias Pleroma.Web.ActivityPub.ActivityPub
14 alias Pleroma.Web.ActivityPub.InternalFetchActor
15 alias Pleroma.Web.ActivityPub.ObjectView
16 alias Pleroma.Web.ActivityPub.Relay
17 alias Pleroma.Web.ActivityPub.Transmogrifier
18 alias Pleroma.Web.ActivityPub.UserView
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.ActivityPub.Visibility
21 alias Pleroma.Web.Federator
22
23 require Logger
24
25 action_fallback(:errors)
26
27 plug(
28 Pleroma.Plugs.Cache,
29 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
30 when action in [:activity, :object]
31 )
32
33 plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
34 plug(:set_requester_reachable when action in [:inbox])
35 plug(:relay_active? when action in [:relay])
36
37 def relay_active?(conn, _) do
38 if Pleroma.Config.get([:instance, :allow_relay]) do
39 conn
40 else
41 conn
42 |> render_error(:not_found, "not found")
43 |> halt()
44 end
45 end
46
47 def user(conn, %{"nickname" => nickname}) do
48 with %User{} = user <- User.get_cached_by_nickname(nickname),
49 {:ok, user} <- User.ensure_keys_present(user) do
50 conn
51 |> put_resp_content_type("application/activity+json")
52 |> json(UserView.render("user.json", %{user: user}))
53 else
54 nil -> {:error, :not_found}
55 end
56 end
57
58 def object(conn, %{"uuid" => uuid}) do
59 with ap_id <- o_status_url(conn, :object, uuid),
60 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
61 {_, true} <- {:public?, Visibility.is_public?(object)} do
62 conn
63 |> assign(:tracking_fun_data, object.id)
64 |> set_cache_ttl_for(object)
65 |> put_resp_content_type("application/activity+json")
66 |> put_view(ObjectView)
67 |> render("object.json", object: object)
68 else
69 {:public?, false} ->
70 {:error, :not_found}
71 end
72 end
73
74 def track_object_fetch(conn, nil), do: conn
75
76 def track_object_fetch(conn, object_id) do
77 with %{assigns: %{user: %User{id: user_id}}} <- conn do
78 Delivery.create(object_id, user_id)
79 end
80
81 conn
82 end
83
84 def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
85 with ap_id <- o_status_url(conn, :object, uuid),
86 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
87 {_, true} <- {:public?, Visibility.is_public?(object)},
88 likes <- Utils.get_object_likes(object) do
89 {page, _} = Integer.parse(page)
90
91 conn
92 |> put_resp_content_type("application/activity+json")
93 |> json(ObjectView.render("likes.json", ap_id, likes, page))
94 else
95 {:public?, false} ->
96 {:error, :not_found}
97 end
98 end
99
100 def object_likes(conn, %{"uuid" => uuid}) do
101 with ap_id <- o_status_url(conn, :object, uuid),
102 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
103 {_, true} <- {:public?, Visibility.is_public?(object)},
104 likes <- Utils.get_object_likes(object) do
105 conn
106 |> put_resp_content_type("application/activity+json")
107 |> json(ObjectView.render("likes.json", ap_id, likes))
108 else
109 {:public?, false} ->
110 {:error, :not_found}
111 end
112 end
113
114 def activity(conn, %{"uuid" => uuid}) do
115 with ap_id <- o_status_url(conn, :activity, uuid),
116 %Activity{} = activity <- Activity.normalize(ap_id),
117 {_, true} <- {:public?, Visibility.is_public?(activity)} do
118 conn
119 |> maybe_set_tracking_data(activity)
120 |> set_cache_ttl_for(activity)
121 |> put_resp_content_type("application/activity+json")
122 |> put_view(ObjectView)
123 |> render("object.json", object: activity)
124 else
125 {:public?, false} -> {:error, :not_found}
126 nil -> {:error, :not_found}
127 end
128 end
129
130 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
131 object_id = Object.normalize(activity).id
132 assign(conn, :tracking_fun_data, object_id)
133 end
134
135 defp maybe_set_tracking_data(conn, _activity), do: conn
136
137 defp set_cache_ttl_for(conn, %Activity{object: object}) do
138 set_cache_ttl_for(conn, object)
139 end
140
141 defp set_cache_ttl_for(conn, entity) do
142 ttl =
143 case entity do
144 %Object{data: %{"type" => "Question"}} ->
145 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
146
147 %Object{} ->
148 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
149
150 _ ->
151 nil
152 end
153
154 assign(conn, :cache_ttl, ttl)
155 end
156
157 # GET /relay/following
158 def following(%{assigns: %{relay: true}} = conn, _params) do
159 conn
160 |> put_resp_content_type("application/activity+json")
161 |> json(UserView.render("following.json", %{user: Relay.get_actor()}))
162 end
163
164 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
165 with %User{} = user <- User.get_cached_by_nickname(nickname),
166 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
167 {:show_follows, true} <-
168 {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do
169 {page, _} = Integer.parse(page)
170
171 conn
172 |> put_resp_content_type("application/activity+json")
173 |> json(UserView.render("following.json", %{user: user, page: page, for: for_user}))
174 else
175 {:show_follows, _} ->
176 conn
177 |> put_resp_content_type("application/activity+json")
178 |> send_resp(403, "")
179 end
180 end
181
182 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
183 with %User{} = user <- User.get_cached_by_nickname(nickname),
184 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
185 conn
186 |> put_resp_content_type("application/activity+json")
187 |> json(UserView.render("following.json", %{user: user, for: for_user}))
188 end
189 end
190
191 # GET /relay/followers
192 def followers(%{assigns: %{relay: true}} = conn, _params) do
193 conn
194 |> put_resp_content_type("application/activity+json")
195 |> json(UserView.render("followers.json", %{user: Relay.get_actor()}))
196 end
197
198 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
199 with %User{} = user <- User.get_cached_by_nickname(nickname),
200 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
201 {:show_followers, true} <-
202 {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do
203 {page, _} = Integer.parse(page)
204
205 conn
206 |> put_resp_content_type("application/activity+json")
207 |> json(UserView.render("followers.json", %{user: user, page: page, for: for_user}))
208 else
209 {:show_followers, _} ->
210 conn
211 |> put_resp_content_type("application/activity+json")
212 |> send_resp(403, "")
213 end
214 end
215
216 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
217 with %User{} = user <- User.get_cached_by_nickname(nickname),
218 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
219 conn
220 |> put_resp_content_type("application/activity+json")
221 |> json(UserView.render("followers.json", %{user: user, for: for_user}))
222 end
223 end
224
225 def outbox(conn, %{"nickname" => nickname} = params) do
226 with %User{} = user <- User.get_cached_by_nickname(nickname),
227 {:ok, user} <- User.ensure_keys_present(user) do
228 conn
229 |> put_resp_content_type("application/activity+json")
230 |> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
231 end
232 end
233
234 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
235 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
236 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
237 true <- Utils.recipient_in_message(recipient, actor, params),
238 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
239 Federator.incoming_ap_doc(params)
240 json(conn, "ok")
241 end
242 end
243
244 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
245 Federator.incoming_ap_doc(params)
246 json(conn, "ok")
247 end
248
249 # only accept relayed Creates
250 def inbox(conn, %{"type" => "Create"} = params) do
251 Logger.info(
252 "Signature missing or not from author, relayed Create message, fetching object from source"
253 )
254
255 Fetcher.fetch_object_from_id(params["object"]["id"])
256
257 json(conn, "ok")
258 end
259
260 def inbox(conn, params) do
261 headers = Enum.into(conn.req_headers, %{})
262
263 if String.contains?(headers["signature"], params["actor"]) do
264 Logger.info(
265 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
266 )
267
268 Logger.info(inspect(conn.req_headers))
269 end
270
271 json(conn, dgettext("errors", "error"))
272 end
273
274 defp represent_service_actor(%User{} = user, conn) do
275 with {:ok, user} <- User.ensure_keys_present(user) do
276 conn
277 |> put_resp_content_type("application/activity+json")
278 |> json(UserView.render("user.json", %{user: user}))
279 else
280 nil -> {:error, :not_found}
281 end
282 end
283
284 defp represent_service_actor(nil, _), do: {:error, :not_found}
285
286 def relay(conn, _params) do
287 Relay.get_actor()
288 |> represent_service_actor(conn)
289 end
290
291 def internal_fetch(conn, _params) do
292 InternalFetchActor.get_actor()
293 |> represent_service_actor(conn)
294 end
295
296 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
297 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
298 conn
299 |> put_resp_content_type("application/activity+json")
300 |> json(UserView.render("user.json", %{user: user}))
301 end
302
303 def whoami(_conn, _params), do: {:error, :not_found}
304
305 def read_inbox(
306 %{assigns: %{user: %{nickname: nickname} = user}} = conn,
307 %{"nickname" => nickname} = params
308 ) do
309 conn
310 |> put_resp_content_type("application/activity+json")
311 |> put_view(UserView)
312 |> render("inbox.json", user: user, max_id: params["max_id"])
313 end
314
315 def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
316 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
317
318 conn
319 |> put_status(:forbidden)
320 |> json(err)
321 end
322
323 def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
324 "nickname" => nickname
325 }) do
326 err =
327 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
328 nickname: nickname,
329 as_nickname: as_nickname
330 )
331
332 conn
333 |> put_status(:forbidden)
334 |> json(err)
335 end
336
337 def handle_user_activity(user, %{"type" => "Create"} = params) do
338 object =
339 params["object"]
340 |> Map.merge(Map.take(params, ["to", "cc"]))
341 |> Map.put("attributedTo", user.ap_id())
342 |> Transmogrifier.fix_object()
343
344 ActivityPub.create(%{
345 to: params["to"],
346 actor: user,
347 context: object["context"],
348 object: object,
349 additional: Map.take(params, ["cc"])
350 })
351 end
352
353 def handle_user_activity(user, %{"type" => "Delete"} = params) do
354 with %Object{} = object <- Object.normalize(params["object"]),
355 true <- user.info.is_moderator || user.ap_id == object.data["actor"],
356 {:ok, delete} <- ActivityPub.delete(object) do
357 {:ok, delete}
358 else
359 _ -> {:error, dgettext("errors", "Can't delete object")}
360 end
361 end
362
363 def handle_user_activity(user, %{"type" => "Like"} = params) do
364 with %Object{} = object <- Object.normalize(params["object"]),
365 {:ok, activity, _object} <- ActivityPub.like(user, object) do
366 {:ok, activity}
367 else
368 _ -> {:error, dgettext("errors", "Can't like object")}
369 end
370 end
371
372 def handle_user_activity(_, _) do
373 {:error, dgettext("errors", "Unhandled activity type")}
374 end
375
376 def update_outbox(
377 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
378 %{"nickname" => nickname} = params
379 ) do
380 actor = user.ap_id()
381
382 params =
383 params
384 |> Map.drop(["id"])
385 |> Map.put("actor", actor)
386 |> Transmogrifier.fix_addressing()
387
388 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
389 conn
390 |> put_status(:created)
391 |> put_resp_header("location", activity.data["id"])
392 |> json(activity.data)
393 else
394 {:error, message} ->
395 conn
396 |> put_status(:bad_request)
397 |> json(message)
398 end
399 end
400
401 def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
402 err =
403 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
404 nickname: nickname,
405 as_nickname: user.nickname
406 )
407
408 conn
409 |> put_status(:forbidden)
410 |> json(err)
411 end
412
413 def errors(conn, {:error, :not_found}) do
414 conn
415 |> put_status(:not_found)
416 |> json(dgettext("errors", "Not found"))
417 end
418
419 def errors(conn, _e) do
420 conn
421 |> put_status(:internal_server_error)
422 |> json(dgettext("errors", "error"))
423 end
424
425 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
426 with actor <- conn.params["actor"],
427 true <- is_binary(actor) do
428 Pleroma.Instances.set_reachable(actor)
429 end
430
431 conn
432 end
433
434 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
435 {:ok, new_user} = User.ensure_keys_present(user)
436
437 for_user =
438 if new_user != user and match?(%User{}, for_user) do
439 User.get_cached_by_nickname(for_user.nickname)
440 else
441 for_user
442 end
443
444 {new_user, for_user}
445 end
446
447 # TODO: Add support for "object" field
448 @doc """
449 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
450
451 Parameters:
452 - (required) `file`: data of the media
453 - (optionnal) `description`: description of the media, intended for accessibility
454
455 Response:
456 - HTTP Code: 201 Created
457 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
458 """
459 def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
460 with {:ok, object} <-
461 ActivityPub.upload(
462 file,
463 actor: User.ap_id(user),
464 description: Map.get(data, "description")
465 ) do
466 Logger.debug(inspect(object))
467
468 conn
469 |> put_status(:created)
470 |> json(object.data)
471 end
472 end
473 end