1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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 [
38 plug(FederatingPlug when action not in @client_to_server_actions)
42 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
43 when action in [:activity, :object]
46 plug(:set_requester_reachable when action in [:inbox])
47 plug(:relay_active? when action in [:relay])
49 def relay_active?(conn, _) do
50 if Pleroma.Config.get([:instance, :allow_relay]) do
54 |> render_error(:not_found, "not found")
59 def user(conn, %{"nickname" => nickname}) do
60 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
61 {:ok, user} <- User.ensure_keys_present(user) do
63 |> put_resp_content_type("application/activity+json")
65 |> render("user.json", %{user: user})
67 nil -> {:error, :not_found}
68 %{local: false} -> {:error, :not_found}
72 def object(conn, %{"uuid" => uuid}) do
73 with ap_id <- o_status_url(conn, :object, uuid),
74 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
75 {_, true} <- {:public?, Visibility.is_public?(object)} do
77 |> assign(:tracking_fun_data, object.id)
78 |> set_cache_ttl_for(object)
79 |> put_resp_content_type("application/activity+json")
80 |> put_view(ObjectView)
81 |> render("object.json", object: object)
88 def track_object_fetch(conn, nil), do: conn
90 def track_object_fetch(conn, object_id) do
91 with %{assigns: %{user: %User{id: user_id}}} <- conn do
92 Delivery.create(object_id, user_id)
98 def activity(conn, %{"uuid" => uuid}) do
99 with ap_id <- o_status_url(conn, :activity, uuid),
100 %Activity{} = activity <- Activity.normalize(ap_id),
101 {_, true} <- {:public?, Visibility.is_public?(activity)} do
103 |> maybe_set_tracking_data(activity)
104 |> set_cache_ttl_for(activity)
105 |> put_resp_content_type("application/activity+json")
106 |> put_view(ObjectView)
107 |> render("object.json", object: activity)
109 {:public?, false} -> {:error, :not_found}
110 nil -> {:error, :not_found}
114 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
115 object_id = Object.normalize(activity).id
116 assign(conn, :tracking_fun_data, object_id)
119 defp maybe_set_tracking_data(conn, _activity), do: conn
121 defp set_cache_ttl_for(conn, %Activity{object: object}) do
122 set_cache_ttl_for(conn, object)
125 defp set_cache_ttl_for(conn, entity) do
128 %Object{data: %{"type" => "Question"}} ->
129 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
132 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
138 assign(conn, :cache_ttl, ttl)
141 # GET /relay/following
142 def following(%{assigns: %{relay: true}} = conn, _params) do
144 |> put_resp_content_type("application/activity+json")
145 |> put_view(UserView)
146 |> render("following.json", %{user: Relay.get_actor()})
149 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
150 with %User{} = user <- User.get_cached_by_nickname(nickname),
151 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
152 {:show_follows, true} <-
153 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
154 {page, _} = Integer.parse(page)
157 |> put_resp_content_type("application/activity+json")
158 |> put_view(UserView)
159 |> render("following.json", %{user: user, page: page, for: for_user})
161 {:show_follows, _} ->
163 |> put_resp_content_type("application/activity+json")
164 |> send_resp(403, "")
168 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
169 with %User{} = user <- User.get_cached_by_nickname(nickname),
170 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
172 |> put_resp_content_type("application/activity+json")
173 |> put_view(UserView)
174 |> render("following.json", %{user: user, for: for_user})
178 # GET /relay/followers
179 def followers(%{assigns: %{relay: true}} = conn, _params) do
181 |> put_resp_content_type("application/activity+json")
182 |> put_view(UserView)
183 |> render("followers.json", %{user: Relay.get_actor()})
186 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
187 with %User{} = user <- User.get_cached_by_nickname(nickname),
188 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
189 {:show_followers, true} <-
190 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
191 {page, _} = Integer.parse(page)
194 |> put_resp_content_type("application/activity+json")
195 |> put_view(UserView)
196 |> render("followers.json", %{user: user, page: page, for: for_user})
198 {:show_followers, _} ->
200 |> put_resp_content_type("application/activity+json")
201 |> send_resp(403, "")
205 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
206 with %User{} = user <- User.get_cached_by_nickname(nickname),
207 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
209 |> put_resp_content_type("application/activity+json")
210 |> put_view(UserView)
211 |> render("followers.json", %{user: user, for: for_user})
215 def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
216 when page? in [true, "true"] do
217 with %User{} = user <- User.get_cached_by_nickname(nickname),
218 {:ok, user} <- User.ensure_keys_present(user) do
220 if params["max_id"] do
221 ActivityPub.fetch_user_activities(user, nil, %{
222 "max_id" => params["max_id"],
223 # This is a hack because postgres generates inefficient queries when filtering by
224 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
225 "include_poll_votes" => true,
229 ActivityPub.fetch_user_activities(user, nil, %{
231 "include_poll_votes" => true
236 |> put_resp_content_type("application/activity+json")
237 |> put_view(UserView)
238 |> render("activity_collection_page.json", %{
239 activities: activities,
240 iri: "#{user.ap_id}/outbox"
245 def outbox(conn, %{"nickname" => nickname}) do
246 with %User{} = user <- User.get_cached_by_nickname(nickname),
247 {:ok, user} <- User.ensure_keys_present(user) do
249 |> put_resp_content_type("application/activity+json")
250 |> put_view(UserView)
251 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
255 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
256 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
257 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
258 true <- Utils.recipient_in_message(recipient, actor, params),
259 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
260 Federator.incoming_ap_doc(params)
265 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
266 Federator.incoming_ap_doc(params)
270 # POST /relay/inbox -or- POST /internal/fetch/inbox
271 def inbox(conn, params) do
272 if params["type"] == "Create" && FederatingPlug.federating?() do
273 post_inbox_relayed_create(conn, params)
275 post_inbox_fallback(conn, params)
279 defp post_inbox_relayed_create(conn, params) do
281 "Signature missing or not from author, relayed Create message, fetching object from source"
284 Fetcher.fetch_object_from_id(params["object"]["id"])
289 defp post_inbox_fallback(conn, params) do
290 headers = Enum.into(conn.req_headers, %{})
292 if String.contains?(headers["signature"], params["actor"]) do
294 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
297 Logger.debug(inspect(conn.req_headers))
300 json(conn, dgettext("errors", "error"))
303 defp represent_service_actor(%User{} = user, conn) do
304 with {:ok, user} <- User.ensure_keys_present(user) do
306 |> put_resp_content_type("application/activity+json")
307 |> put_view(UserView)
308 |> render("user.json", %{user: user})
310 nil -> {:error, :not_found}
314 defp represent_service_actor(nil, _), do: {:error, :not_found}
316 def relay(conn, _params) do
318 |> represent_service_actor(conn)
321 def internal_fetch(conn, _params) do
322 InternalFetchActor.get_actor()
323 |> represent_service_actor(conn)
326 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
327 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
329 |> put_resp_content_type("application/activity+json")
330 |> put_view(UserView)
331 |> render("user.json", %{user: user})
334 def whoami(_conn, _params), do: {:error, :not_found}
337 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
338 %{"nickname" => nickname, "page" => page?} = params
340 when page? in [true, "true"] do
342 if params["max_id"] do
343 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
344 "max_id" => params["max_id"],
348 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
352 |> put_resp_content_type("application/activity+json")
353 |> put_view(UserView)
354 |> render("activity_collection_page.json", %{
355 activities: activities,
356 iri: "#{user.ap_id}/inbox"
360 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
361 "nickname" => nickname
363 with {:ok, user} <- User.ensure_keys_present(user) do
365 |> put_resp_content_type("application/activity+json")
366 |> put_view(UserView)
367 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
371 def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
372 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
375 |> put_status(:forbidden)
379 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
380 "nickname" => nickname
383 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
385 as_nickname: as_nickname
389 |> put_status(:forbidden)
393 def handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
396 |> Map.merge(Map.take(params, ["to", "cc"]))
397 |> Map.put("attributedTo", user.ap_id())
398 |> Transmogrifier.fix_object()
400 ActivityPub.create(%{
403 context: object["context"],
405 additional: Map.take(params, ["cc"])
409 def handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
410 with %Object{} = object <- Object.normalize(params["object"]),
411 true <- user.is_moderator || user.ap_id == object.data["actor"],
412 {:ok, delete} <- ActivityPub.delete(object) do
415 _ -> {:error, dgettext("errors", "Can't delete object")}
419 def handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
420 with %Object{} = object <- Object.normalize(params["object"]),
421 {:ok, activity, _object} <- ActivityPub.like(user, object) do
424 _ -> {:error, dgettext("errors", "Can't like object")}
428 def handle_user_activity(_, _) do
429 {:error, dgettext("errors", "Unhandled activity type")}
433 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
434 %{"nickname" => nickname} = params
441 |> Map.put("actor", actor)
442 |> Transmogrifier.fix_addressing()
444 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
446 |> put_status(:created)
447 |> put_resp_header("location", activity.data["id"])
448 |> json(activity.data)
452 |> put_status(:bad_request)
457 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
459 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
461 as_nickname: user.nickname
465 |> put_status(:forbidden)
469 def errors(conn, {:error, :not_found}) do
471 |> put_status(:not_found)
472 |> json(dgettext("errors", "Not found"))
475 def errors(conn, _e) do
477 |> put_status(:internal_server_error)
478 |> json(dgettext("errors", "error"))
481 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
482 with actor <- conn.params["actor"],
483 true <- is_binary(actor) do
484 Pleroma.Instances.set_reachable(actor)
490 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
491 {:ok, new_user} = User.ensure_keys_present(user)
494 if new_user != user and match?(%User{}, for_user) do
495 User.get_cached_by_nickname(for_user.nickname)
503 # TODO: Add support for "object" field
505 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
508 - (required) `file`: data of the media
509 - (optionnal) `description`: description of the media, intended for accessibility
512 - HTTP Code: 201 Created
513 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
515 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
516 with {:ok, object} <-
519 actor: User.ap_id(user),
520 description: Map.get(data, "description")
522 Logger.debug(inspect(object))
525 |> put_status(:created)