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 activity(conn, %{"uuid" => uuid}) do
86 with ap_id <- o_status_url(conn, :activity, uuid),
87 %Activity{} = activity <- Activity.normalize(ap_id),
88 {_, true} <- {:public?, Visibility.is_public?(activity)} do
90 |> maybe_set_tracking_data(activity)
91 |> set_cache_ttl_for(activity)
92 |> put_resp_content_type("application/activity+json")
93 |> put_view(ObjectView)
94 |> render("object.json", object: activity)
96 {:public?, false} -> {:error, :not_found}
97 nil -> {:error, :not_found}
101 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
102 object_id = Object.normalize(activity).id
103 assign(conn, :tracking_fun_data, object_id)
106 defp maybe_set_tracking_data(conn, _activity), do: conn
108 defp set_cache_ttl_for(conn, %Activity{object: object}) do
109 set_cache_ttl_for(conn, object)
112 defp set_cache_ttl_for(conn, entity) do
115 %Object{data: %{"type" => "Question"}} ->
116 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
119 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
125 assign(conn, :cache_ttl, ttl)
128 # GET /relay/following
129 def following(%{assigns: %{relay: true}} = conn, _params) do
131 |> put_resp_content_type("application/activity+json")
132 |> put_view(UserView)
133 |> render("following.json", %{user: Relay.get_actor()})
136 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
137 with %User{} = user <- User.get_cached_by_nickname(nickname),
138 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
139 {:show_follows, true} <-
140 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
141 {page, _} = Integer.parse(page)
144 |> put_resp_content_type("application/activity+json")
145 |> put_view(UserView)
146 |> render("following.json", %{user: user, page: page, for: for_user})
148 {:show_follows, _} ->
150 |> put_resp_content_type("application/activity+json")
151 |> send_resp(403, "")
155 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
156 with %User{} = user <- User.get_cached_by_nickname(nickname),
157 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
159 |> put_resp_content_type("application/activity+json")
160 |> put_view(UserView)
161 |> render("following.json", %{user: user, for: for_user})
165 # GET /relay/followers
166 def followers(%{assigns: %{relay: true}} = conn, _params) do
168 |> put_resp_content_type("application/activity+json")
169 |> put_view(UserView)
170 |> render("followers.json", %{user: Relay.get_actor()})
173 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) 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),
176 {:show_followers, true} <-
177 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
178 {page, _} = Integer.parse(page)
181 |> put_resp_content_type("application/activity+json")
182 |> put_view(UserView)
183 |> render("followers.json", %{user: user, page: page, for: for_user})
185 {:show_followers, _} ->
187 |> put_resp_content_type("application/activity+json")
188 |> send_resp(403, "")
192 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
193 with %User{} = user <- User.get_cached_by_nickname(nickname),
194 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
196 |> put_resp_content_type("application/activity+json")
197 |> put_view(UserView)
198 |> render("followers.json", %{user: user, for: for_user})
202 def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
203 when page? in [true, "true"] do
204 with %User{} = user <- User.get_cached_by_nickname(nickname),
205 {:ok, user} <- User.ensure_keys_present(user) do
207 if params["max_id"] do
208 ActivityPub.fetch_user_activities(user, nil, %{
209 "max_id" => params["max_id"],
210 # This is a hack because postgres generates inefficient queries when filtering by
211 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
212 "include_poll_votes" => true,
216 ActivityPub.fetch_user_activities(user, nil, %{
218 "include_poll_votes" => true
223 |> put_resp_content_type("application/activity+json")
224 |> put_view(UserView)
225 |> render("activity_collection_page.json", %{
226 activities: activities,
227 iri: "#{user.ap_id}/outbox"
232 def outbox(conn, %{"nickname" => nickname}) do
233 with %User{} = user <- User.get_cached_by_nickname(nickname),
234 {:ok, user} <- User.ensure_keys_present(user) do
236 |> put_resp_content_type("application/activity+json")
237 |> put_view(UserView)
238 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
242 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
243 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
244 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
245 true <- Utils.recipient_in_message(recipient, actor, params),
246 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
247 Federator.incoming_ap_doc(params)
252 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
253 Federator.incoming_ap_doc(params)
257 # only accept relayed Creates
258 def inbox(conn, %{"type" => "Create"} = params) do
260 "Signature missing or not from author, relayed Create message, fetching object from source"
263 Fetcher.fetch_object_from_id(params["object"]["id"])
268 def inbox(conn, params) do
269 headers = Enum.into(conn.req_headers, %{})
271 if String.contains?(headers["signature"], params["actor"]) do
273 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
276 Logger.info(inspect(conn.req_headers))
279 json(conn, dgettext("errors", "error"))
282 defp represent_service_actor(%User{} = user, conn) do
283 with {:ok, user} <- User.ensure_keys_present(user) do
285 |> put_resp_content_type("application/activity+json")
286 |> put_view(UserView)
287 |> render("user.json", %{user: user})
289 nil -> {:error, :not_found}
293 defp represent_service_actor(nil, _), do: {:error, :not_found}
295 def relay(conn, _params) do
297 |> represent_service_actor(conn)
300 def internal_fetch(conn, _params) do
301 InternalFetchActor.get_actor()
302 |> represent_service_actor(conn)
305 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
306 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
308 |> put_resp_content_type("application/activity+json")
309 |> put_view(UserView)
310 |> render("user.json", %{user: user})
313 def whoami(_conn, _params), do: {:error, :not_found}
316 %{assigns: %{user: %{nickname: nickname} = user}} = conn,
317 %{"nickname" => nickname, "page" => page?} = params
319 when page? in [true, "true"] do
321 if params["max_id"] do
322 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
323 "max_id" => params["max_id"],
327 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
331 |> put_resp_content_type("application/activity+json")
332 |> put_view(UserView)
333 |> render("activity_collection_page.json", %{
334 activities: activities,
335 iri: "#{user.ap_id}/inbox"
339 def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
340 "nickname" => nickname
342 with {:ok, user} <- User.ensure_keys_present(user) do
344 |> put_resp_content_type("application/activity+json")
345 |> put_view(UserView)
346 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
350 def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
351 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
354 |> put_status(:forbidden)
358 def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
359 "nickname" => nickname
362 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
364 as_nickname: as_nickname
368 |> put_status(:forbidden)
372 def handle_user_activity(user, %{"type" => "Create"} = params) do
375 |> Map.merge(Map.take(params, ["to", "cc"]))
376 |> Map.put("attributedTo", user.ap_id())
377 |> Transmogrifier.fix_object()
379 ActivityPub.create(%{
382 context: object["context"],
384 additional: Map.take(params, ["cc"])
388 def handle_user_activity(user, %{"type" => "Delete"} = params) do
389 with %Object{} = object <- Object.normalize(params["object"]),
390 true <- user.is_moderator || user.ap_id == object.data["actor"],
391 {:ok, delete} <- ActivityPub.delete(object) do
394 _ -> {:error, dgettext("errors", "Can't delete object")}
398 def handle_user_activity(user, %{"type" => "Like"} = params) do
399 with %Object{} = object <- Object.normalize(params["object"]),
400 {:ok, activity, _object} <- ActivityPub.like(user, object) do
403 _ -> {:error, dgettext("errors", "Can't like object")}
407 def handle_user_activity(_, _) do
408 {:error, dgettext("errors", "Unhandled activity type")}
412 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
413 %{"nickname" => nickname} = params
420 |> Map.put("actor", actor)
421 |> Transmogrifier.fix_addressing()
423 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
425 |> put_status(:created)
426 |> put_resp_header("location", activity.data["id"])
427 |> json(activity.data)
431 |> put_status(:bad_request)
436 def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
438 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
440 as_nickname: user.nickname
444 |> put_status(:forbidden)
448 def errors(conn, {:error, :not_found}) do
450 |> put_status(:not_found)
451 |> json(dgettext("errors", "Not found"))
454 def errors(conn, _e) do
456 |> put_status(:internal_server_error)
457 |> json(dgettext("errors", "error"))
460 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
461 with actor <- conn.params["actor"],
462 true <- is_binary(actor) do
463 Pleroma.Instances.set_reachable(actor)
469 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
470 {:ok, new_user} = User.ensure_keys_present(user)
473 if new_user != user and match?(%User{}, for_user) do
474 User.get_cached_by_nickname(for_user.nickname)
482 # TODO: Add support for "object" field
484 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
487 - (required) `file`: data of the media
488 - (optionnal) `description`: description of the media, intended for accessibility
491 - HTTP Code: 201 Created
492 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
494 def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
495 with {:ok, object} <-
498 actor: User.ap_id(user),
499 description: Map.get(data, "description")
501 Logger.debug(inspect(object))
504 |> put_status(:created)