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 def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do
288 |> put_status(:bad_request)
289 |> json("Invalid HTTP Signature")
292 # POST /relay/inbox -or- POST /internal/fetch/inbox
293 def inbox(conn, %{"type" => "Create"} = params) do
294 if FederatingPlug.federating?() do
295 post_inbox_relayed_create(conn, params)
298 |> put_status(:bad_request)
299 |> json("Not federating")
303 def inbox(conn, _params) do
305 |> put_status(:bad_request)
306 |> json("error, missing HTTP Signature")
309 defp post_inbox_relayed_create(conn, params) do
311 "Signature missing or not from author, relayed Create message, fetching object from source"
314 Fetcher.fetch_object_from_id(params["object"]["id"])
319 defp represent_service_actor(%User{} = user, conn) do
320 with {:ok, user} <- User.ensure_keys_present(user) do
322 |> put_resp_content_type("application/activity+json")
323 |> put_view(UserView)
324 |> render("user.json", %{user: user})
326 nil -> {:error, :not_found}
330 defp represent_service_actor(nil, _), do: {:error, :not_found}
332 def relay(conn, _params) do
334 |> represent_service_actor(conn)
337 def internal_fetch(conn, _params) do
338 InternalFetchActor.get_actor()
339 |> represent_service_actor(conn)
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
345 |> put_resp_content_type("application/activity+json")
346 |> put_view(UserView)
347 |> render("user.json", %{user: user})
351 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
352 %{"nickname" => nickname, "page" => page?} = params
354 when page? in [true, "true"] do
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)
363 [user.ap_id | User.following(user)]
364 |> ActivityPub.fetch_activities(params)
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"
377 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
378 "nickname" => nickname
380 with {:ok, user} <- User.ensure_keys_present(user) do
382 |> put_resp_content_type("application/activity+json")
383 |> put_view(UserView)
384 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
388 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
389 "nickname" => nickname
392 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
394 as_nickname: as_nickname
398 |> put_status(:forbidden)
402 defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
403 when is_map(object) do
405 [object["content"], object["summary"], object["name"]]
406 |> Enum.filter(&is_binary(&1))
410 limit = Pleroma.Config.get([:instance, :limit])
415 |> Transmogrifier.strip_internal_fields()
416 |> Map.put("attributedTo", actor)
417 |> Map.put("actor", actor)
418 |> Map.put("id", Utils.generate_object_id())
420 {:ok, Map.put(activity, "object", object)}
425 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
432 defp fix_user_message(
433 %User{ap_id: actor} = user,
434 %{"type" => "Delete", "object" => object} = activity
436 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
437 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
441 {:error, "No such object found"}
444 {:forbidden, "You can't delete this object"}
448 defp fix_user_message(%User{}, activity) do
453 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
454 %{"nickname" => nickname} = params
458 |> Map.drop(["nickname"])
459 |> Map.put("id", Utils.generate_activity_id())
460 |> Map.put("actor", actor)
462 with {:ok, params} <- fix_user_message(user, params),
463 {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
464 %Activity{data: activity_data} <- Activity.normalize(activity) do
466 |> put_status(:created)
467 |> put_resp_header("location", activity_data["id"])
468 |> json(activity_data)
470 {:forbidden, message} ->
472 |> put_status(:forbidden)
477 |> put_status(:bad_request)
481 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
484 |> put_status(:bad_request)
485 |> json("Bad Request")
489 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
491 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
493 as_nickname: user.nickname
497 |> put_status(:forbidden)
501 defp errors(conn, {:error, :not_found}) do
503 |> put_status(:not_found)
504 |> json(dgettext("errors", "Not found"))
507 defp errors(conn, _e) do
509 |> put_status(:internal_server_error)
510 |> json(dgettext("errors", "error"))
513 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
514 with actor <- conn.params["actor"],
515 true <- is_binary(actor) do
516 Pleroma.Instances.set_reachable(actor)
522 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
523 {:ok, new_user} = User.ensure_keys_present(user)
526 if new_user != user and match?(%User{}, for_user) do
527 User.get_cached_by_nickname(for_user.nickname)
535 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
536 with {:ok, object} <-
539 actor: User.ap_id(user),
540 description: Map.get(data, "description")
542 Logger.debug(inspect(object))
545 |> put_status(:created)
550 def pinned(conn, %{"nickname" => nickname}) do
551 with %User{} = user <- User.get_cached_by_nickname(nickname) do
553 |> put_resp_header("content-type", "application/activity+json")
554 |> json(UserView.render("featured.json", %{user: user}))