1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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),
70 {:ok, user} <- User.ensure_keys_present(user) do
72 |> put_resp_content_type("application/activity+json")
74 |> render("user.json", %{user: user})
76 nil -> {:error, :not_found}
77 %{local: false} -> {:error, :not_found}
81 def object(%{assigns: assigns} = conn, _) do
82 with ap_id <- Endpoint.url() <> conn.request_path,
83 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
84 user <- Map.get(assigns, :user, nil),
85 {_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
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_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)
121 {:visible?, false} -> {:error, :not_found}
122 {:local?, false} -> {:error, :not_found}
123 nil -> {:error, :not_found}
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)
132 defp maybe_set_tracking_data(conn, _activity), do: conn
134 defp set_cache_ttl_for(conn, %Activity{object: object}) do
135 set_cache_ttl_for(conn, object)
138 defp set_cache_ttl_for(conn, entity) do
141 %Object{data: %{"type" => "Question"}} ->
142 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
145 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
151 assign(conn, :cache_ttl, ttl)
154 # GET /relay/following
155 def relay_following(conn, _params) do
156 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
158 |> put_resp_content_type("application/activity+json")
159 |> put_view(UserView)
160 |> render("following.json", %{user: Relay.get_actor()})
164 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
165 with %User{} = user <- User.get_cached_by_nickname(nickname),
166 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
167 {:show_follows, true} <-
168 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
169 {page, _} = Integer.parse(page)
172 |> put_resp_content_type("application/activity+json")
173 |> put_view(UserView)
174 |> render("following.json", %{user: user, page: page, for: for_user})
176 {:show_follows, _} ->
178 |> put_resp_content_type("application/activity+json")
179 |> send_resp(403, "")
183 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
184 with %User{} = user <- User.get_cached_by_nickname(nickname),
185 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
187 |> put_resp_content_type("application/activity+json")
188 |> put_view(UserView)
189 |> render("following.json", %{user: user, for: for_user})
193 # GET /relay/followers
194 def relay_followers(conn, _params) do
195 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
197 |> put_resp_content_type("application/activity+json")
198 |> put_view(UserView)
199 |> render("followers.json", %{user: Relay.get_actor()})
203 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
204 with %User{} = user <- User.get_cached_by_nickname(nickname),
205 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
206 {:show_followers, true} <-
207 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
208 {page, _} = Integer.parse(page)
211 |> put_resp_content_type("application/activity+json")
212 |> put_view(UserView)
213 |> render("followers.json", %{user: user, page: page, for: for_user})
215 {:show_followers, _} ->
217 |> put_resp_content_type("application/activity+json")
218 |> send_resp(403, "")
222 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
223 with %User{} = user <- User.get_cached_by_nickname(nickname),
224 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
226 |> put_resp_content_type("application/activity+json")
227 |> put_view(UserView)
228 |> render("followers.json", %{user: user, for: for_user})
233 %{assigns: %{user: for_user}} = conn,
234 %{"nickname" => nickname, "page" => page?} = params
236 when page? in [true, "true"] do
237 with %User{} = user <- User.get_cached_by_nickname(nickname),
238 {:ok, user} <- User.ensure_keys_present(user) do
239 # "include_poll_votes" is a hack because postgres generates inefficient
240 # queries when filtering by 'Answer', poll votes will be hidden by the
241 # visibility filter in this case anyway
244 |> Map.drop(["nickname", "page"])
245 |> Map.put("include_poll_votes", true)
246 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
248 activities = ActivityPub.fetch_user_activities(user, for_user, params)
251 |> put_resp_content_type("application/activity+json")
252 |> put_view(UserView)
253 |> render("activity_collection_page.json", %{
254 activities: activities,
255 pagination: ControllerHelper.get_pagination_fields(conn, activities),
256 iri: "#{user.ap_id}/outbox"
261 def outbox(conn, %{"nickname" => nickname}) do
262 with %User{} = user <- User.get_cached_by_nickname(nickname),
263 {:ok, user} <- User.ensure_keys_present(user) do
265 |> put_resp_content_type("application/activity+json")
266 |> put_view(UserView)
267 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
271 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
272 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
273 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
274 true <- Utils.recipient_in_message(recipient, actor, params),
275 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
276 Federator.incoming_ap_doc(params)
281 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
282 Federator.incoming_ap_doc(params)
286 # POST /relay/inbox -or- POST /internal/fetch/inbox
287 def inbox(conn, params) do
288 if params["type"] == "Create" && FederatingPlug.federating?() do
289 post_inbox_relayed_create(conn, params)
291 post_inbox_fallback(conn, params)
295 defp post_inbox_relayed_create(conn, params) do
297 "Signature missing or not from author, relayed Create message, fetching object from source"
300 Fetcher.fetch_object_from_id(params["object"]["id"])
305 defp post_inbox_fallback(conn, params) do
306 headers = Enum.into(conn.req_headers, %{})
308 if headers["signature"] && params["actor"] &&
309 String.contains?(headers["signature"], params["actor"]) do
311 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
314 Logger.debug(inspect(conn.req_headers))
318 |> put_status(:bad_request)
319 |> json(dgettext("errors", "error"))
322 defp represent_service_actor(%User{} = user, conn) do
323 with {:ok, user} <- User.ensure_keys_present(user) do
325 |> put_resp_content_type("application/activity+json")
326 |> put_view(UserView)
327 |> render("user.json", %{user: user})
329 nil -> {:error, :not_found}
333 defp represent_service_actor(nil, _), do: {:error, :not_found}
335 def relay(conn, _params) do
337 |> represent_service_actor(conn)
340 def internal_fetch(conn, _params) do
341 InternalFetchActor.get_actor()
342 |> represent_service_actor(conn)
345 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
346 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
348 |> put_resp_content_type("application/activity+json")
349 |> put_view(UserView)
350 |> render("user.json", %{user: user})
354 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
355 %{"nickname" => nickname, "page" => page?} = params
357 when page? in [true, "true"] do
360 |> Map.drop(["nickname", "page"])
361 |> Map.put("blocking_user", user)
362 |> Map.put("user", user)
363 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
366 [user.ap_id | User.following(user)]
367 |> ActivityPub.fetch_activities(params)
371 |> put_resp_content_type("application/activity+json")
372 |> put_view(UserView)
373 |> render("activity_collection_page.json", %{
374 activities: activities,
375 pagination: ControllerHelper.get_pagination_fields(conn, activities),
376 iri: "#{user.ap_id}/inbox"
380 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
381 "nickname" => nickname
383 with {:ok, user} <- User.ensure_keys_present(user) do
385 |> put_resp_content_type("application/activity+json")
386 |> put_view(UserView)
387 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
391 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
392 "nickname" => nickname
395 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
397 as_nickname: as_nickname
401 |> put_status(:forbidden)
405 defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
406 when is_map(object) do
408 [object["content"], object["summary"], object["name"]]
409 |> Enum.filter(&is_binary(&1))
413 limit = Pleroma.Config.get([:instance, :limit])
418 |> Transmogrifier.strip_internal_fields()
419 |> Map.put("attributedTo", actor)
420 |> Map.put("actor", actor)
421 |> Map.put("id", Utils.generate_object_id())
423 {:ok, Map.put(activity, "object", object)}
428 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
435 defp fix_user_message(
436 %User{ap_id: actor} = user,
437 %{"type" => "Delete", "object" => object} = activity
439 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
440 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
444 {:error, "No such object found"}
447 {:forbidden, "You can't delete this object"}
451 defp fix_user_message(%User{}, activity) do
456 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
457 %{"nickname" => nickname} = params
461 |> Map.drop(["nickname"])
462 |> Map.put("id", Utils.generate_activity_id())
463 |> Map.put("actor", actor)
465 with {:ok, params} <- fix_user_message(user, params),
466 {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
467 %Activity{data: activity_data} <- Activity.normalize(activity) do
469 |> put_status(:created)
470 |> put_resp_header("location", activity_data["id"])
471 |> json(activity_data)
473 {:forbidden, message} ->
475 |> put_status(:forbidden)
480 |> put_status(:bad_request)
484 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
487 |> put_status(:bad_request)
488 |> json("Bad Request")
492 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
494 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
496 as_nickname: user.nickname
500 |> put_status(:forbidden)
504 defp errors(conn, {:error, :not_found}) do
506 |> put_status(:not_found)
507 |> json(dgettext("errors", "Not found"))
510 defp errors(conn, _e) do
512 |> put_status(:internal_server_error)
513 |> json(dgettext("errors", "error"))
516 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
517 with actor <- conn.params["actor"],
518 true <- is_binary(actor) do
519 Pleroma.Instances.set_reachable(actor)
525 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
526 {:ok, new_user} = User.ensure_keys_present(user)
529 if new_user != user and match?(%User{}, for_user) do
530 User.get_cached_by_nickname(for_user.nickname)
538 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
539 with {:ok, object} <-
542 actor: User.ap_id(user),
543 description: Map.get(data, "description")
545 Logger.debug(inspect(object))
548 |> put_status(:created)
553 def pinned(conn, %{"nickname" => nickname}) do
554 with %User{} = user <- User.get_cached_by_nickname(nickname) do
556 |> put_resp_header("content-type", "application/activity+json")
557 |> json(UserView.render("featured.json", %{user: user}))