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