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.FederatingPlug
25 alias Pleroma.Web.Federator
29 action_fallback(:errors)
31 @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
33 plug(FederatingPlug when action in @federating_only_actions)
36 EnsureAuthenticatedPlug,
37 [unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions
40 # Note: :following and :followers must be served even without authentication (as via :api)
42 EnsureAuthenticatedPlug
43 when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
48 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
49 when action in [:activity, :object]
52 plug(:set_requester_reachable when action in [:inbox])
53 plug(:relay_active? when action in [:relay])
55 defp relay_active?(conn, _) do
56 if Pleroma.Config.get([:instance, :allow_relay]) do
60 |> render_error(:not_found, "not found")
65 def user(conn, %{"nickname" => nickname}) do
66 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
67 {:ok, user} <- User.ensure_keys_present(user) do
69 |> put_resp_content_type("application/activity+json")
71 |> render("user.json", %{user: user})
73 nil -> {:error, :not_found}
74 %{local: false} -> {:error, :not_found}
78 def object(conn, %{"uuid" => uuid}) do
79 with ap_id <- o_status_url(conn, :object, uuid),
80 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
81 {_, true} <- {:public?, Visibility.is_public?(object)} do
83 |> assign(:tracking_fun_data, object.id)
84 |> set_cache_ttl_for(object)
85 |> put_resp_content_type("application/activity+json")
86 |> put_view(ObjectView)
87 |> render("object.json", object: object)
94 def track_object_fetch(conn, nil), do: conn
96 def track_object_fetch(conn, object_id) do
97 with %{assigns: %{user: %User{id: user_id}}} <- conn do
98 Delivery.create(object_id, user_id)
104 def activity(conn, %{"uuid" => uuid}) do
105 with ap_id <- o_status_url(conn, :activity, uuid),
106 %Activity{} = activity <- Activity.normalize(ap_id),
107 {_, true} <- {:public?, Visibility.is_public?(activity)} do
109 |> maybe_set_tracking_data(activity)
110 |> set_cache_ttl_for(activity)
111 |> put_resp_content_type("application/activity+json")
112 |> put_view(ObjectView)
113 |> render("object.json", object: activity)
115 {:public?, false} -> {:error, :not_found}
116 nil -> {:error, :not_found}
120 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
121 object_id = Object.normalize(activity).id
122 assign(conn, :tracking_fun_data, object_id)
125 defp maybe_set_tracking_data(conn, _activity), do: conn
127 defp set_cache_ttl_for(conn, %Activity{object: object}) do
128 set_cache_ttl_for(conn, object)
131 defp set_cache_ttl_for(conn, entity) do
134 %Object{data: %{"type" => "Question"}} ->
135 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
138 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
144 assign(conn, :cache_ttl, ttl)
147 # GET /relay/following
148 def relay_following(conn, _params) do
149 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
151 |> put_resp_content_type("application/activity+json")
152 |> put_view(UserView)
153 |> render("following.json", %{user: Relay.get_actor()})
157 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
158 with %User{} = user <- User.get_cached_by_nickname(nickname),
159 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
160 {:show_follows, true} <-
161 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
162 {page, _} = Integer.parse(page)
165 |> put_resp_content_type("application/activity+json")
166 |> put_view(UserView)
167 |> render("following.json", %{user: user, page: page, for: for_user})
169 {:show_follows, _} ->
171 |> put_resp_content_type("application/activity+json")
172 |> send_resp(403, "")
176 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
177 with %User{} = user <- User.get_cached_by_nickname(nickname),
178 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
180 |> put_resp_content_type("application/activity+json")
181 |> put_view(UserView)
182 |> render("following.json", %{user: user, for: for_user})
186 # GET /relay/followers
187 def relay_followers(conn, _params) do
188 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
190 |> put_resp_content_type("application/activity+json")
191 |> put_view(UserView)
192 |> render("followers.json", %{user: Relay.get_actor()})
196 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
197 with %User{} = user <- User.get_cached_by_nickname(nickname),
198 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
199 {:show_followers, true} <-
200 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
201 {page, _} = Integer.parse(page)
204 |> put_resp_content_type("application/activity+json")
205 |> put_view(UserView)
206 |> render("followers.json", %{user: user, page: page, for: for_user})
208 {:show_followers, _} ->
210 |> put_resp_content_type("application/activity+json")
211 |> send_resp(403, "")
215 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
216 with %User{} = user <- User.get_cached_by_nickname(nickname),
217 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
219 |> put_resp_content_type("application/activity+json")
220 |> put_view(UserView)
221 |> render("followers.json", %{user: user, for: for_user})
226 %{assigns: %{user: for_user}} = conn,
227 %{"nickname" => nickname, "page" => page?} = params
229 when page? in [true, "true"] do
230 with %User{} = user <- User.get_cached_by_nickname(nickname),
231 {:ok, user} <- User.ensure_keys_present(user) do
233 if params["max_id"] do
234 ActivityPub.fetch_user_activities(user, for_user, %{
235 "max_id" => params["max_id"],
236 # This is a hack because postgres generates inefficient queries when filtering by
237 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
238 "include_poll_votes" => true,
242 ActivityPub.fetch_user_activities(user, for_user, %{
244 "include_poll_votes" => true
249 |> put_resp_content_type("application/activity+json")
250 |> put_view(UserView)
251 |> render("activity_collection_page.json", %{
252 activities: 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
356 if params["max_id"] do
357 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
358 "max_id" => params["max_id"],
362 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
366 |> put_resp_content_type("application/activity+json")
367 |> put_view(UserView)
368 |> render("activity_collection_page.json", %{
369 activities: activities,
370 iri: "#{user.ap_id}/inbox"
374 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
375 "nickname" => nickname
377 with {:ok, user} <- User.ensure_keys_present(user) do
379 |> put_resp_content_type("application/activity+json")
380 |> put_view(UserView)
381 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
385 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
386 "nickname" => nickname
389 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
391 as_nickname: as_nickname
395 |> put_status(:forbidden)
399 defp handle_user_activity(
401 %{"type" => "Create", "object" => %{"type" => "Note"}} = params
405 |> Map.merge(Map.take(params, ["to", "cc"]))
406 |> Map.put("attributedTo", user.ap_id())
407 |> Transmogrifier.fix_object()
409 ActivityPub.create(%{
412 context: object["context"],
414 additional: Map.take(params, ["cc"])
418 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
419 with %Object{} = object <- Object.normalize(params["object"]),
420 true <- user.is_moderator || user.ap_id == object.data["actor"],
421 {:ok, delete} <- ActivityPub.delete(object) do
424 _ -> {:error, dgettext("errors", "Can't delete object")}
428 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
429 with %Object{} = object <- Object.normalize(params["object"]),
430 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
431 {_, {:ok, %Activity{} = activity, _meta}} <-
433 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
436 _ -> {:error, dgettext("errors", "Can't like object")}
440 defp handle_user_activity(_, _) do
441 {:error, dgettext("errors", "Unhandled activity type")}
445 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
446 %{"nickname" => nickname} = params
453 |> Map.put("actor", actor)
454 |> Transmogrifier.fix_addressing()
456 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
458 |> put_status(:created)
459 |> put_resp_header("location", activity.data["id"])
460 |> json(activity.data)
464 |> put_status(:bad_request)
469 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
471 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
473 as_nickname: user.nickname
477 |> put_status(:forbidden)
481 defp errors(conn, {:error, :not_found}) do
483 |> put_status(:not_found)
484 |> json(dgettext("errors", "Not found"))
487 defp errors(conn, _e) do
489 |> put_status(:internal_server_error)
490 |> json(dgettext("errors", "error"))
493 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
494 with actor <- conn.params["actor"],
495 true <- is_binary(actor) do
496 Pleroma.Instances.set_reachable(actor)
502 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
503 {:ok, new_user} = User.ensure_keys_present(user)
506 if new_user != user and match?(%User{}, for_user) do
507 User.get_cached_by_nickname(for_user.nickname)
515 # TODO: Add support for "object" field
517 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
520 - (required) `file`: data of the media
521 - (optionnal) `description`: description of the media, intended for accessibility
524 - HTTP Code: 201 Created
525 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
527 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
528 with {:ok, object} <-
531 actor: User.ap_id(user),
532 description: Map.get(data, "description")
534 Logger.debug(inspect(object))
537 |> put_status(:created)