Merge branch 'plug-if-unless-func-options-refactoring' into 'develop'
[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.FederatingPlug
25 alias Pleroma.Web.Federator
26
27 require Logger
28
29 action_fallback(:errors)
30
31 @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
32
33 plug(FederatingPlug when action in @federating_only_actions)
34
35 plug(
36 EnsureAuthenticatedPlug,
37 [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
38 )
39
40 # Note: :following and :followers must be served even without authentication (as via :api)
41 plug(
42 EnsureAuthenticatedPlug
43 when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
44 )
45
46 plug(
47 Pleroma.Plugs.Cache,
48 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
49 when action in [:activity, :object]
50 )
51
52 plug(:set_requester_reachable when action in [:inbox])
53 plug(:relay_active? when action in [:relay])
54
55 defp relay_active?(conn, _) do
56 if Pleroma.Config.get([:instance, :allow_relay]) do
57 conn
58 else
59 conn
60 |> render_error(:not_found, "not found")
61 |> halt()
62 end
63 end
64
65 def user(conn, %{"nickname" => nickname}) do
66 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
67 {:ok, user} <- User.ensure_keys_present(user) do
68 conn
69 |> put_resp_content_type("application/activity+json")
70 |> put_view(UserView)
71 |> render("user.json", %{user: user})
72 else
73 nil -> {:error, :not_found}
74 %{local: false} -> {:error, :not_found}
75 end
76 end
77
78 def object(conn, %{"uuid" => uuid}) do
79 with ap_id <- o_status_url(conn, :object, uuid),
80 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
81 {_, true} <- {:public?, Visibility.is_public?(object)} do
82 conn
83 |> assign(:tracking_fun_data, object.id)
84 |> set_cache_ttl_for(object)
85 |> put_resp_content_type("application/activity+json")
86 |> put_view(ObjectView)
87 |> render("object.json", object: object)
88 else
89 {:public?, false} ->
90 {:error, :not_found}
91 end
92 end
93
94 def track_object_fetch(conn, nil), do: conn
95
96 def track_object_fetch(conn, object_id) do
97 with %{assigns: %{user: %User{id: user_id}}} <- conn do
98 Delivery.create(object_id, user_id)
99 end
100
101 conn
102 end
103
104 def activity(conn, %{"uuid" => uuid}) do
105 with ap_id <- o_status_url(conn, :activity, uuid),
106 %Activity{} = activity <- Activity.normalize(ap_id),
107 {_, true} <- {:public?, Visibility.is_public?(activity)} do
108 conn
109 |> maybe_set_tracking_data(activity)
110 |> set_cache_ttl_for(activity)
111 |> put_resp_content_type("application/activity+json")
112 |> put_view(ObjectView)
113 |> render("object.json", object: activity)
114 else
115 {:public?, false} -> {:error, :not_found}
116 nil -> {:error, :not_found}
117 end
118 end
119
120 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
121 object_id = Object.normalize(activity).id
122 assign(conn, :tracking_fun_data, object_id)
123 end
124
125 defp maybe_set_tracking_data(conn, _activity), do: conn
126
127 defp set_cache_ttl_for(conn, %Activity{object: object}) do
128 set_cache_ttl_for(conn, object)
129 end
130
131 defp set_cache_ttl_for(conn, entity) do
132 ttl =
133 case entity do
134 %Object{data: %{"type" => "Question"}} ->
135 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
136
137 %Object{} ->
138 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
139
140 _ ->
141 nil
142 end
143
144 assign(conn, :cache_ttl, ttl)
145 end
146
147 # GET /relay/following
148 def relay_following(conn, _params) do
149 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
150 conn
151 |> put_resp_content_type("application/activity+json")
152 |> put_view(UserView)
153 |> render("following.json", %{user: Relay.get_actor()})
154 end
155 end
156
157 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
158 with %User{} = user <- User.get_cached_by_nickname(nickname),
159 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
160 {:show_follows, true} <-
161 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
162 {page, _} = Integer.parse(page)
163
164 conn
165 |> put_resp_content_type("application/activity+json")
166 |> put_view(UserView)
167 |> render("following.json", %{user: user, page: page, for: for_user})
168 else
169 {:show_follows, _} ->
170 conn
171 |> put_resp_content_type("application/activity+json")
172 |> send_resp(403, "")
173 end
174 end
175
176 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
177 with %User{} = user <- User.get_cached_by_nickname(nickname),
178 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
179 conn
180 |> put_resp_content_type("application/activity+json")
181 |> put_view(UserView)
182 |> render("following.json", %{user: user, for: for_user})
183 end
184 end
185
186 # GET /relay/followers
187 def relay_followers(conn, _params) do
188 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
189 conn
190 |> put_resp_content_type("application/activity+json")
191 |> put_view(UserView)
192 |> render("followers.json", %{user: Relay.get_actor()})
193 end
194 end
195
196 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
197 with %User{} = user <- User.get_cached_by_nickname(nickname),
198 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
199 {:show_followers, true} <-
200 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
201 {page, _} = Integer.parse(page)
202
203 conn
204 |> put_resp_content_type("application/activity+json")
205 |> put_view(UserView)
206 |> render("followers.json", %{user: user, page: page, for: for_user})
207 else
208 {:show_followers, _} ->
209 conn
210 |> put_resp_content_type("application/activity+json")
211 |> send_resp(403, "")
212 end
213 end
214
215 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
216 with %User{} = user <- User.get_cached_by_nickname(nickname),
217 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
218 conn
219 |> put_resp_content_type("application/activity+json")
220 |> put_view(UserView)
221 |> render("followers.json", %{user: user, for: for_user})
222 end
223 end
224
225 def outbox(
226 %{assigns: %{user: for_user}} = conn,
227 %{"nickname" => nickname, "page" => page?} = params
228 )
229 when page? in [true, "true"] do
230 with %User{} = user <- User.get_cached_by_nickname(nickname),
231 {:ok, user} <- User.ensure_keys_present(user) do
232 activities =
233 if params["max_id"] do
234 ActivityPub.fetch_user_activities(user, for_user, %{
235 "max_id" => params["max_id"],
236 # This is a hack because postgres generates inefficient queries when filtering by
237 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
238 "include_poll_votes" => true,
239 "limit" => 10
240 })
241 else
242 ActivityPub.fetch_user_activities(user, for_user, %{
243 "limit" => 10,
244 "include_poll_votes" => true
245 })
246 end
247
248 conn
249 |> put_resp_content_type("application/activity+json")
250 |> put_view(UserView)
251 |> render("activity_collection_page.json", %{
252 activities: 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 activities =
356 if params["max_id"] do
357 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
358 "max_id" => params["max_id"],
359 "limit" => 10
360 })
361 else
362 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
363 end
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 iri: "#{user.ap_id}/inbox"
371 })
372 end
373
374 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
375 "nickname" => nickname
376 }) do
377 with {:ok, user} <- User.ensure_keys_present(user) do
378 conn
379 |> put_resp_content_type("application/activity+json")
380 |> put_view(UserView)
381 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
382 end
383 end
384
385 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
386 "nickname" => nickname
387 }) do
388 err =
389 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
390 nickname: nickname,
391 as_nickname: as_nickname
392 )
393
394 conn
395 |> put_status(:forbidden)
396 |> json(err)
397 end
398
399 defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
400 object =
401 params["object"]
402 |> Map.merge(Map.take(params, ["to", "cc"]))
403 |> Map.put("attributedTo", user.ap_id())
404 |> Transmogrifier.fix_object()
405
406 ActivityPub.create(%{
407 to: params["to"],
408 actor: user,
409 context: object["context"],
410 object: object,
411 additional: Map.take(params, ["cc"])
412 })
413 end
414
415 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
416 with %Object{} = object <- Object.normalize(params["object"]),
417 true <- user.is_moderator || user.ap_id == object.data["actor"],
418 {:ok, delete} <- ActivityPub.delete(object) do
419 {:ok, delete}
420 else
421 _ -> {:error, dgettext("errors", "Can't delete object")}
422 end
423 end
424
425 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
426 with %Object{} = object <- Object.normalize(params["object"]),
427 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
428 {_, {:ok, %Activity{} = activity, _meta}} <-
429 {:common_pipeline,
430 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
431 {:ok, activity}
432 else
433 _ -> {:error, dgettext("errors", "Can't like object")}
434 end
435 end
436
437 defp handle_user_activity(_, _) do
438 {:error, dgettext("errors", "Unhandled activity type")}
439 end
440
441 def update_outbox(
442 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
443 %{"nickname" => nickname} = params
444 ) do
445 actor = user.ap_id()
446
447 params =
448 params
449 |> Map.drop(["id"])
450 |> Map.put("actor", actor)
451 |> Transmogrifier.fix_addressing()
452
453 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
454 conn
455 |> put_status(:created)
456 |> put_resp_header("location", activity.data["id"])
457 |> json(activity.data)
458 else
459 {:error, message} ->
460 conn
461 |> put_status(:bad_request)
462 |> json(message)
463 end
464 end
465
466 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
467 err =
468 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
469 nickname: nickname,
470 as_nickname: user.nickname
471 )
472
473 conn
474 |> put_status(:forbidden)
475 |> json(err)
476 end
477
478 defp errors(conn, {:error, :not_found}) do
479 conn
480 |> put_status(:not_found)
481 |> json(dgettext("errors", "Not found"))
482 end
483
484 defp errors(conn, _e) do
485 conn
486 |> put_status(:internal_server_error)
487 |> json(dgettext("errors", "error"))
488 end
489
490 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
491 with actor <- conn.params["actor"],
492 true <- is_binary(actor) do
493 Pleroma.Instances.set_reachable(actor)
494 end
495
496 conn
497 end
498
499 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
500 {:ok, new_user} = User.ensure_keys_present(user)
501
502 for_user =
503 if new_user != user and match?(%User{}, for_user) do
504 User.get_cached_by_nickname(for_user.nickname)
505 else
506 for_user
507 end
508
509 {new_user, for_user}
510 end
511
512 # TODO: Add support for "object" field
513 @doc """
514 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
515
516 Parameters:
517 - (required) `file`: data of the media
518 - (optionnal) `description`: description of the media, intended for accessibility
519
520 Response:
521 - HTTP Code: 201 Created
522 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
523 """
524 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
525 with {:ok, object} <-
526 ActivityPub.upload(
527 file,
528 actor: User.ap_id(user),
529 description: Map.get(data, "description")
530 ) do
531 Logger.debug(inspect(object))
532
533 conn
534 |> put_status(:created)
535 |> json(object.data)
536 end
537 end
538 end