1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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.ControllerHelper
22 alias Pleroma.Web.Federator
26 action_fallback(:errors)
30 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
31 when action in [:activity, :object]
34 plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
35 plug(:set_requester_reachable when action in [:inbox])
36 plug(:relay_active? when action in [:relay])
38 def relay_active?(conn, _) do
39 if Pleroma.Config.get([:instance, :allow_relay]) do
43 |> render_error(:not_found, "not found")
48 def user(conn, %{"nickname" => nickname}) do
49 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
50 {:ok, user} <- User.ensure_keys_present(user) do
52 |> put_resp_content_type("application/activity+json")
54 |> render("user.json", %{user: user})
56 nil -> {:error, :not_found}
57 %{local: false} -> {:error, :not_found}
61 def object(conn, %{"uuid" => uuid}) do
62 with ap_id <- o_status_url(conn, :object, uuid),
63 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
64 {_, true} <- {:public?, Visibility.is_public?(object)} do
66 |> assign(:tracking_fun_data, object.id)
67 |> set_cache_ttl_for(object)
68 |> put_resp_content_type("application/activity+json")
69 |> put_view(ObjectView)
70 |> render("object.json", object: object)
77 def track_object_fetch(conn, nil), do: conn
79 def track_object_fetch(conn, object_id) do
80 with %{assigns: %{user: %User{id: user_id}}} <- conn do
81 Delivery.create(object_id, user_id)
87 def activity(conn, %{"uuid" => uuid}) do
88 with ap_id <- o_status_url(conn, :activity, uuid),
89 %Activity{} = activity <- Activity.normalize(ap_id),
90 {_, true} <- {:public?, Visibility.is_public?(activity)} do
92 |> maybe_set_tracking_data(activity)
93 |> set_cache_ttl_for(activity)
94 |> put_resp_content_type("application/activity+json")
95 |> put_view(ObjectView)
96 |> render("object.json", object: activity)
98 {:public?, false} -> {:error, :not_found}
99 nil -> {:error, :not_found}
103 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
104 object_id = Object.normalize(activity).id
105 assign(conn, :tracking_fun_data, object_id)
108 defp maybe_set_tracking_data(conn, _activity), do: conn
110 defp set_cache_ttl_for(conn, %Activity{object: object}) do
111 set_cache_ttl_for(conn, object)
114 defp set_cache_ttl_for(conn, entity) do
117 %Object{data: %{"type" => "Question"}} ->
118 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
121 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
127 assign(conn, :cache_ttl, ttl)
130 # GET /relay/following
131 def following(%{assigns: %{relay: true}} = conn, _params) do
133 |> put_resp_content_type("application/activity+json")
134 |> put_view(UserView)
135 |> render("following.json", %{user: Relay.get_actor()})
138 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
139 with %User{} = user <- User.get_cached_by_nickname(nickname),
140 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
141 {:show_follows, true} <-
142 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
143 {page, _} = Integer.parse(page)
146 |> put_resp_content_type("application/activity+json")
147 |> put_view(UserView)
148 |> render("following.json", %{user: user, page: page, for: for_user})
150 {:show_follows, _} ->
152 |> put_resp_content_type("application/activity+json")
153 |> send_resp(403, "")
157 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
158 with %User{} = user <- User.get_cached_by_nickname(nickname),
159 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
161 |> put_resp_content_type("application/activity+json")
162 |> put_view(UserView)
163 |> render("following.json", %{user: user, for: for_user})
167 # GET /relay/followers
168 def followers(%{assigns: %{relay: true}} = conn, _params) do
170 |> put_resp_content_type("application/activity+json")
171 |> put_view(UserView)
172 |> render("followers.json", %{user: Relay.get_actor()})
175 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
176 with %User{} = user <- User.get_cached_by_nickname(nickname),
177 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
178 {:show_followers, true} <-
179 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
180 {page, _} = Integer.parse(page)
183 |> put_resp_content_type("application/activity+json")
184 |> put_view(UserView)
185 |> render("followers.json", %{user: user, page: page, for: for_user})
187 {:show_followers, _} ->
189 |> put_resp_content_type("application/activity+json")
190 |> send_resp(403, "")
194 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
195 with %User{} = user <- User.get_cached_by_nickname(nickname),
196 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
198 |> put_resp_content_type("application/activity+json")
199 |> put_view(UserView)
200 |> render("followers.json", %{user: user, for: for_user})
205 %{assigns: %{user: for_user}} = conn,
206 %{"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
211 # "include_poll_votes" is a hack because postgres generates inefficient
212 # queries when filtering by 'Answer', poll votes will be hidden by the
213 # visibility filter in this case anyway
216 |> Map.drop(["nickname", "page"])
217 |> Map.put("include_poll_votes", true)
219 activities = ActivityPub.fetch_user_activities(user, for_user, params)
222 |> put_resp_content_type("application/activity+json")
223 |> put_view(UserView)
224 |> render("activity_collection_page.json", %{
225 activities: activities,
226 pagination: ControllerHelper.get_pagination_fields(conn, 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.debug(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
322 |> Map.drop(["nickname", "page"])
323 |> Map.put("blocking_user", user)
324 |> Map.put("user", user)
327 [user.ap_id | User.following(user)]
328 |> ActivityPub.fetch_activities(params)
332 |> put_resp_content_type("application/activity+json")
333 |> put_view(UserView)
334 |> render("activity_collection_page.json", %{
335 activities: activities,
336 pagination: ControllerHelper.get_pagination_fields(conn, activities),
337 iri: "#{user.ap_id}/inbox"
341 def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
342 "nickname" => nickname
344 with {:ok, user} <- User.ensure_keys_present(user) do
346 |> put_resp_content_type("application/activity+json")
347 |> put_view(UserView)
348 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
352 def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
353 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
356 |> put_status(:forbidden)
360 def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
361 "nickname" => nickname
364 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
366 as_nickname: as_nickname
370 |> put_status(:forbidden)
374 defp handle_user_activity(
376 %{"type" => "Create", "object" => %{"type" => "Note"}} = params
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 defp handle_user_activity(user, %{"type" => "Delete"} = params) do
394 with %Object{} = object <- Object.normalize(params["object"]),
395 true <- user.is_moderator || user.ap_id == object.data["actor"],
396 {:ok, delete} <- ActivityPub.delete(object) do
399 _ -> {:error, dgettext("errors", "Can't delete object")}
403 defp 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 defp 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)