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]
34 Pleroma.Plugs.OAuthScopesPlug,
35 %{scopes: ["read:accounts"]} when action in [:followers, :following]
38 plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
39 plug(:set_requester_reachable when action in [:inbox])
40 plug(:relay_active? when action in [:relay])
42 def relay_active?(conn, _) do
43 if Pleroma.Config.get([:instance, :allow_relay]) do
47 |> render_error(:not_found, "not found")
52 def user(conn, %{"nickname" => nickname}) do
53 with %User{} = user <- User.get_cached_by_nickname(nickname),
54 {:ok, user} <- User.ensure_keys_present(user) do
56 |> put_resp_content_type("application/activity+json")
58 |> render("user.json", %{user: user})
60 nil -> {:error, :not_found}
64 def object(conn, %{"uuid" => uuid}) do
65 with ap_id <- o_status_url(conn, :object, uuid),
66 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
67 {_, true} <- {:public?, Visibility.is_public?(object)} do
69 |> assign(:tracking_fun_data, object.id)
70 |> set_cache_ttl_for(object)
71 |> put_resp_content_type("application/activity+json")
72 |> put_view(ObjectView)
73 |> render("object.json", object: object)
80 def track_object_fetch(conn, nil), do: conn
82 def track_object_fetch(conn, object_id) do
83 with %{assigns: %{user: %User{id: user_id}}} <- conn do
84 Delivery.create(object_id, user_id)
90 def activity(conn, %{"uuid" => uuid}) do
91 with ap_id <- o_status_url(conn, :activity, uuid),
92 %Activity{} = activity <- Activity.normalize(ap_id),
93 {_, true} <- {:public?, Visibility.is_public?(activity)} do
95 |> maybe_set_tracking_data(activity)
96 |> set_cache_ttl_for(activity)
97 |> put_resp_content_type("application/activity+json")
98 |> put_view(ObjectView)
99 |> render("object.json", object: activity)
101 {:public?, false} -> {:error, :not_found}
102 nil -> {:error, :not_found}
106 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
107 object_id = Object.normalize(activity).id
108 assign(conn, :tracking_fun_data, object_id)
111 defp maybe_set_tracking_data(conn, _activity), do: conn
113 defp set_cache_ttl_for(conn, %Activity{object: object}) do
114 set_cache_ttl_for(conn, object)
117 defp set_cache_ttl_for(conn, entity) do
120 %Object{data: %{"type" => "Question"}} ->
121 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
124 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
130 assign(conn, :cache_ttl, ttl)
133 # GET /relay/following
134 def following(%{assigns: %{relay: true}} = conn, _params) do
136 |> put_resp_content_type("application/activity+json")
137 |> put_view(UserView)
138 |> render("following.json", %{user: Relay.get_actor()})
141 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
142 with %User{} = user <- User.get_cached_by_nickname(nickname),
143 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
144 {:show_follows, true} <-
145 {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do
146 {page, _} = Integer.parse(page)
149 |> put_resp_content_type("application/activity+json")
150 |> put_view(UserView)
151 |> render("following.json", %{user: user, page: page, for: for_user})
153 {:show_follows, _} ->
155 |> put_resp_content_type("application/activity+json")
156 |> send_resp(403, "")
160 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
161 with %User{} = user <- User.get_cached_by_nickname(nickname),
162 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
164 |> put_resp_content_type("application/activity+json")
165 |> put_view(UserView)
166 |> render("following.json", %{user: user, for: for_user})
170 # GET /relay/followers
171 def followers(%{assigns: %{relay: true}} = conn, _params) do
173 |> put_resp_content_type("application/activity+json")
174 |> put_view(UserView)
175 |> render("followers.json", %{user: Relay.get_actor()})
178 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
179 with %User{} = user <- User.get_cached_by_nickname(nickname),
180 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
181 {:show_followers, true} <-
182 {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do
183 {page, _} = Integer.parse(page)
186 |> put_resp_content_type("application/activity+json")
187 |> put_view(UserView)
188 |> render("followers.json", %{user: user, page: page, for: for_user})
190 {:show_followers, _} ->
192 |> put_resp_content_type("application/activity+json")
193 |> send_resp(403, "")
197 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
198 with %User{} = user <- User.get_cached_by_nickname(nickname),
199 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
201 |> put_resp_content_type("application/activity+json")
202 |> put_view(UserView)
203 |> render("followers.json", %{user: user, for: for_user})
207 def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
208 when page? in [true, "true"] do
209 with %User{} = user <- User.get_cached_by_nickname(nickname),
210 {:ok, user} <- User.ensure_keys_present(user) do
212 if params["max_id"] do
213 ActivityPub.fetch_user_activities(user, nil, %{
214 "max_id" => params["max_id"],
215 # This is a hack because postgres generates inefficient queries when filtering by
216 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
217 "include_poll_votes" => true,
221 ActivityPub.fetch_user_activities(user, nil, %{
223 "include_poll_votes" => true
228 |> put_resp_content_type("application/activity+json")
229 |> put_view(UserView)
230 |> render("activity_collection_page.json", %{
231 activities: activities,
232 iri: "#{user.ap_id}/outbox"
237 def outbox(conn, %{"nickname" => nickname}) do
238 with %User{} = user <- User.get_cached_by_nickname(nickname),
239 {:ok, user} <- User.ensure_keys_present(user) do
241 |> put_resp_content_type("application/activity+json")
242 |> put_view(UserView)
243 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
247 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
248 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
249 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
250 true <- Utils.recipient_in_message(recipient, actor, params),
251 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
252 Federator.incoming_ap_doc(params)
257 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
258 Federator.incoming_ap_doc(params)
262 # only accept relayed Creates
263 def inbox(conn, %{"type" => "Create"} = params) do
265 "Signature missing or not from author, relayed Create message, fetching object from source"
268 Fetcher.fetch_object_from_id(params["object"]["id"])
273 def inbox(conn, params) do
274 headers = Enum.into(conn.req_headers, %{})
276 if String.contains?(headers["signature"], params["actor"]) do
278 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
281 Logger.info(inspect(conn.req_headers))
284 json(conn, dgettext("errors", "error"))
287 defp represent_service_actor(%User{} = user, conn) do
288 with {:ok, user} <- User.ensure_keys_present(user) do
290 |> put_resp_content_type("application/activity+json")
291 |> put_view(UserView)
292 |> render("user.json", %{user: user})
294 nil -> {:error, :not_found}
298 defp represent_service_actor(nil, _), do: {:error, :not_found}
300 def relay(conn, _params) do
302 |> represent_service_actor(conn)
305 def internal_fetch(conn, _params) do
306 InternalFetchActor.get_actor()
307 |> represent_service_actor(conn)
310 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
311 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
313 |> put_resp_content_type("application/activity+json")
314 |> put_view(UserView)
315 |> render("user.json", %{user: user})
318 def whoami(_conn, _params), do: {:error, :not_found}
321 %{assigns: %{user: %{nickname: nickname} = user}} = conn,
322 %{"nickname" => nickname, "page" => page?} = params
324 when page? in [true, "true"] do
326 if params["max_id"] do
327 ActivityPub.fetch_activities([user.ap_id | user.following], %{
328 "max_id" => params["max_id"],
332 ActivityPub.fetch_activities([user.ap_id | user.following], %{"limit" => 10})
336 |> put_resp_content_type("application/activity+json")
337 |> put_view(UserView)
338 |> render("activity_collection_page.json", %{
339 activities: activities,
340 iri: "#{user.ap_id}/inbox"
344 def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
345 "nickname" => nickname
347 with {:ok, user} <- User.ensure_keys_present(user) do
349 |> put_resp_content_type("application/activity+json")
350 |> put_view(UserView)
351 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
355 def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
356 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
359 |> put_status(:forbidden)
363 def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
364 "nickname" => nickname
367 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
369 as_nickname: as_nickname
373 |> put_status(:forbidden)
377 def handle_user_activity(user, %{"type" => "Create"} = params) do
380 |> Map.merge(Map.take(params, ["to", "cc"]))
381 |> Map.put("attributedTo", user.ap_id())
382 |> Transmogrifier.fix_object()
384 ActivityPub.create(%{
387 context: object["context"],
389 additional: Map.take(params, ["cc"])
393 def handle_user_activity(user, %{"type" => "Delete"} = params) do
394 with %Object{} = object <- Object.normalize(params["object"]),
395 true <- user.info.is_moderator || user.ap_id == object.data["actor"],
396 {:ok, delete} <- ActivityPub.delete(object) do
399 _ -> {:error, dgettext("errors", "Can't delete object")}
403 def handle_user_activity(user, %{"type" => "Like"} = params) do
404 with %Object{} = object <- Object.normalize(params["object"]),
405 {:ok, activity, _object} <- ActivityPub.like(user, object) do
408 _ -> {:error, dgettext("errors", "Can't like object")}
412 def handle_user_activity(_, _) do
413 {:error, dgettext("errors", "Unhandled activity type")}
417 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
418 %{"nickname" => nickname} = params
425 |> Map.put("actor", actor)
426 |> Transmogrifier.fix_addressing()
428 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
430 |> put_status(:created)
431 |> put_resp_header("location", activity.data["id"])
432 |> json(activity.data)
436 |> put_status(:bad_request)
441 def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
443 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
445 as_nickname: user.nickname
449 |> put_status(:forbidden)
453 def errors(conn, {:error, :not_found}) do
455 |> put_status(:not_found)
456 |> json(dgettext("errors", "Not found"))
459 def errors(conn, _e) do
461 |> put_status(:internal_server_error)
462 |> json(dgettext("errors", "error"))
465 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
466 with actor <- conn.params["actor"],
467 true <- is_binary(actor) do
468 Pleroma.Instances.set_reachable(actor)
474 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
475 {:ok, new_user} = User.ensure_keys_present(user)
478 if new_user != user and match?(%User{}, for_user) do
479 User.get_cached_by_nickname(for_user.nickname)
487 # TODO: Add support for "object" field
489 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
492 - (required) `file`: data of the media
493 - (optionnal) `description`: description of the media, intended for accessibility
496 - HTTP Code: 201 Created
497 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
499 def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
500 with {:ok, object} <-
503 actor: User.ap_id(user),
504 description: Map.get(data, "description")
506 Logger.debug(inspect(object))
509 |> put_status(:created)