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