total rebase
[anni] / lib / pleroma / web / activity_pub / activity_pub_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 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{is_active: true} = recipient <- User.get_cached_by_nickname(nickname),
277          {:ok, %User{is_active: true} = 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     else
283       _ ->
284         conn
285         |> put_status(:bad_request)
286         |> json("Invalid request.")
287     end
288   end
289
290   def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
291     Federator.incoming_ap_doc(params)
292     json(conn, "ok")
293   end
294
295   def inbox(%{assigns: %{valid_signature: false}, req_headers: req_headers} = conn, params) do
296     Federator.incoming_ap_doc(%{req_headers: req_headers, params: params})
297     json(conn, "ok")
298   end
299
300   # POST /relay/inbox -or- POST /internal/fetch/inbox
301   def inbox(conn, %{"type" => "Create"} = params) do
302     if FederatingPlug.federating?() do
303       post_inbox_relayed_create(conn, params)
304     else
305       conn
306       |> put_status(:bad_request)
307       |> json("Not federating")
308     end
309   end
310
311   def inbox(conn, _params) do
312     conn
313     |> put_status(:bad_request)
314     |> json("error, missing HTTP Signature")
315   end
316
317   defp post_inbox_relayed_create(conn, params) do
318     Logger.debug(
319       "Signature missing or not from author, relayed Create message, fetching object from source"
320     )
321
322     Fetcher.fetch_object_from_id(params["object"]["id"])
323
324     json(conn, "ok")
325   end
326
327   defp represent_service_actor(%User{} = user, conn) do
328     conn
329     |> put_resp_content_type("application/activity+json")
330     |> put_view(UserView)
331     |> render("user.json", %{user: user})
332   end
333
334   defp represent_service_actor(nil, _), do: {:error, :not_found}
335
336   def relay(conn, _params) do
337     Relay.get_actor()
338     |> represent_service_actor(conn)
339   end
340
341   def internal_fetch(conn, _params) do
342     InternalFetchActor.get_actor()
343     |> represent_service_actor(conn)
344   end
345
346   @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
347   def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
348     conn
349     |> put_resp_content_type("application/activity+json")
350     |> put_view(UserView)
351     |> render("user.json", %{user: user})
352   end
353
354   def read_inbox(
355         %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
356         %{"nickname" => nickname, "page" => page?} = params
357       )
358       when page? in [true, "true"] do
359     params =
360       params
361       |> Map.drop(["nickname", "page"])
362       |> Map.put("blocking_user", user)
363       |> Map.put("user", user)
364       |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
365
366     activities =
367       [user.ap_id | User.following(user)]
368       |> ActivityPub.fetch_activities(params)
369       |> Enum.reverse()
370
371     conn
372     |> put_resp_content_type("application/activity+json")
373     |> put_view(UserView)
374     |> render("activity_collection_page.json", %{
375       activities: activities,
376       pagination: ControllerHelper.get_pagination_fields(conn, activities),
377       iri: "#{user.ap_id}/inbox"
378     })
379   end
380
381   def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
382         "nickname" => nickname
383       }) do
384     conn
385     |> put_resp_content_type("application/activity+json")
386     |> put_view(UserView)
387     |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
388   end
389
390   def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
391         "nickname" => nickname
392       }) do
393     err =
394       dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
395         nickname: nickname,
396         as_nickname: as_nickname
397       )
398
399     conn
400     |> put_status(:forbidden)
401     |> json(err)
402   end
403
404   defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
405        when is_map(object) do
406     length =
407       [object["content"], object["summary"], object["name"]]
408       |> Enum.filter(&is_binary(&1))
409       |> Enum.join("")
410       |> String.length()
411
412     limit = Pleroma.Config.get([:instance, :limit])
413
414     if length < limit do
415       object =
416         object
417         |> Transmogrifier.strip_internal_fields()
418         |> Map.put("attributedTo", actor)
419         |> Map.put("actor", actor)
420         |> Map.put("id", Utils.generate_object_id())
421
422       {:ok, Map.put(activity, "object", object)}
423     else
424       {:error,
425        dgettext(
426          "errors",
427          "Character limit (%{limit} characters) exceeded, contains %{length} characters",
428          limit: limit,
429          length: length
430        )}
431     end
432   end
433
434   defp fix_user_message(
435          %User{ap_id: actor} = user,
436          %{"type" => "Delete", "object" => object} = activity
437        ) do
438     with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
439          {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
440       {:ok, activity}
441     else
442       {:normalize, _} ->
443         {:error, "No such object found"}
444
445       {:permission, _} ->
446         {:forbidden, "You can't delete this object"}
447     end
448   end
449
450   defp fix_user_message(%User{}, activity) do
451     {:ok, activity}
452   end
453
454   def update_outbox(
455         %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
456         %{"nickname" => nickname} = params
457       ) do
458     params =
459       params
460       |> Map.drop(["nickname"])
461       |> Map.put("id", Utils.generate_activity_id())
462       |> Map.put("actor", actor)
463
464     with {:ok, params} <- fix_user_message(user, params),
465          {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
466          %Activity{data: activity_data} <- Activity.normalize(activity) do
467       conn
468       |> put_status(:created)
469       |> put_resp_header("location", activity_data["id"])
470       |> json(activity_data)
471     else
472       {:forbidden, message} ->
473         conn
474         |> put_status(:forbidden)
475         |> json(message)
476
477       {:error, message} ->
478         conn
479         |> put_status(:bad_request)
480         |> json(message)
481
482       e ->
483         Logger.warning(fn -> "AP C2S: #{inspect(e)}" end)
484
485         conn
486         |> put_status(:bad_request)
487         |> json("Bad Request")
488     end
489   end
490
491   def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
492     err =
493       dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
494         nickname: nickname,
495         as_nickname: user.nickname
496       )
497
498     conn
499     |> put_status(:forbidden)
500     |> json(err)
501   end
502
503   defp errors(conn, {:error, :not_found}) do
504     conn
505     |> put_status(:not_found)
506     |> json(dgettext("errors", "Not found"))
507   end
508
509   defp errors(conn, _e) do
510     conn
511     |> put_status(:internal_server_error)
512     |> json(dgettext("errors", "error"))
513   end
514
515   defp set_requester_reachable(%Plug.Conn{} = conn, _) do
516     with actor <- conn.params["actor"],
517          true <- is_binary(actor) do
518       Pleroma.Instances.set_reachable(actor)
519     end
520
521     conn
522   end
523
524   def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
525     with {:ok, object} <-
526            ActivityPub.upload(
527              file,
528              actor: User.ap_id(user),
529              description: Map.get(data, "description")
530            ) do
531       Logger.debug(inspect(object))
532
533       conn
534       |> put_status(:created)
535       |> json(object.data)
536     end
537   end
538
539   def pinned(conn, %{"nickname" => nickname}) do
540     with %User{} = user <- User.get_cached_by_nickname(nickname) do
541       conn
542       |> put_resp_header("content-type", "application/activity+json")
543       |> json(UserView.render("featured.json", %{user: user}))
544     end
545   end
546 end