move to 2.5.5
[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{} = 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