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"} = object} = params
404 content = if is_binary(object["content"]), do: object["content"], else: ""
405 name = if is_binary(object["name"]), do: object["name"], else: ""
406 summary = if is_binary(object["summary"]), do: object["summary"], else: ""
407 length = String.length(content <> name <> summary)
409 if length > Pleroma.Config.get([:instance, :limit]) do
410 {:error, dgettext("errors", "Note is over the character limit")}
414 |> Map.merge(Map.take(params, ["to", "cc"]))
415 |> Map.put("attributedTo", user.ap_id())
416 |> Transmogrifier.fix_object()
418 ActivityPub.create(%{
421 context: object["context"],
423 additional: Map.take(params, ["cc"])
428 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
429 with %Object{} = object <- Object.normalize(params["object"]),
430 true <- user.is_moderator || user.ap_id == object.data["actor"],
431 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
432 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
435 _ -> {:error, dgettext("errors", "Can't delete object")}
439 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
440 with %Object{} = object <- Object.normalize(params["object"]),
441 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
442 {_, {:ok, %Activity{} = activity, _meta}} <-
444 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
447 _ -> {:error, dgettext("errors", "Can't like object")}
451 defp handle_user_activity(_, _) do
452 {:error, dgettext("errors", "Unhandled activity type")}
456 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
457 %{"nickname" => nickname} = params
464 |> Map.put("actor", actor)
465 |> Transmogrifier.fix_addressing()
467 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
469 |> put_status(:created)
470 |> put_resp_header("location", activity.data["id"])
471 |> json(activity.data)
475 |> put_status(:bad_request)
480 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
482 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
484 as_nickname: user.nickname
488 |> put_status(:forbidden)
492 defp errors(conn, {:error, :not_found}) do
494 |> put_status(:not_found)
495 |> json(dgettext("errors", "Not found"))
498 defp errors(conn, _e) do
500 |> put_status(:internal_server_error)
501 |> json(dgettext("errors", "error"))
504 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
505 with actor <- conn.params["actor"],
506 true <- is_binary(actor) do
507 Pleroma.Instances.set_reachable(actor)
513 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
514 {:ok, new_user} = User.ensure_keys_present(user)
517 if new_user != user and match?(%User{}, for_user) do
518 User.get_cached_by_nickname(for_user.nickname)
527 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
530 - (required) `file`: data of the media
531 - (optionnal) `description`: description of the media, intended for accessibility
534 - HTTP Code: 201 Created
535 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
537 Note: Will not point to a URL with a `Location` header because no standalone Activity has been created.
539 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
540 with {:ok, object} <-
543 actor: User.ap_id(user),
544 description: Map.get(data, "description")
546 Logger.debug(inspect(object))
549 |> put_status(:created)