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)
242 activities = ActivityPub.fetch_user_activities(user, for_user, params)
245 |> put_resp_content_type("application/activity+json")
246 |> put_view(UserView)
247 |> render("activity_collection_page.json", %{
248 activities: activities,
249 pagination: ControllerHelper.get_pagination_fields(conn, activities),
250 iri: "#{user.ap_id}/outbox"
255 def outbox(conn, %{"nickname" => nickname}) do
256 with %User{} = user <- User.get_cached_by_nickname(nickname),
257 {:ok, user} <- User.ensure_keys_present(user) do
259 |> put_resp_content_type("application/activity+json")
260 |> put_view(UserView)
261 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
265 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
266 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
267 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
268 true <- Utils.recipient_in_message(recipient, actor, params),
269 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
270 Federator.incoming_ap_doc(params)
275 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
276 Federator.incoming_ap_doc(params)
280 # POST /relay/inbox -or- POST /internal/fetch/inbox
281 def inbox(conn, params) do
282 if params["type"] == "Create" && FederatingPlug.federating?() do
283 post_inbox_relayed_create(conn, params)
285 post_inbox_fallback(conn, params)
289 defp post_inbox_relayed_create(conn, params) do
291 "Signature missing or not from author, relayed Create message, fetching object from source"
294 Fetcher.fetch_object_from_id(params["object"]["id"])
299 defp post_inbox_fallback(conn, params) do
300 headers = Enum.into(conn.req_headers, %{})
302 if headers["signature"] && params["actor"] &&
303 String.contains?(headers["signature"], params["actor"]) do
305 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
308 Logger.debug(inspect(conn.req_headers))
312 |> put_status(:bad_request)
313 |> json(dgettext("errors", "error"))
316 defp represent_service_actor(%User{} = user, conn) do
317 with {:ok, user} <- User.ensure_keys_present(user) do
319 |> put_resp_content_type("application/activity+json")
320 |> put_view(UserView)
321 |> render("user.json", %{user: user})
323 nil -> {:error, :not_found}
327 defp represent_service_actor(nil, _), do: {:error, :not_found}
329 def relay(conn, _params) do
331 |> represent_service_actor(conn)
334 def internal_fetch(conn, _params) do
335 InternalFetchActor.get_actor()
336 |> represent_service_actor(conn)
339 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
340 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
342 |> put_resp_content_type("application/activity+json")
343 |> put_view(UserView)
344 |> render("user.json", %{user: user})
348 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
349 %{"nickname" => nickname, "page" => page?} = params
351 when page? in [true, "true"] do
354 |> Map.drop(["nickname", "page"])
355 |> Map.put("blocking_user", user)
356 |> Map.put("user", user)
359 [user.ap_id | User.following(user)]
360 |> ActivityPub.fetch_activities(params)
364 |> put_resp_content_type("application/activity+json")
365 |> put_view(UserView)
366 |> render("activity_collection_page.json", %{
367 activities: activities,
368 pagination: ControllerHelper.get_pagination_fields(conn, activities),
369 iri: "#{user.ap_id}/inbox"
373 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
374 "nickname" => nickname
376 with {:ok, user} <- User.ensure_keys_present(user) do
378 |> put_resp_content_type("application/activity+json")
379 |> put_view(UserView)
380 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
384 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
385 "nickname" => nickname
388 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
390 as_nickname: as_nickname
394 |> put_status(:forbidden)
398 defp handle_user_activity(
400 %{"type" => "Create", "object" => %{"type" => "Note"}} = params
404 |> Map.merge(Map.take(params, ["to", "cc"]))
405 |> Map.put("attributedTo", user.ap_id())
406 |> Transmogrifier.fix_object()
408 ActivityPub.create(%{
411 context: object["context"],
413 additional: Map.take(params, ["cc"])
417 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
418 with %Object{} = object <- Object.normalize(params["object"]),
419 true <- user.is_moderator || user.ap_id == object.data["actor"],
420 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
421 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
424 _ -> {:error, dgettext("errors", "Can't delete object")}
428 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
429 with %Object{} = object <- Object.normalize(params["object"]),
430 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
431 {_, {:ok, %Activity{} = activity, _meta}} <-
433 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
436 _ -> {:error, dgettext("errors", "Can't like object")}
440 defp handle_user_activity(_, _) do
441 {:error, dgettext("errors", "Unhandled activity type")}
445 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
446 %{"nickname" => nickname} = params
453 |> Map.put("actor", actor)
454 |> Transmogrifier.fix_addressing()
456 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
458 |> put_status(:created)
459 |> put_resp_header("location", activity.data["id"])
460 |> json(activity.data)
464 |> put_status(:bad_request)
469 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
471 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
473 as_nickname: user.nickname
477 |> put_status(:forbidden)
481 defp errors(conn, {:error, :not_found}) do
483 |> put_status(:not_found)
484 |> json(dgettext("errors", "Not found"))
487 defp errors(conn, _e) do
489 |> put_status(:internal_server_error)
490 |> json(dgettext("errors", "error"))
493 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
494 with actor <- conn.params["actor"],
495 true <- is_binary(actor) do
496 Pleroma.Instances.set_reachable(actor)
502 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
503 {:ok, new_user} = User.ensure_keys_present(user)
506 if new_user != user and match?(%User{}, for_user) do
507 User.get_cached_by_nickname(for_user.nickname)
515 # TODO: Add support for "object" field
517 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
520 - (required) `file`: data of the media
521 - (optionnal) `description`: description of the media, intended for accessibility
524 - HTTP Code: 201 Created
525 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
527 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
528 with {:ok, object} <-
531 actor: User.ap_id(user),
532 description: Map.get(data, "description")
534 Logger.debug(inspect(object))
537 |> put_status(:created)