Remove support for outdated Create 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.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 def inbox(conn, _params) do
297 conn
298 |> put_status(:bad_request)
299 |> json("error, missing HTTP Signature")
300 end
301
302 defp represent_service_actor(%User{} = user, conn) do
303 conn
304 |> put_resp_content_type("application/activity+json")
305 |> put_view(UserView)
306 |> render("user.json", %{user: user})
307 end
308
309 defp represent_service_actor(nil, _), do: {:error, :not_found}
310
311 def relay(conn, _params) do
312 Relay.get_actor()
313 |> represent_service_actor(conn)
314 end
315
316 def internal_fetch(conn, _params) do
317 InternalFetchActor.get_actor()
318 |> represent_service_actor(conn)
319 end
320
321 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
322 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
323 conn
324 |> put_resp_content_type("application/activity+json")
325 |> put_view(UserView)
326 |> render("user.json", %{user: user})
327 end
328
329 def read_inbox(
330 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
331 %{"nickname" => nickname, "page" => page?} = params
332 )
333 when page? in [true, "true"] do
334 params =
335 params
336 |> Map.drop(["nickname", "page"])
337 |> Map.put("blocking_user", user)
338 |> Map.put("user", user)
339 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
340
341 activities =
342 [user.ap_id | User.following(user)]
343 |> ActivityPub.fetch_activities(params)
344 |> Enum.reverse()
345
346 conn
347 |> put_resp_content_type("application/activity+json")
348 |> put_view(UserView)
349 |> render("activity_collection_page.json", %{
350 activities: activities,
351 pagination: ControllerHelper.get_pagination_fields(conn, activities),
352 iri: "#{user.ap_id}/inbox"
353 })
354 end
355
356 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
357 "nickname" => nickname
358 }) do
359 conn
360 |> put_resp_content_type("application/activity+json")
361 |> put_view(UserView)
362 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
363 end
364
365 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
366 "nickname" => nickname
367 }) do
368 err =
369 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
370 nickname: nickname,
371 as_nickname: as_nickname
372 )
373
374 conn
375 |> put_status(:forbidden)
376 |> json(err)
377 end
378
379 defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
380 when is_map(object) do
381 length =
382 [object["content"], object["summary"], object["name"]]
383 |> Enum.filter(&is_binary(&1))
384 |> Enum.join("")
385 |> String.length()
386
387 limit = Pleroma.Config.get([:instance, :limit])
388
389 if length < limit do
390 object =
391 object
392 |> Transmogrifier.strip_internal_fields()
393 |> Map.put("attributedTo", actor)
394 |> Map.put("actor", actor)
395 |> Map.put("id", Utils.generate_object_id())
396
397 {:ok, Map.put(activity, "object", object)}
398 else
399 {:error,
400 dgettext(
401 "errors",
402 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
403 limit: limit,
404 length: length
405 )}
406 end
407 end
408
409 defp fix_user_message(
410 %User{ap_id: actor} = user,
411 %{"type" => "Delete", "object" => object} = activity
412 ) do
413 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
414 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
415 {:ok, activity}
416 else
417 {:normalize, _} ->
418 {:error, "No such object found"}
419
420 {:permission, _} ->
421 {:forbidden, "You can't delete this object"}
422 end
423 end
424
425 defp fix_user_message(%User{}, activity) do
426 {:ok, activity}
427 end
428
429 def update_outbox(
430 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
431 %{"nickname" => nickname} = params
432 ) do
433 params =
434 params
435 |> Map.drop(["nickname"])
436 |> Map.put("id", Utils.generate_activity_id())
437 |> Map.put("actor", actor)
438
439 with {:ok, params} <- fix_user_message(user, params),
440 {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
441 %Activity{data: activity_data} <- Activity.normalize(activity) do
442 conn
443 |> put_status(:created)
444 |> put_resp_header("location", activity_data["id"])
445 |> json(activity_data)
446 else
447 {:forbidden, message} ->
448 conn
449 |> put_status(:forbidden)
450 |> json(message)
451
452 {:error, message} ->
453 conn
454 |> put_status(:bad_request)
455 |> json(message)
456
457 e ->
458 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
459
460 conn
461 |> put_status(:bad_request)
462 |> json("Bad Request")
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 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
500 with {:ok, object} <-
501 ActivityPub.upload(
502 file,
503 actor: User.ap_id(user),
504 description: Map.get(data, "description")
505 ) do
506 Logger.debug(inspect(object))
507
508 conn
509 |> put_status(:created)
510 |> json(object.data)
511 end
512 end
513
514 def pinned(conn, %{"nickname" => nickname}) do
515 with %User{} = user <- User.get_cached_by_nickname(nickname) do
516 conn
517 |> put_resp_header("content-type", "application/activity+json")
518 |> json(UserView.render("featured.json", %{user: user}))
519 end
520 end
521 end