1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.ActivityPub.ActivityPubController do
6 use Pleroma.Web, :controller
11 alias Pleroma.Object.Fetcher
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
30 action_fallback(:errors)
32 @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
34 plug(FederatingPlug when action in @federating_only_actions)
37 EnsureAuthenticatedPlug,
38 [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
41 # Note: :following and :followers must be served even without authentication (as via :api)
43 EnsureAuthenticatedPlug
44 when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
47 plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
50 Pleroma.Web.Plugs.Cache,
51 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
52 when action in [:activity, :object]
55 plug(:set_requester_reachable when action in [:inbox])
56 plug(:relay_active? when action in [:relay])
58 defp relay_active?(conn, _) do
59 if Pleroma.Config.get([:instance, :allow_relay]) do
63 |> render_error(:not_found, "not found")
68 def user(conn, %{"nickname" => nickname}) do
69 with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
71 |> put_resp_content_type("application/activity+json")
73 |> render("user.json", %{user: user})
75 nil -> {:error, :not_found}
76 %{local: false} -> {:error, :not_found}
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
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)
93 {:visible?, false} -> {:error, :not_found}
94 nil -> {:error, :not_found}
98 def track_object_fetch(conn, nil), do: conn
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)
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
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)
122 {:visible?, false} -> {:error, :not_found}
123 {:local?, false} -> {:error, :not_found}
124 nil -> {:error, :not_found}
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)
133 defp maybe_set_tracking_data(conn, _activity), do: conn
135 defp set_cache_ttl_for(conn, %Activity{object: object}) do
136 set_cache_ttl_for(conn, object)
139 defp set_cache_ttl_for(conn, entity) do
142 %Object{data: %{"type" => "Question"}} ->
143 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
146 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
152 assign(conn, :cache_ttl, ttl)
155 def maybe_skip_cache(conn, user) do
158 |> assign(:skip_cache, true)
164 # GET /relay/following
165 def relay_following(conn, _params) do
166 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
168 |> put_resp_content_type("application/activity+json")
169 |> put_view(UserView)
170 |> render("following.json", %{user: Relay.get_actor()})
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)
181 |> put_resp_content_type("application/activity+json")
182 |> put_view(UserView)
183 |> render("following.json", %{user: user, page: page, for: for_user})
185 {:show_follows, _} ->
187 |> put_resp_content_type("application/activity+json")
188 |> send_resp(403, "")
192 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
193 with %User{} = user <- User.get_cached_by_nickname(nickname) do
195 |> put_resp_content_type("application/activity+json")
196 |> put_view(UserView)
197 |> render("following.json", %{user: user, for: for_user})
201 # GET /relay/followers
202 def relay_followers(conn, _params) do
203 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
205 |> put_resp_content_type("application/activity+json")
206 |> put_view(UserView)
207 |> render("followers.json", %{user: Relay.get_actor()})
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)
218 |> put_resp_content_type("application/activity+json")
219 |> put_view(UserView)
220 |> render("followers.json", %{user: user, page: page, for: for_user})
222 {:show_followers, _} ->
224 |> put_resp_content_type("application/activity+json")
225 |> send_resp(403, "")
229 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
230 with %User{} = user <- User.get_cached_by_nickname(nickname) do
232 |> put_resp_content_type("application/activity+json")
233 |> put_view(UserView)
234 |> render("followers.json", %{user: user, for: for_user})
239 %{assigns: %{user: for_user}} = conn,
240 %{"nickname" => nickname, "page" => page?} = params
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
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)
253 activities = ActivityPub.fetch_user_activities(user, for_user, params)
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"
266 def outbox(conn, %{"nickname" => nickname}) do
267 with %User{} = user <- User.get_cached_by_nickname(nickname) do
269 |> put_resp_content_type("application/activity+json")
270 |> put_view(UserView)
271 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
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)
285 |> put_status(:bad_request)
286 |> json("Invalid request.")
290 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
291 Federator.incoming_ap_doc(params)
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})
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)
306 |> put_status(:bad_request)
307 |> json("Not federating")
311 def inbox(conn, _params) do
313 |> put_status(:bad_request)
314 |> json("error, missing HTTP Signature")
317 defp post_inbox_relayed_create(conn, params) do
319 "Signature missing or not from author, relayed Create message, fetching object from source"
322 Fetcher.fetch_object_from_id(params["object"]["id"])
327 defp represent_service_actor(%User{} = user, conn) do
329 |> put_resp_content_type("application/activity+json")
330 |> put_view(UserView)
331 |> render("user.json", %{user: user})
334 defp represent_service_actor(nil, _), do: {:error, :not_found}
336 def relay(conn, _params) do
338 |> represent_service_actor(conn)
341 def internal_fetch(conn, _params) do
342 InternalFetchActor.get_actor()
343 |> represent_service_actor(conn)
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
349 |> put_resp_content_type("application/activity+json")
350 |> put_view(UserView)
351 |> render("user.json", %{user: user})
355 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
356 %{"nickname" => nickname, "page" => page?} = params
358 when page? in [true, "true"] do
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)
367 [user.ap_id | User.following(user)]
368 |> ActivityPub.fetch_activities(params)
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"
381 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
382 "nickname" => nickname
385 |> put_resp_content_type("application/activity+json")
386 |> put_view(UserView)
387 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
390 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
391 "nickname" => nickname
394 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
396 as_nickname: as_nickname
400 |> put_status(:forbidden)
404 defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
405 when is_map(object) do
407 [object["content"], object["summary"], object["name"]]
408 |> Enum.filter(&is_binary(&1))
412 limit = Pleroma.Config.get([:instance, :limit])
417 |> Transmogrifier.strip_internal_fields()
418 |> Map.put("attributedTo", actor)
419 |> Map.put("actor", actor)
420 |> Map.put("id", Utils.generate_object_id())
422 {:ok, Map.put(activity, "object", object)}
427 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
434 defp fix_user_message(
435 %User{ap_id: actor} = user,
436 %{"type" => "Delete", "object" => object} = activity
438 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
439 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
443 {:error, "No such object found"}
446 {:forbidden, "You can't delete this object"}
450 defp fix_user_message(%User{}, activity) do
455 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
456 %{"nickname" => nickname} = params
460 |> Map.drop(["nickname"])
461 |> Map.put("id", Utils.generate_activity_id())
462 |> Map.put("actor", actor)
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
468 |> put_status(:created)
469 |> put_resp_header("location", activity_data["id"])
470 |> json(activity_data)
472 {:forbidden, message} ->
474 |> put_status(:forbidden)
479 |> put_status(:bad_request)
483 Logger.warning(fn -> "AP C2S: #{inspect(e)}" end)
486 |> put_status(:bad_request)
487 |> json("Bad Request")
491 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
493 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
495 as_nickname: user.nickname
499 |> put_status(:forbidden)
503 defp errors(conn, {:error, :not_found}) do
505 |> put_status(:not_found)
506 |> json(dgettext("errors", "Not found"))
509 defp errors(conn, _e) do
511 |> put_status(:internal_server_error)
512 |> json(dgettext("errors", "error"))
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)
524 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
525 with {:ok, object} <-
528 actor: User.ap_id(user),
529 description: Map.get(data, "description")
531 Logger.debug(inspect(object))
534 |> put_status(:created)
539 def pinned(conn, %{"nickname" => nickname}) do
540 with %User{} = user <- User.get_cached_by_nickname(nickname) do
542 |> put_resp_header("content-type", "application/activity+json")
543 |> json(UserView.render("featured.json", %{user: user}))