c349a704816572a9907ce2ce886b7b0d64e2dc6c
[akkoma] / lib / pleroma / web / activity_pub / activity_pub_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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.Federator
22
23 require Logger
24
25 action_fallback(:errors)
26
27 plug(
28 Pleroma.Plugs.Cache,
29 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
30 when action in [:activity, :object]
31 )
32
33 plug(
34 Pleroma.Plugs.OAuthScopesPlug,
35 %{scopes: ["read:accounts"]} when action in [:followers, :following]
36 )
37
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])
41
42 def relay_active?(conn, _) do
43 if Pleroma.Config.get([:instance, :allow_relay]) do
44 conn
45 else
46 conn
47 |> render_error(:not_found, "not found")
48 |> halt()
49 end
50 end
51
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
55 conn
56 |> put_resp_content_type("application/activity+json")
57 |> put_view(UserView)
58 |> render("user.json", %{user: user})
59 else
60 nil -> {:error, :not_found}
61 end
62 end
63
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
68 conn
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)
74 else
75 {:public?, false} ->
76 {:error, :not_found}
77 end
78 end
79
80 def track_object_fetch(conn, nil), do: conn
81
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)
85 end
86
87 conn
88 end
89
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)
96
97 conn
98 |> put_resp_content_type("application/activity+json")
99 |> put_view(ObjectView)
100 |> render("likes.json", %{ap_id: ap_id, likes: likes, page: page})
101 else
102 {:public?, false} ->
103 {:error, :not_found}
104 end
105 end
106
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
112 conn
113 |> put_resp_content_type("application/activity+json")
114 |> put_view(ObjectView)
115 |> render("likes.json", %{ap_id: ap_id, likes: likes})
116 else
117 {:public?, false} ->
118 {:error, :not_found}
119 end
120 end
121
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
126 conn
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)
132 else
133 {:public?, false} -> {:error, :not_found}
134 nil -> {:error, :not_found}
135 end
136 end
137
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)
141 end
142
143 defp maybe_set_tracking_data(conn, _activity), do: conn
144
145 defp set_cache_ttl_for(conn, %Activity{object: object}) do
146 set_cache_ttl_for(conn, object)
147 end
148
149 defp set_cache_ttl_for(conn, entity) do
150 ttl =
151 case entity do
152 %Object{data: %{"type" => "Question"}} ->
153 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
154
155 %Object{} ->
156 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
157
158 _ ->
159 nil
160 end
161
162 assign(conn, :cache_ttl, ttl)
163 end
164
165 # GET /relay/following
166 def following(%{assigns: %{relay: true}} = conn, _params) do
167 conn
168 |> put_resp_content_type("application/activity+json")
169 |> put_view(UserView)
170 |> render("following.json", %{user: Relay.get_actor()})
171 end
172
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)
179
180 conn
181 |> put_resp_content_type("application/activity+json")
182 |> put_view(UserView)
183 |> render("following.json", %{user: user, page: page, for: for_user})
184 else
185 {:show_follows, _} ->
186 conn
187 |> put_resp_content_type("application/activity+json")
188 |> send_resp(403, "")
189 end
190 end
191
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
195 conn
196 |> put_resp_content_type("application/activity+json")
197 |> put_view(UserView)
198 |> render("following.json", %{user: user, for: for_user})
199 end
200 end
201
202 # GET /relay/followers
203 def followers(%{assigns: %{relay: true}} = conn, _params) do
204 conn
205 |> put_resp_content_type("application/activity+json")
206 |> put_view(UserView)
207 |> render("followers.json", %{user: Relay.get_actor()})
208 end
209
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)
216
217 conn
218 |> put_resp_content_type("application/activity+json")
219 |> put_view(UserView)
220 |> render("followers.json", %{user: user, page: page, for: for_user})
221 else
222 {:show_followers, _} ->
223 conn
224 |> put_resp_content_type("application/activity+json")
225 |> send_resp(403, "")
226 end
227 end
228
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
232 conn
233 |> put_resp_content_type("application/activity+json")
234 |> put_view(UserView)
235 |> render("followers.json", %{user: user, for: for_user})
236 end
237 end
238
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
243 activities =
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,
250 "limit" => 10
251 })
252 else
253 ActivityPub.fetch_user_activities(user, nil, %{
254 "limit" => 10,
255 "include_poll_votes" => true
256 })
257 end
258
259 conn
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"
265 })
266 end
267 end
268
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
272 conn
273 |> put_resp_content_type("application/activity+json")
274 |> put_view(UserView)
275 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
276 end
277 end
278
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)
285 json(conn, "ok")
286 end
287 end
288
289 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
290 Federator.incoming_ap_doc(params)
291 json(conn, "ok")
292 end
293
294 # only accept relayed Creates
295 def inbox(conn, %{"type" => "Create"} = params) do
296 Logger.info(
297 "Signature missing or not from author, relayed Create message, fetching object from source"
298 )
299
300 Fetcher.fetch_object_from_id(params["object"]["id"])
301
302 json(conn, "ok")
303 end
304
305 def inbox(conn, params) do
306 headers = Enum.into(conn.req_headers, %{})
307
308 if String.contains?(headers["signature"], params["actor"]) do
309 Logger.info(
310 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
311 )
312
313 Logger.info(inspect(conn.req_headers))
314 end
315
316 json(conn, dgettext("errors", "error"))
317 end
318
319 defp represent_service_actor(%User{} = user, conn) do
320 with {:ok, user} <- User.ensure_keys_present(user) do
321 conn
322 |> put_resp_content_type("application/activity+json")
323 |> put_view(UserView)
324 |> render("user.json", %{user: user})
325 else
326 nil -> {:error, :not_found}
327 end
328 end
329
330 defp represent_service_actor(nil, _), do: {:error, :not_found}
331
332 def relay(conn, _params) do
333 Relay.get_actor()
334 |> represent_service_actor(conn)
335 end
336
337 def internal_fetch(conn, _params) do
338 InternalFetchActor.get_actor()
339 |> represent_service_actor(conn)
340 end
341
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
344 conn
345 |> put_resp_content_type("application/activity+json")
346 |> put_view(UserView)
347 |> render("user.json", %{user: user})
348 end
349
350 def whoami(_conn, _params), do: {:error, :not_found}
351
352 def read_inbox(
353 %{assigns: %{user: %{nickname: nickname} = user}} = conn,
354 %{"nickname" => nickname, "page" => page?} = params
355 )
356 when page? in [true, "true"] do
357 activities =
358 if params["max_id"] do
359 ActivityPub.fetch_activities([user.ap_id | user.following], %{
360 "max_id" => params["max_id"],
361 "limit" => 10
362 })
363 else
364 ActivityPub.fetch_activities([user.ap_id | user.following], %{"limit" => 10})
365 end
366
367 conn
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"
373 })
374 end
375
376 def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
377 "nickname" => nickname
378 }) do
379 with {:ok, user} <- User.ensure_keys_present(user) do
380 conn
381 |> put_resp_content_type("application/activity+json")
382 |> put_view(UserView)
383 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
384 end
385 end
386
387 def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
388 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
389
390 conn
391 |> put_status(:forbidden)
392 |> json(err)
393 end
394
395 def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
396 "nickname" => nickname
397 }) do
398 err =
399 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
400 nickname: nickname,
401 as_nickname: as_nickname
402 )
403
404 conn
405 |> put_status(:forbidden)
406 |> json(err)
407 end
408
409 def handle_user_activity(user, %{"type" => "Create"} = params) do
410 object =
411 params["object"]
412 |> Map.merge(Map.take(params, ["to", "cc"]))
413 |> Map.put("attributedTo", user.ap_id())
414 |> Transmogrifier.fix_object()
415
416 ActivityPub.create(%{
417 to: params["to"],
418 actor: user,
419 context: object["context"],
420 object: object,
421 additional: Map.take(params, ["cc"])
422 })
423 end
424
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
429 {:ok, delete}
430 else
431 _ -> {:error, dgettext("errors", "Can't delete object")}
432 end
433 end
434
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
438 {:ok, activity}
439 else
440 _ -> {:error, dgettext("errors", "Can't like object")}
441 end
442 end
443
444 def handle_user_activity(_, _) do
445 {:error, dgettext("errors", "Unhandled activity type")}
446 end
447
448 def update_outbox(
449 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
450 %{"nickname" => nickname} = params
451 ) do
452 actor = user.ap_id()
453
454 params =
455 params
456 |> Map.drop(["id"])
457 |> Map.put("actor", actor)
458 |> Transmogrifier.fix_addressing()
459
460 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
461 conn
462 |> put_status(:created)
463 |> put_resp_header("location", activity.data["id"])
464 |> json(activity.data)
465 else
466 {:error, message} ->
467 conn
468 |> put_status(:bad_request)
469 |> json(message)
470 end
471 end
472
473 def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
474 err =
475 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
476 nickname: nickname,
477 as_nickname: user.nickname
478 )
479
480 conn
481 |> put_status(:forbidden)
482 |> json(err)
483 end
484
485 def errors(conn, {:error, :not_found}) do
486 conn
487 |> put_status(:not_found)
488 |> json(dgettext("errors", "Not found"))
489 end
490
491 def errors(conn, _e) do
492 conn
493 |> put_status(:internal_server_error)
494 |> json(dgettext("errors", "error"))
495 end
496
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)
501 end
502
503 conn
504 end
505
506 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
507 {:ok, new_user} = User.ensure_keys_present(user)
508
509 for_user =
510 if new_user != user and match?(%User{}, for_user) do
511 User.get_cached_by_nickname(for_user.nickname)
512 else
513 for_user
514 end
515
516 {new_user, for_user}
517 end
518
519 # TODO: Add support for "object" field
520 @doc """
521 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
522
523 Parameters:
524 - (required) `file`: data of the media
525 - (optionnal) `description`: description of the media, intended for accessibility
526
527 Response:
528 - HTTP Code: 201 Created
529 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
530 """
531 def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
532 with {:ok, object} <-
533 ActivityPub.upload(
534 file,
535 actor: User.ap_id(user),
536 description: Map.get(data, "description")
537 ) do
538 Logger.debug(inspect(object))
539
540 conn
541 |> put_status(:created)
542 |> json(object.data)
543 end
544 end
545 end