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
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.InternalFetchActor
14 alias Pleroma.Web.ActivityPub.ObjectView
15 alias Pleroma.Web.ActivityPub.Pipeline
16 alias Pleroma.Web.ActivityPub.Relay
17 alias Pleroma.Web.ActivityPub.Transmogrifier
18 alias Pleroma.Web.ActivityPub.UserView
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.ActivityPub.Visibility
21 alias Pleroma.Web.ControllerHelper
22 alias Pleroma.Web.Endpoint
23 alias Pleroma.Web.Federator
24 alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
25 alias Pleroma.Web.Plugs.FederatingPlug
29 action_fallback(:errors)
31 @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
33 plug(FederatingPlug when action in @federating_only_actions)
36 EnsureAuthenticatedPlug,
37 [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
40 # Note: :following and :followers must be served even without authentication (as via :api)
42 EnsureAuthenticatedPlug
43 when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
46 plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
49 Pleroma.Web.Plugs.Cache,
50 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
51 when action in [:activity, :object]
54 plug(:set_requester_reachable when action in [:inbox])
55 plug(:relay_active? when action in [:relay])
57 defp relay_active?(conn, _) do
58 if Pleroma.Config.get([:instance, :allow_relay]) do
62 |> render_error(:not_found, "not found")
67 def user(conn, %{"nickname" => nickname}) do
68 with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
70 |> put_resp_content_type("application/activity+json")
72 |> render("user.json", %{user: user})
74 nil -> {:error, :not_found}
75 %{local: false} -> {:error, :not_found}
79 def object(%{assigns: assigns} = conn, _) do
80 with ap_id <- Endpoint.url() <> conn.request_path,
81 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
82 user <- Map.get(assigns, :user, nil),
83 {_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
85 |> maybe_skip_cache(user)
86 |> assign(:tracking_fun_data, object.id)
87 |> set_cache_ttl_for(object)
88 |> put_resp_content_type("application/activity+json")
89 |> put_view(ObjectView)
90 |> render("object.json", object: object)
92 {:visible?, false} -> {:error, :not_found}
93 nil -> {:error, :not_found}
97 def track_object_fetch(conn, nil), do: conn
99 def track_object_fetch(conn, object_id) do
100 with %{assigns: %{user: %User{id: user_id}}} <- conn do
101 Delivery.create(object_id, user_id)
107 def activity(%{assigns: assigns} = conn, _) do
108 with ap_id <- Endpoint.url() <> conn.request_path,
109 %Activity{} = activity <- Activity.normalize(ap_id),
110 {_, true} <- {:local?, activity.local},
111 user <- Map.get(assigns, :user, nil),
112 {_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
114 |> maybe_skip_cache(user)
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 def maybe_skip_cache(conn, user) do
157 |> assign(:skip_cache, true)
163 # GET /relay/following
164 def relay_following(conn, _params) do
165 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
167 |> put_resp_content_type("application/activity+json")
168 |> put_view(UserView)
169 |> render("following.json", %{user: Relay.get_actor()})
173 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
174 with %User{} = user <- User.get_cached_by_nickname(nickname),
175 {:show_follows, true} <-
176 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
177 {page, _} = Integer.parse(page)
180 |> put_resp_content_type("application/activity+json")
181 |> put_view(UserView)
182 |> render("following.json", %{user: user, page: page, for: for_user})
184 {:show_follows, _} ->
186 |> put_resp_content_type("application/activity+json")
187 |> send_resp(403, "")
191 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
192 with %User{} = user <- User.get_cached_by_nickname(nickname) do
194 |> put_resp_content_type("application/activity+json")
195 |> put_view(UserView)
196 |> render("following.json", %{user: user, for: for_user})
200 # GET /relay/followers
201 def relay_followers(conn, _params) do
202 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
204 |> put_resp_content_type("application/activity+json")
205 |> put_view(UserView)
206 |> render("followers.json", %{user: Relay.get_actor()})
210 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
211 with %User{} = user <- User.get_cached_by_nickname(nickname),
212 {:show_followers, true} <-
213 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
214 {page, _} = Integer.parse(page)
217 |> put_resp_content_type("application/activity+json")
218 |> put_view(UserView)
219 |> render("followers.json", %{user: user, page: page, for: for_user})
221 {:show_followers, _} ->
223 |> put_resp_content_type("application/activity+json")
224 |> send_resp(403, "")
228 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
229 with %User{} = user <- User.get_cached_by_nickname(nickname) do
231 |> put_resp_content_type("application/activity+json")
232 |> put_view(UserView)
233 |> render("followers.json", %{user: user, for: for_user})
238 %{assigns: %{user: for_user}} = conn,
239 %{"nickname" => nickname, "page" => page?} = params
241 when page? in [true, "true"] do
242 with %User{} = user <- User.get_cached_by_nickname(nickname) do
243 # "include_poll_votes" is a hack because postgres generates inefficient
244 # queries when filtering by 'Answer', poll votes will be hidden by the
245 # visibility filter in this case anyway
248 |> Map.drop(["nickname", "page"])
249 |> Map.put("include_poll_votes", true)
250 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
252 activities = ActivityPub.fetch_user_activities(user, for_user, params)
255 |> put_resp_content_type("application/activity+json")
256 |> put_view(UserView)
257 |> render("activity_collection_page.json", %{
258 activities: activities,
259 pagination: ControllerHelper.get_pagination_fields(conn, activities),
260 iri: "#{user.ap_id}/outbox"
265 def outbox(conn, %{"nickname" => nickname}) do
266 with %User{} = user <- User.get_cached_by_nickname(nickname) do
268 |> put_resp_content_type("application/activity+json")
269 |> put_view(UserView)
270 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
274 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
275 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
276 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
277 true <- Utils.recipient_in_message(recipient, actor, params),
278 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
279 Federator.incoming_ap_doc(params)
284 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
285 Federator.incoming_ap_doc(params)
289 def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do
291 |> put_status(:bad_request)
292 |> json("Invalid HTTP Signature")
295 def inbox(conn, _params) do
297 |> put_status(:bad_request)
298 |> json("error, missing HTTP Signature")
301 defp represent_service_actor(%User{} = user, conn) do
303 |> put_resp_content_type("application/activity+json")
304 |> put_view(UserView)
305 |> render("user.json", %{user: user})
308 defp represent_service_actor(nil, _), do: {:error, :not_found}
310 def relay(conn, _params) do
312 |> represent_service_actor(conn)
315 def internal_fetch(conn, _params) do
316 InternalFetchActor.get_actor()
317 |> represent_service_actor(conn)
320 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
321 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
323 |> put_resp_content_type("application/activity+json")
324 |> put_view(UserView)
325 |> render("user.json", %{user: user})
329 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
330 %{"nickname" => nickname, "page" => page?} = params
332 when page? in [true, "true"] do
335 |> Map.drop(["nickname", "page"])
336 |> Map.put("blocking_user", user)
337 |> Map.put("user", user)
338 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
341 [user.ap_id | User.following(user)]
342 |> ActivityPub.fetch_activities(params)
346 |> put_resp_content_type("application/activity+json")
347 |> put_view(UserView)
348 |> render("activity_collection_page.json", %{
349 activities: activities,
350 pagination: ControllerHelper.get_pagination_fields(conn, activities),
351 iri: "#{user.ap_id}/inbox"
355 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
356 "nickname" => nickname
359 |> put_resp_content_type("application/activity+json")
360 |> put_view(UserView)
361 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
364 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
365 "nickname" => nickname
368 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
370 as_nickname: as_nickname
374 |> put_status(:forbidden)
378 defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
379 when is_map(object) do
381 [object["content"], object["summary"], object["name"]]
382 |> Enum.filter(&is_binary(&1))
386 limit = Pleroma.Config.get([:instance, :limit])
391 |> Transmogrifier.strip_internal_fields()
392 |> Map.put("attributedTo", actor)
393 |> Map.put("actor", actor)
394 |> Map.put("id", Utils.generate_object_id())
396 {:ok, Map.put(activity, "object", object)}
401 "Character limit (%{limit} characters) exceeded, contains %{length} characters",
408 defp fix_user_message(
409 %User{ap_id: actor} = user,
410 %{"type" => "Delete", "object" => object} = activity
412 with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
413 {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
417 {:error, "No such object found"}
420 {:forbidden, "You can't delete this object"}
424 defp fix_user_message(%User{}, activity) do
429 %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
430 %{"nickname" => nickname} = params
434 |> Map.drop(["nickname"])
435 |> Map.put("id", Utils.generate_activity_id())
436 |> Map.put("actor", actor)
438 with {:ok, params} <- fix_user_message(user, params),
439 {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
440 %Activity{data: activity_data} <- Activity.normalize(activity) do
442 |> put_status(:created)
443 |> put_resp_header("location", activity_data["id"])
444 |> json(activity_data)
446 {:forbidden, message} ->
448 |> put_status(:forbidden)
453 |> put_status(:bad_request)
457 Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
460 |> put_status(:bad_request)
461 |> json("Bad Request")
465 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
467 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
469 as_nickname: user.nickname
473 |> put_status(:forbidden)
477 defp errors(conn, {:error, :not_found}) do
479 |> put_status(:not_found)
480 |> json(dgettext("errors", "Not found"))
483 defp errors(conn, _e) do
485 |> put_status(:internal_server_error)
486 |> json(dgettext("errors", "error"))
489 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
490 with actor <- conn.params["actor"],
491 true <- is_binary(actor) do
492 Pleroma.Instances.set_reachable(actor)
498 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
499 with {:ok, object} <-
502 actor: User.ap_id(user),
503 description: Map.get(data, "description")
505 Logger.debug(inspect(object))
508 |> put_status(:created)
513 def pinned(conn, %{"nickname" => nickname}) do
514 with %User{} = user <- User.get_cached_by_nickname(nickname) do
516 |> put_resp_header("content-type", "application/activity+json")
517 |> json(UserView.render("featured.json", %{user: user}))