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
13 alias Pleroma.Web.ActivityPub.ActivityPub
14 alias Pleroma.Web.ActivityPub.InternalFetchActor
15 alias Pleroma.Web.ActivityPub.ObjectView
16 alias Pleroma.Web.ActivityPub.Relay
17 alias Pleroma.Web.ActivityPub.Transmogrifier
18 alias Pleroma.Web.ActivityPub.UserView
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.ActivityPub.Visibility
21 alias Pleroma.Web.FederatingPlug
22 alias Pleroma.Web.Federator
26 action_fallback(:errors)
28 # Note: some of the following actions (like :update_inbox) may be server-to-server as well
29 @client_to_server_actions [
39 plug(FederatingPlug when action not in @client_to_server_actions)
43 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
44 when action in [:activity, :object]
47 plug(:set_requester_reachable when action in [:inbox])
48 plug(:relay_active? when action in [:relay])
50 def relay_active?(conn, _) do
51 if Pleroma.Config.get([:instance, :allow_relay]) do
55 |> render_error(:not_found, "not found")
60 def user(conn, %{"nickname" => nickname}) do
61 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
62 {:ok, user} <- User.ensure_keys_present(user) do
64 |> put_resp_content_type("application/activity+json")
66 |> render("user.json", %{user: user})
68 nil -> {:error, :not_found}
69 %{local: false} -> {:error, :not_found}
73 def object(conn, %{"uuid" => uuid}) do
74 with ap_id <- o_status_url(conn, :object, uuid),
75 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
76 {_, true} <- {:public?, Visibility.is_public?(object)} do
78 |> assign(:tracking_fun_data, object.id)
79 |> set_cache_ttl_for(object)
80 |> put_resp_content_type("application/activity+json")
81 |> put_view(ObjectView)
82 |> render("object.json", object: object)
89 def track_object_fetch(conn, nil), do: conn
91 def track_object_fetch(conn, object_id) do
92 with %{assigns: %{user: %User{id: user_id}}} <- conn do
93 Delivery.create(object_id, user_id)
99 def activity(conn, %{"uuid" => uuid}) do
100 with ap_id <- o_status_url(conn, :activity, uuid),
101 %Activity{} = activity <- Activity.normalize(ap_id),
102 {_, true} <- {:public?, Visibility.is_public?(activity)} do
104 |> maybe_set_tracking_data(activity)
105 |> set_cache_ttl_for(activity)
106 |> put_resp_content_type("application/activity+json")
107 |> put_view(ObjectView)
108 |> render("object.json", object: activity)
110 {:public?, false} -> {:error, :not_found}
111 nil -> {:error, :not_found}
115 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
116 object_id = Object.normalize(activity).id
117 assign(conn, :tracking_fun_data, object_id)
120 defp maybe_set_tracking_data(conn, _activity), do: conn
122 defp set_cache_ttl_for(conn, %Activity{object: object}) do
123 set_cache_ttl_for(conn, object)
126 defp set_cache_ttl_for(conn, entity) do
129 %Object{data: %{"type" => "Question"}} ->
130 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
133 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
139 assign(conn, :cache_ttl, ttl)
142 # GET /relay/following
143 def following(%{assigns: %{relay: true}} = conn, _params) do
144 if FederatingPlug.federating?() do
146 |> put_resp_content_type("application/activity+json")
147 |> put_view(UserView)
148 |> render("following.json", %{user: Relay.get_actor()})
150 FederatingPlug.fail(conn)
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 followers(%{assigns: %{relay: true}} = conn, _params) do
185 if FederatingPlug.federating?() do
187 |> put_resp_content_type("application/activity+json")
188 |> put_view(UserView)
189 |> render("followers.json", %{user: Relay.get_actor()})
191 FederatingPlug.fail(conn)
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})
224 def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
225 when page? in [true, "true"] do
226 with %User{} = user <- User.get_cached_by_nickname(nickname),
227 {:ok, user} <- User.ensure_keys_present(user) do
229 if params["max_id"] do
230 ActivityPub.fetch_user_activities(user, nil, %{
231 "max_id" => params["max_id"],
232 # This is a hack because postgres generates inefficient queries when filtering by
233 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
234 "include_poll_votes" => true,
238 ActivityPub.fetch_user_activities(user, nil, %{
240 "include_poll_votes" => true
245 |> put_resp_content_type("application/activity+json")
246 |> put_view(UserView)
247 |> render("activity_collection_page.json", %{
248 activities: activities,
249 iri: "#{user.ap_id}/outbox"
254 def outbox(conn, %{"nickname" => nickname}) do
255 with %User{} = user <- User.get_cached_by_nickname(nickname),
256 {:ok, user} <- User.ensure_keys_present(user) do
258 |> put_resp_content_type("application/activity+json")
259 |> put_view(UserView)
260 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
264 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
265 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
266 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
267 true <- Utils.recipient_in_message(recipient, actor, params),
268 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
269 Federator.incoming_ap_doc(params)
274 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
275 Federator.incoming_ap_doc(params)
279 # POST /relay/inbox -or- POST /internal/fetch/inbox
280 def inbox(conn, params) do
281 if params["type"] == "Create" && FederatingPlug.federating?() do
282 post_inbox_relayed_create(conn, params)
284 post_inbox_fallback(conn, params)
288 defp post_inbox_relayed_create(conn, params) do
290 "Signature missing or not from author, relayed Create message, fetching object from source"
293 Fetcher.fetch_object_from_id(params["object"]["id"])
298 defp post_inbox_fallback(conn, params) do
299 headers = Enum.into(conn.req_headers, %{})
301 if String.contains?(headers["signature"], params["actor"]) do
303 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
306 Logger.debug(inspect(conn.req_headers))
309 json(conn, dgettext("errors", "error"))
312 defp represent_service_actor(%User{} = user, conn) do
313 with {:ok, user} <- User.ensure_keys_present(user) do
315 |> put_resp_content_type("application/activity+json")
316 |> put_view(UserView)
317 |> render("user.json", %{user: user})
319 nil -> {:error, :not_found}
323 defp represent_service_actor(nil, _), do: {:error, :not_found}
325 def relay(conn, _params) do
327 |> represent_service_actor(conn)
330 def internal_fetch(conn, _params) do
331 InternalFetchActor.get_actor()
332 |> represent_service_actor(conn)
335 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
336 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
338 |> put_resp_content_type("application/activity+json")
339 |> put_view(UserView)
340 |> render("user.json", %{user: user})
343 def whoami(_conn, _params), do: {:error, :not_found}
346 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
347 %{"nickname" => nickname, "page" => page?} = params
349 when page? in [true, "true"] do
351 if params["max_id"] do
352 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
353 "max_id" => params["max_id"],
357 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
361 |> put_resp_content_type("application/activity+json")
362 |> put_view(UserView)
363 |> render("activity_collection_page.json", %{
364 activities: activities,
365 iri: "#{user.ap_id}/inbox"
369 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
370 "nickname" => nickname
372 with {:ok, user} <- User.ensure_keys_present(user) do
374 |> put_resp_content_type("application/activity+json")
375 |> put_view(UserView)
376 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
380 def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
381 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
384 |> put_status(:forbidden)
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 def handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
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 def 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 def handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
429 with %Object{} = object <- Object.normalize(params["object"]),
430 {:ok, activity, _object} <- ActivityPub.like(user, object) do
433 _ -> {:error, dgettext("errors", "Can't like object")}
437 def 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 def errors(conn, {:error, :not_found}) do
480 |> put_status(:not_found)
481 |> json(dgettext("errors", "Not found"))
484 def 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)