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 def inbox(conn, _params) do
298 |> put_status(:bad_request)
299 |> json("error, missing HTTP Signature")
302 defp represent_service_actor(%User{} = user, conn) do
304 |> put_resp_content_type("application/activity+json")
305 |> put_view(UserView)
306 |> render("user.json", %{user: user})
309 defp represent_service_actor(nil, _), do: {:error, :not_found}
311 def relay(conn, _params) do
313 |> represent_service_actor(conn)
316 def internal_fetch(conn, _params) do
317 InternalFetchActor.get_actor()
318 |> represent_service_actor(conn)
321 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
322 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
324 |> put_resp_content_type("application/activity+json")
325 |> put_view(UserView)
326 |> render("user.json", %{user: user})
330 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
331 %{"nickname" => nickname, "page" => page?} = params
333 when page? in [true, "true"] do
336 |> Map.drop(["nickname", "page"])
337 |> Map.put("blocking_user", user)
338 |> Map.put("user", user)
339 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
342 [user.ap_id | User.following(user)]
343 |> ActivityPub.fetch_activities(params)
347 |> put_resp_content_type("application/activity+json")
348 |> put_view(UserView)
349 |> render("activity_collection_page.json", %{
350 activities: activities,
351 pagination: ControllerHelper.get_pagination_fields(conn, activities),
352 iri: "#{user.ap_id}/inbox"
356 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
357 "nickname" => nickname
360 |> put_resp_content_type("application/activity+json")
361 |> put_view(UserView)
362 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
365 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
366 "nickname" => nickname
369 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
371 as_nickname: as_nickname
375 |> put_status(:forbidden)
379 defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
380 when is_map(object) do
382 [object["content"], object["summary"], object["name"]]
383 |> Enum.filter(&is_binary(&1))
387 limit = Pleroma.Config.get([:instance, :limit])
392 |> Transmogrifier.strip_internal_fields()
393 |> Map.put("attributedTo", actor)
394 |> Map.put("actor", actor)
395 |> Map.put("id", Utils.generate_object_id())
397 {:ok, Map.put(activity, "object", object)}
402 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
409 defp fix_user_message(
410 %User{ap_id: actor} = user,
411 %{"type" => "Delete", "object" => object} = activity
413 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
414 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
418 {:error, "No such object found"}
421 {:forbidden, "You can't delete this object"}
425 defp fix_user_message(%User{}, activity) do
430 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
431 %{"nickname" => nickname} = params
435 |> Map.drop(["nickname"])
436 |> Map.put("id", Utils.generate_activity_id())
437 |> Map.put("actor", actor)
439 with {:ok, params} <- fix_user_message(user, params),
440 {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
441 %Activity{data: activity_data} <- Activity.normalize(activity) do
443 |> put_status(:created)
444 |> put_resp_header("location", activity_data["id"])
445 |> json(activity_data)
447 {:forbidden, message} ->
449 |> put_status(:forbidden)
454 |> put_status(:bad_request)
458 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
461 |> put_status(:bad_request)
462 |> json("Bad Request")
466 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
468 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
470 as_nickname: user.nickname
474 |> put_status(:forbidden)
478 defp errors(conn, {:error, :not_found}) do
480 |> put_status(:not_found)
481 |> json(dgettext("errors", "Not found"))
484 defp errors(conn, _e) do
486 |> put_status(:internal_server_error)
487 |> json(dgettext("errors", "error"))
490 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
491 with actor <- conn.params["actor"],
492 true <- is_binary(actor) do
493 Pleroma.Instances.set_reachable(actor)
499 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
500 with {:ok, object} <-
503 actor: User.ap_id(user),
504 description: Map.get(data, "description")
506 Logger.debug(inspect(object))
509 |> put_status(:created)
514 def pinned(conn, %{"nickname" => nickname}) do
515 with %User{} = user <- User.get_cached_by_nickname(nickname) do
517 |> put_resp_header("content-type", "application/activity+json")
518 |> json(UserView.render("featured.json", %{user: user}))