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.Endpoint
25 alias Pleroma.Web.FederatingPlug
26 alias Pleroma.Web.Federator
30 action_fallback(:errors)
32 @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
34 plug(FederatingPlug when action in @federating_only_actions)
37 EnsureAuthenticatedPlug,
38 [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
41 # Note: :following and :followers must be served even without authentication (as via :api)
43 EnsureAuthenticatedPlug
44 when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
49 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
50 when action in [:activity, :object]
53 plug(:set_requester_reachable when action in [:inbox])
54 plug(:relay_active? when action in [:relay])
56 defp relay_active?(conn, _) do
57 if Pleroma.Config.get([:instance, :allow_relay]) do
61 |> render_error(:not_found, "not found")
66 def user(conn, %{"nickname" => nickname}) do
67 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
68 {:ok, user} <- User.ensure_keys_present(user) do
70 |> put_resp_content_type("application/activity+json")
72 |> render("user.json", %{user: user})
74 nil -> {:error, :not_found}
75 %{local: false} -> {:error, :not_found}
79 def object(conn, _) do
80 with ap_id <- Endpoint.url() <> conn.request_path,
81 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
82 {_, true} <- {:public?, Visibility.is_public?(object)} do
84 |> assign(:tracking_fun_data, object.id)
85 |> set_cache_ttl_for(object)
86 |> put_resp_content_type("application/activity+json")
87 |> put_view(ObjectView)
88 |> render("object.json", object: object)
95 def track_object_fetch(conn, nil), do: conn
97 def track_object_fetch(conn, object_id) do
98 with %{assigns: %{user: %User{id: user_id}}} <- conn do
99 Delivery.create(object_id, user_id)
105 def activity(conn, _params) do
106 with ap_id <- Endpoint.url() <> conn.request_path,
107 %Activity{} = activity <- Activity.normalize(ap_id),
108 {_, true} <- {:public?, Visibility.is_public?(activity)} do
110 |> maybe_set_tracking_data(activity)
111 |> set_cache_ttl_for(activity)
112 |> put_resp_content_type("application/activity+json")
113 |> put_view(ObjectView)
114 |> render("object.json", object: activity)
116 {:public?, false} -> {:error, :not_found}
117 nil -> {:error, :not_found}
121 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
122 object_id = Object.normalize(activity).id
123 assign(conn, :tracking_fun_data, object_id)
126 defp maybe_set_tracking_data(conn, _activity), do: conn
128 defp set_cache_ttl_for(conn, %Activity{object: object}) do
129 set_cache_ttl_for(conn, object)
132 defp set_cache_ttl_for(conn, entity) do
135 %Object{data: %{"type" => "Question"}} ->
136 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
139 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
145 assign(conn, :cache_ttl, ttl)
148 # GET /relay/following
149 def relay_following(conn, _params) do
150 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
152 |> put_resp_content_type("application/activity+json")
153 |> put_view(UserView)
154 |> render("following.json", %{user: Relay.get_actor()})
158 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
159 with %User{} = user <- User.get_cached_by_nickname(nickname),
160 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
161 {:show_follows, true} <-
162 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
163 {page, _} = Integer.parse(page)
166 |> put_resp_content_type("application/activity+json")
167 |> put_view(UserView)
168 |> render("following.json", %{user: user, page: page, for: for_user})
170 {:show_follows, _} ->
172 |> put_resp_content_type("application/activity+json")
173 |> send_resp(403, "")
177 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
178 with %User{} = user <- User.get_cached_by_nickname(nickname),
179 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
181 |> put_resp_content_type("application/activity+json")
182 |> put_view(UserView)
183 |> render("following.json", %{user: user, for: for_user})
187 # GET /relay/followers
188 def relay_followers(conn, _params) do
189 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
191 |> put_resp_content_type("application/activity+json")
192 |> put_view(UserView)
193 |> render("followers.json", %{user: Relay.get_actor()})
197 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
198 with %User{} = user <- User.get_cached_by_nickname(nickname),
199 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
200 {:show_followers, true} <-
201 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
202 {page, _} = Integer.parse(page)
205 |> put_resp_content_type("application/activity+json")
206 |> put_view(UserView)
207 |> render("followers.json", %{user: user, page: page, for: for_user})
209 {:show_followers, _} ->
211 |> put_resp_content_type("application/activity+json")
212 |> send_resp(403, "")
216 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
217 with %User{} = user <- User.get_cached_by_nickname(nickname),
218 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
220 |> put_resp_content_type("application/activity+json")
221 |> put_view(UserView)
222 |> render("followers.json", %{user: user, for: for_user})
227 %{assigns: %{user: for_user}} = conn,
228 %{"nickname" => nickname, "page" => page?} = params
230 when page? in [true, "true"] do
231 with %User{} = user <- User.get_cached_by_nickname(nickname),
232 {:ok, user} <- User.ensure_keys_present(user) do
234 if params["max_id"] do
235 ActivityPub.fetch_user_activities(user, for_user, %{
236 "max_id" => params["max_id"],
237 # This is a hack because postgres generates inefficient queries when filtering by
238 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
239 "include_poll_votes" => true,
243 ActivityPub.fetch_user_activities(user, for_user, %{
245 "include_poll_votes" => true
250 |> put_resp_content_type("application/activity+json")
251 |> put_view(UserView)
252 |> render("activity_collection_page.json", %{
253 activities: activities,
254 iri: "#{user.ap_id}/outbox"
259 def outbox(conn, %{"nickname" => nickname}) do
260 with %User{} = user <- User.get_cached_by_nickname(nickname),
261 {:ok, user} <- User.ensure_keys_present(user) do
263 |> put_resp_content_type("application/activity+json")
264 |> put_view(UserView)
265 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
269 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
270 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
271 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
272 true <- Utils.recipient_in_message(recipient, actor, params),
273 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
274 Federator.incoming_ap_doc(params)
279 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
280 Federator.incoming_ap_doc(params)
284 # POST /relay/inbox -or- POST /internal/fetch/inbox
285 def inbox(conn, params) do
286 if params["type"] == "Create" && FederatingPlug.federating?() do
287 post_inbox_relayed_create(conn, params)
289 post_inbox_fallback(conn, params)
293 defp post_inbox_relayed_create(conn, params) do
295 "Signature missing or not from author, relayed Create message, fetching object from source"
298 Fetcher.fetch_object_from_id(params["object"]["id"])
303 defp post_inbox_fallback(conn, params) do
304 headers = Enum.into(conn.req_headers, %{})
306 if headers["signature"] && params["actor"] &&
307 String.contains?(headers["signature"], params["actor"]) do
309 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
312 Logger.debug(inspect(conn.req_headers))
316 |> put_status(:bad_request)
317 |> json(dgettext("errors", "error"))
320 defp represent_service_actor(%User{} = user, conn) do
321 with {:ok, user} <- User.ensure_keys_present(user) do
323 |> put_resp_content_type("application/activity+json")
324 |> put_view(UserView)
325 |> render("user.json", %{user: user})
327 nil -> {:error, :not_found}
331 defp represent_service_actor(nil, _), do: {:error, :not_found}
333 def relay(conn, _params) do
335 |> represent_service_actor(conn)
338 def internal_fetch(conn, _params) do
339 InternalFetchActor.get_actor()
340 |> represent_service_actor(conn)
343 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
344 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
346 |> put_resp_content_type("application/activity+json")
347 |> put_view(UserView)
348 |> render("user.json", %{user: user})
352 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
353 %{"nickname" => nickname, "page" => page?} = params
355 when page? in [true, "true"] do
357 if params["max_id"] do
358 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
359 "max_id" => params["max_id"],
363 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
367 |> put_resp_content_type("application/activity+json")
368 |> put_view(UserView)
369 |> render("activity_collection_page.json", %{
370 activities: 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"}} = params
406 |> Map.merge(Map.take(params, ["to", "cc"]))
407 |> Map.put("attributedTo", user.ap_id())
408 |> Transmogrifier.fix_object()
410 ActivityPub.create(%{
413 context: object["context"],
415 additional: Map.take(params, ["cc"])
419 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
420 with %Object{} = object <- Object.normalize(params["object"]),
421 true <- user.is_moderator || user.ap_id == object.data["actor"],
422 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
423 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
426 _ -> {:error, dgettext("errors", "Can't delete object")}
430 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
431 with %Object{} = object <- Object.normalize(params["object"]),
432 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
433 {_, {:ok, %Activity{} = activity, _meta}} <-
435 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
438 _ -> {:error, dgettext("errors", "Can't like object")}
442 defp handle_user_activity(_, _) do
443 {:error, dgettext("errors", "Unhandled activity type")}
447 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
448 %{"nickname" => nickname} = params
455 |> Map.put("actor", actor)
456 |> Transmogrifier.fix_addressing()
458 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
460 |> put_status(:created)
461 |> put_resp_header("location", activity.data["id"])
462 |> json(activity.data)
466 |> put_status(:bad_request)
471 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
473 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
475 as_nickname: user.nickname
479 |> put_status(:forbidden)
483 defp errors(conn, {:error, :not_found}) do
485 |> put_status(:not_found)
486 |> json(dgettext("errors", "Not found"))
489 defp errors(conn, _e) do
491 |> put_status(:internal_server_error)
492 |> json(dgettext("errors", "error"))
495 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
496 with actor <- conn.params["actor"],
497 true <- is_binary(actor) do
498 Pleroma.Instances.set_reachable(actor)
504 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
505 {:ok, new_user} = User.ensure_keys_present(user)
508 if new_user != user and match?(%User{}, for_user) do
509 User.get_cached_by_nickname(for_user.nickname)
517 # TODO: Add support for "object" field
519 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
522 - (required) `file`: data of the media
523 - (optionnal) `description`: description of the media, intended for accessibility
526 - HTTP Code: 201 Created
527 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
529 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
530 with {:ok, object} <-
533 actor: User.ap_id(user),
534 description: Map.get(data, "description")
536 Logger.debug(inspect(object))
539 |> put_status(:created)