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
235 if params["max_id"] do
236 ActivityPub.fetch_user_activities(user, for_user, %{
237 "max_id" => params["max_id"],
238 # This is a hack because postgres generates inefficient queries when filtering by
239 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
240 "include_poll_votes" => true,
244 ActivityPub.fetch_user_activities(user, for_user, %{
246 "include_poll_votes" => true
251 |> put_resp_content_type("application/activity+json")
252 |> put_view(UserView)
253 |> render("activity_collection_page.json", %{
254 activities: activities,
255 pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}),
256 iri: "#{user.ap_id}/outbox"
261 def outbox(conn, %{"nickname" => nickname}) do
262 with %User{} = user <- User.get_cached_by_nickname(nickname),
263 {:ok, user} <- User.ensure_keys_present(user) do
265 |> put_resp_content_type("application/activity+json")
266 |> put_view(UserView)
267 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
271 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
272 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
273 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
274 true <- Utils.recipient_in_message(recipient, actor, params),
275 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
276 Federator.incoming_ap_doc(params)
281 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
282 Federator.incoming_ap_doc(params)
286 # POST /relay/inbox -or- POST /internal/fetch/inbox
287 def inbox(conn, params) do
288 if params["type"] == "Create" && FederatingPlug.federating?() do
289 post_inbox_relayed_create(conn, params)
291 post_inbox_fallback(conn, params)
295 defp post_inbox_relayed_create(conn, params) do
297 "Signature missing or not from author, relayed Create message, fetching object from source"
300 Fetcher.fetch_object_from_id(params["object"]["id"])
305 defp post_inbox_fallback(conn, params) do
306 headers = Enum.into(conn.req_headers, %{})
308 if headers["signature"] && params["actor"] &&
309 String.contains?(headers["signature"], params["actor"]) do
311 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
314 Logger.debug(inspect(conn.req_headers))
318 |> put_status(:bad_request)
319 |> json(dgettext("errors", "error"))
322 defp represent_service_actor(%User{} = user, conn) do
323 with {:ok, user} <- User.ensure_keys_present(user) do
325 |> put_resp_content_type("application/activity+json")
326 |> put_view(UserView)
327 |> render("user.json", %{user: user})
329 nil -> {:error, :not_found}
333 defp represent_service_actor(nil, _), do: {:error, :not_found}
335 def relay(conn, _params) do
337 |> represent_service_actor(conn)
340 def internal_fetch(conn, _params) do
341 InternalFetchActor.get_actor()
342 |> represent_service_actor(conn)
345 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
346 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
348 |> put_resp_content_type("application/activity+json")
349 |> put_view(UserView)
350 |> render("user.json", %{user: user})
354 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
355 %{"nickname" => nickname, "page" => page?} = params
357 when page? in [true, "true"] do
359 if params["max_id"] do
360 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
361 "max_id" => params["max_id"],
365 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
369 |> put_resp_content_type("application/activity+json")
370 |> put_view(UserView)
371 |> render("activity_collection_page.json", %{
372 activities: activities,
373 pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}),
374 iri: "#{user.ap_id}/inbox"
378 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
379 "nickname" => nickname
381 with {:ok, user} <- User.ensure_keys_present(user) do
383 |> put_resp_content_type("application/activity+json")
384 |> put_view(UserView)
385 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
389 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
390 "nickname" => nickname
393 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
395 as_nickname: as_nickname
399 |> put_status(:forbidden)
403 defp handle_user_activity(
405 %{"type" => "Create", "object" => %{"type" => "Note"}} = params
409 |> Map.merge(Map.take(params, ["to", "cc"]))
410 |> Map.put("attributedTo", user.ap_id())
411 |> Transmogrifier.fix_object()
413 ActivityPub.create(%{
416 context: object["context"],
418 additional: Map.take(params, ["cc"])
422 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
423 with %Object{} = object <- Object.normalize(params["object"]),
424 true <- user.is_moderator || user.ap_id == object.data["actor"],
425 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
426 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
429 _ -> {:error, dgettext("errors", "Can't delete object")}
433 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
434 with %Object{} = object <- Object.normalize(params["object"]),
435 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
436 {_, {:ok, %Activity{} = activity, _meta}} <-
438 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
441 _ -> {:error, dgettext("errors", "Can't like object")}
445 defp handle_user_activity(_, _) do
446 {:error, dgettext("errors", "Unhandled activity type")}
450 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
451 %{"nickname" => nickname} = params
458 |> Map.put("actor", actor)
459 |> Transmogrifier.fix_addressing()
461 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
463 |> put_status(:created)
464 |> put_resp_header("location", activity.data["id"])
465 |> json(activity.data)
469 |> put_status(:bad_request)
474 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
476 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
478 as_nickname: user.nickname
482 |> put_status(:forbidden)
486 defp errors(conn, {:error, :not_found}) do
488 |> put_status(:not_found)
489 |> json(dgettext("errors", "Not found"))
492 defp errors(conn, _e) do
494 |> put_status(:internal_server_error)
495 |> json(dgettext("errors", "error"))
498 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
499 with actor <- conn.params["actor"],
500 true <- is_binary(actor) do
501 Pleroma.Instances.set_reachable(actor)
507 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
508 {:ok, new_user} = User.ensure_keys_present(user)
511 if new_user != user and match?(%User{}, for_user) do
512 User.get_cached_by_nickname(for_user.nickname)
520 # TODO: Add support for "object" field
522 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
525 - (required) `file`: data of the media
526 - (optionnal) `description`: description of the media, intended for accessibility
529 - HTTP Code: 201 Created
530 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
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)