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