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 |> maybe_skip_cache(user)
88 |> assign(:tracking_fun_data, object.id)
89 |> set_cache_ttl_for(object)
90 |> put_resp_content_type("application/activity+json")
91 |> put_view(ObjectView)
92 |> render("object.json", object: object)
94 {:visible?, false} -> {:error, :not_found}
95 nil -> {:error, :not_found}
99 def track_object_fetch(conn, nil), do: conn
101 def track_object_fetch(conn, object_id) do
102 with %{assigns: %{user: %User{id: user_id}}} <- conn do
103 Delivery.create(object_id, user_id)
109 def activity(%{assigns: assigns} = conn, _) do
110 with ap_id <- Endpoint.url() <> conn.request_path,
111 %Activity{} = activity <- Activity.normalize(ap_id),
112 {_, true} <- {:local?, activity.local},
113 user <- Map.get(assigns, :user, nil),
114 {_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
116 |> maybe_skip_cache(user)
117 |> maybe_set_tracking_data(activity)
118 |> set_cache_ttl_for(activity)
119 |> put_resp_content_type("application/activity+json")
120 |> put_view(ObjectView)
121 |> render("object.json", object: activity)
123 {:visible?, false} -> {:error, :not_found}
124 {:local?, false} -> {:error, :not_found}
125 nil -> {:error, :not_found}
129 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
130 object_id = Object.normalize(activity, fetch: false).id
131 assign(conn, :tracking_fun_data, object_id)
134 defp maybe_set_tracking_data(conn, _activity), do: conn
136 defp set_cache_ttl_for(conn, %Activity{object: object}) do
137 set_cache_ttl_for(conn, object)
140 defp set_cache_ttl_for(conn, entity) do
143 %Object{data: %{"type" => "Question"}} ->
144 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
147 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
153 assign(conn, :cache_ttl, ttl)
156 def maybe_skip_cache(conn, user) do
159 |> assign(:skip_cache, true)
165 # GET /relay/following
166 def relay_following(conn, _params) do
167 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
169 |> put_resp_content_type("application/activity+json")
170 |> put_view(UserView)
171 |> render("following.json", %{user: Relay.get_actor()})
175 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
176 with %User{} = user <- User.get_cached_by_nickname(nickname),
177 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
178 {:show_follows, true} <-
179 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
180 {page, _} = Integer.parse(page)
183 |> put_resp_content_type("application/activity+json")
184 |> put_view(UserView)
185 |> render("following.json", %{user: user, page: page, for: for_user})
187 {:show_follows, _} ->
189 |> put_resp_content_type("application/activity+json")
190 |> send_resp(403, "")
194 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
195 with %User{} = user <- User.get_cached_by_nickname(nickname),
196 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
198 |> put_resp_content_type("application/activity+json")
199 |> put_view(UserView)
200 |> render("following.json", %{user: user, for: for_user})
204 # GET /relay/followers
205 def relay_followers(conn, _params) do
206 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
208 |> put_resp_content_type("application/activity+json")
209 |> put_view(UserView)
210 |> render("followers.json", %{user: Relay.get_actor()})
214 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
215 with %User{} = user <- User.get_cached_by_nickname(nickname),
216 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
217 {:show_followers, true} <-
218 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
219 {page, _} = Integer.parse(page)
222 |> put_resp_content_type("application/activity+json")
223 |> put_view(UserView)
224 |> render("followers.json", %{user: user, page: page, for: for_user})
226 {:show_followers, _} ->
228 |> put_resp_content_type("application/activity+json")
229 |> send_resp(403, "")
233 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
234 with %User{} = user <- User.get_cached_by_nickname(nickname),
235 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
237 |> put_resp_content_type("application/activity+json")
238 |> put_view(UserView)
239 |> render("followers.json", %{user: user, for: for_user})
244 %{assigns: %{user: for_user}} = conn,
245 %{"nickname" => nickname, "page" => page?} = params
247 when page? in [true, "true"] do
248 with %User{} = user <- User.get_cached_by_nickname(nickname),
249 {:ok, user} <- User.ensure_keys_present(user) do
250 # "include_poll_votes" is a hack because postgres generates inefficient
251 # queries when filtering by 'Answer', poll votes will be hidden by the
252 # visibility filter in this case anyway
255 |> Map.drop(["nickname", "page"])
256 |> Map.put("include_poll_votes", true)
257 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
259 activities = ActivityPub.fetch_user_activities(user, for_user, params)
262 |> put_resp_content_type("application/activity+json")
263 |> put_view(UserView)
264 |> render("activity_collection_page.json", %{
265 activities: activities,
266 pagination: ControllerHelper.get_pagination_fields(conn, activities),
267 iri: "#{user.ap_id}/outbox"
272 def outbox(conn, %{"nickname" => nickname}) do
273 with %User{} = user <- User.get_cached_by_nickname(nickname),
274 {:ok, user} <- User.ensure_keys_present(user) do
276 |> put_resp_content_type("application/activity+json")
277 |> put_view(UserView)
278 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
282 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
283 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
284 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
285 true <- Utils.recipient_in_message(recipient, actor, params),
286 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
287 Federator.incoming_ap_doc(params)
292 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
293 Federator.incoming_ap_doc(params)
297 def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do
299 |> put_status(:bad_request)
300 |> json("Invalid HTTP Signature")
303 # POST /relay/inbox -or- POST /internal/fetch/inbox
304 def inbox(conn, %{"type" => "Create"} = params) do
305 if FederatingPlug.federating?() do
306 post_inbox_relayed_create(conn, params)
309 |> put_status(:bad_request)
310 |> json("Not federating")
314 def inbox(conn, _params) do
316 |> put_status(:bad_request)
317 |> json("error, missing HTTP Signature")
320 defp post_inbox_relayed_create(conn, params) do
322 "Signature missing or not from author, relayed Create message, fetching object from source"
325 Fetcher.fetch_object_from_id(params["object"]["id"])
330 defp represent_service_actor(%User{} = user, conn) do
331 with {:ok, user} <- User.ensure_keys_present(user) do
333 |> put_resp_content_type("application/activity+json")
334 |> put_view(UserView)
335 |> render("user.json", %{user: user})
337 nil -> {:error, :not_found}
341 defp represent_service_actor(nil, _), do: {:error, :not_found}
343 def relay(conn, _params) do
345 |> represent_service_actor(conn)
348 def internal_fetch(conn, _params) do
349 InternalFetchActor.get_actor()
350 |> represent_service_actor(conn)
353 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
354 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
356 |> put_resp_content_type("application/activity+json")
357 |> put_view(UserView)
358 |> render("user.json", %{user: user})
362 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
363 %{"nickname" => nickname, "page" => page?} = params
365 when page? in [true, "true"] do
368 |> Map.drop(["nickname", "page"])
369 |> Map.put("blocking_user", user)
370 |> Map.put("user", user)
371 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
374 [user.ap_id | User.following(user)]
375 |> ActivityPub.fetch_activities(params)
379 |> put_resp_content_type("application/activity+json")
380 |> put_view(UserView)
381 |> render("activity_collection_page.json", %{
382 activities: activities,
383 pagination: ControllerHelper.get_pagination_fields(conn, activities),
384 iri: "#{user.ap_id}/inbox"
388 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
389 "nickname" => nickname
391 with {:ok, user} <- User.ensure_keys_present(user) do
393 |> put_resp_content_type("application/activity+json")
394 |> put_view(UserView)
395 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
399 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
400 "nickname" => nickname
403 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
405 as_nickname: as_nickname
409 |> put_status(:forbidden)
413 defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
414 when is_map(object) do
416 [object["content"], object["summary"], object["name"]]
417 |> Enum.filter(&is_binary(&1))
421 limit = Pleroma.Config.get([:instance, :limit])
426 |> Transmogrifier.strip_internal_fields()
427 |> Map.put("attributedTo", actor)
428 |> Map.put("actor", actor)
429 |> Map.put("id", Utils.generate_object_id())
431 {:ok, Map.put(activity, "object", object)}
436 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
443 defp fix_user_message(
444 %User{ap_id: actor} = user,
445 %{"type" => "Delete", "object" => object} = activity
447 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
448 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
452 {:error, "No such object found"}
455 {:forbidden, "You can't delete this object"}
459 defp fix_user_message(%User{}, activity) do
464 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
465 %{"nickname" => nickname} = params
469 |> Map.drop(["nickname"])
470 |> Map.put("id", Utils.generate_activity_id())
471 |> Map.put("actor", actor)
473 with {:ok, params} <- fix_user_message(user, params),
474 {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
475 %Activity{data: activity_data} <- Activity.normalize(activity) do
477 |> put_status(:created)
478 |> put_resp_header("location", activity_data["id"])
479 |> json(activity_data)
481 {:forbidden, message} ->
483 |> put_status(:forbidden)
488 |> put_status(:bad_request)
492 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
495 |> put_status(:bad_request)
496 |> json("Bad Request")
500 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
502 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
504 as_nickname: user.nickname
508 |> put_status(:forbidden)
512 defp errors(conn, {:error, :not_found}) do
514 |> put_status(:not_found)
515 |> json(dgettext("errors", "Not found"))
518 defp errors(conn, _e) do
520 |> put_status(:internal_server_error)
521 |> json(dgettext("errors", "error"))
524 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
525 with actor <- conn.params["actor"],
526 true <- is_binary(actor) do
527 Pleroma.Instances.set_reachable(actor)
533 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
534 {:ok, new_user} = User.ensure_keys_present(user)
537 if new_user != user and match?(%User{}, for_user) do
538 User.get_cached_by_nickname(for_user.nickname)
546 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
547 with {:ok, object} <-
550 actor: User.ap_id(user),
551 description: Map.get(data, "description")
553 Logger.debug(inspect(object))
556 |> put_status(:created)
561 def pinned(conn, %{"nickname" => nickname}) do
562 with %User{} = user <- User.get_cached_by_nickname(nickname) do
564 |> put_resp_header("content-type", "application/activity+json")
565 |> json(UserView.render("featured.json", %{user: user}))