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.InternalFetchActor
16 alias Pleroma.Web.ActivityPub.ObjectView
17 alias Pleroma.Web.ActivityPub.Relay
18 alias Pleroma.Web.ActivityPub.Transmogrifier
19 alias Pleroma.Web.ActivityPub.UserView
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.FederatingPlug
23 alias Pleroma.Web.Federator
27 action_fallback(:errors)
29 @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
31 plug(FederatingPlug when action in @federating_only_actions)
34 EnsureAuthenticatedPlug,
35 [unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions
39 EnsureAuthenticatedPlug
40 when action in [:read_inbox, :update_outbox, :whoami, :upload_media, :following, :followers]
45 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
46 when action in [:activity, :object]
49 plug(:set_requester_reachable when action in [:inbox])
50 plug(:relay_active? when action in [:relay])
52 defp relay_active?(conn, _) do
53 if Pleroma.Config.get([:instance, :allow_relay]) do
57 |> render_error(:not_found, "not found")
62 def user(conn, %{"nickname" => nickname}) do
63 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
64 {:ok, user} <- User.ensure_keys_present(user) do
66 |> put_resp_content_type("application/activity+json")
68 |> render("user.json", %{user: user})
70 nil -> {:error, :not_found}
71 %{local: false} -> {:error, :not_found}
75 def object(conn, %{"uuid" => uuid}) do
76 with ap_id <- o_status_url(conn, :object, uuid),
77 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
78 {_, true} <- {:public?, Visibility.is_public?(object)} do
80 |> assign(:tracking_fun_data, object.id)
81 |> set_cache_ttl_for(object)
82 |> put_resp_content_type("application/activity+json")
83 |> put_view(ObjectView)
84 |> render("object.json", object: object)
91 def track_object_fetch(conn, nil), do: conn
93 def track_object_fetch(conn, object_id) do
94 with %{assigns: %{user: %User{id: user_id}}} <- conn do
95 Delivery.create(object_id, user_id)
101 def activity(conn, %{"uuid" => uuid}) do
102 with ap_id <- o_status_url(conn, :activity, uuid),
103 %Activity{} = activity <- Activity.normalize(ap_id),
104 {_, true} <- {:public?, Visibility.is_public?(activity)} do
106 |> maybe_set_tracking_data(activity)
107 |> set_cache_ttl_for(activity)
108 |> put_resp_content_type("application/activity+json")
109 |> put_view(ObjectView)
110 |> render("object.json", object: activity)
112 {:public?, false} -> {:error, :not_found}
113 nil -> {:error, :not_found}
117 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
118 object_id = Object.normalize(activity).id
119 assign(conn, :tracking_fun_data, object_id)
122 defp maybe_set_tracking_data(conn, _activity), do: conn
124 defp set_cache_ttl_for(conn, %Activity{object: object}) do
125 set_cache_ttl_for(conn, object)
128 defp set_cache_ttl_for(conn, entity) do
131 %Object{data: %{"type" => "Question"}} ->
132 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
135 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
141 assign(conn, :cache_ttl, ttl)
144 # GET /relay/following
145 def relay_following(conn, _params) do
146 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
148 |> put_resp_content_type("application/activity+json")
149 |> put_view(UserView)
150 |> render("following.json", %{user: Relay.get_actor()})
154 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
155 with %User{} = user <- User.get_cached_by_nickname(nickname),
156 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
157 {:show_follows, true} <-
158 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
159 {page, _} = Integer.parse(page)
162 |> put_resp_content_type("application/activity+json")
163 |> put_view(UserView)
164 |> render("following.json", %{user: user, page: page, for: for_user})
166 {:show_follows, _} ->
168 |> put_resp_content_type("application/activity+json")
169 |> send_resp(403, "")
173 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
174 with %User{} = user <- User.get_cached_by_nickname(nickname),
175 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
177 |> put_resp_content_type("application/activity+json")
178 |> put_view(UserView)
179 |> render("following.json", %{user: user, for: for_user})
183 # GET /relay/followers
184 def relay_followers(conn, _params) do
185 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
187 |> put_resp_content_type("application/activity+json")
188 |> put_view(UserView)
189 |> render("followers.json", %{user: Relay.get_actor()})
193 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
194 with %User{} = user <- User.get_cached_by_nickname(nickname),
195 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
196 {:show_followers, true} <-
197 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
198 {page, _} = Integer.parse(page)
201 |> put_resp_content_type("application/activity+json")
202 |> put_view(UserView)
203 |> render("followers.json", %{user: user, page: page, for: for_user})
205 {:show_followers, _} ->
207 |> put_resp_content_type("application/activity+json")
208 |> send_resp(403, "")
212 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
213 with %User{} = user <- User.get_cached_by_nickname(nickname),
214 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
216 |> put_resp_content_type("application/activity+json")
217 |> put_view(UserView)
218 |> render("followers.json", %{user: user, for: for_user})
223 %{assigns: %{user: for_user}} = conn,
224 %{"nickname" => nickname, "page" => page?} = params
226 when page? in [true, "true"] do
227 with %User{} = user <- User.get_cached_by_nickname(nickname),
228 {:ok, user} <- User.ensure_keys_present(user) do
230 if params["max_id"] do
231 ActivityPub.fetch_user_activities(user, for_user, %{
232 "max_id" => params["max_id"],
233 # This is a hack because postgres generates inefficient queries when filtering by
234 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
235 "include_poll_votes" => true,
239 ActivityPub.fetch_user_activities(user, for_user, %{
241 "include_poll_votes" => true
246 |> put_resp_content_type("application/activity+json")
247 |> put_view(UserView)
248 |> render("activity_collection_page.json", %{
249 activities: activities,
250 iri: "#{user.ap_id}/outbox"
255 def outbox(conn, %{"nickname" => nickname}) do
256 with %User{} = user <- User.get_cached_by_nickname(nickname),
257 {:ok, user} <- User.ensure_keys_present(user) do
259 |> put_resp_content_type("application/activity+json")
260 |> put_view(UserView)
261 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
265 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
266 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
267 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
268 true <- Utils.recipient_in_message(recipient, actor, params),
269 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
270 Federator.incoming_ap_doc(params)
275 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
276 Federator.incoming_ap_doc(params)
280 # POST /relay/inbox -or- POST /internal/fetch/inbox
281 def inbox(conn, params) do
282 if params["type"] == "Create" && FederatingPlug.federating?() do
283 post_inbox_relayed_create(conn, params)
285 post_inbox_fallback(conn, params)
289 defp post_inbox_relayed_create(conn, params) do
291 "Signature missing or not from author, relayed Create message, fetching object from source"
294 Fetcher.fetch_object_from_id(params["object"]["id"])
299 defp post_inbox_fallback(conn, params) do
300 headers = Enum.into(conn.req_headers, %{})
302 if headers["signature"] && params["actor"] &&
303 String.contains?(headers["signature"], params["actor"]) do
305 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
308 Logger.debug(inspect(conn.req_headers))
312 |> put_status(:bad_request)
313 |> json(dgettext("errors", "error"))
316 defp represent_service_actor(%User{} = user, conn) do
317 with {:ok, user} <- User.ensure_keys_present(user) do
319 |> put_resp_content_type("application/activity+json")
320 |> put_view(UserView)
321 |> render("user.json", %{user: user})
323 nil -> {:error, :not_found}
327 defp represent_service_actor(nil, _), do: {:error, :not_found}
329 def relay(conn, _params) do
331 |> represent_service_actor(conn)
334 def internal_fetch(conn, _params) do
335 InternalFetchActor.get_actor()
336 |> represent_service_actor(conn)
339 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
340 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
342 |> put_resp_content_type("application/activity+json")
343 |> put_view(UserView)
344 |> render("user.json", %{user: user})
348 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
349 %{"nickname" => nickname, "page" => page?} = params
351 when page? in [true, "true"] do
353 if params["max_id"] do
354 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
355 "max_id" => params["max_id"],
359 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
363 |> put_resp_content_type("application/activity+json")
364 |> put_view(UserView)
365 |> render("activity_collection_page.json", %{
366 activities: activities,
367 iri: "#{user.ap_id}/inbox"
371 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
372 "nickname" => nickname
374 with {:ok, user} <- User.ensure_keys_present(user) do
376 |> put_resp_content_type("application/activity+json")
377 |> put_view(UserView)
378 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
382 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
383 "nickname" => nickname
386 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
388 as_nickname: as_nickname
392 |> put_status(:forbidden)
396 defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
399 |> Map.merge(Map.take(params, ["to", "cc"]))
400 |> Map.put("attributedTo", user.ap_id())
401 |> Transmogrifier.fix_object()
403 ActivityPub.create(%{
406 context: object["context"],
408 additional: Map.take(params, ["cc"])
412 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
413 with %Object{} = object <- Object.normalize(params["object"]),
414 true <- user.is_moderator || user.ap_id == object.data["actor"],
415 {:ok, delete} <- ActivityPub.delete(object) do
418 _ -> {:error, dgettext("errors", "Can't delete object")}
422 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
423 with %Object{} = object <- Object.normalize(params["object"]),
424 {:ok, activity, _object} <- ActivityPub.like(user, object) do
427 _ -> {:error, dgettext("errors", "Can't like object")}
431 defp handle_user_activity(_, _) do
432 {:error, dgettext("errors", "Unhandled activity type")}
436 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
437 %{"nickname" => nickname} = params
444 |> Map.put("actor", actor)
445 |> Transmogrifier.fix_addressing()
447 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
449 |> put_status(:created)
450 |> put_resp_header("location", activity.data["id"])
451 |> json(activity.data)
455 |> put_status(:bad_request)
460 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
462 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
464 as_nickname: user.nickname
468 |> put_status(:forbidden)
472 defp errors(conn, {:error, :not_found}) do
474 |> put_status(:not_found)
475 |> json(dgettext("errors", "Not found"))
478 defp errors(conn, _e) do
480 |> put_status(:internal_server_error)
481 |> json(dgettext("errors", "error"))
484 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
485 with actor <- conn.params["actor"],
486 true <- is_binary(actor) do
487 Pleroma.Instances.set_reachable(actor)
493 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
494 {:ok, new_user} = User.ensure_keys_present(user)
497 if new_user != user and match?(%User{}, for_user) do
498 User.get_cached_by_nickname(for_user.nickname)
506 # TODO: Add support for "object" field
508 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
511 - (required) `file`: data of the media
512 - (optionnal) `description`: description of the media, intended for accessibility
515 - HTTP Code: 201 Created
516 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
518 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
519 with {:ok, object} <-
522 actor: User.ap_id(user),
523 description: Map.get(data, "description")
525 Logger.debug(inspect(object))
528 |> put_status(:created)