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
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(conn, _) do
83 with ap_id <- Endpoint.url() <> conn.request_path,
84 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
85 {_, true} <- {:public?, Visibility.is_public?(object)} do
87 |> assign(:tracking_fun_data, object.id)
88 |> set_cache_ttl_for(object)
89 |> put_resp_content_type("application/activity+json")
90 |> put_view(ObjectView)
91 |> render("object.json", object: object)
98 def track_object_fetch(conn, nil), do: conn
100 def track_object_fetch(conn, object_id) do
101 with %{assigns: %{user: %User{id: user_id}}} <- conn do
102 Delivery.create(object_id, user_id)
108 def activity(conn, _params) do
109 with ap_id <- Endpoint.url() <> conn.request_path,
110 %Activity{} = activity <- Activity.normalize(ap_id),
111 {_, true} <- {:public?, Visibility.is_public?(activity)} do
113 |> maybe_set_tracking_data(activity)
114 |> set_cache_ttl_for(activity)
115 |> put_resp_content_type("application/activity+json")
116 |> put_view(ObjectView)
117 |> render("object.json", object: activity)
119 {:public?, false} -> {:error, :not_found}
120 nil -> {:error, :not_found}
124 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
125 object_id = Object.normalize(activity).id
126 assign(conn, :tracking_fun_data, object_id)
129 defp maybe_set_tracking_data(conn, _activity), do: conn
131 defp set_cache_ttl_for(conn, %Activity{object: object}) do
132 set_cache_ttl_for(conn, object)
135 defp set_cache_ttl_for(conn, entity) do
138 %Object{data: %{"type" => "Question"}} ->
139 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
142 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
148 assign(conn, :cache_ttl, ttl)
151 # GET /relay/following
152 def relay_following(conn, _params) do
153 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
155 |> put_resp_content_type("application/activity+json")
156 |> put_view(UserView)
157 |> render("following.json", %{user: Relay.get_actor()})
161 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
162 with %User{} = user <- User.get_cached_by_nickname(nickname),
163 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
164 {:show_follows, true} <-
165 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
166 {page, _} = Integer.parse(page)
169 |> put_resp_content_type("application/activity+json")
170 |> put_view(UserView)
171 |> render("following.json", %{user: user, page: page, for: for_user})
173 {:show_follows, _} ->
175 |> put_resp_content_type("application/activity+json")
176 |> send_resp(403, "")
180 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
181 with %User{} = user <- User.get_cached_by_nickname(nickname),
182 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
184 |> put_resp_content_type("application/activity+json")
185 |> put_view(UserView)
186 |> render("following.json", %{user: user, for: for_user})
190 # GET /relay/followers
191 def relay_followers(conn, _params) do
192 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
194 |> put_resp_content_type("application/activity+json")
195 |> put_view(UserView)
196 |> render("followers.json", %{user: Relay.get_actor()})
200 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
201 with %User{} = user <- User.get_cached_by_nickname(nickname),
202 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
203 {:show_followers, true} <-
204 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
205 {page, _} = Integer.parse(page)
208 |> put_resp_content_type("application/activity+json")
209 |> put_view(UserView)
210 |> render("followers.json", %{user: user, page: page, for: for_user})
212 {:show_followers, _} ->
214 |> put_resp_content_type("application/activity+json")
215 |> send_resp(403, "")
219 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
220 with %User{} = user <- User.get_cached_by_nickname(nickname),
221 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
223 |> put_resp_content_type("application/activity+json")
224 |> put_view(UserView)
225 |> render("followers.json", %{user: user, for: for_user})
230 %{assigns: %{user: for_user}} = conn,
231 %{"nickname" => nickname, "page" => page?} = params
233 when page? in [true, "true"] do
234 with %User{} = user <- User.get_cached_by_nickname(nickname),
235 {:ok, user} <- User.ensure_keys_present(user) do
236 # "include_poll_votes" is a hack because postgres generates inefficient
237 # queries when filtering by 'Answer', poll votes will be hidden by the
238 # visibility filter in this case anyway
241 |> Map.drop(["nickname", "page"])
242 |> Map.put("include_poll_votes", true)
243 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
245 activities = ActivityPub.fetch_user_activities(user, for_user, params)
248 |> put_resp_content_type("application/activity+json")
249 |> put_view(UserView)
250 |> render("activity_collection_page.json", %{
251 activities: activities,
252 pagination: ControllerHelper.get_pagination_fields(conn, activities),
253 iri: "#{user.ap_id}/outbox"
258 def outbox(conn, %{"nickname" => nickname}) do
259 with %User{} = user <- User.get_cached_by_nickname(nickname),
260 {:ok, user} <- User.ensure_keys_present(user) do
262 |> put_resp_content_type("application/activity+json")
263 |> put_view(UserView)
264 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
268 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
269 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
270 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
271 true <- Utils.recipient_in_message(recipient, actor, params),
272 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
273 Federator.incoming_ap_doc(params)
278 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
279 Federator.incoming_ap_doc(params)
283 # POST /relay/inbox -or- POST /internal/fetch/inbox
284 def inbox(conn, params) do
285 if params["type"] == "Create" && FederatingPlug.federating?() do
286 post_inbox_relayed_create(conn, params)
288 post_inbox_fallback(conn, params)
292 defp post_inbox_relayed_create(conn, params) do
294 "Signature missing or not from author, relayed Create message, fetching object from source"
297 Fetcher.fetch_object_from_id(params["object"]["id"])
302 defp post_inbox_fallback(conn, params) do
303 headers = Enum.into(conn.req_headers, %{})
305 if headers["signature"] && params["actor"] &&
306 String.contains?(headers["signature"], params["actor"]) do
308 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
311 Logger.debug(inspect(conn.req_headers))
315 |> put_status(:bad_request)
316 |> json(dgettext("errors", "error"))
319 defp represent_service_actor(%User{} = user, conn) do
320 with {:ok, user} <- User.ensure_keys_present(user) do
322 |> put_resp_content_type("application/activity+json")
323 |> put_view(UserView)
324 |> render("user.json", %{user: user})
326 nil -> {:error, :not_found}
330 defp represent_service_actor(nil, _), do: {:error, :not_found}
332 def relay(conn, _params) do
334 |> represent_service_actor(conn)
337 def internal_fetch(conn, _params) do
338 InternalFetchActor.get_actor()
339 |> represent_service_actor(conn)
342 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
343 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
345 |> put_resp_content_type("application/activity+json")
346 |> put_view(UserView)
347 |> render("user.json", %{user: user})
351 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
352 %{"nickname" => nickname, "page" => page?} = params
354 when page? in [true, "true"] do
357 |> Map.drop(["nickname", "page"])
358 |> Map.put("blocking_user", user)
359 |> Map.put("user", user)
360 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
363 [user.ap_id | User.following(user)]
364 |> ActivityPub.fetch_activities(params)
368 |> put_resp_content_type("application/activity+json")
369 |> put_view(UserView)
370 |> render("activity_collection_page.json", %{
371 activities: activities,
372 pagination: ControllerHelper.get_pagination_fields(conn, activities),
373 iri: "#{user.ap_id}/inbox"
377 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
378 "nickname" => nickname
380 with {:ok, user} <- User.ensure_keys_present(user) do
382 |> put_resp_content_type("application/activity+json")
383 |> put_view(UserView)
384 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
388 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
389 "nickname" => nickname
392 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
394 as_nickname: as_nickname
398 |> put_status(:forbidden)
402 defp handle_user_activity(
404 %{"type" => "Create", "object" => %{"type" => "Note"} = object} = params
406 content = if is_binary(object["content"]), do: object["content"], else: ""
407 name = if is_binary(object["name"]), do: object["name"], else: ""
408 summary = if is_binary(object["summary"]), do: object["summary"], else: ""
409 length = String.length(content <> name <> summary)
411 if length > Pleroma.Config.get([:instance, :limit]) do
412 {:error, dgettext("errors", "Note is over the character limit")}
416 |> Map.merge(Map.take(params, ["to", "cc"]))
417 |> Map.put("attributedTo", user.ap_id)
418 |> Transmogrifier.fix_object()
420 ActivityPub.create(%{
423 context: object["context"],
425 additional: Map.take(params, ["cc"])
430 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
431 with %Object{} = object <- Object.normalize(params["object"]),
432 true <- user.is_moderator || user.ap_id == object.data["actor"],
433 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
434 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
437 _ -> {:error, dgettext("errors", "Can't delete object")}
441 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
442 with %Object{} = object <- Object.normalize(params["object"]),
443 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
444 {_, {:ok, %Activity{} = activity, _meta}} <-
446 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
449 _ -> {:error, dgettext("errors", "Can't like object")}
453 defp handle_user_activity(_, _) do
454 {:error, dgettext("errors", "Unhandled activity type")}
458 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
459 %{"nickname" => nickname} = params
466 |> Map.put("actor", actor)
467 |> Transmogrifier.fix_addressing()
469 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
471 |> put_status(:created)
472 |> put_resp_header("location", activity.data["id"])
473 |> json(activity.data)
477 |> put_status(:bad_request)
482 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
484 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
486 as_nickname: user.nickname
490 |> put_status(:forbidden)
494 defp errors(conn, {:error, :not_found}) do
496 |> put_status(:not_found)
497 |> json(dgettext("errors", "Not found"))
500 defp errors(conn, _e) do
502 |> put_status(:internal_server_error)
503 |> json(dgettext("errors", "error"))
506 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
507 with actor <- conn.params["actor"],
508 true <- is_binary(actor) do
509 Pleroma.Instances.set_reachable(actor)
515 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
516 {:ok, new_user} = User.ensure_keys_present(user)
519 if new_user != user and match?(%User{}, for_user) do
520 User.get_cached_by_nickname(for_user.nickname)
528 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
529 with {:ok, object} <-
532 actor: User.ap_id(user),
533 description: Map.get(data, "description")
535 Logger.debug(inspect(object))
538 |> put_status(:created)