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]
48 plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
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"}} = params
408 |> Map.merge(Map.take(params, ["to", "cc"]))
409 |> Map.put("attributedTo", user.ap_id())
410 |> Transmogrifier.fix_object()
412 ActivityPub.create(%{
415 context: object["context"],
417 additional: Map.take(params, ["cc"])
421 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
422 with %Object{} = object <- Object.normalize(params["object"]),
423 true <- user.is_moderator || user.ap_id == object.data["actor"],
424 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
425 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
428 _ -> {:error, dgettext("errors", "Can't delete object")}
432 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
433 with %Object{} = object <- Object.normalize(params["object"]),
434 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
435 {_, {:ok, %Activity{} = activity, _meta}} <-
437 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
440 _ -> {:error, dgettext("errors", "Can't like object")}
444 defp handle_user_activity(_, _) do
445 {:error, dgettext("errors", "Unhandled activity type")}
449 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
450 %{"nickname" => nickname} = params
457 |> Map.put("actor", actor)
458 |> Transmogrifier.fix_addressing()
460 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
462 |> put_status(:created)
463 |> put_resp_header("location", activity.data["id"])
464 |> json(activity.data)
468 |> put_status(:bad_request)
473 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
475 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
477 as_nickname: user.nickname
481 |> put_status(:forbidden)
485 defp errors(conn, {:error, :not_found}) do
487 |> put_status(:not_found)
488 |> json(dgettext("errors", "Not found"))
491 defp errors(conn, _e) do
493 |> put_status(:internal_server_error)
494 |> json(dgettext("errors", "error"))
497 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
498 with actor <- conn.params["actor"],
499 true <- is_binary(actor) do
500 Pleroma.Instances.set_reachable(actor)
506 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
507 {:ok, new_user} = User.ensure_keys_present(user)
510 if new_user != user and match?(%User{}, for_user) do
511 User.get_cached_by_nickname(for_user.nickname)
519 # TODO: Add support for "object" field
521 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
524 - (required) `file`: data of the media
525 - (optionnal) `description`: description of the media, intended for accessibility
528 - HTTP Code: 201 Created
529 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
531 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
532 with {:ok, object} <-
535 actor: User.ap_id(user),
536 description: Map.get(data, "description")
538 Logger.debug(inspect(object))
541 |> put_status(:created)