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.Builder
15 alias Pleroma.Web.ActivityPub.InternalFetchActor
16 alias Pleroma.Web.ActivityPub.ObjectView
17 alias Pleroma.Web.ActivityPub.Pipeline
18 alias Pleroma.Web.ActivityPub.Relay
19 alias Pleroma.Web.ActivityPub.Transmogrifier
20 alias Pleroma.Web.ActivityPub.UserView
21 alias Pleroma.Web.ActivityPub.Utils
22 alias Pleroma.Web.ActivityPub.Visibility
23 alias Pleroma.Web.ControllerHelper
24 alias Pleroma.Web.Endpoint
25 alias Pleroma.Web.Federator
26 alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
27 alias Pleroma.Web.Plugs.FederatingPlug
31 action_fallback(:errors)
33 @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
35 plug(FederatingPlug when action in @federating_only_actions)
38 EnsureAuthenticatedPlug,
39 [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
42 # Note: :following and :followers must be served even without authentication (as via :api)
44 EnsureAuthenticatedPlug
45 when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
48 plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
51 Pleroma.Web.Plugs.Cache,
52 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
53 when action in [:activity, :object]
56 plug(:set_requester_reachable when action in [:inbox])
57 plug(:relay_active? when action in [:relay])
59 defp relay_active?(conn, _) do
60 if Pleroma.Config.get([:instance, :allow_relay]) do
64 |> render_error(:not_found, "not found")
69 def user(conn, %{"nickname" => nickname}) do
70 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
71 {:ok, user} <- User.ensure_keys_present(user) do
73 |> put_resp_content_type("application/activity+json")
75 |> render("user.json", %{user: user})
77 nil -> {:error, :not_found}
78 %{local: false} -> {:error, :not_found}
82 def object(%{assigns: assigns} = conn, _) do
83 with ap_id <- Endpoint.url() <> conn.request_path,
84 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
85 user <- Map.get(assigns, :user, nil),
86 {_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
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_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 # GET /relay/following
156 def relay_following(conn, _params) do
157 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
159 |> put_resp_content_type("application/activity+json")
160 |> put_view(UserView)
161 |> render("following.json", %{user: Relay.get_actor()})
165 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
166 with %User{} = user <- User.get_cached_by_nickname(nickname),
167 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
168 {:show_follows, true} <-
169 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
170 {page, _} = Integer.parse(page)
173 |> put_resp_content_type("application/activity+json")
174 |> put_view(UserView)
175 |> render("following.json", %{user: user, page: page, for: for_user})
177 {:show_follows, _} ->
179 |> put_resp_content_type("application/activity+json")
180 |> send_resp(403, "")
184 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
185 with %User{} = user <- User.get_cached_by_nickname(nickname),
186 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
188 |> put_resp_content_type("application/activity+json")
189 |> put_view(UserView)
190 |> render("following.json", %{user: user, for: for_user})
194 # GET /relay/followers
195 def relay_followers(conn, _params) do
196 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
198 |> put_resp_content_type("application/activity+json")
199 |> put_view(UserView)
200 |> render("followers.json", %{user: Relay.get_actor()})
204 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
205 with %User{} = user <- User.get_cached_by_nickname(nickname),
206 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
207 {:show_followers, true} <-
208 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
209 {page, _} = Integer.parse(page)
212 |> put_resp_content_type("application/activity+json")
213 |> put_view(UserView)
214 |> render("followers.json", %{user: user, page: page, for: for_user})
216 {:show_followers, _} ->
218 |> put_resp_content_type("application/activity+json")
219 |> send_resp(403, "")
223 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
224 with %User{} = user <- User.get_cached_by_nickname(nickname),
225 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
227 |> put_resp_content_type("application/activity+json")
228 |> put_view(UserView)
229 |> render("followers.json", %{user: user, for: for_user})
234 %{assigns: %{user: for_user}} = conn,
235 %{"nickname" => nickname, "page" => page?} = params
237 when page? in [true, "true"] do
238 with %User{} = user <- User.get_cached_by_nickname(nickname),
239 {:ok, user} <- User.ensure_keys_present(user) do
240 # "include_poll_votes" is a hack because postgres generates inefficient
241 # queries when filtering by 'Answer', poll votes will be hidden by the
242 # visibility filter in this case anyway
245 |> Map.drop(["nickname", "page"])
246 |> Map.put("include_poll_votes", true)
247 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
249 activities = ActivityPub.fetch_user_activities(user, for_user, params)
252 |> put_resp_content_type("application/activity+json")
253 |> put_view(UserView)
254 |> render("activity_collection_page.json", %{
255 activities: activities,
256 pagination: ControllerHelper.get_pagination_fields(conn, activities),
257 iri: "#{user.ap_id}/outbox"
262 def outbox(conn, %{"nickname" => nickname}) do
263 with %User{} = user <- User.get_cached_by_nickname(nickname),
264 {:ok, user} <- User.ensure_keys_present(user) do
266 |> put_resp_content_type("application/activity+json")
267 |> put_view(UserView)
268 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
272 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
273 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
274 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
275 true <- Utils.recipient_in_message(recipient, actor, params),
276 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
277 Federator.incoming_ap_doc(params)
282 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
283 Federator.incoming_ap_doc(params)
287 # POST /relay/inbox -or- POST /internal/fetch/inbox
288 def inbox(conn, params) do
289 if params["type"] == "Create" && FederatingPlug.federating?() do
290 post_inbox_relayed_create(conn, params)
292 post_inbox_fallback(conn, params)
296 defp post_inbox_relayed_create(conn, params) do
298 "Signature missing or not from author, relayed Create message, fetching object from source"
301 Fetcher.fetch_object_from_id(params["object"]["id"])
306 defp post_inbox_fallback(conn, params) do
307 headers = Enum.into(conn.req_headers, %{})
309 if headers["signature"] && params["actor"] &&
310 String.contains?(headers["signature"], params["actor"]) do
312 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
315 Logger.debug(inspect(conn.req_headers))
319 |> put_status(:bad_request)
320 |> json(dgettext("errors", "error"))
323 defp represent_service_actor(%User{} = user, conn) do
324 with {:ok, user} <- User.ensure_keys_present(user) do
326 |> put_resp_content_type("application/activity+json")
327 |> put_view(UserView)
328 |> render("user.json", %{user: user})
330 nil -> {:error, :not_found}
334 defp represent_service_actor(nil, _), do: {:error, :not_found}
336 def relay(conn, _params) do
338 |> represent_service_actor(conn)
341 def internal_fetch(conn, _params) do
342 InternalFetchActor.get_actor()
343 |> represent_service_actor(conn)
346 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
347 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
349 |> put_resp_content_type("application/activity+json")
350 |> put_view(UserView)
351 |> render("user.json", %{user: user})
355 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
356 %{"nickname" => nickname, "page" => page?} = params
358 when page? in [true, "true"] do
361 |> Map.drop(["nickname", "page"])
362 |> Map.put("blocking_user", user)
363 |> Map.put("user", user)
364 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
367 [user.ap_id | User.following(user)]
368 |> ActivityPub.fetch_activities(params)
372 |> put_resp_content_type("application/activity+json")
373 |> put_view(UserView)
374 |> render("activity_collection_page.json", %{
375 activities: activities,
376 pagination: ControllerHelper.get_pagination_fields(conn, activities),
377 iri: "#{user.ap_id}/inbox"
381 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
382 "nickname" => nickname
384 with {:ok, user} <- User.ensure_keys_present(user) do
386 |> put_resp_content_type("application/activity+json")
387 |> put_view(UserView)
388 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
392 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
393 "nickname" => nickname
396 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
398 as_nickname: as_nickname
402 |> put_status(:forbidden)
406 defp handle_user_activity(
408 %{"type" => "Create", "object" => %{"type" => "Note"} = object} = params
410 content = if is_binary(object["content"]), do: object["content"], else: ""
411 name = if is_binary(object["name"]), do: object["name"], else: ""
412 summary = if is_binary(object["summary"]), do: object["summary"], else: ""
413 length = String.length(content <> name <> summary)
415 if length > Pleroma.Config.get([:instance, :limit]) do
416 {:error, dgettext("errors", "Note is over the character limit")}
420 |> Map.merge(Map.take(params, ["to", "cc"]))
421 |> Map.put("attributedTo", user.ap_id)
422 |> Transmogrifier.fix_object()
424 ActivityPub.create(%{
427 context: object["context"],
429 additional: Map.take(params, ["cc"])
434 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
435 with %Object{} = object <- Object.normalize(params["object"], fetch: false),
436 true <- user.is_moderator || user.ap_id == object.data["actor"],
437 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
438 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
441 _ -> {:error, dgettext("errors", "Can't delete object")}
445 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
446 with %Object{} = object <- Object.normalize(params["object"], fetch: false),
447 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
448 {_, {:ok, %Activity{} = activity, _meta}} <-
450 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
453 _ -> {:error, dgettext("errors", "Can't like object")}
457 defp handle_user_activity(_, _) do
458 {:error, dgettext("errors", "Unhandled activity type")}
462 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
463 %{"nickname" => nickname} = params
470 |> Map.put("actor", actor)
471 |> Transmogrifier.fix_addressing()
473 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
475 |> put_status(:created)
476 |> put_resp_header("location", activity.data["id"])
477 |> json(activity.data)
481 |> put_status(:bad_request)
486 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
488 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
490 as_nickname: user.nickname
494 |> put_status(:forbidden)
498 defp errors(conn, {:error, :not_found}) do
500 |> put_status(:not_found)
501 |> json(dgettext("errors", "Not found"))
504 defp errors(conn, _e) do
506 |> put_status(:internal_server_error)
507 |> json(dgettext("errors", "error"))
510 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
511 with actor <- conn.params["actor"],
512 true <- is_binary(actor) do
513 Pleroma.Instances.set_reachable(actor)
519 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
520 {:ok, new_user} = User.ensure_keys_present(user)
523 if new_user != user and match?(%User{}, for_user) do
524 User.get_cached_by_nickname(for_user.nickname)
532 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
533 with {:ok, object} <-
536 actor: User.ap_id(user),
537 description: Map.get(data, "description")
539 Logger.debug(inspect(object))
542 |> put_status(:created)
547 def pinned(conn, %{"nickname" => nickname}) do
548 with %User{} = user <- User.get_cached_by_nickname(nickname) do
550 |> put_resp_header("content-type", "application/activity+json")
551 |> json(UserView.render("featured.json", %{user: user}))