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 object_likes(conn, %{"uuid" => uuid, "page" => page}) do
91 with ap_id <- o_status_url(conn, :object, uuid),
92 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
93 {_, true} <- {:public?, Visibility.is_public?(object)},
94 likes <- Utils.get_object_likes(object) do
95 {page, _} = Integer.parse(page)
98 |> put_resp_content_type("application/activity+json")
99 |> put_view(ObjectView)
100 |> render("likes.json", %{ap_id: ap_id, likes: likes, page: page})
107 def object_likes(conn, %{"uuid" => uuid}) do
108 with ap_id <- o_status_url(conn, :object, uuid),
109 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
110 {_, true} <- {:public?, Visibility.is_public?(object)},
111 likes <- Utils.get_object_likes(object) do
113 |> put_resp_content_type("application/activity+json")
114 |> put_view(ObjectView)
115 |> render("likes.json", %{ap_id: ap_id, likes: likes})
122 def activity(conn, %{"uuid" => uuid}) do
123 with ap_id <- o_status_url(conn, :activity, uuid),
124 %Activity{} = activity <- Activity.normalize(ap_id),
125 {_, true} <- {:public?, Visibility.is_public?(activity)} do
127 |> maybe_set_tracking_data(activity)
128 |> set_cache_ttl_for(activity)
129 |> put_resp_content_type("application/activity+json")
130 |> put_view(ObjectView)
131 |> render("object.json", object: activity)
133 {:public?, false} -> {:error, :not_found}
134 nil -> {:error, :not_found}
138 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
139 object_id = Object.normalize(activity).id
140 assign(conn, :tracking_fun_data, object_id)
143 defp maybe_set_tracking_data(conn, _activity), do: conn
145 defp set_cache_ttl_for(conn, %Activity{object: object}) do
146 set_cache_ttl_for(conn, object)
149 defp set_cache_ttl_for(conn, entity) do
152 %Object{data: %{"type" => "Question"}} ->
153 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
156 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
162 assign(conn, :cache_ttl, ttl)
165 # GET /relay/following
166 def following(%{assigns: %{relay: true}} = conn, _params) do
168 |> put_resp_content_type("application/activity+json")
169 |> put_view(UserView)
170 |> render("following.json", %{user: Relay.get_actor()})
173 def following(%{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_follows, true} <-
177 {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do
178 {page, _} = Integer.parse(page)
181 |> put_resp_content_type("application/activity+json")
182 |> put_view(UserView)
183 |> render("following.json", %{user: user, page: page, for: for_user})
185 {:show_follows, _} ->
187 |> put_resp_content_type("application/activity+json")
188 |> send_resp(403, "")
192 def following(%{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("following.json", %{user: user, for: for_user})
202 # GET /relay/followers
203 def followers(%{assigns: %{relay: true}} = conn, _params) do
205 |> put_resp_content_type("application/activity+json")
206 |> put_view(UserView)
207 |> render("followers.json", %{user: Relay.get_actor()})
210 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
211 with %User{} = user <- User.get_cached_by_nickname(nickname),
212 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
213 {:show_followers, true} <-
214 {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do
215 {page, _} = Integer.parse(page)
218 |> put_resp_content_type("application/activity+json")
219 |> put_view(UserView)
220 |> render("followers.json", %{user: user, page: page, for: for_user})
222 {:show_followers, _} ->
224 |> put_resp_content_type("application/activity+json")
225 |> send_resp(403, "")
229 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
230 with %User{} = user <- User.get_cached_by_nickname(nickname),
231 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
233 |> put_resp_content_type("application/activity+json")
234 |> put_view(UserView)
235 |> render("followers.json", %{user: user, for: for_user})
239 def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
240 when page? in [true, "true"] do
241 with %User{} = user <- User.get_cached_by_nickname(nickname),
242 {:ok, user} <- User.ensure_keys_present(user) do
244 if params["max_id"] do
245 ActivityPub.fetch_user_activities(user, nil, %{
246 "max_id" => params["max_id"],
247 # This is a hack because postgres generates inefficient queries when filtering by
248 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
249 "include_poll_votes" => true,
253 ActivityPub.fetch_user_activities(user, nil, %{
255 "include_poll_votes" => true
260 |> put_resp_content_type("application/activity+json")
261 |> put_view(UserView)
262 |> render("activity_collection_page.json", %{
263 activities: activities,
264 iri: "#{user.ap_id}/outbox"
269 def outbox(conn, %{"nickname" => nickname}) do
270 with %User{} = user <- User.get_cached_by_nickname(nickname),
271 {:ok, user} <- User.ensure_keys_present(user) do
273 |> put_resp_content_type("application/activity+json")
274 |> put_view(UserView)
275 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
279 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
280 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
281 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
282 true <- Utils.recipient_in_message(recipient, actor, params),
283 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
284 Federator.incoming_ap_doc(params)
289 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
290 Federator.incoming_ap_doc(params)
294 # only accept relayed Creates
295 def inbox(conn, %{"type" => "Create"} = params) do
297 "Signature missing or not from author, relayed Create message, fetching object from source"
300 Fetcher.fetch_object_from_id(params["object"]["id"])
305 def inbox(conn, params) do
306 headers = Enum.into(conn.req_headers, %{})
308 if String.contains?(headers["signature"], params["actor"]) do
310 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
313 Logger.info(inspect(conn.req_headers))
316 json(conn, dgettext("errors", "error"))
319 defp represent_service_actor(%User{} = user, conn) do
320 with {:ok, user} <- User.ensure_keys_present(user) do
322 |> put_resp_content_type("application/activity+json")
323 |> put_view(UserView)
324 |> render("user.json", %{user: user})
326 nil -> {:error, :not_found}
330 defp represent_service_actor(nil, _), do: {:error, :not_found}
332 def relay(conn, _params) do
334 |> represent_service_actor(conn)
337 def internal_fetch(conn, _params) do
338 InternalFetchActor.get_actor()
339 |> represent_service_actor(conn)
342 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
343 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
345 |> put_resp_content_type("application/activity+json")
346 |> put_view(UserView)
347 |> render("user.json", %{user: user})
350 def whoami(_conn, _params), do: {:error, :not_found}
353 %{assigns: %{user: %{nickname: nickname} = user}} = conn,
354 %{"nickname" => nickname, "page" => page?} = params
356 when page? in [true, "true"] do
358 if params["max_id"] do
359 ActivityPub.fetch_activities([user.ap_id | user.following], %{
360 "max_id" => params["max_id"],
364 ActivityPub.fetch_activities([user.ap_id | user.following], %{"limit" => 10})
368 |> put_resp_content_type("application/activity+json")
369 |> put_view(UserView)
370 |> render("activity_collection_page.json", %{
371 activities: activities,
372 iri: "#{user.ap_id}/inbox"
376 def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
377 "nickname" => nickname
379 with {:ok, user} <- User.ensure_keys_present(user) do
381 |> put_resp_content_type("application/activity+json")
382 |> put_view(UserView)
383 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
387 def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
388 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
391 |> put_status(:forbidden)
395 def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
396 "nickname" => nickname
399 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
401 as_nickname: as_nickname
405 |> put_status(:forbidden)
409 def handle_user_activity(user, %{"type" => "Create"} = params) do
412 |> Map.merge(Map.take(params, ["to", "cc"]))
413 |> Map.put("attributedTo", user.ap_id())
414 |> Transmogrifier.fix_object()
416 ActivityPub.create(%{
419 context: object["context"],
421 additional: Map.take(params, ["cc"])
425 def handle_user_activity(user, %{"type" => "Delete"} = params) do
426 with %Object{} = object <- Object.normalize(params["object"]),
427 true <- user.info.is_moderator || user.ap_id == object.data["actor"],
428 {:ok, delete} <- ActivityPub.delete(object) do
431 _ -> {:error, dgettext("errors", "Can't delete object")}
435 def handle_user_activity(user, %{"type" => "Like"} = params) do
436 with %Object{} = object <- Object.normalize(params["object"]),
437 {:ok, activity, _object} <- ActivityPub.like(user, object) do
440 _ -> {:error, dgettext("errors", "Can't like object")}
444 def handle_user_activity(_, _) do
445 {:error, dgettext("errors", "Unhandled activity type")}
449 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
450 %{"nickname" => nickname} = params
457 |> Map.put("actor", actor)
458 |> Transmogrifier.fix_addressing()
460 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
462 |> put_status(:created)
463 |> put_resp_header("location", activity.data["id"])
464 |> json(activity.data)
468 |> put_status(:bad_request)
473 def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
475 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
477 as_nickname: user.nickname
481 |> put_status(:forbidden)
485 def errors(conn, {:error, :not_found}) do
487 |> put_status(:not_found)
488 |> json(dgettext("errors", "Not found"))
491 def errors(conn, _e) do
493 |> put_status(:internal_server_error)
494 |> json(dgettext("errors", "error"))
497 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
498 with actor <- conn.params["actor"],
499 true <- is_binary(actor) do
500 Pleroma.Instances.set_reachable(actor)
506 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
507 {:ok, new_user} = User.ensure_keys_present(user)
510 if new_user != user and match?(%User{}, for_user) do
511 User.get_cached_by_nickname(for_user.nickname)
519 # TODO: Add support for "object" field
521 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
524 - (required) `file`: data of the media
525 - (optionnal) `description`: description of the media, intended for accessibility
528 - HTTP Code: 201 Created
529 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
531 def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
532 with {:ok, object} <-
535 actor: User.ap_id(user),
536 description: Map.get(data, "description")
538 Logger.debug(inspect(object))
541 |> put_status(:created)