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
41 EnsureAuthenticatedPlug
42 when action in [:read_inbox, :update_outbox, :whoami, :upload_media, :following, :followers]
47 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
48 when action in [:activity, :object]
51 plug(:set_requester_reachable when action in [:inbox])
52 plug(:relay_active? when action in [:relay])
54 defp relay_active?(conn, _) do
55 if Pleroma.Config.get([:instance, :allow_relay]) do
59 |> render_error(:not_found, "not found")
64 def user(conn, %{"nickname" => nickname}) do
65 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
66 {:ok, user} <- User.ensure_keys_present(user) do
68 |> put_resp_content_type("application/activity+json")
70 |> render("user.json", %{user: user})
72 nil -> {:error, :not_found}
73 %{local: false} -> {:error, :not_found}
77 def object(conn, %{"uuid" => uuid}) do
78 with ap_id <- o_status_url(conn, :object, uuid),
79 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
80 {_, true} <- {:public?, Visibility.is_public?(object)} do
82 |> assign(:tracking_fun_data, object.id)
83 |> set_cache_ttl_for(object)
84 |> put_resp_content_type("application/activity+json")
85 |> put_view(ObjectView)
86 |> render("object.json", object: object)
93 def track_object_fetch(conn, nil), do: conn
95 def track_object_fetch(conn, object_id) do
96 with %{assigns: %{user: %User{id: user_id}}} <- conn do
97 Delivery.create(object_id, user_id)
103 def activity(conn, %{"uuid" => uuid}) do
104 with ap_id <- o_status_url(conn, :activity, uuid),
105 %Activity{} = activity <- Activity.normalize(ap_id),
106 {_, true} <- {:public?, Visibility.is_public?(activity)} do
108 |> maybe_set_tracking_data(activity)
109 |> set_cache_ttl_for(activity)
110 |> put_resp_content_type("application/activity+json")
111 |> put_view(ObjectView)
112 |> render("object.json", object: activity)
114 {:public?, false} -> {:error, :not_found}
115 nil -> {:error, :not_found}
119 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
120 object_id = Object.normalize(activity).id
121 assign(conn, :tracking_fun_data, object_id)
124 defp maybe_set_tracking_data(conn, _activity), do: conn
126 defp set_cache_ttl_for(conn, %Activity{object: object}) do
127 set_cache_ttl_for(conn, object)
130 defp set_cache_ttl_for(conn, entity) do
133 %Object{data: %{"type" => "Question"}} ->
134 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
137 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
143 assign(conn, :cache_ttl, ttl)
146 # GET /relay/following
147 def relay_following(conn, _params) do
148 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
150 |> put_resp_content_type("application/activity+json")
151 |> put_view(UserView)
152 |> render("following.json", %{user: Relay.get_actor()})
156 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
157 with %User{} = user <- User.get_cached_by_nickname(nickname),
158 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
159 {:show_follows, true} <-
160 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
161 {page, _} = Integer.parse(page)
164 |> put_resp_content_type("application/activity+json")
165 |> put_view(UserView)
166 |> render("following.json", %{user: user, page: page, for: for_user})
168 {:show_follows, _} ->
170 |> put_resp_content_type("application/activity+json")
171 |> send_resp(403, "")
175 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
176 with %User{} = user <- User.get_cached_by_nickname(nickname),
177 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
179 |> put_resp_content_type("application/activity+json")
180 |> put_view(UserView)
181 |> render("following.json", %{user: user, for: for_user})
185 # GET /relay/followers
186 def relay_followers(conn, _params) do
187 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
189 |> put_resp_content_type("application/activity+json")
190 |> put_view(UserView)
191 |> render("followers.json", %{user: Relay.get_actor()})
195 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
196 with %User{} = user <- User.get_cached_by_nickname(nickname),
197 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
198 {:show_followers, true} <-
199 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
200 {page, _} = Integer.parse(page)
203 |> put_resp_content_type("application/activity+json")
204 |> put_view(UserView)
205 |> render("followers.json", %{user: user, page: page, for: for_user})
207 {:show_followers, _} ->
209 |> put_resp_content_type("application/activity+json")
210 |> send_resp(403, "")
214 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
215 with %User{} = user <- User.get_cached_by_nickname(nickname),
216 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
218 |> put_resp_content_type("application/activity+json")
219 |> put_view(UserView)
220 |> render("followers.json", %{user: user, for: for_user})
225 %{assigns: %{user: for_user}} = conn,
226 %{"nickname" => nickname, "page" => page?} = params
228 when page? in [true, "true"] do
229 with %User{} = user <- User.get_cached_by_nickname(nickname),
230 {:ok, user} <- User.ensure_keys_present(user) do
232 if params["max_id"] do
233 ActivityPub.fetch_user_activities(user, for_user, %{
234 "max_id" => params["max_id"],
235 # This is a hack because postgres generates inefficient queries when filtering by
236 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
237 "include_poll_votes" => true,
241 ActivityPub.fetch_user_activities(user, for_user, %{
243 "include_poll_votes" => true
248 |> put_resp_content_type("application/activity+json")
249 |> put_view(UserView)
250 |> render("activity_collection_page.json", %{
251 activities: activities,
252 iri: "#{user.ap_id}/outbox"
257 def outbox(conn, %{"nickname" => nickname}) do
258 with %User{} = user <- User.get_cached_by_nickname(nickname),
259 {:ok, user} <- User.ensure_keys_present(user) do
261 |> put_resp_content_type("application/activity+json")
262 |> put_view(UserView)
263 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
267 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
268 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
269 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
270 true <- Utils.recipient_in_message(recipient, actor, params),
271 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
272 Federator.incoming_ap_doc(params)
277 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
278 Federator.incoming_ap_doc(params)
282 # POST /relay/inbox -or- POST /internal/fetch/inbox
283 def inbox(conn, params) do
284 if params["type"] == "Create" && FederatingPlug.federating?() do
285 post_inbox_relayed_create(conn, params)
287 post_inbox_fallback(conn, params)
291 defp post_inbox_relayed_create(conn, params) do
293 "Signature missing or not from author, relayed Create message, fetching object from source"
296 Fetcher.fetch_object_from_id(params["object"]["id"])
301 defp post_inbox_fallback(conn, params) do
302 headers = Enum.into(conn.req_headers, %{})
304 if headers["signature"] && params["actor"] &&
305 String.contains?(headers["signature"], params["actor"]) do
307 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
310 Logger.debug(inspect(conn.req_headers))
314 |> put_status(:bad_request)
315 |> json(dgettext("errors", "error"))
318 defp represent_service_actor(%User{} = user, conn) do
319 with {:ok, user} <- User.ensure_keys_present(user) do
321 |> put_resp_content_type("application/activity+json")
322 |> put_view(UserView)
323 |> render("user.json", %{user: user})
325 nil -> {:error, :not_found}
329 defp represent_service_actor(nil, _), do: {:error, :not_found}
331 def relay(conn, _params) do
333 |> represent_service_actor(conn)
336 def internal_fetch(conn, _params) do
337 InternalFetchActor.get_actor()
338 |> represent_service_actor(conn)
341 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
342 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
344 |> put_resp_content_type("application/activity+json")
345 |> put_view(UserView)
346 |> render("user.json", %{user: user})
350 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
351 %{"nickname" => nickname, "page" => page?} = params
353 when page? in [true, "true"] do
355 if params["max_id"] do
356 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
357 "max_id" => params["max_id"],
361 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
365 |> put_resp_content_type("application/activity+json")
366 |> put_view(UserView)
367 |> render("activity_collection_page.json", %{
368 activities: activities,
369 iri: "#{user.ap_id}/inbox"
373 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
374 "nickname" => nickname
376 with {:ok, user} <- User.ensure_keys_present(user) do
378 |> put_resp_content_type("application/activity+json")
379 |> put_view(UserView)
380 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
384 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
385 "nickname" => nickname
388 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
390 as_nickname: as_nickname
394 |> put_status(:forbidden)
398 defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
401 |> Map.merge(Map.take(params, ["to", "cc"]))
402 |> Map.put("attributedTo", user.ap_id())
403 |> Transmogrifier.fix_object()
405 ActivityPub.create(%{
408 context: object["context"],
410 additional: Map.take(params, ["cc"])
414 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
415 with %Object{} = object <- Object.normalize(params["object"]),
416 true <- user.is_moderator || user.ap_id == object.data["actor"],
417 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
418 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
421 _ -> {:error, dgettext("errors", "Can't delete object")}
425 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
426 with %Object{} = object <- Object.normalize(params["object"]),
427 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
428 {_, {:ok, %Activity{} = activity, _meta}} <-
430 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
433 _ -> {:error, dgettext("errors", "Can't like object")}
437 defp handle_user_activity(_, _) do
438 {:error, dgettext("errors", "Unhandled activity type")}
442 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
443 %{"nickname" => nickname} = params
450 |> Map.put("actor", actor)
451 |> Transmogrifier.fix_addressing()
453 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
455 |> put_status(:created)
456 |> put_resp_header("location", activity.data["id"])
457 |> json(activity.data)
461 |> put_status(:bad_request)
466 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
468 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
470 as_nickname: user.nickname
474 |> put_status(:forbidden)
478 defp errors(conn, {:error, :not_found}) do
480 |> put_status(:not_found)
481 |> json(dgettext("errors", "Not found"))
484 defp errors(conn, _e) do
486 |> put_status(:internal_server_error)
487 |> json(dgettext("errors", "error"))
490 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
491 with actor <- conn.params["actor"],
492 true <- is_binary(actor) do
493 Pleroma.Instances.set_reachable(actor)
499 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
500 {:ok, new_user} = User.ensure_keys_present(user)
503 if new_user != user and match?(%User{}, for_user) do
504 User.get_cached_by_nickname(for_user.nickname)
512 # TODO: Add support for "object" field
514 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
517 - (required) `file`: data of the media
518 - (optionnal) `description`: description of the media, intended for accessibility
521 - HTTP Code: 201 Created
522 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
524 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
525 with {:ok, object} <-
528 actor: User.ap_id(user),
529 description: Map.get(data, "description")
531 Logger.debug(inspect(object))
534 |> put_status(:created)