57ac40b42816273ff92616207ee5937fb16a70b1
[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 # POST /relay/inbox -or- POST /internal/fetch/inbox
287 def inbox(conn, params) do
288 if params["type"] == "Create" && FederatingPlug.federating?() do
289 post_inbox_relayed_create(conn, params)
290 else
291 post_inbox_fallback(conn, params)
292 end
293 end
294
295 defp post_inbox_relayed_create(conn, params) do
296 Logger.debug(
297 "Signature missing or not from author, relayed Create message, fetching object from source"
298 )
299
300 Fetcher.fetch_object_from_id(params["object"]["id"])
301
302 json(conn, "ok")
303 end
304
305 defp post_inbox_fallback(conn, params) do
306 headers = Enum.into(conn.req_headers, %{})
307
308 if headers["signature"] && params["actor"] &&
309 String.contains?(headers["signature"], params["actor"]) do
310 Logger.debug(
311 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
312 )
313
314 Logger.debug(inspect(conn.req_headers))
315 end
316
317 conn
318 |> put_status(:bad_request)
319 |> json(dgettext("errors", "error"))
320 end
321
322 defp represent_service_actor(%User{} = user, conn) do
323 with {:ok, user} <- User.ensure_keys_present(user) do
324 conn
325 |> put_resp_content_type("application/activity+json")
326 |> put_view(UserView)
327 |> render("user.json", %{user: user})
328 else
329 nil -> {:error, :not_found}
330 end
331 end
332
333 defp represent_service_actor(nil, _), do: {:error, :not_found}
334
335 def relay(conn, _params) do
336 Relay.get_actor()
337 |> represent_service_actor(conn)
338 end
339
340 def internal_fetch(conn, _params) do
341 InternalFetchActor.get_actor()
342 |> represent_service_actor(conn)
343 end
344
345 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
346 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
347 conn
348 |> put_resp_content_type("application/activity+json")
349 |> put_view(UserView)
350 |> render("user.json", %{user: user})
351 end
352
353 def read_inbox(
354 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
355 %{"nickname" => nickname, "page" => page?} = params
356 )
357 when page? in [true, "true"] do
358 params =
359 params
360 |> Map.drop(["nickname", "page"])
361 |> Map.put("blocking_user", user)
362 |> Map.put("user", user)
363 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
364
365 activities =
366 [user.ap_id | User.following(user)]
367 |> ActivityPub.fetch_activities(params)
368 |> Enum.reverse()
369
370 conn
371 |> put_resp_content_type("application/activity+json")
372 |> put_view(UserView)
373 |> render("activity_collection_page.json", %{
374 activities: activities,
375 pagination: ControllerHelper.get_pagination_fields(conn, activities),
376 iri: "#{user.ap_id}/inbox"
377 })
378 end
379
380 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
381 "nickname" => nickname
382 }) do
383 with {:ok, user} <- User.ensure_keys_present(user) do
384 conn
385 |> put_resp_content_type("application/activity+json")
386 |> put_view(UserView)
387 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
388 end
389 end
390
391 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
392 "nickname" => nickname
393 }) do
394 err =
395 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
396 nickname: nickname,
397 as_nickname: as_nickname
398 )
399
400 conn
401 |> put_status(:forbidden)
402 |> json(err)
403 end
404
405 defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
406 when is_map(object) do
407 length =
408 [object["content"], object["summary"], object["name"]]
409 |> Enum.filter(&is_binary(&1))
410 |> Enum.join("")
411 |> String.length()
412
413 limit = Pleroma.Config.get([:instance, :limit])
414
415 if length < limit do
416 object =
417 object
418 |> Transmogrifier.strip_internal_fields()
419 |> Map.put("attributedTo", actor)
420 |> Map.put("actor", actor)
421 |> Map.put("id", Utils.generate_object_id())
422
423 {:ok, Map.put(activity, "object", object)}
424 else
425 {:error,
426 dgettext(
427 "errors",
428 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
429 limit: limit,
430 length: length
431 )}
432 end
433 end
434
435 defp fix_user_message(
436 %User{ap_id: actor} = user,
437 %{"type" => "Delete", "object" => object} = activity
438 ) do
439 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
440 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
441 {:ok, activity}
442 else
443 {:normalize, _} ->
444 {:error, "No such object found"}
445
446 {:permission, _} ->
447 {:forbidden, "You can't delete this object"}
448 end
449 end
450
451 defp fix_user_message(%User{}, activity) do
452 {:ok, activity}
453 end
454
455 def update_outbox(
456 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
457 %{"nickname" => nickname} = params
458 ) do
459 params =
460 params
461 |> Map.drop(["nickname"])
462 |> Map.put("id", Utils.generate_activity_id())
463 |> Map.put("actor", actor)
464
465 with {:ok, params} <- fix_user_message(user, params),
466 {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
467 %Activity{data: activity_data} <- Activity.normalize(activity) do
468 conn
469 |> put_status(:created)
470 |> put_resp_header("location", activity_data["id"])
471 |> json(activity_data)
472 else
473 {:forbidden, message} ->
474 conn
475 |> put_status(:forbidden)
476 |> json(message)
477
478 {:error, message} ->
479 conn
480 |> put_status(:bad_request)
481 |> json(message)
482
483 e ->
484 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
485
486 conn
487 |> put_status(:bad_request)
488 |> json("Bad Request")
489 end
490 end
491
492 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
493 err =
494 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
495 nickname: nickname,
496 as_nickname: user.nickname
497 )
498
499 conn
500 |> put_status(:forbidden)
501 |> json(err)
502 end
503
504 defp errors(conn, {:error, :not_found}) do
505 conn
506 |> put_status(:not_found)
507 |> json(dgettext("errors", "Not found"))
508 end
509
510 defp errors(conn, _e) do
511 conn
512 |> put_status(:internal_server_error)
513 |> json(dgettext("errors", "error"))
514 end
515
516 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
517 with actor <- conn.params["actor"],
518 true <- is_binary(actor) do
519 Pleroma.Instances.set_reachable(actor)
520 end
521
522 conn
523 end
524
525 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
526 {:ok, new_user} = User.ensure_keys_present(user)
527
528 for_user =
529 if new_user != user and match?(%User{}, for_user) do
530 User.get_cached_by_nickname(for_user.nickname)
531 else
532 for_user
533 end
534
535 {new_user, for_user}
536 end
537
538 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
539 with {:ok, object} <-
540 ActivityPub.upload(
541 file,
542 actor: User.ap_id(user),
543 description: Map.get(data, "description")
544 ) do
545 Logger.debug(inspect(object))
546
547 conn
548 |> put_status(:created)
549 |> json(object.data)
550 end
551 end
552
553 def pinned(conn, %{"nickname" => nickname}) do
554 with %User{} = user <- User.get_cached_by_nickname(nickname) do
555 conn
556 |> put_resp_header("content-type", "application/activity+json")
557 |> json(UserView.render("featured.json", %{user: user}))
558 end
559 end
560 end