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) 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{} = 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)
285 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
286 Federator.incoming_ap_doc(params)
290 def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do
292 |> put_status(:bad_request)
293 |> json("Invalid HTTP Signature")
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)
302 |> put_status(:bad_request)
303 |> json("Not federating")
307 def inbox(conn, _params) do
309 |> put_status(:bad_request)
310 |> json("error, missing HTTP Signature")
313 defp post_inbox_relayed_create(conn, params) do
315 "Signature missing or not from author, relayed Create message, fetching object from source"
318 Fetcher.fetch_object_from_id(params["object"]["id"])
323 defp represent_service_actor(%User{} = user, conn) do
325 |> put_resp_content_type("application/activity+json")
326 |> put_view(UserView)
327 |> render("user.json", %{user: user})
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
381 |> put_resp_content_type("application/activity+json")
382 |> put_view(UserView)
383 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
386 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
387 "nickname" => nickname
390 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
392 as_nickname: as_nickname
396 |> put_status(:forbidden)
400 defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
401 when is_map(object) do
403 [object["content"], object["summary"], object["name"]]
404 |> Enum.filter(&is_binary(&1))
408 limit = Pleroma.Config.get([:instance, :limit])
413 |> Transmogrifier.strip_internal_fields()
414 |> Map.put("attributedTo", actor)
415 |> Map.put("actor", actor)
416 |> Map.put("id", Utils.generate_object_id())
418 {:ok, Map.put(activity, "object", object)}
423 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
430 defp fix_user_message(
431 %User{ap_id: actor} = user,
432 %{"type" => "Delete", "object" => object} = activity
434 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
435 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
439 {:error, "No such object found"}
442 {:forbidden, "You can't delete this object"}
446 defp fix_user_message(%User{}, activity) do
451 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
452 %{"nickname" => nickname} = params
456 |> Map.drop(["nickname"])
457 |> Map.put("id", Utils.generate_activity_id())
458 |> Map.put("actor", actor)
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
464 |> put_status(:created)
465 |> put_resp_header("location", activity_data["id"])
466 |> json(activity_data)
468 {:forbidden, message} ->
470 |> put_status(:forbidden)
475 |> put_status(:bad_request)
479 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
482 |> put_status(:bad_request)
483 |> json("Bad Request")
487 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
489 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
491 as_nickname: user.nickname
495 |> put_status(:forbidden)
499 defp errors(conn, {:error, :not_found}) do
501 |> put_status(:not_found)
502 |> json(dgettext("errors", "Not found"))
505 defp errors(conn, _e) do
507 |> put_status(:internal_server_error)
508 |> json(dgettext("errors", "error"))
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)
520 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
521 with {:ok, object} <-
524 actor: User.ap_id(user),
525 description: Map.get(data, "description")
527 Logger.debug(inspect(object))
530 |> put_status(:created)
535 def pinned(conn, %{"nickname" => nickname}) do
536 with %User{} = user <- User.get_cached_by_nickname(nickname) do
538 |> put_resp_header("content-type", "application/activity+json")
539 |> json(UserView.render("featured.json", %{user: user}))