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