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