Mix format
[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.User
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.InternalFetchActor
14 alias Pleroma.Web.ActivityPub.ObjectView
15 alias Pleroma.Web.ActivityPub.Pipeline
16 alias Pleroma.Web.ActivityPub.Relay
17 alias Pleroma.Web.ActivityPub.Transmogrifier
18 alias Pleroma.Web.ActivityPub.UserView
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.ActivityPub.Visibility
21 alias Pleroma.Web.ControllerHelper
22 alias Pleroma.Web.Endpoint
23 alias Pleroma.Web.Federator
24 alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
25 alias Pleroma.Web.Plugs.FederatingPlug
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(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
47
48 plug(
49 Pleroma.Web.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) 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(%{assigns: assigns} = conn, _) do
80 with ap_id <- Endpoint.url() <> conn.request_path,
81 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
82 user <- Map.get(assigns, :user, nil),
83 {_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
84 conn
85 |> maybe_skip_cache(user)
86 |> assign(:tracking_fun_data, object.id)
87 |> set_cache_ttl_for(object)
88 |> put_resp_content_type("application/activity+json")
89 |> put_view(ObjectView)
90 |> render("object.json", object: object)
91 else
92 {:visible?, false} -> {:error, :not_found}
93 nil -> {:error, :not_found}
94 end
95 end
96
97 def track_object_fetch(conn, nil), do: conn
98
99 def track_object_fetch(conn, object_id) do
100 with %{assigns: %{user: %User{id: user_id}}} <- conn do
101 Delivery.create(object_id, user_id)
102 end
103
104 conn
105 end
106
107 def activity(%{assigns: assigns} = conn, _) do
108 with ap_id <- Endpoint.url() <> conn.request_path,
109 %Activity{} = activity <- Activity.normalize(ap_id),
110 {_, true} <- {:local?, activity.local},
111 user <- Map.get(assigns, :user, nil),
112 {_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
113 conn
114 |> maybe_skip_cache(user)
115 |> maybe_set_tracking_data(activity)
116 |> set_cache_ttl_for(activity)
117 |> put_resp_content_type("application/activity+json")
118 |> put_view(ObjectView)
119 |> render("object.json", object: activity)
120 else
121 {:visible?, false} -> {:error, :not_found}
122 {:local?, false} -> {:error, :not_found}
123 nil -> {:error, :not_found}
124 end
125 end
126
127 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
128 object_id = Object.normalize(activity, fetch: false).id
129 assign(conn, :tracking_fun_data, object_id)
130 end
131
132 defp maybe_set_tracking_data(conn, _activity), do: conn
133
134 defp set_cache_ttl_for(conn, %Activity{object: object}) do
135 set_cache_ttl_for(conn, object)
136 end
137
138 defp set_cache_ttl_for(conn, entity) do
139 ttl =
140 case entity do
141 %Object{data: %{"type" => "Question"}} ->
142 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
143
144 %Object{} ->
145 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
146
147 _ ->
148 nil
149 end
150
151 assign(conn, :cache_ttl, ttl)
152 end
153
154 def maybe_skip_cache(conn, user) do
155 if user do
156 conn
157 |> assign(:skip_cache, true)
158 else
159 conn
160 end
161 end
162
163 # GET /relay/following
164 def relay_following(conn, _params) do
165 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
166 conn
167 |> put_resp_content_type("application/activity+json")
168 |> put_view(UserView)
169 |> render("following.json", %{user: Relay.get_actor()})
170 end
171 end
172
173 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
174 with %User{} = user <- User.get_cached_by_nickname(nickname),
175 {:show_follows, true} <-
176 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
177 {page, _} = Integer.parse(page)
178
179 conn
180 |> put_resp_content_type("application/activity+json")
181 |> put_view(UserView)
182 |> render("following.json", %{user: user, page: page, for: for_user})
183 else
184 {:show_follows, _} ->
185 conn
186 |> put_resp_content_type("application/activity+json")
187 |> send_resp(403, "")
188 end
189 end
190
191 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
192 with %User{} = user <- User.get_cached_by_nickname(nickname) do
193 conn
194 |> put_resp_content_type("application/activity+json")
195 |> put_view(UserView)
196 |> render("following.json", %{user: user, for: for_user})
197 end
198 end
199
200 # GET /relay/followers
201 def relay_followers(conn, _params) do
202 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
203 conn
204 |> put_resp_content_type("application/activity+json")
205 |> put_view(UserView)
206 |> render("followers.json", %{user: Relay.get_actor()})
207 end
208 end
209
210 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
211 with %User{} = user <- User.get_cached_by_nickname(nickname),
212 {:show_followers, true} <-
213 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
214 {page, _} = Integer.parse(page)
215
216 conn
217 |> put_resp_content_type("application/activity+json")
218 |> put_view(UserView)
219 |> render("followers.json", %{user: user, page: page, for: for_user})
220 else
221 {:show_followers, _} ->
222 conn
223 |> put_resp_content_type("application/activity+json")
224 |> send_resp(403, "")
225 end
226 end
227
228 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
229 with %User{} = user <- User.get_cached_by_nickname(nickname) do
230 conn
231 |> put_resp_content_type("application/activity+json")
232 |> put_view(UserView)
233 |> render("followers.json", %{user: user, for: for_user})
234 end
235 end
236
237 def outbox(
238 %{assigns: %{user: for_user}} = conn,
239 %{"nickname" => nickname, "page" => page?} = params
240 )
241 when page? in [true, "true"] do
242 with %User{} = user <- User.get_cached_by_nickname(nickname) do
243 # "include_poll_votes" is a hack because postgres generates inefficient
244 # queries when filtering by 'Answer', poll votes will be hidden by the
245 # visibility filter in this case anyway
246 params =
247 params
248 |> Map.drop(["nickname", "page"])
249 |> Map.put("include_poll_votes", true)
250 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
251
252 activities = ActivityPub.fetch_user_activities(user, for_user, params)
253
254 conn
255 |> put_resp_content_type("application/activity+json")
256 |> put_view(UserView)
257 |> render("activity_collection_page.json", %{
258 activities: activities,
259 pagination: ControllerHelper.get_pagination_fields(conn, activities),
260 iri: "#{user.ap_id}/outbox"
261 })
262 end
263 end
264
265 def outbox(conn, %{"nickname" => nickname}) do
266 with %User{} = user <- User.get_cached_by_nickname(nickname) do
267 conn
268 |> put_resp_content_type("application/activity+json")
269 |> put_view(UserView)
270 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
271 end
272 end
273
274 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
275 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
276 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
277 true <- Utils.recipient_in_message(recipient, actor, params),
278 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
279 Federator.incoming_ap_doc(params)
280 json(conn, "ok")
281 end
282 end
283
284 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
285 Federator.incoming_ap_doc(params)
286 json(conn, "ok")
287 end
288
289 def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do
290 conn
291 |> put_status(:bad_request)
292 |> json("Invalid HTTP Signature")
293 end
294
295 def inbox(conn, _params) do
296 conn
297 |> put_status(:bad_request)
298 |> json("error, missing HTTP Signature")
299 end
300
301 defp represent_service_actor(%User{} = user, conn) do
302 conn
303 |> put_resp_content_type("application/activity+json")
304 |> put_view(UserView)
305 |> render("user.json", %{user: user})
306 end
307
308 defp represent_service_actor(nil, _), do: {:error, :not_found}
309
310 def relay(conn, _params) do
311 Relay.get_actor()
312 |> represent_service_actor(conn)
313 end
314
315 def internal_fetch(conn, _params) do
316 InternalFetchActor.get_actor()
317 |> represent_service_actor(conn)
318 end
319
320 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
321 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
322 conn
323 |> put_resp_content_type("application/activity+json")
324 |> put_view(UserView)
325 |> render("user.json", %{user: user})
326 end
327
328 def read_inbox(
329 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
330 %{"nickname" => nickname, "page" => page?} = params
331 )
332 when page? in [true, "true"] do
333 params =
334 params
335 |> Map.drop(["nickname", "page"])
336 |> Map.put("blocking_user", user)
337 |> Map.put("user", user)
338 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
339
340 activities =
341 [user.ap_id | User.following(user)]
342 |> ActivityPub.fetch_activities(params)
343 |> Enum.reverse()
344
345 conn
346 |> put_resp_content_type("application/activity+json")
347 |> put_view(UserView)
348 |> render("activity_collection_page.json", %{
349 activities: activities,
350 pagination: ControllerHelper.get_pagination_fields(conn, activities),
351 iri: "#{user.ap_id}/inbox"
352 })
353 end
354
355 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
356 "nickname" => nickname
357 }) do
358 conn
359 |> put_resp_content_type("application/activity+json")
360 |> put_view(UserView)
361 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
362 end
363
364 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
365 "nickname" => nickname
366 }) do
367 err =
368 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
369 nickname: nickname,
370 as_nickname: as_nickname
371 )
372
373 conn
374 |> put_status(:forbidden)
375 |> json(err)
376 end
377
378 defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
379 when is_map(object) do
380 length =
381 [object["content"], object["summary"], object["name"]]
382 |> Enum.filter(&is_binary(&1))
383 |> Enum.join("")
384 |> String.length()
385
386 limit = Pleroma.Config.get([:instance, :limit])
387
388 if length < limit do
389 object =
390 object
391 |> Transmogrifier.strip_internal_fields()
392 |> Map.put("attributedTo", actor)
393 |> Map.put("actor", actor)
394 |> Map.put("id", Utils.generate_object_id())
395
396 {:ok, Map.put(activity, "object", object)}
397 else
398 {:error,
399 dgettext(
400 "errors",
401 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
402 limit: limit,
403 length: length
404 )}
405 end
406 end
407
408 defp fix_user_message(
409 %User{ap_id: actor} = user,
410 %{"type" => "Delete", "object" => object} = activity
411 ) do
412 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
413 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
414 {:ok, activity}
415 else
416 {:normalize, _} ->
417 {:error, "No such object found"}
418
419 {:permission, _} ->
420 {:forbidden, "You can't delete this object"}
421 end
422 end
423
424 defp fix_user_message(%User{}, activity) do
425 {:ok, activity}
426 end
427
428 def update_outbox(
429 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
430 %{"nickname" => nickname} = params
431 ) do
432 params =
433 params
434 |> Map.drop(["nickname"])
435 |> Map.put("id", Utils.generate_activity_id())
436 |> Map.put("actor", actor)
437
438 with {:ok, params} <- fix_user_message(user, params),
439 {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
440 %Activity{data: activity_data} <- Activity.normalize(activity) do
441 conn
442 |> put_status(:created)
443 |> put_resp_header("location", activity_data["id"])
444 |> json(activity_data)
445 else
446 {:forbidden, message} ->
447 conn
448 |> put_status(:forbidden)
449 |> json(message)
450
451 {:error, message} ->
452 conn
453 |> put_status(:bad_request)
454 |> json(message)
455
456 e ->
457 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
458
459 conn
460 |> put_status(:bad_request)
461 |> json("Bad Request")
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 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
499 with {:ok, object} <-
500 ActivityPub.upload(
501 file,
502 actor: User.ap_id(user),
503 description: Map.get(data, "description")
504 ) do
505 Logger.debug(inspect(object))
506
507 conn
508 |> put_status(:created)
509 |> json(object.data)
510 end
511 end
512
513 def pinned(conn, %{"nickname" => nickname}) do
514 with %User{} = user <- User.get_cached_by_nickname(nickname) do
515 conn
516 |> put_resp_header("content-type", "application/activity+json")
517 |> json(UserView.render("featured.json", %{user: user}))
518 end
519 end
520 end