Merge branch 'features/apc2s-pagination' into 'develop'
[akkoma] / lib / pleroma / web / activity_pub / activity_pub_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.ActivityPubController do
6 use Pleroma.Web, :controller
7
8 alias Pleroma.Activity
9 alias Pleroma.Delivery
10 alias Pleroma.Object
11 alias Pleroma.Object.Fetcher
12 alias Pleroma.User
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
23
24 require Logger
25
26 action_fallback(:errors)
27
28 plug(
29 Pleroma.Plugs.Cache,
30 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
31 when action in [:activity, :object]
32 )
33
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])
37
38 def relay_active?(conn, _) do
39 if Pleroma.Config.get([:instance, :allow_relay]) do
40 conn
41 else
42 conn
43 |> render_error(:not_found, "not found")
44 |> halt()
45 end
46 end
47
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
51 conn
52 |> put_resp_content_type("application/activity+json")
53 |> put_view(UserView)
54 |> render("user.json", %{user: user})
55 else
56 nil -> {:error, :not_found}
57 %{local: false} -> {:error, :not_found}
58 end
59 end
60
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
65 conn
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)
71 else
72 {:public?, false} ->
73 {:error, :not_found}
74 end
75 end
76
77 def track_object_fetch(conn, nil), do: conn
78
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)
82 end
83
84 conn
85 end
86
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
91 conn
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)
97 else
98 {:public?, false} -> {:error, :not_found}
99 nil -> {:error, :not_found}
100 end
101 end
102
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)
106 end
107
108 defp maybe_set_tracking_data(conn, _activity), do: conn
109
110 defp set_cache_ttl_for(conn, %Activity{object: object}) do
111 set_cache_ttl_for(conn, object)
112 end
113
114 defp set_cache_ttl_for(conn, entity) do
115 ttl =
116 case entity do
117 %Object{data: %{"type" => "Question"}} ->
118 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
119
120 %Object{} ->
121 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
122
123 _ ->
124 nil
125 end
126
127 assign(conn, :cache_ttl, ttl)
128 end
129
130 # GET /relay/following
131 def following(%{assigns: %{relay: true}} = conn, _params) do
132 conn
133 |> put_resp_content_type("application/activity+json")
134 |> put_view(UserView)
135 |> render("following.json", %{user: Relay.get_actor()})
136 end
137
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)
144
145 conn
146 |> put_resp_content_type("application/activity+json")
147 |> put_view(UserView)
148 |> render("following.json", %{user: user, page: page, for: for_user})
149 else
150 {:show_follows, _} ->
151 conn
152 |> put_resp_content_type("application/activity+json")
153 |> send_resp(403, "")
154 end
155 end
156
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
160 conn
161 |> put_resp_content_type("application/activity+json")
162 |> put_view(UserView)
163 |> render("following.json", %{user: user, for: for_user})
164 end
165 end
166
167 # GET /relay/followers
168 def followers(%{assigns: %{relay: true}} = conn, _params) do
169 conn
170 |> put_resp_content_type("application/activity+json")
171 |> put_view(UserView)
172 |> render("followers.json", %{user: Relay.get_actor()})
173 end
174
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)
181
182 conn
183 |> put_resp_content_type("application/activity+json")
184 |> put_view(UserView)
185 |> render("followers.json", %{user: user, page: page, for: for_user})
186 else
187 {:show_followers, _} ->
188 conn
189 |> put_resp_content_type("application/activity+json")
190 |> send_resp(403, "")
191 end
192 end
193
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
197 conn
198 |> put_resp_content_type("application/activity+json")
199 |> put_view(UserView)
200 |> render("followers.json", %{user: user, for: for_user})
201 end
202 end
203
204 def outbox(
205 %{assigns: %{user: for_user}} = conn,
206 %{"nickname" => nickname, "page" => page?} = params
207 )
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
214 params =
215 params
216 |> Map.drop(["nickname", "page"])
217 |> Map.put("include_poll_votes", true)
218
219 activities = ActivityPub.fetch_user_activities(user, for_user, params)
220
221 conn
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"
228 })
229 end
230 end
231
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
235 conn
236 |> put_resp_content_type("application/activity+json")
237 |> put_view(UserView)
238 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
239 end
240 end
241
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)
248 json(conn, "ok")
249 end
250 end
251
252 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
253 Federator.incoming_ap_doc(params)
254 json(conn, "ok")
255 end
256
257 # only accept relayed Creates
258 def inbox(conn, %{"type" => "Create"} = params) do
259 Logger.debug(
260 "Signature missing or not from author, relayed Create message, fetching object from source"
261 )
262
263 Fetcher.fetch_object_from_id(params["object"]["id"])
264
265 json(conn, "ok")
266 end
267
268 def inbox(conn, params) do
269 headers = Enum.into(conn.req_headers, %{})
270
271 if String.contains?(headers["signature"], params["actor"]) do
272 Logger.debug(
273 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
274 )
275
276 Logger.debug(inspect(conn.req_headers))
277 end
278
279 json(conn, dgettext("errors", "error"))
280 end
281
282 defp represent_service_actor(%User{} = user, conn) do
283 with {:ok, user} <- User.ensure_keys_present(user) do
284 conn
285 |> put_resp_content_type("application/activity+json")
286 |> put_view(UserView)
287 |> render("user.json", %{user: user})
288 else
289 nil -> {:error, :not_found}
290 end
291 end
292
293 defp represent_service_actor(nil, _), do: {:error, :not_found}
294
295 def relay(conn, _params) do
296 Relay.get_actor()
297 |> represent_service_actor(conn)
298 end
299
300 def internal_fetch(conn, _params) do
301 InternalFetchActor.get_actor()
302 |> represent_service_actor(conn)
303 end
304
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
307 conn
308 |> put_resp_content_type("application/activity+json")
309 |> put_view(UserView)
310 |> render("user.json", %{user: user})
311 end
312
313 def whoami(_conn, _params), do: {:error, :not_found}
314
315 def read_inbox(
316 %{assigns: %{user: %{nickname: nickname} = user}} = conn,
317 %{"nickname" => nickname, "page" => page?} = params
318 )
319 when page? in [true, "true"] do
320 params =
321 params
322 |> Map.drop(["nickname", "page"])
323 |> Map.put("blocking_user", user)
324 |> Map.put("user", user)
325
326 activities =
327 [user.ap_id | User.following(user)]
328 |> ActivityPub.fetch_activities(params)
329 |> Enum.reverse()
330
331 conn
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"
338 })
339 end
340
341 def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
342 "nickname" => nickname
343 }) do
344 with {:ok, user} <- User.ensure_keys_present(user) do
345 conn
346 |> put_resp_content_type("application/activity+json")
347 |> put_view(UserView)
348 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
349 end
350 end
351
352 def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
353 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
354
355 conn
356 |> put_status(:forbidden)
357 |> json(err)
358 end
359
360 def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
361 "nickname" => nickname
362 }) do
363 err =
364 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
365 nickname: nickname,
366 as_nickname: as_nickname
367 )
368
369 conn
370 |> put_status(:forbidden)
371 |> json(err)
372 end
373
374 defp handle_user_activity(
375 %User{} = user,
376 %{"type" => "Create", "object" => %{"type" => "Note"}} = params
377 ) do
378 object =
379 params["object"]
380 |> Map.merge(Map.take(params, ["to", "cc"]))
381 |> Map.put("attributedTo", user.ap_id())
382 |> Transmogrifier.fix_object()
383
384 ActivityPub.create(%{
385 to: params["to"],
386 actor: user,
387 context: object["context"],
388 object: object,
389 additional: Map.take(params, ["cc"])
390 })
391 end
392
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
397 {:ok, delete}
398 else
399 _ -> {:error, dgettext("errors", "Can't delete object")}
400 end
401 end
402
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
406 {:ok, activity}
407 else
408 _ -> {:error, dgettext("errors", "Can't like object")}
409 end
410 end
411
412 defp handle_user_activity(_, _) do
413 {:error, dgettext("errors", "Unhandled activity type")}
414 end
415
416 def update_outbox(
417 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
418 %{"nickname" => nickname} = params
419 ) do
420 actor = user.ap_id()
421
422 params =
423 params
424 |> Map.drop(["id"])
425 |> Map.put("actor", actor)
426 |> Transmogrifier.fix_addressing()
427
428 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
429 conn
430 |> put_status(:created)
431 |> put_resp_header("location", activity.data["id"])
432 |> json(activity.data)
433 else
434 {:error, message} ->
435 conn
436 |> put_status(:bad_request)
437 |> json(message)
438 end
439 end
440
441 def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
442 err =
443 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
444 nickname: nickname,
445 as_nickname: user.nickname
446 )
447
448 conn
449 |> put_status(:forbidden)
450 |> json(err)
451 end
452
453 def errors(conn, {:error, :not_found}) do
454 conn
455 |> put_status(:not_found)
456 |> json(dgettext("errors", "Not found"))
457 end
458
459 def errors(conn, _e) do
460 conn
461 |> put_status(:internal_server_error)
462 |> json(dgettext("errors", "error"))
463 end
464
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)
469 end
470
471 conn
472 end
473
474 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
475 {:ok, new_user} = User.ensure_keys_present(user)
476
477 for_user =
478 if new_user != user and match?(%User{}, for_user) do
479 User.get_cached_by_nickname(for_user.nickname)
480 else
481 for_user
482 end
483
484 {new_user, for_user}
485 end
486
487 # TODO: Add support for "object" field
488 @doc """
489 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
490
491 Parameters:
492 - (required) `file`: data of the media
493 - (optionnal) `description`: description of the media, intended for accessibility
494
495 Response:
496 - HTTP Code: 201 Created
497 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
498 """
499 def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
500 with {:ok, object} <-
501 ActivityPub.upload(
502 file,
503 actor: User.ap_id(user),
504 description: Map.get(data, "description")
505 ) do
506 Logger.debug(inspect(object))
507
508 conn
509 |> put_status(:created)
510 |> json(object.data)
511 end
512 end
513 end