Merge branch 'develop' into feature/gen-magic
[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"} = object} = params
405 ) do
406 content = if is_binary(object["content"]), do: object["content"], else: ""
407 name = if is_binary(object["name"]), do: object["name"], else: ""
408 summary = if is_binary(object["summary"]), do: object["summary"], else: ""
409 length = String.length(content <> name <> summary)
410
411 if length > Pleroma.Config.get([:instance, :limit]) do
412 {:error, dgettext("errors", "Note is over the character limit")}
413 else
414 object =
415 object
416 |> Map.merge(Map.take(params, ["to", "cc"]))
417 |> Map.put("attributedTo", user.ap_id())
418 |> Transmogrifier.fix_object()
419
420 ActivityPub.create(%{
421 to: params["to"],
422 actor: user,
423 context: object["context"],
424 object: object,
425 additional: Map.take(params, ["cc"])
426 })
427 end
428 end
429
430 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
431 with %Object{} = object <- Object.normalize(params["object"]),
432 true <- user.is_moderator || user.ap_id == object.data["actor"],
433 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
434 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
435 {:ok, delete}
436 else
437 _ -> {:error, dgettext("errors", "Can't delete object")}
438 end
439 end
440
441 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
442 with %Object{} = object <- Object.normalize(params["object"]),
443 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
444 {_, {:ok, %Activity{} = activity, _meta}} <-
445 {:common_pipeline,
446 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
447 {:ok, activity}
448 else
449 _ -> {:error, dgettext("errors", "Can't like object")}
450 end
451 end
452
453 defp handle_user_activity(_, _) do
454 {:error, dgettext("errors", "Unhandled activity type")}
455 end
456
457 def update_outbox(
458 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
459 %{"nickname" => nickname} = params
460 ) do
461 actor = user.ap_id()
462
463 params =
464 params
465 |> Map.drop(["id"])
466 |> Map.put("actor", actor)
467 |> Transmogrifier.fix_addressing()
468
469 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
470 conn
471 |> put_status(:created)
472 |> put_resp_header("location", activity.data["id"])
473 |> json(activity.data)
474 else
475 {:error, message} ->
476 conn
477 |> put_status(:bad_request)
478 |> json(message)
479 end
480 end
481
482 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
483 err =
484 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
485 nickname: nickname,
486 as_nickname: user.nickname
487 )
488
489 conn
490 |> put_status(:forbidden)
491 |> json(err)
492 end
493
494 defp errors(conn, {:error, :not_found}) do
495 conn
496 |> put_status(:not_found)
497 |> json(dgettext("errors", "Not found"))
498 end
499
500 defp errors(conn, _e) do
501 conn
502 |> put_status(:internal_server_error)
503 |> json(dgettext("errors", "error"))
504 end
505
506 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
507 with actor <- conn.params["actor"],
508 true <- is_binary(actor) do
509 Pleroma.Instances.set_reachable(actor)
510 end
511
512 conn
513 end
514
515 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
516 {:ok, new_user} = User.ensure_keys_present(user)
517
518 for_user =
519 if new_user != user and match?(%User{}, for_user) do
520 User.get_cached_by_nickname(for_user.nickname)
521 else
522 for_user
523 end
524
525 {new_user, for_user}
526 end
527
528 @doc """
529 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
530
531 Parameters:
532 - (required) `file`: data of the media
533 - (optionnal) `description`: description of the media, intended for accessibility
534
535 Response:
536 - HTTP Code: 201 Created
537 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
538
539 Note: Will not point to a URL with a `Location` header because no standalone Activity has been created.
540 """
541 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
542 with {:ok, object} <-
543 ActivityPub.upload(
544 file,
545 actor: User.ap_id(user),
546 description: Map.get(data, "description")
547 ) do
548 Logger.debug(inspect(object))
549
550 conn
551 |> put_status(:created)
552 |> json(object.data)
553 end
554 end
555 end