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