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.Federator
25 action_fallback(:errors)
29 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
30 when action in [:activity, :object]
33 plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
34 plug(:set_requester_reachable when action in [:inbox])
35 plug(:relay_active? when action in [:relay])
37 def relay_active?(conn, _) do
38 if Pleroma.Config.get([:instance, :allow_relay]) do
42 |> render_error(:not_found, "not found")
47 def user(conn, %{"nickname" => nickname}) do
48 with %User{} = user <- User.get_cached_by_nickname(nickname),
49 {:ok, user} <- User.ensure_keys_present(user) do
51 |> put_resp_content_type("application/activity+json")
53 |> render("user.json", %{user: user})
55 nil -> {:error, :not_found}
59 def object(conn, %{"uuid" => uuid}) do
60 with ap_id <- o_status_url(conn, :object, uuid),
61 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
62 {_, true} <- {:public?, Visibility.is_public?(object)} do
64 |> assign(:tracking_fun_data, object.id)
65 |> set_cache_ttl_for(object)
66 |> put_resp_content_type("application/activity+json")
67 |> put_view(ObjectView)
68 |> render("object.json", object: object)
75 def track_object_fetch(conn, nil), do: conn
77 def track_object_fetch(conn, object_id) do
78 with %{assigns: %{user: %User{id: user_id}}} <- conn do
79 Delivery.create(object_id, user_id)
85 def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
86 with ap_id <- o_status_url(conn, :object, uuid),
87 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
88 {_, true} <- {:public?, Visibility.is_public?(object)},
89 likes <- Utils.get_object_likes(object) do
90 {page, _} = Integer.parse(page)
93 |> put_resp_content_type("application/activity+json")
94 |> put_view(ObjectView)
95 |> render("likes.json", %{ap_id: ap_id, likes: likes, page: page})
102 def object_likes(conn, %{"uuid" => uuid}) do
103 with ap_id <- o_status_url(conn, :object, uuid),
104 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
105 {_, true} <- {:public?, Visibility.is_public?(object)},
106 likes <- Utils.get_object_likes(object) do
108 |> put_resp_content_type("application/activity+json")
109 |> put_view(ObjectView)
110 |> render("likes.json", %{ap_id: ap_id, likes: likes})
117 def activity(conn, %{"uuid" => uuid}) do
118 with ap_id <- o_status_url(conn, :activity, uuid),
119 %Activity{} = activity <- Activity.normalize(ap_id),
120 {_, true} <- {:public?, Visibility.is_public?(activity)} do
122 |> maybe_set_tracking_data(activity)
123 |> set_cache_ttl_for(activity)
124 |> put_resp_content_type("application/activity+json")
125 |> put_view(ObjectView)
126 |> render("object.json", object: activity)
128 {:public?, false} -> {:error, :not_found}
129 nil -> {:error, :not_found}
133 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
134 object_id = Object.normalize(activity).id
135 assign(conn, :tracking_fun_data, object_id)
138 defp maybe_set_tracking_data(conn, _activity), do: conn
140 defp set_cache_ttl_for(conn, %Activity{object: object}) do
141 set_cache_ttl_for(conn, object)
144 defp set_cache_ttl_for(conn, entity) do
147 %Object{data: %{"type" => "Question"}} ->
148 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
151 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
157 assign(conn, :cache_ttl, ttl)
160 # GET /relay/following
161 def following(%{assigns: %{relay: true}} = conn, _params) do
163 |> put_resp_content_type("application/activity+json")
164 |> put_view(UserView)
165 |> render("following.json", %{user: Relay.get_actor()})
168 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) 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),
171 {:show_follows, true} <-
172 {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do
173 {page, _} = Integer.parse(page)
176 |> put_resp_content_type("application/activity+json")
177 |> put_view(UserView)
178 |> render("following.json", %{user: user, page: page, for: for_user})
180 {:show_follows, _} ->
182 |> put_resp_content_type("application/activity+json")
183 |> send_resp(403, "")
187 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
188 with %User{} = user <- User.get_cached_by_nickname(nickname),
189 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
191 |> put_resp_content_type("application/activity+json")
192 |> put_view(UserView)
193 |> render("following.json", %{user: user, for: for_user})
197 # GET /relay/followers
198 def followers(%{assigns: %{relay: true}} = conn, _params) do
200 |> put_resp_content_type("application/activity+json")
201 |> put_view(UserView)
202 |> render("followers.json", %{user: Relay.get_actor()})
205 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) 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),
208 {:show_followers, true} <-
209 {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do
210 {page, _} = Integer.parse(page)
213 |> put_resp_content_type("application/activity+json")
214 |> put_view(UserView)
215 |> render("followers.json", %{user: user, page: page, for: for_user})
217 {:show_followers, _} ->
219 |> put_resp_content_type("application/activity+json")
220 |> send_resp(403, "")
224 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
225 with %User{} = user <- User.get_cached_by_nickname(nickname),
226 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
228 |> put_resp_content_type("application/activity+json")
229 |> put_view(UserView)
230 |> render("followers.json", %{user: user, for: for_user})
234 def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
235 when page? in [true, "true"] do
236 with %User{} = user <- User.get_cached_by_nickname(nickname),
237 {:ok, user} <- User.ensure_keys_present(user) do
239 if params["max_id"] do
240 ActivityPub.fetch_user_activities(user, nil, %{
241 "max_id" => params["max_id"],
242 # This is a hack because postgres generates inefficient queries when filtering by
243 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
244 "include_poll_votes" => true,
248 ActivityPub.fetch_user_activities(user, nil, %{
250 "include_poll_votes" => true
255 |> put_resp_content_type("application/activity+json")
256 |> put_view(UserView)
257 |> render("activity_collection_page.json", %{
258 activities: activities,
259 iri: "#{user.ap_id}/outbox"
264 def outbox(conn, %{"nickname" => nickname}) do
265 with %User{} = user <- User.get_cached_by_nickname(nickname),
266 {:ok, user} <- User.ensure_keys_present(user) do
268 |> put_resp_content_type("application/activity+json")
269 |> put_view(UserView)
270 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
274 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
275 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
276 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
277 true <- Utils.recipient_in_message(recipient, actor, params),
278 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
279 Federator.incoming_ap_doc(params)
284 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
285 Federator.incoming_ap_doc(params)
289 # only accept relayed Creates
290 def inbox(conn, %{"type" => "Create"} = params) do
292 "Signature missing or not from author, relayed Create message, fetching object from source"
295 Fetcher.fetch_object_from_id(params["object"]["id"])
300 def inbox(conn, params) do
301 headers = Enum.into(conn.req_headers, %{})
303 if 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.info(inspect(conn.req_headers))
311 json(conn, dgettext("errors", "error"))
314 defp represent_service_actor(%User{} = user, conn) do
315 with {:ok, user} <- User.ensure_keys_present(user) do
317 |> put_resp_content_type("application/activity+json")
318 |> put_view(UserView)
319 |> render("user.json", %{user: user})
321 nil -> {:error, :not_found}
325 defp represent_service_actor(nil, _), do: {:error, :not_found}
327 def relay(conn, _params) do
329 |> represent_service_actor(conn)
332 def internal_fetch(conn, _params) do
333 InternalFetchActor.get_actor()
334 |> represent_service_actor(conn)
337 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
338 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
340 |> put_resp_content_type("application/activity+json")
341 |> put_view(UserView)
342 |> render("user.json", %{user: user})
345 def whoami(_conn, _params), do: {:error, :not_found}
348 %{assigns: %{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], %{
355 "max_id" => params["max_id"],
359 ActivityPub.fetch_activities([user.ap_id | user.following], %{"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: %{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: nil}} = conn, %{"nickname" => nickname}) do
383 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
386 |> put_status(:forbidden)
390 def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
391 "nickname" => nickname
394 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
396 as_nickname: as_nickname
400 |> put_status(:forbidden)
404 def handle_user_activity(user, %{"type" => "Create"} = params) do
407 |> Map.merge(Map.take(params, ["to", "cc"]))
408 |> Map.put("attributedTo", user.ap_id())
409 |> Transmogrifier.fix_object()
411 ActivityPub.create(%{
414 context: object["context"],
416 additional: Map.take(params, ["cc"])
420 def handle_user_activity(user, %{"type" => "Delete"} = params) do
421 with %Object{} = object <- Object.normalize(params["object"]),
422 true <- user.info.is_moderator || user.ap_id == object.data["actor"],
423 {:ok, delete} <- ActivityPub.delete(object) do
426 _ -> {:error, dgettext("errors", "Can't delete object")}
430 def handle_user_activity(user, %{"type" => "Like"} = params) do
431 with %Object{} = object <- Object.normalize(params["object"]),
432 {:ok, activity, _object} <- ActivityPub.like(user, object) do
435 _ -> {:error, dgettext("errors", "Can't like object")}
439 def handle_user_activity(_, _) do
440 {:error, dgettext("errors", "Unhandled activity type")}
444 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
445 %{"nickname" => nickname} = params
452 |> Map.put("actor", actor)
453 |> Transmogrifier.fix_addressing()
455 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
457 |> put_status(:created)
458 |> put_resp_header("location", activity.data["id"])
459 |> json(activity.data)
463 |> put_status(:bad_request)
468 def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
470 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
472 as_nickname: user.nickname
476 |> put_status(:forbidden)
480 def errors(conn, {:error, :not_found}) do
482 |> put_status(:not_found)
483 |> json(dgettext("errors", "Not found"))
486 def errors(conn, _e) do
488 |> put_status(:internal_server_error)
489 |> json(dgettext("errors", "error"))
492 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
493 with actor <- conn.params["actor"],
494 true <- is_binary(actor) do
495 Pleroma.Instances.set_reachable(actor)
501 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
502 {:ok, new_user} = User.ensure_keys_present(user)
505 if new_user != user and match?(%User{}, for_user) do
506 User.get_cached_by_nickname(for_user.nickname)
514 # TODO: Add support for "object" field
516 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
519 - (required) `file`: data of the media
520 - (optionnal) `description`: description of the media, intended for accessibility
523 - HTTP Code: 201 Created
524 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
526 def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
527 with {:ok, object} <-
530 actor: User.ap_id(user),
531 description: Map.get(data, "description")
533 Logger.debug(inspect(object))
536 |> put_status(:created)