c07f91b2e9cb0e31dd85071d0608f1869a20683c
[akkoma] / lib / pleroma / web / activity_pub / activity_pub_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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.InternalFetchActor
15 alias Pleroma.Web.ActivityPub.ObjectView
16 alias Pleroma.Web.ActivityPub.Pipeline
17 alias Pleroma.Web.ActivityPub.Relay
18 alias Pleroma.Web.ActivityPub.Transmogrifier
19 alias Pleroma.Web.ActivityPub.UserView
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.ControllerHelper
23 alias Pleroma.Web.Endpoint
24 alias Pleroma.Web.Federator
25 alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
26 alias Pleroma.Web.Plugs.FederatingPlug
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(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
48
49 plug(
50 Pleroma.Web.Plugs.Cache,
51 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
52 when action in [:activity, :object]
53 )
54
55 plug(:set_requester_reachable when action in [:inbox])
56 plug(:relay_active? when action in [:relay])
57
58 defp relay_active?(conn, _) do
59 if Pleroma.Config.get([:instance, :allow_relay]) do
60 conn
61 else
62 conn
63 |> render_error(:not_found, "not found")
64 |> halt()
65 end
66 end
67
68 def user(conn, %{"nickname" => nickname}) do
69 with %User{local: true} = user <- User.get_cached_by_nickname(nickname) 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(%{assigns: assigns} = conn, _) do
81 with ap_id <- Endpoint.url() <> conn.request_path,
82 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
83 user <- Map.get(assigns, :user, nil),
84 {_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
85 conn
86 |> maybe_skip_cache(user)
87 |> assign(:tracking_fun_data, object.id)
88 |> set_cache_ttl_for(object)
89 |> put_resp_content_type("application/activity+json")
90 |> put_view(ObjectView)
91 |> render("object.json", object: object)
92 else
93 {:visible?, false} -> {:error, :not_found}
94 nil -> {:error, :not_found}
95 end
96 end
97
98 def track_object_fetch(conn, nil), do: conn
99
100 def track_object_fetch(conn, object_id) do
101 with %{assigns: %{user: %User{id: user_id}}} <- conn do
102 Delivery.create(object_id, user_id)
103 end
104
105 conn
106 end
107
108 def activity(%{assigns: assigns} = conn, _) do
109 with ap_id <- Endpoint.url() <> conn.request_path,
110 %Activity{} = activity <- Activity.normalize(ap_id),
111 {_, true} <- {:local?, activity.local},
112 user <- Map.get(assigns, :user, nil),
113 {_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
114 conn
115 |> maybe_skip_cache(user)
116 |> maybe_set_tracking_data(activity)
117 |> set_cache_ttl_for(activity)
118 |> put_resp_content_type("application/activity+json")
119 |> put_view(ObjectView)
120 |> render("object.json", object: activity)
121 else
122 {:visible?, false} -> {:error, :not_found}
123 {:local?, false} -> {:error, :not_found}
124 nil -> {:error, :not_found}
125 end
126 end
127
128 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
129 object_id = Object.normalize(activity, fetch: false).id
130 assign(conn, :tracking_fun_data, object_id)
131 end
132
133 defp maybe_set_tracking_data(conn, _activity), do: conn
134
135 defp set_cache_ttl_for(conn, %Activity{object: object}) do
136 set_cache_ttl_for(conn, object)
137 end
138
139 defp set_cache_ttl_for(conn, entity) do
140 ttl =
141 case entity do
142 %Object{data: %{"type" => "Question"}} ->
143 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
144
145 %Object{} ->
146 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
147
148 _ ->
149 nil
150 end
151
152 assign(conn, :cache_ttl, ttl)
153 end
154
155 def maybe_skip_cache(conn, user) do
156 if user do
157 conn
158 |> assign(:skip_cache, true)
159 else
160 conn
161 end
162 end
163
164 # GET /relay/following
165 def relay_following(conn, _params) do
166 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
167 conn
168 |> put_resp_content_type("application/activity+json")
169 |> put_view(UserView)
170 |> render("following.json", %{user: Relay.get_actor()})
171 end
172 end
173
174 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
175 with %User{} = user <- User.get_cached_by_nickname(nickname),
176 {:show_follows, true} <-
177 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
178 {page, _} = Integer.parse(page)
179
180 conn
181 |> put_resp_content_type("application/activity+json")
182 |> put_view(UserView)
183 |> render("following.json", %{user: user, page: page, for: for_user})
184 else
185 {:show_follows, _} ->
186 conn
187 |> put_resp_content_type("application/activity+json")
188 |> send_resp(403, "")
189 end
190 end
191
192 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
193 with %User{} = user <- User.get_cached_by_nickname(nickname) do
194 conn
195 |> put_resp_content_type("application/activity+json")
196 |> put_view(UserView)
197 |> render("following.json", %{user: user, for: for_user})
198 end
199 end
200
201 # GET /relay/followers
202 def relay_followers(conn, _params) do
203 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
204 conn
205 |> put_resp_content_type("application/activity+json")
206 |> put_view(UserView)
207 |> render("followers.json", %{user: Relay.get_actor()})
208 end
209 end
210
211 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
212 with %User{} = user <- User.get_cached_by_nickname(nickname),
213 {:show_followers, true} <-
214 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
215 {page, _} = Integer.parse(page)
216
217 conn
218 |> put_resp_content_type("application/activity+json")
219 |> put_view(UserView)
220 |> render("followers.json", %{user: user, page: page, for: for_user})
221 else
222 {:show_followers, _} ->
223 conn
224 |> put_resp_content_type("application/activity+json")
225 |> send_resp(403, "")
226 end
227 end
228
229 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
230 with %User{} = user <- User.get_cached_by_nickname(nickname) do
231 conn
232 |> put_resp_content_type("application/activity+json")
233 |> put_view(UserView)
234 |> render("followers.json", %{user: user, for: for_user})
235 end
236 end
237
238 def outbox(
239 %{assigns: %{user: for_user}} = conn,
240 %{"nickname" => nickname, "page" => page?} = params
241 )
242 when page? in [true, "true"] do
243 with %User{} = user <- User.get_cached_by_nickname(nickname) do
244 # "include_poll_votes" is a hack because postgres generates inefficient
245 # queries when filtering by 'Answer', poll votes will be hidden by the
246 # visibility filter in this case anyway
247 params =
248 params
249 |> Map.drop(["nickname", "page"])
250 |> Map.put("include_poll_votes", true)
251 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
252
253 activities = ActivityPub.fetch_user_activities(user, for_user, params)
254
255 conn
256 |> put_resp_content_type("application/activity+json")
257 |> put_view(UserView)
258 |> render("activity_collection_page.json", %{
259 activities: activities,
260 pagination: ControllerHelper.get_pagination_fields(conn, activities),
261 iri: "#{user.ap_id}/outbox"
262 })
263 end
264 end
265
266 def outbox(conn, %{"nickname" => nickname}) do
267 with %User{} = user <- User.get_cached_by_nickname(nickname) do
268 conn
269 |> put_resp_content_type("application/activity+json")
270 |> put_view(UserView)
271 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
272 end
273 end
274
275 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
276 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
277 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
278 true <- Utils.recipient_in_message(recipient, actor, params),
279 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
280 Federator.incoming_ap_doc(params)
281 json(conn, "ok")
282 end
283 end
284
285 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
286 Federator.incoming_ap_doc(params)
287 json(conn, "ok")
288 end
289
290 def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do
291 conn
292 |> put_status(:bad_request)
293 |> json("Invalid HTTP Signature")
294 end
295
296 # POST /relay/inbox -or- POST /internal/fetch/inbox
297 def inbox(conn, %{"type" => "Create"} = params) do
298 if FederatingPlug.federating?() do
299 post_inbox_relayed_create(conn, params)
300 else
301 conn
302 |> put_status(:bad_request)
303 |> json("Not federating")
304 end
305 end
306
307 def inbox(conn, _params) do
308 conn
309 |> put_status(:bad_request)
310 |> json("error, missing HTTP Signature")
311 end
312
313 defp post_inbox_relayed_create(conn, params) do
314 Logger.debug(
315 "Signature missing or not from author, relayed Create message, fetching object from source"
316 )
317
318 Fetcher.fetch_object_from_id(params["object"]["id"])
319
320 json(conn, "ok")
321 end
322
323 defp represent_service_actor(%User{} = user, conn) do
324 conn
325 |> put_resp_content_type("application/activity+json")
326 |> put_view(UserView)
327 |> render("user.json", %{user: user})
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 params =
356 params
357 |> Map.drop(["nickname", "page"])
358 |> Map.put("blocking_user", user)
359 |> Map.put("user", user)
360 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
361
362 activities =
363 [user.ap_id | User.following(user)]
364 |> ActivityPub.fetch_activities(params)
365 |> Enum.reverse()
366
367 conn
368 |> put_resp_content_type("application/activity+json")
369 |> put_view(UserView)
370 |> render("activity_collection_page.json", %{
371 activities: activities,
372 pagination: ControllerHelper.get_pagination_fields(conn, activities),
373 iri: "#{user.ap_id}/inbox"
374 })
375 end
376
377 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
378 "nickname" => nickname
379 }) do
380 conn
381 |> put_resp_content_type("application/activity+json")
382 |> put_view(UserView)
383 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
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 fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
401 when is_map(object) do
402 length =
403 [object["content"], object["summary"], object["name"]]
404 |> Enum.filter(&is_binary(&1))
405 |> Enum.join("")
406 |> String.length()
407
408 limit = Pleroma.Config.get([:instance, :limit])
409
410 if length < limit do
411 object =
412 object
413 |> Transmogrifier.strip_internal_fields()
414 |> Map.put("attributedTo", actor)
415 |> Map.put("actor", actor)
416 |> Map.put("id", Utils.generate_object_id())
417
418 {:ok, Map.put(activity, "object", object)}
419 else
420 {:error,
421 dgettext(
422 "errors",
423 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
424 limit: limit,
425 length: length
426 )}
427 end
428 end
429
430 defp fix_user_message(
431 %User{ap_id: actor} = user,
432 %{"type" => "Delete", "object" => object} = activity
433 ) do
434 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
435 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
436 {:ok, activity}
437 else
438 {:normalize, _} ->
439 {:error, "No such object found"}
440
441 {:permission, _} ->
442 {:forbidden, "You can't delete this object"}
443 end
444 end
445
446 defp fix_user_message(%User{}, activity) do
447 {:ok, activity}
448 end
449
450 def update_outbox(
451 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
452 %{"nickname" => nickname} = params
453 ) do
454 params =
455 params
456 |> Map.drop(["nickname"])
457 |> Map.put("id", Utils.generate_activity_id())
458 |> Map.put("actor", actor)
459
460 with {:ok, params} <- fix_user_message(user, params),
461 {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
462 %Activity{data: activity_data} <- Activity.normalize(activity) do
463 conn
464 |> put_status(:created)
465 |> put_resp_header("location", activity_data["id"])
466 |> json(activity_data)
467 else
468 {:forbidden, message} ->
469 conn
470 |> put_status(:forbidden)
471 |> json(message)
472
473 {:error, message} ->
474 conn
475 |> put_status(:bad_request)
476 |> json(message)
477
478 e ->
479 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
480
481 conn
482 |> put_status(:bad_request)
483 |> json("Bad Request")
484 end
485 end
486
487 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
488 err =
489 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
490 nickname: nickname,
491 as_nickname: user.nickname
492 )
493
494 conn
495 |> put_status(:forbidden)
496 |> json(err)
497 end
498
499 defp errors(conn, {:error, :not_found}) do
500 conn
501 |> put_status(:not_found)
502 |> json(dgettext("errors", "Not found"))
503 end
504
505 defp errors(conn, _e) do
506 conn
507 |> put_status(:internal_server_error)
508 |> json(dgettext("errors", "error"))
509 end
510
511 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
512 with actor <- conn.params["actor"],
513 true <- is_binary(actor) do
514 Pleroma.Instances.set_reachable(actor)
515 end
516
517 conn
518 end
519
520 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
521 with {:ok, object} <-
522 ActivityPub.upload(
523 file,
524 actor: User.ap_id(user),
525 description: Map.get(data, "description")
526 ) do
527 Logger.debug(inspect(object))
528
529 conn
530 |> put_status(:created)
531 |> json(object.data)
532 end
533 end
534
535 def pinned(conn, %{"nickname" => nickname}) do
536 with %User{} = user <- User.get_cached_by_nickname(nickname) do
537 conn
538 |> put_resp_header("content-type", "application/activity+json")
539 |> json(UserView.render("featured.json", %{user: user}))
540 end
541 end
542 end