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.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{local: true} = 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}
56 %{local: false} -> {:error, :not_found}
60 def object(conn, %{"uuid" => uuid}) do
61 with ap_id <- o_status_url(conn, :object, uuid),
62 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
63 {_, true} <- {:public?, Visibility.is_public?(object)} do
65 |> assign(:tracking_fun_data, object.id)
66 |> set_cache_ttl_for(object)
67 |> put_resp_content_type("application/activity+json")
68 |> put_view(ObjectView)
69 |> render("object.json", object: object)
76 def track_object_fetch(conn, nil), do: conn
78 def track_object_fetch(conn, object_id) do
79 with %{assigns: %{user: %User{id: user_id}}} <- conn do
80 Delivery.create(object_id, user_id)
86 def activity(conn, %{"uuid" => uuid}) do
87 with ap_id <- o_status_url(conn, :activity, uuid),
88 %Activity{} = activity <- Activity.normalize(ap_id),
89 {_, true} <- {:public?, Visibility.is_public?(activity)} do
91 |> maybe_set_tracking_data(activity)
92 |> set_cache_ttl_for(activity)
93 |> put_resp_content_type("application/activity+json")
94 |> put_view(ObjectView)
95 |> render("object.json", object: activity)
97 {:public?, false} -> {:error, :not_found}
98 nil -> {:error, :not_found}
102 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
103 object_id = Object.normalize(activity).id
104 assign(conn, :tracking_fun_data, object_id)
107 defp maybe_set_tracking_data(conn, _activity), do: conn
109 defp set_cache_ttl_for(conn, %Activity{object: object}) do
110 set_cache_ttl_for(conn, object)
113 defp set_cache_ttl_for(conn, entity) do
116 %Object{data: %{"type" => "Question"}} ->
117 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
120 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
126 assign(conn, :cache_ttl, ttl)
129 # GET /relay/following
130 def following(%{assigns: %{relay: true}} = conn, _params) do
132 |> put_resp_content_type("application/activity+json")
133 |> put_view(UserView)
134 |> render("following.json", %{user: Relay.get_actor()})
137 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
138 with %User{} = user <- User.get_cached_by_nickname(nickname),
139 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
140 {:show_follows, true} <-
141 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
142 {page, _} = Integer.parse(page)
145 |> put_resp_content_type("application/activity+json")
146 |> put_view(UserView)
147 |> render("following.json", %{user: user, page: page, for: for_user})
149 {:show_follows, _} ->
151 |> put_resp_content_type("application/activity+json")
152 |> send_resp(403, "")
156 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
157 with %User{} = user <- User.get_cached_by_nickname(nickname),
158 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
160 |> put_resp_content_type("application/activity+json")
161 |> put_view(UserView)
162 |> render("following.json", %{user: user, for: for_user})
166 # GET /relay/followers
167 def followers(%{assigns: %{relay: true}} = conn, _params) do
169 |> put_resp_content_type("application/activity+json")
170 |> put_view(UserView)
171 |> render("followers.json", %{user: Relay.get_actor()})
174 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
175 with %User{} = user <- User.get_cached_by_nickname(nickname),
176 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
177 {:show_followers, true} <-
178 {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
179 {page, _} = Integer.parse(page)
182 |> put_resp_content_type("application/activity+json")
183 |> put_view(UserView)
184 |> render("followers.json", %{user: user, page: page, for: for_user})
186 {:show_followers, _} ->
188 |> put_resp_content_type("application/activity+json")
189 |> send_resp(403, "")
193 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
194 with %User{} = user <- User.get_cached_by_nickname(nickname),
195 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
197 |> put_resp_content_type("application/activity+json")
198 |> put_view(UserView)
199 |> render("followers.json", %{user: user, for: for_user})
203 def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
204 when page? in [true, "true"] do
205 with %User{} = user <- User.get_cached_by_nickname(nickname),
206 {:ok, user} <- User.ensure_keys_present(user) do
208 if params["max_id"] do
209 ActivityPub.fetch_user_activities(user, nil, %{
210 "max_id" => params["max_id"],
211 # This is a hack because postgres generates inefficient queries when filtering by
212 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
213 "include_poll_votes" => true,
217 ActivityPub.fetch_user_activities(user, nil, %{
219 "include_poll_votes" => true
224 |> put_resp_content_type("application/activity+json")
225 |> put_view(UserView)
226 |> render("activity_collection_page.json", %{
227 activities: activities,
228 iri: "#{user.ap_id}/outbox"
233 def outbox(conn, %{"nickname" => nickname}) do
234 with %User{} = user <- User.get_cached_by_nickname(nickname),
235 {:ok, user} <- User.ensure_keys_present(user) do
237 |> put_resp_content_type("application/activity+json")
238 |> put_view(UserView)
239 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
243 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
244 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
245 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
246 true <- Utils.recipient_in_message(recipient, actor, params),
247 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
248 Federator.incoming_ap_doc(params)
253 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
254 Federator.incoming_ap_doc(params)
258 # only accept relayed Creates
259 def inbox(conn, %{"type" => "Create"} = params) do
261 "Signature missing or not from author, relayed Create message, fetching object from source"
264 Fetcher.fetch_object_from_id(params["object"]["id"])
269 def inbox(conn, params) do
270 headers = Enum.into(conn.req_headers, %{})
272 if String.contains?(headers["signature"], params["actor"]) do
274 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
277 Logger.debug(inspect(conn.req_headers))
280 json(conn, dgettext("errors", "error"))
283 defp represent_service_actor(%User{} = user, conn) do
284 with {:ok, user} <- User.ensure_keys_present(user) do
286 |> put_resp_content_type("application/activity+json")
287 |> put_view(UserView)
288 |> render("user.json", %{user: user})
290 nil -> {:error, :not_found}
294 defp represent_service_actor(nil, _), do: {:error, :not_found}
296 def relay(conn, _params) do
298 |> represent_service_actor(conn)
301 def internal_fetch(conn, _params) do
302 InternalFetchActor.get_actor()
303 |> represent_service_actor(conn)
306 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
307 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
309 |> put_resp_content_type("application/activity+json")
310 |> put_view(UserView)
311 |> render("user.json", %{user: user})
314 def whoami(_conn, _params), do: {:error, :not_found}
317 %{assigns: %{user: %{nickname: nickname} = user}} = conn,
318 %{"nickname" => nickname, "page" => page?} = params
320 when page? in [true, "true"] do
322 if params["max_id"] do
323 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
324 "max_id" => params["max_id"],
328 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
332 |> put_resp_content_type("application/activity+json")
333 |> put_view(UserView)
334 |> render("activity_collection_page.json", %{
335 activities: activities,
336 iri: "#{user.ap_id}/inbox"
340 def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
341 "nickname" => nickname
343 with {:ok, user} <- User.ensure_keys_present(user) do
345 |> put_resp_content_type("application/activity+json")
346 |> put_view(UserView)
347 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
351 def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
352 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
355 |> put_status(:forbidden)
359 def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
360 "nickname" => nickname
363 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
365 as_nickname: as_nickname
369 |> put_status(:forbidden)
373 def handle_user_activity(user, %{"type" => "Create"} = params) do
376 |> Map.merge(Map.take(params, ["to", "cc"]))
377 |> Map.put("attributedTo", user.ap_id())
378 |> Transmogrifier.fix_object()
380 ActivityPub.create(%{
383 context: object["context"],
385 additional: Map.take(params, ["cc"])
389 def handle_user_activity(user, %{"type" => "Delete"} = params) do
390 with %Object{} = object <- Object.normalize(params["object"]),
391 true <- user.is_moderator || user.ap_id == object.data["actor"],
392 {:ok, delete} <- ActivityPub.delete(object) do
395 _ -> {:error, dgettext("errors", "Can't delete object")}
399 def handle_user_activity(user, %{"type" => "Like"} = params) do
400 with %Object{} = object <- Object.normalize(params["object"]),
401 {:ok, activity, _object} <- ActivityPub.like(user, object) do
404 _ -> {:error, dgettext("errors", "Can't like object")}
408 def handle_user_activity(_, _) do
409 {:error, dgettext("errors", "Unhandled activity type")}
413 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
414 %{"nickname" => nickname} = params
421 |> Map.put("actor", actor)
422 |> Transmogrifier.fix_addressing()
424 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
426 |> put_status(:created)
427 |> put_resp_header("location", activity.data["id"])
428 |> json(activity.data)
432 |> put_status(:bad_request)
437 def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
439 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
441 as_nickname: user.nickname
445 |> put_status(:forbidden)
449 def errors(conn, {:error, :not_found}) do
451 |> put_status(:not_found)
452 |> json(dgettext("errors", "Not found"))
455 def errors(conn, _e) do
457 |> put_status(:internal_server_error)
458 |> json(dgettext("errors", "error"))
461 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
462 with actor <- conn.params["actor"],
463 true <- is_binary(actor) do
464 Pleroma.Instances.set_reachable(actor)
470 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
471 {:ok, new_user} = User.ensure_keys_present(user)
474 if new_user != user and match?(%User{}, for_user) do
475 User.get_cached_by_nickname(for_user.nickname)
483 # TODO: Add support for "object" field
485 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
488 - (required) `file`: data of the media
489 - (optionnal) `description`: description of the media, intended for accessibility
492 - HTTP Code: 201 Created
493 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
495 def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
496 with {:ok, object} <-
499 actor: User.ap_id(user),
500 description: Map.get(data, "description")
502 Logger.debug(inspect(object))
505 |> put_status(:created)