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