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)},
86 {_, false} <- {:local?, Visibility.is_local_public?(object)} do
88 |> assign(:tracking_fun_data, object.id)
89 |> set_cache_ttl_for(object)
90 |> put_resp_content_type("application/activity+json")
91 |> put_view(ObjectView)
92 |> render("object.json", object: object)
102 def track_object_fetch(conn, nil), do: conn
104 def track_object_fetch(conn, object_id) do
105 with %{assigns: %{user: %User{id: user_id}}} <- conn do
106 Delivery.create(object_id, user_id)
112 def activity(conn, _params) do
113 with ap_id <- Endpoint.url() <> conn.request_path,
114 %Activity{} = activity <- Activity.normalize(ap_id),
115 {_, true} <- {:public?, Visibility.is_public?(activity)},
116 {_, false} <- {:local?, Visibility.is_local_public?(activity)} do
118 |> maybe_set_tracking_data(activity)
119 |> set_cache_ttl_for(activity)
120 |> put_resp_content_type("application/activity+json")
121 |> put_view(ObjectView)
122 |> render("object.json", object: activity)
124 {:public?, false} -> {:error, :not_found}
125 {:local?, true} -> {:error, :not_found}
126 nil -> {:error, :not_found}
130 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
131 object_id = Object.normalize(activity, fetch: false).id
132 assign(conn, :tracking_fun_data, object_id)
135 defp maybe_set_tracking_data(conn, _activity), do: conn
137 defp set_cache_ttl_for(conn, %Activity{object: object}) do
138 set_cache_ttl_for(conn, object)
141 defp set_cache_ttl_for(conn, entity) do
144 %Object{data: %{"type" => "Question"}} ->
145 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
148 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
154 assign(conn, :cache_ttl, ttl)
157 # GET /relay/following
158 def relay_following(conn, _params) do
159 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
161 |> put_resp_content_type("application/activity+json")
162 |> put_view(UserView)
163 |> render("following.json", %{user: Relay.get_actor()})
167 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
168 with %User{} = user <- User.get_cached_by_nickname(nickname),
169 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
170 {:show_follows, true} <-
171 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
172 {page, _} = Integer.parse(page)
175 |> put_resp_content_type("application/activity+json")
176 |> put_view(UserView)
177 |> render("following.json", %{user: user, page: page, for: for_user})
179 {:show_follows, _} ->
181 |> put_resp_content_type("application/activity+json")
182 |> send_resp(403, "")
186 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
187 with %User{} = user <- User.get_cached_by_nickname(nickname),
188 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
190 |> put_resp_content_type("application/activity+json")
191 |> put_view(UserView)
192 |> render("following.json", %{user: user, for: for_user})
196 # GET /relay/followers
197 def relay_followers(conn, _params) do
198 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
200 |> put_resp_content_type("application/activity+json")
201 |> put_view(UserView)
202 |> render("followers.json", %{user: Relay.get_actor()})
206 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
207 with %User{} = user <- User.get_cached_by_nickname(nickname),
208 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
209 {:show_followers, true} <-
210 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
211 {page, _} = Integer.parse(page)
214 |> put_resp_content_type("application/activity+json")
215 |> put_view(UserView)
216 |> render("followers.json", %{user: user, page: page, for: for_user})
218 {:show_followers, _} ->
220 |> put_resp_content_type("application/activity+json")
221 |> send_resp(403, "")
225 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
226 with %User{} = user <- User.get_cached_by_nickname(nickname),
227 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
229 |> put_resp_content_type("application/activity+json")
230 |> put_view(UserView)
231 |> render("followers.json", %{user: user, for: for_user})
236 %{assigns: %{user: for_user}} = conn,
237 %{"nickname" => nickname, "page" => page?} = params
239 when page? in [true, "true"] do
240 with %User{} = user <- User.get_cached_by_nickname(nickname),
241 {:ok, user} <- User.ensure_keys_present(user) do
242 # "include_poll_votes" is a hack because postgres generates inefficient
243 # queries when filtering by 'Answer', poll votes will be hidden by the
244 # visibility filter in this case anyway
247 |> Map.drop(["nickname", "page"])
248 |> Map.put("include_poll_votes", true)
249 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
251 activities = ActivityPub.fetch_user_activities(user, for_user, params)
254 |> put_resp_content_type("application/activity+json")
255 |> put_view(UserView)
256 |> render("activity_collection_page.json", %{
257 activities: activities,
258 pagination: ControllerHelper.get_pagination_fields(conn, activities),
259 iri: "#{user.ap_id}/outbox"
264 def outbox(conn, %{"nickname" => nickname}) do
265 with %User{} = user <- User.get_cached_by_nickname(nickname),
266 {:ok, user} <- User.ensure_keys_present(user) do
268 |> put_resp_content_type("application/activity+json")
269 |> put_view(UserView)
270 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
274 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
275 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
276 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
277 true <- Utils.recipient_in_message(recipient, actor, params),
278 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
279 Federator.incoming_ap_doc(params)
284 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
285 Federator.incoming_ap_doc(params)
289 # POST /relay/inbox -or- POST /internal/fetch/inbox
290 def inbox(conn, params) do
291 if params["type"] == "Create" && FederatingPlug.federating?() do
292 post_inbox_relayed_create(conn, params)
294 post_inbox_fallback(conn, params)
298 defp post_inbox_relayed_create(conn, params) do
300 "Signature missing or not from author, relayed Create message, fetching object from source"
303 Fetcher.fetch_object_from_id(params["object"]["id"])
308 defp post_inbox_fallback(conn, params) do
309 headers = Enum.into(conn.req_headers, %{})
311 if headers["signature"] && params["actor"] &&
312 String.contains?(headers["signature"], params["actor"]) do
314 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
317 Logger.debug(inspect(conn.req_headers))
321 |> put_status(:bad_request)
322 |> json(dgettext("errors", "error"))
325 defp represent_service_actor(%User{} = user, conn) do
326 with {:ok, user} <- User.ensure_keys_present(user) do
328 |> put_resp_content_type("application/activity+json")
329 |> put_view(UserView)
330 |> render("user.json", %{user: user})
332 nil -> {:error, :not_found}
336 defp represent_service_actor(nil, _), do: {:error, :not_found}
338 def relay(conn, _params) do
340 |> represent_service_actor(conn)
343 def internal_fetch(conn, _params) do
344 InternalFetchActor.get_actor()
345 |> represent_service_actor(conn)
348 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
349 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
351 |> put_resp_content_type("application/activity+json")
352 |> put_view(UserView)
353 |> render("user.json", %{user: user})
357 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
358 %{"nickname" => nickname, "page" => page?} = params
360 when page? in [true, "true"] do
363 |> Map.drop(["nickname", "page"])
364 |> Map.put("blocking_user", user)
365 |> Map.put("user", user)
366 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
369 [user.ap_id | User.following(user)]
370 |> ActivityPub.fetch_activities(params)
374 |> put_resp_content_type("application/activity+json")
375 |> put_view(UserView)
376 |> render("activity_collection_page.json", %{
377 activities: activities,
378 pagination: ControllerHelper.get_pagination_fields(conn, activities),
379 iri: "#{user.ap_id}/inbox"
383 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
384 "nickname" => nickname
386 with {:ok, user} <- User.ensure_keys_present(user) do
388 |> put_resp_content_type("application/activity+json")
389 |> put_view(UserView)
390 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
394 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
395 "nickname" => nickname
398 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
400 as_nickname: as_nickname
404 |> put_status(:forbidden)
408 defp handle_user_activity(
410 %{"type" => "Create", "object" => %{"type" => "Note"} = object} = params
412 content = if is_binary(object["content"]), do: object["content"], else: ""
413 name = if is_binary(object["name"]), do: object["name"], else: ""
414 summary = if is_binary(object["summary"]), do: object["summary"], else: ""
415 length = String.length(content <> name <> summary)
417 if length > Pleroma.Config.get([:instance, :limit]) do
418 {:error, dgettext("errors", "Note is over the character limit")}
422 |> Map.merge(Map.take(params, ["to", "cc"]))
423 |> Map.put("attributedTo", user.ap_id)
424 |> Transmogrifier.fix_object()
426 ActivityPub.create(%{
429 context: object["context"],
431 additional: Map.take(params, ["cc"])
436 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
437 with %Object{} = object <- Object.normalize(params["object"], fetch: false),
438 true <- user.is_moderator || user.ap_id == object.data["actor"],
439 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
440 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
443 _ -> {:error, dgettext("errors", "Can't delete object")}
447 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
448 with %Object{} = object <- Object.normalize(params["object"], fetch: false),
449 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
450 {_, {:ok, %Activity{} = activity, _meta}} <-
452 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
455 _ -> {:error, dgettext("errors", "Can't like object")}
459 defp handle_user_activity(_, _) do
460 {:error, dgettext("errors", "Unhandled activity type")}
464 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
465 %{"nickname" => nickname} = params
472 |> Map.put("actor", actor)
473 |> Transmogrifier.fix_addressing()
475 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
477 |> put_status(:created)
478 |> put_resp_header("location", activity.data["id"])
479 |> json(activity.data)
483 |> put_status(:bad_request)
488 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
490 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
492 as_nickname: user.nickname
496 |> put_status(:forbidden)
500 defp errors(conn, {:error, :not_found}) do
502 |> put_status(:not_found)
503 |> json(dgettext("errors", "Not found"))
506 defp errors(conn, _e) do
508 |> put_status(:internal_server_error)
509 |> json(dgettext("errors", "error"))
512 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
513 with actor <- conn.params["actor"],
514 true <- is_binary(actor) do
515 Pleroma.Instances.set_reachable(actor)
521 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
522 {:ok, new_user} = User.ensure_keys_present(user)
525 if new_user != user and match?(%User{}, for_user) do
526 User.get_cached_by_nickname(for_user.nickname)
534 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
535 with {:ok, object} <-
538 actor: User.ap_id(user),
539 description: Map.get(data, "description")
541 Logger.debug(inspect(object))
544 |> put_status(:created)