Add `account_activation_required` to /api/v1/instance
[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.Endpoint
25 alias Pleroma.Web.FederatingPlug
26 alias Pleroma.Web.Federator
27
28 require Logger
29
30 action_fallback(:errors)
31
32 @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
33
34 plug(FederatingPlug when action in @federating_only_actions)
35
36 plug(
37 EnsureAuthenticatedPlug,
38 [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
39 )
40
41 # Note: :following and :followers must be served even without authentication (as via :api)
42 plug(
43 EnsureAuthenticatedPlug
44 when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
45 )
46
47 plug(
48 Pleroma.Plugs.Cache,
49 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
50 when action in [:activity, :object]
51 )
52
53 plug(:set_requester_reachable when action in [:inbox])
54 plug(:relay_active? when action in [:relay])
55
56 defp relay_active?(conn, _) do
57 if Pleroma.Config.get([:instance, :allow_relay]) do
58 conn
59 else
60 conn
61 |> render_error(:not_found, "not found")
62 |> halt()
63 end
64 end
65
66 def user(conn, %{"nickname" => nickname}) do
67 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
68 {:ok, user} <- User.ensure_keys_present(user) do
69 conn
70 |> put_resp_content_type("application/activity+json")
71 |> put_view(UserView)
72 |> render("user.json", %{user: user})
73 else
74 nil -> {:error, :not_found}
75 %{local: false} -> {:error, :not_found}
76 end
77 end
78
79 def object(conn, _) do
80 with ap_id <- Endpoint.url() <> conn.request_path,
81 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
82 {_, true} <- {:public?, Visibility.is_public?(object)} do
83 conn
84 |> assign(:tracking_fun_data, object.id)
85 |> set_cache_ttl_for(object)
86 |> put_resp_content_type("application/activity+json")
87 |> put_view(ObjectView)
88 |> render("object.json", object: object)
89 else
90 {:public?, false} ->
91 {:error, :not_found}
92 end
93 end
94
95 def track_object_fetch(conn, nil), do: conn
96
97 def track_object_fetch(conn, object_id) do
98 with %{assigns: %{user: %User{id: user_id}}} <- conn do
99 Delivery.create(object_id, user_id)
100 end
101
102 conn
103 end
104
105 def activity(conn, _params) do
106 with ap_id <- Endpoint.url() <> conn.request_path,
107 %Activity{} = activity <- Activity.normalize(ap_id),
108 {_, true} <- {:public?, Visibility.is_public?(activity)} do
109 conn
110 |> maybe_set_tracking_data(activity)
111 |> set_cache_ttl_for(activity)
112 |> put_resp_content_type("application/activity+json")
113 |> put_view(ObjectView)
114 |> render("object.json", object: activity)
115 else
116 {:public?, false} -> {:error, :not_found}
117 nil -> {:error, :not_found}
118 end
119 end
120
121 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
122 object_id = Object.normalize(activity).id
123 assign(conn, :tracking_fun_data, object_id)
124 end
125
126 defp maybe_set_tracking_data(conn, _activity), do: conn
127
128 defp set_cache_ttl_for(conn, %Activity{object: object}) do
129 set_cache_ttl_for(conn, object)
130 end
131
132 defp set_cache_ttl_for(conn, entity) do
133 ttl =
134 case entity do
135 %Object{data: %{"type" => "Question"}} ->
136 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
137
138 %Object{} ->
139 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
140
141 _ ->
142 nil
143 end
144
145 assign(conn, :cache_ttl, ttl)
146 end
147
148 # GET /relay/following
149 def relay_following(conn, _params) do
150 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
151 conn
152 |> put_resp_content_type("application/activity+json")
153 |> put_view(UserView)
154 |> render("following.json", %{user: Relay.get_actor()})
155 end
156 end
157
158 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
159 with %User{} = user <- User.get_cached_by_nickname(nickname),
160 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
161 {:show_follows, true} <-
162 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
163 {page, _} = Integer.parse(page)
164
165 conn
166 |> put_resp_content_type("application/activity+json")
167 |> put_view(UserView)
168 |> render("following.json", %{user: user, page: page, for: for_user})
169 else
170 {:show_follows, _} ->
171 conn
172 |> put_resp_content_type("application/activity+json")
173 |> send_resp(403, "")
174 end
175 end
176
177 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
178 with %User{} = user <- User.get_cached_by_nickname(nickname),
179 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
180 conn
181 |> put_resp_content_type("application/activity+json")
182 |> put_view(UserView)
183 |> render("following.json", %{user: user, for: for_user})
184 end
185 end
186
187 # GET /relay/followers
188 def relay_followers(conn, _params) do
189 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
190 conn
191 |> put_resp_content_type("application/activity+json")
192 |> put_view(UserView)
193 |> render("followers.json", %{user: Relay.get_actor()})
194 end
195 end
196
197 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
198 with %User{} = user <- User.get_cached_by_nickname(nickname),
199 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
200 {:show_followers, true} <-
201 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
202 {page, _} = Integer.parse(page)
203
204 conn
205 |> put_resp_content_type("application/activity+json")
206 |> put_view(UserView)
207 |> render("followers.json", %{user: user, page: page, for: for_user})
208 else
209 {:show_followers, _} ->
210 conn
211 |> put_resp_content_type("application/activity+json")
212 |> send_resp(403, "")
213 end
214 end
215
216 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
217 with %User{} = user <- User.get_cached_by_nickname(nickname),
218 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
219 conn
220 |> put_resp_content_type("application/activity+json")
221 |> put_view(UserView)
222 |> render("followers.json", %{user: user, for: for_user})
223 end
224 end
225
226 def outbox(
227 %{assigns: %{user: for_user}} = conn,
228 %{"nickname" => nickname, "page" => page?} = params
229 )
230 when page? in [true, "true"] do
231 with %User{} = user <- User.get_cached_by_nickname(nickname),
232 {:ok, user} <- User.ensure_keys_present(user) do
233 activities =
234 if params["max_id"] do
235 ActivityPub.fetch_user_activities(user, for_user, %{
236 "max_id" => params["max_id"],
237 # This is a hack because postgres generates inefficient queries when filtering by
238 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
239 "include_poll_votes" => true,
240 "limit" => 10
241 })
242 else
243 ActivityPub.fetch_user_activities(user, for_user, %{
244 "limit" => 10,
245 "include_poll_votes" => true
246 })
247 end
248
249 conn
250 |> put_resp_content_type("application/activity+json")
251 |> put_view(UserView)
252 |> render("activity_collection_page.json", %{
253 activities: activities,
254 iri: "#{user.ap_id}/outbox"
255 })
256 end
257 end
258
259 def outbox(conn, %{"nickname" => nickname}) do
260 with %User{} = user <- User.get_cached_by_nickname(nickname),
261 {:ok, user} <- User.ensure_keys_present(user) do
262 conn
263 |> put_resp_content_type("application/activity+json")
264 |> put_view(UserView)
265 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
266 end
267 end
268
269 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
270 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
271 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
272 true <- Utils.recipient_in_message(recipient, actor, params),
273 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
274 Federator.incoming_ap_doc(params)
275 json(conn, "ok")
276 end
277 end
278
279 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
280 Federator.incoming_ap_doc(params)
281 json(conn, "ok")
282 end
283
284 # POST /relay/inbox -or- POST /internal/fetch/inbox
285 def inbox(conn, params) do
286 if params["type"] == "Create" && FederatingPlug.federating?() do
287 post_inbox_relayed_create(conn, params)
288 else
289 post_inbox_fallback(conn, params)
290 end
291 end
292
293 defp post_inbox_relayed_create(conn, params) do
294 Logger.debug(
295 "Signature missing or not from author, relayed Create message, fetching object from source"
296 )
297
298 Fetcher.fetch_object_from_id(params["object"]["id"])
299
300 json(conn, "ok")
301 end
302
303 defp post_inbox_fallback(conn, params) do
304 headers = Enum.into(conn.req_headers, %{})
305
306 if headers["signature"] && params["actor"] &&
307 String.contains?(headers["signature"], params["actor"]) do
308 Logger.debug(
309 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
310 )
311
312 Logger.debug(inspect(conn.req_headers))
313 end
314
315 conn
316 |> put_status(:bad_request)
317 |> json(dgettext("errors", "error"))
318 end
319
320 defp represent_service_actor(%User{} = user, conn) do
321 with {:ok, user} <- User.ensure_keys_present(user) do
322 conn
323 |> put_resp_content_type("application/activity+json")
324 |> put_view(UserView)
325 |> render("user.json", %{user: user})
326 else
327 nil -> {:error, :not_found}
328 end
329 end
330
331 defp represent_service_actor(nil, _), do: {:error, :not_found}
332
333 def relay(conn, _params) do
334 Relay.get_actor()
335 |> represent_service_actor(conn)
336 end
337
338 def internal_fetch(conn, _params) do
339 InternalFetchActor.get_actor()
340 |> represent_service_actor(conn)
341 end
342
343 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
344 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
345 conn
346 |> put_resp_content_type("application/activity+json")
347 |> put_view(UserView)
348 |> render("user.json", %{user: user})
349 end
350
351 def read_inbox(
352 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
353 %{"nickname" => nickname, "page" => page?} = params
354 )
355 when page? in [true, "true"] do
356 activities =
357 if params["max_id"] do
358 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
359 "max_id" => params["max_id"],
360 "limit" => 10
361 })
362 else
363 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
364 end
365
366 conn
367 |> put_resp_content_type("application/activity+json")
368 |> put_view(UserView)
369 |> render("activity_collection_page.json", %{
370 activities: 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 # TODO: Add support for "object" field
518 @doc """
519 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
520
521 Parameters:
522 - (required) `file`: data of the media
523 - (optionnal) `description`: description of the media, intended for accessibility
524
525 Response:
526 - HTTP Code: 201 Created
527 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
528 """
529 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
530 with {:ok, object} <-
531 ActivityPub.upload(
532 file,
533 actor: User.ap_id(user),
534 description: Map.get(data, "description")
535 ) do
536 Logger.debug(inspect(object))
537
538 conn
539 |> put_status(:created)
540 |> json(object.data)
541 end
542 end
543 end