1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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
12 alias Pleroma.Plugs.EnsureAuthenticatedPlug
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.ActivityPub.Builder
16 alias Pleroma.Web.ActivityPub.InternalFetchActor
17 alias Pleroma.Web.ActivityPub.ObjectView
18 alias Pleroma.Web.ActivityPub.Pipeline
19 alias Pleroma.Web.ActivityPub.Relay
20 alias Pleroma.Web.ActivityPub.Transmogrifier
21 alias Pleroma.Web.ActivityPub.UserView
22 alias Pleroma.Web.ActivityPub.Utils
23 alias Pleroma.Web.ActivityPub.Visibility
24 alias Pleroma.Web.ControllerHelper
25 alias Pleroma.Web.Endpoint
26 alias Pleroma.Web.FederatingPlug
27 alias Pleroma.Web.Federator
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]
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),
69 {:ok, user} <- User.ensure_keys_present(user) 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(conn, _) do
81 with ap_id <- Endpoint.url() <> conn.request_path,
82 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
83 {_, true} <- {:public?, Visibility.is_public?(object)} do
85 |> assign(:tracking_fun_data, object.id)
86 |> set_cache_ttl_for(object)
87 |> put_resp_content_type("application/activity+json")
88 |> put_view(ObjectView)
89 |> render("object.json", object: object)
96 def track_object_fetch(conn, nil), do: conn
98 def track_object_fetch(conn, object_id) do
99 with %{assigns: %{user: %User{id: user_id}}} <- conn do
100 Delivery.create(object_id, user_id)
106 def activity(conn, _params) do
107 with ap_id <- Endpoint.url() <> conn.request_path,
108 %Activity{} = activity <- Activity.normalize(ap_id),
109 {_, true} <- {:public?, Visibility.is_public?(activity)} do
111 |> maybe_set_tracking_data(activity)
112 |> set_cache_ttl_for(activity)
113 |> put_resp_content_type("application/activity+json")
114 |> put_view(ObjectView)
115 |> render("object.json", object: activity)
117 {:public?, false} -> {:error, :not_found}
118 nil -> {:error, :not_found}
122 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
123 object_id = Object.normalize(activity).id
124 assign(conn, :tracking_fun_data, object_id)
127 defp maybe_set_tracking_data(conn, _activity), do: conn
129 defp set_cache_ttl_for(conn, %Activity{object: object}) do
130 set_cache_ttl_for(conn, object)
133 defp set_cache_ttl_for(conn, entity) do
136 %Object{data: %{"type" => "Question"}} ->
137 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
140 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
146 assign(conn, :cache_ttl, ttl)
149 # GET /relay/following
150 def relay_following(conn, _params) do
151 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
153 |> put_resp_content_type("application/activity+json")
154 |> put_view(UserView)
155 |> render("following.json", %{user: Relay.get_actor()})
159 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
160 with %User{} = user <- User.get_cached_by_nickname(nickname),
161 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
162 {:show_follows, true} <-
163 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
164 {page, _} = Integer.parse(page)
167 |> put_resp_content_type("application/activity+json")
168 |> put_view(UserView)
169 |> render("following.json", %{user: user, page: page, for: for_user})
171 {:show_follows, _} ->
173 |> put_resp_content_type("application/activity+json")
174 |> send_resp(403, "")
178 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
179 with %User{} = user <- User.get_cached_by_nickname(nickname),
180 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
182 |> put_resp_content_type("application/activity+json")
183 |> put_view(UserView)
184 |> render("following.json", %{user: user, for: for_user})
188 # GET /relay/followers
189 def relay_followers(conn, _params) do
190 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
192 |> put_resp_content_type("application/activity+json")
193 |> put_view(UserView)
194 |> render("followers.json", %{user: Relay.get_actor()})
198 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
199 with %User{} = user <- User.get_cached_by_nickname(nickname),
200 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
201 {:show_followers, true} <-
202 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
203 {page, _} = Integer.parse(page)
206 |> put_resp_content_type("application/activity+json")
207 |> put_view(UserView)
208 |> render("followers.json", %{user: user, page: page, for: for_user})
210 {:show_followers, _} ->
212 |> put_resp_content_type("application/activity+json")
213 |> send_resp(403, "")
217 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
218 with %User{} = user <- User.get_cached_by_nickname(nickname),
219 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
221 |> put_resp_content_type("application/activity+json")
222 |> put_view(UserView)
223 |> render("followers.json", %{user: user, for: for_user})
228 %{assigns: %{user: for_user}} = conn,
229 %{"nickname" => nickname, "page" => page?} = params
231 when page? in [true, "true"] do
232 with %User{} = user <- User.get_cached_by_nickname(nickname),
233 {:ok, user} <- User.ensure_keys_present(user) do
234 # "include_poll_votes" is a hack because postgres generates inefficient
235 # queries when filtering by 'Answer', poll votes will be hidden by the
236 # visibility filter in this case anyway
239 |> Map.drop(["nickname", "page"])
240 |> Map.put("include_poll_votes", true)
241 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
243 activities = ActivityPub.fetch_user_activities(user, for_user, params)
246 |> put_resp_content_type("application/activity+json")
247 |> put_view(UserView)
248 |> render("activity_collection_page.json", %{
249 activities: activities,
250 pagination: ControllerHelper.get_pagination_fields(conn, activities),
251 iri: "#{user.ap_id}/outbox"
256 def outbox(conn, %{"nickname" => nickname}) do
257 with %User{} = user <- User.get_cached_by_nickname(nickname),
258 {:ok, user} <- User.ensure_keys_present(user) do
260 |> put_resp_content_type("application/activity+json")
261 |> put_view(UserView)
262 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
266 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
267 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
268 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
269 true <- Utils.recipient_in_message(recipient, actor, params),
270 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
271 Federator.incoming_ap_doc(params)
276 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
277 Federator.incoming_ap_doc(params)
281 # POST /relay/inbox -or- POST /internal/fetch/inbox
282 def inbox(conn, params) do
283 if params["type"] == "Create" && FederatingPlug.federating?() do
284 post_inbox_relayed_create(conn, params)
286 post_inbox_fallback(conn, params)
290 defp post_inbox_relayed_create(conn, params) do
292 "Signature missing or not from author, relayed Create message, fetching object from source"
295 Fetcher.fetch_object_from_id(params["object"]["id"])
300 defp post_inbox_fallback(conn, params) do
301 headers = Enum.into(conn.req_headers, %{})
303 if headers["signature"] && params["actor"] &&
304 String.contains?(headers["signature"], params["actor"]) do
306 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
309 Logger.debug(inspect(conn.req_headers))
313 |> put_status(:bad_request)
314 |> json(dgettext("errors", "error"))
317 defp represent_service_actor(%User{} = user, conn) do
318 with {:ok, user} <- User.ensure_keys_present(user) do
320 |> put_resp_content_type("application/activity+json")
321 |> put_view(UserView)
322 |> render("user.json", %{user: user})
324 nil -> {:error, :not_found}
328 defp represent_service_actor(nil, _), do: {:error, :not_found}
330 def relay(conn, _params) do
332 |> represent_service_actor(conn)
335 def internal_fetch(conn, _params) do
336 InternalFetchActor.get_actor()
337 |> represent_service_actor(conn)
340 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
341 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
343 |> put_resp_content_type("application/activity+json")
344 |> put_view(UserView)
345 |> render("user.json", %{user: user})
349 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
350 %{"nickname" => nickname, "page" => page?} = params
352 when page? in [true, "true"] do
355 |> Map.drop(["nickname", "page"])
356 |> Map.put("blocking_user", user)
357 |> Map.put("user", user)
358 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
361 [user.ap_id | User.following(user)]
362 |> ActivityPub.fetch_activities(params)
366 |> put_resp_content_type("application/activity+json")
367 |> put_view(UserView)
368 |> render("activity_collection_page.json", %{
369 activities: activities,
370 pagination: ControllerHelper.get_pagination_fields(conn, activities),
371 iri: "#{user.ap_id}/inbox"
375 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
376 "nickname" => nickname
378 with {:ok, user} <- User.ensure_keys_present(user) do
380 |> put_resp_content_type("application/activity+json")
381 |> put_view(UserView)
382 |> 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 handle_user_activity(
402 %{"type" => "Create", "object" => %{"type" => "Note"}} = params
406 |> Map.merge(Map.take(params, ["to", "cc"]))
407 |> Map.put("attributedTo", user.ap_id())
408 |> Transmogrifier.fix_object()
410 ActivityPub.create(%{
413 context: object["context"],
415 additional: Map.take(params, ["cc"])
419 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
420 with %Object{} = object <- Object.normalize(params["object"]),
421 true <- user.is_moderator || user.ap_id == object.data["actor"],
422 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
423 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
426 _ -> {:error, dgettext("errors", "Can't delete object")}
430 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
431 with %Object{} = object <- Object.normalize(params["object"]),
432 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
433 {_, {:ok, %Activity{} = activity, _meta}} <-
435 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
438 _ -> {:error, dgettext("errors", "Can't like object")}
442 defp handle_user_activity(_, _) do
443 {:error, dgettext("errors", "Unhandled activity type")}
447 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
448 %{"nickname" => nickname} = params
455 |> Map.put("actor", actor)
456 |> Transmogrifier.fix_addressing()
458 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
460 |> put_status(:created)
461 |> put_resp_header("location", activity.data["id"])
462 |> json(activity.data)
466 |> put_status(:bad_request)
471 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
473 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
475 as_nickname: user.nickname
479 |> put_status(:forbidden)
483 defp errors(conn, {:error, :not_found}) do
485 |> put_status(:not_found)
486 |> json(dgettext("errors", "Not found"))
489 defp errors(conn, _e) do
491 |> put_status(:internal_server_error)
492 |> json(dgettext("errors", "error"))
495 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
496 with actor <- conn.params["actor"],
497 true <- is_binary(actor) do
498 Pleroma.Instances.set_reachable(actor)
504 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
505 {:ok, new_user} = User.ensure_keys_present(user)
508 if new_user != user and match?(%User{}, for_user) do
509 User.get_cached_by_nickname(for_user.nickname)
518 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
521 - (required) `file`: data of the media
522 - (optionnal) `description`: description of the media, intended for accessibility
525 - HTTP Code: 201 Created
526 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
528 Note: Will not point to a URL with a `Location` header because no standalone Activity has been created.
530 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
531 with {:ok, object} <-
534 actor: User.ap_id(user),
535 description: Map.get(data, "description")
537 Logger.debug(inspect(object))
540 |> put_status(:created)