Merge branch 'profile-directory' into 'develop'
[akkoma] / lib / pleroma / web / activity_pub / activity_pub_controller.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.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.Pipeline
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.ControllerHelper
23 alias Pleroma.Web.Endpoint
24 alias Pleroma.Web.Federator
25 alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
26 alias Pleroma.Web.Plugs.FederatingPlug
27
28 require Logger
29
30 action_fallback(:errors)
31
32 @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
33
34 plug(FederatingPlug when action in @federating_only_actions)
35
36 plug(
37 EnsureAuthenticatedPlug,
38 [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
39 )
40
41 # Note: :following and :followers must be served even without authentication (as via :api)
42 plug(
43 EnsureAuthenticatedPlug
44 when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
45 )
46
47 plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
48
49 plug(
50 Pleroma.Web.Plugs.Cache,
51 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
52 when action in [:activity, :object]
53 )
54
55 plug(:set_requester_reachable when action in [:inbox])
56 plug(:relay_active? when action in [:relay])
57
58 defp relay_active?(conn, _) do
59 if Pleroma.Config.get([:instance, :allow_relay]) do
60 conn
61 else
62 conn
63 |> render_error(:not_found, "not found")
64 |> halt()
65 end
66 end
67
68 def user(conn, %{"nickname" => nickname}) do
69 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
70 {:ok, user} <- User.ensure_keys_present(user) do
71 conn
72 |> put_resp_content_type("application/activity+json")
73 |> put_view(UserView)
74 |> render("user.json", %{user: user})
75 else
76 nil -> {:error, :not_found}
77 %{local: false} -> {:error, :not_found}
78 end
79 end
80
81 def object(%{assigns: assigns} = conn, _) do
82 with ap_id <- Endpoint.url() <> conn.request_path,
83 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
84 user <- Map.get(assigns, :user, nil),
85 {_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} 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 {:visible?, false} -> {:error, :not_found}
94 nil -> {: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(%{assigns: assigns} = conn, _) do
109 with ap_id <- Endpoint.url() <> conn.request_path,
110 %Activity{} = activity <- Activity.normalize(ap_id),
111 {_, true} <- {:local?, activity.local},
112 user <- Map.get(assigns, :user, nil),
113 {_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
114 conn
115 |> maybe_set_tracking_data(activity)
116 |> set_cache_ttl_for(activity)
117 |> put_resp_content_type("application/activity+json")
118 |> put_view(ObjectView)
119 |> render("object.json", object: activity)
120 else
121 {:visible?, false} -> {:error, :not_found}
122 {:local?, false} -> {:error, :not_found}
123 nil -> {:error, :not_found}
124 end
125 end
126
127 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
128 object_id = Object.normalize(activity, fetch: false).id
129 assign(conn, :tracking_fun_data, object_id)
130 end
131
132 defp maybe_set_tracking_data(conn, _activity), do: conn
133
134 defp set_cache_ttl_for(conn, %Activity{object: object}) do
135 set_cache_ttl_for(conn, object)
136 end
137
138 defp set_cache_ttl_for(conn, entity) do
139 ttl =
140 case entity do
141 %Object{data: %{"type" => "Question"}} ->
142 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
143
144 %Object{} ->
145 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
146
147 _ ->
148 nil
149 end
150
151 assign(conn, :cache_ttl, ttl)
152 end
153
154 # GET /relay/following
155 def relay_following(conn, _params) do
156 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
157 conn
158 |> put_resp_content_type("application/activity+json")
159 |> put_view(UserView)
160 |> render("following.json", %{user: Relay.get_actor()})
161 end
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.hide_follows} do
169 {page, _} = Integer.parse(page)
170
171 conn
172 |> put_resp_content_type("application/activity+json")
173 |> put_view(UserView)
174 |> render("following.json", %{user: user, page: page, for: for_user})
175 else
176 {:show_follows, _} ->
177 conn
178 |> put_resp_content_type("application/activity+json")
179 |> send_resp(403, "")
180 end
181 end
182
183 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
184 with %User{} = user <- User.get_cached_by_nickname(nickname),
185 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
186 conn
187 |> put_resp_content_type("application/activity+json")
188 |> put_view(UserView)
189 |> render("following.json", %{user: user, for: for_user})
190 end
191 end
192
193 # GET /relay/followers
194 def relay_followers(conn, _params) do
195 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
196 conn
197 |> put_resp_content_type("application/activity+json")
198 |> put_view(UserView)
199 |> render("followers.json", %{user: Relay.get_actor()})
200 end
201 end
202
203 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
204 with %User{} = user <- User.get_cached_by_nickname(nickname),
205 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
206 {:show_followers, true} <-
207 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
208 {page, _} = Integer.parse(page)
209
210 conn
211 |> put_resp_content_type("application/activity+json")
212 |> put_view(UserView)
213 |> render("followers.json", %{user: user, page: page, for: for_user})
214 else
215 {:show_followers, _} ->
216 conn
217 |> put_resp_content_type("application/activity+json")
218 |> send_resp(403, "")
219 end
220 end
221
222 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
223 with %User{} = user <- User.get_cached_by_nickname(nickname),
224 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
225 conn
226 |> put_resp_content_type("application/activity+json")
227 |> put_view(UserView)
228 |> render("followers.json", %{user: user, for: for_user})
229 end
230 end
231
232 def outbox(
233 %{assigns: %{user: for_user}} = conn,
234 %{"nickname" => nickname, "page" => page?} = params
235 )
236 when page? in [true, "true"] do
237 with %User{} = user <- User.get_cached_by_nickname(nickname),
238 {:ok, user} <- User.ensure_keys_present(user) do
239 # "include_poll_votes" is a hack because postgres generates inefficient
240 # queries when filtering by 'Answer', poll votes will be hidden by the
241 # visibility filter in this case anyway
242 params =
243 params
244 |> Map.drop(["nickname", "page"])
245 |> Map.put("include_poll_votes", true)
246 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
247
248 activities = ActivityPub.fetch_user_activities(user, for_user, params)
249
250 conn
251 |> put_resp_content_type("application/activity+json")
252 |> put_view(UserView)
253 |> render("activity_collection_page.json", %{
254 activities: activities,
255 pagination: ControllerHelper.get_pagination_fields(conn, activities),
256 iri: "#{user.ap_id}/outbox"
257 })
258 end
259 end
260
261 def outbox(conn, %{"nickname" => nickname}) do
262 with %User{} = user <- User.get_cached_by_nickname(nickname),
263 {:ok, user} <- User.ensure_keys_present(user) do
264 conn
265 |> put_resp_content_type("application/activity+json")
266 |> put_view(UserView)
267 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
268 end
269 end
270
271 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
272 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
273 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
274 true <- Utils.recipient_in_message(recipient, actor, params),
275 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
276 Federator.incoming_ap_doc(params)
277 json(conn, "ok")
278 end
279 end
280
281 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
282 Federator.incoming_ap_doc(params)
283 json(conn, "ok")
284 end
285
286 def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do
287 conn
288 |> put_status(:bad_request)
289 |> json("Invalid HTTP Signature")
290 end
291
292 # POST /relay/inbox -or- POST /internal/fetch/inbox
293 def inbox(conn, %{"type" => "Create"} = params) do
294 if FederatingPlug.federating?() do
295 post_inbox_relayed_create(conn, params)
296 else
297 conn
298 |> put_status(:bad_request)
299 |> json("Not federating")
300 end
301 end
302
303 def inbox(conn, _params) do
304 conn
305 |> put_status(:bad_request)
306 |> json("error, missing HTTP Signature")
307 end
308
309 defp post_inbox_relayed_create(conn, params) do
310 Logger.debug(
311 "Signature missing or not from author, relayed Create message, fetching object from source"
312 )
313
314 Fetcher.fetch_object_from_id(params["object"]["id"])
315
316 json(conn, "ok")
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 fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
403 when is_map(object) do
404 length =
405 [object["content"], object["summary"], object["name"]]
406 |> Enum.filter(&is_binary(&1))
407 |> Enum.join("")
408 |> String.length()
409
410 limit = Pleroma.Config.get([:instance, :limit])
411
412 if length < limit do
413 object =
414 object
415 |> Transmogrifier.strip_internal_fields()
416 |> Map.put("attributedTo", actor)
417 |> Map.put("actor", actor)
418 |> Map.put("id", Utils.generate_object_id())
419
420 {:ok, Map.put(activity, "object", object)}
421 else
422 {:error,
423 dgettext(
424 "errors",
425 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
426 limit: limit,
427 length: length
428 )}
429 end
430 end
431
432 defp fix_user_message(
433 %User{ap_id: actor} = user,
434 %{"type" => "Delete", "object" => object} = activity
435 ) do
436 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
437 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
438 {:ok, activity}
439 else
440 {:normalize, _} ->
441 {:error, "No such object found"}
442
443 {:permission, _} ->
444 {:forbidden, "You can't delete this object"}
445 end
446 end
447
448 defp fix_user_message(%User{}, activity) do
449 {:ok, activity}
450 end
451
452 def update_outbox(
453 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
454 %{"nickname" => nickname} = params
455 ) do
456 params =
457 params
458 |> Map.drop(["nickname"])
459 |> Map.put("id", Utils.generate_activity_id())
460 |> Map.put("actor", actor)
461
462 with {:ok, params} <- fix_user_message(user, params),
463 {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
464 %Activity{data: activity_data} <- Activity.normalize(activity) do
465 conn
466 |> put_status(:created)
467 |> put_resp_header("location", activity_data["id"])
468 |> json(activity_data)
469 else
470 {:forbidden, message} ->
471 conn
472 |> put_status(:forbidden)
473 |> json(message)
474
475 {:error, message} ->
476 conn
477 |> put_status(:bad_request)
478 |> json(message)
479
480 e ->
481 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
482
483 conn
484 |> put_status(:bad_request)
485 |> json("Bad Request")
486 end
487 end
488
489 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
490 err =
491 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
492 nickname: nickname,
493 as_nickname: user.nickname
494 )
495
496 conn
497 |> put_status(:forbidden)
498 |> json(err)
499 end
500
501 defp errors(conn, {:error, :not_found}) do
502 conn
503 |> put_status(:not_found)
504 |> json(dgettext("errors", "Not found"))
505 end
506
507 defp errors(conn, _e) do
508 conn
509 |> put_status(:internal_server_error)
510 |> json(dgettext("errors", "error"))
511 end
512
513 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
514 with actor <- conn.params["actor"],
515 true <- is_binary(actor) do
516 Pleroma.Instances.set_reachable(actor)
517 end
518
519 conn
520 end
521
522 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
523 {:ok, new_user} = User.ensure_keys_present(user)
524
525 for_user =
526 if new_user != user and match?(%User{}, for_user) do
527 User.get_cached_by_nickname(for_user.nickname)
528 else
529 for_user
530 end
531
532 {new_user, for_user}
533 end
534
535 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
536 with {:ok, object} <-
537 ActivityPub.upload(
538 file,
539 actor: User.ap_id(user),
540 description: Map.get(data, "description")
541 ) do
542 Logger.debug(inspect(object))
543
544 conn
545 |> put_status(:created)
546 |> json(object.data)
547 end
548 end
549
550 def pinned(conn, %{"nickname" => nickname}) do
551 with %User{} = user <- User.get_cached_by_nickname(nickname) do
552 conn
553 |> put_resp_header("content-type", "application/activity+json")
554 |> json(UserView.render("featured.json", %{user: user}))
555 end
556 end
557 end