Merge remote-tracking branch 'origin/develop' into reactions
[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(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])
36
37 def relay_active?(conn, _) do
38 if Pleroma.Config.get([:instance, :allow_relay]) do
39 conn
40 else
41 conn
42 |> render_error(:not_found, "not found")
43 |> halt()
44 end
45 end
46
47 def user(conn, %{"nickname" => nickname}) do
48 with %User{} = user <- User.get_cached_by_nickname(nickname),
49 {:ok, user} <- User.ensure_keys_present(user) do
50 conn
51 |> put_resp_content_type("application/activity+json")
52 |> put_view(UserView)
53 |> render("user.json", %{user: user})
54 else
55 nil -> {:error, :not_found}
56 end
57 end
58
59 def object(conn, %{"uuid" => uuid}) do
60 with ap_id <- o_status_url(conn, :object, uuid),
61 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
62 {_, true} <- {:public?, Visibility.is_public?(object)} do
63 conn
64 |> assign(:tracking_fun_data, object.id)
65 |> set_cache_ttl_for(object)
66 |> put_resp_content_type("application/activity+json")
67 |> put_view(ObjectView)
68 |> render("object.json", object: object)
69 else
70 {:public?, false} ->
71 {:error, :not_found}
72 end
73 end
74
75 def track_object_fetch(conn, nil), do: conn
76
77 def track_object_fetch(conn, object_id) do
78 with %{assigns: %{user: %User{id: user_id}}} <- conn do
79 Delivery.create(object_id, user_id)
80 end
81
82 conn
83 end
84
85 def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
86 with ap_id <- o_status_url(conn, :object, uuid),
87 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
88 {_, true} <- {:public?, Visibility.is_public?(object)},
89 likes <- Utils.get_object_likes(object) do
90 {page, _} = Integer.parse(page)
91
92 conn
93 |> put_resp_content_type("application/activity+json")
94 |> put_view(ObjectView)
95 |> render("likes.json", %{ap_id: ap_id, likes: likes, page: page})
96 else
97 {:public?, false} ->
98 {:error, :not_found}
99 end
100 end
101
102 def object_likes(conn, %{"uuid" => uuid}) do
103 with ap_id <- o_status_url(conn, :object, uuid),
104 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
105 {_, true} <- {:public?, Visibility.is_public?(object)},
106 likes <- Utils.get_object_likes(object) do
107 conn
108 |> put_resp_content_type("application/activity+json")
109 |> put_view(ObjectView)
110 |> render("likes.json", %{ap_id: ap_id, likes: likes})
111 else
112 {:public?, false} ->
113 {:error, :not_found}
114 end
115 end
116
117 def activity(conn, %{"uuid" => uuid}) do
118 with ap_id <- o_status_url(conn, :activity, uuid),
119 %Activity{} = activity <- Activity.normalize(ap_id),
120 {_, true} <- {:public?, Visibility.is_public?(activity)} do
121 conn
122 |> maybe_set_tracking_data(activity)
123 |> set_cache_ttl_for(activity)
124 |> put_resp_content_type("application/activity+json")
125 |> put_view(ObjectView)
126 |> render("object.json", object: activity)
127 else
128 {:public?, false} -> {:error, :not_found}
129 nil -> {:error, :not_found}
130 end
131 end
132
133 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
134 object_id = Object.normalize(activity).id
135 assign(conn, :tracking_fun_data, object_id)
136 end
137
138 defp maybe_set_tracking_data(conn, _activity), do: conn
139
140 defp set_cache_ttl_for(conn, %Activity{object: object}) do
141 set_cache_ttl_for(conn, object)
142 end
143
144 defp set_cache_ttl_for(conn, entity) do
145 ttl =
146 case entity do
147 %Object{data: %{"type" => "Question"}} ->
148 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
149
150 %Object{} ->
151 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
152
153 _ ->
154 nil
155 end
156
157 assign(conn, :cache_ttl, ttl)
158 end
159
160 # GET /relay/following
161 def following(%{assigns: %{relay: true}} = conn, _params) do
162 conn
163 |> put_resp_content_type("application/activity+json")
164 |> put_view(UserView)
165 |> render("following.json", %{user: Relay.get_actor()})
166 end
167
168 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
169 with %User{} = user <- User.get_cached_by_nickname(nickname),
170 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
171 {:show_follows, true} <-
172 {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do
173 {page, _} = Integer.parse(page)
174
175 conn
176 |> put_resp_content_type("application/activity+json")
177 |> put_view(UserView)
178 |> render("following.json", %{user: user, page: page, for: for_user})
179 else
180 {:show_follows, _} ->
181 conn
182 |> put_resp_content_type("application/activity+json")
183 |> send_resp(403, "")
184 end
185 end
186
187 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
188 with %User{} = user <- User.get_cached_by_nickname(nickname),
189 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
190 conn
191 |> put_resp_content_type("application/activity+json")
192 |> put_view(UserView)
193 |> render("following.json", %{user: user, for: for_user})
194 end
195 end
196
197 # GET /relay/followers
198 def followers(%{assigns: %{relay: true}} = conn, _params) do
199 conn
200 |> put_resp_content_type("application/activity+json")
201 |> put_view(UserView)
202 |> render("followers.json", %{user: Relay.get_actor()})
203 end
204
205 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
206 with %User{} = user <- User.get_cached_by_nickname(nickname),
207 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
208 {:show_followers, true} <-
209 {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do
210 {page, _} = Integer.parse(page)
211
212 conn
213 |> put_resp_content_type("application/activity+json")
214 |> put_view(UserView)
215 |> render("followers.json", %{user: user, page: page, for: for_user})
216 else
217 {:show_followers, _} ->
218 conn
219 |> put_resp_content_type("application/activity+json")
220 |> send_resp(403, "")
221 end
222 end
223
224 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
225 with %User{} = user <- User.get_cached_by_nickname(nickname),
226 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
227 conn
228 |> put_resp_content_type("application/activity+json")
229 |> put_view(UserView)
230 |> render("followers.json", %{user: user, for: for_user})
231 end
232 end
233
234 def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
235 when page? in [true, "true"] do
236 with %User{} = user <- User.get_cached_by_nickname(nickname),
237 {:ok, user} <- User.ensure_keys_present(user) do
238 activities =
239 if params["max_id"] do
240 ActivityPub.fetch_user_activities(user, nil, %{
241 "max_id" => params["max_id"],
242 # This is a hack because postgres generates inefficient queries when filtering by
243 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
244 "include_poll_votes" => true,
245 "limit" => 10
246 })
247 else
248 ActivityPub.fetch_user_activities(user, nil, %{
249 "limit" => 10,
250 "include_poll_votes" => true
251 })
252 end
253
254 conn
255 |> put_resp_content_type("application/activity+json")
256 |> put_view(UserView)
257 |> render("activity_collection_page.json", %{
258 activities: activities,
259 iri: "#{user.ap_id}/outbox"
260 })
261 end
262 end
263
264 def outbox(conn, %{"nickname" => nickname}) do
265 with %User{} = user <- User.get_cached_by_nickname(nickname),
266 {:ok, user} <- User.ensure_keys_present(user) do
267 conn
268 |> put_resp_content_type("application/activity+json")
269 |> put_view(UserView)
270 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
271 end
272 end
273
274 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
275 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
276 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
277 true <- Utils.recipient_in_message(recipient, actor, params),
278 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
279 Federator.incoming_ap_doc(params)
280 json(conn, "ok")
281 end
282 end
283
284 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
285 Federator.incoming_ap_doc(params)
286 json(conn, "ok")
287 end
288
289 # only accept relayed Creates
290 def inbox(conn, %{"type" => "Create"} = params) do
291 Logger.info(
292 "Signature missing or not from author, relayed Create message, fetching object from source"
293 )
294
295 Fetcher.fetch_object_from_id(params["object"]["id"])
296
297 json(conn, "ok")
298 end
299
300 def inbox(conn, params) do
301 headers = Enum.into(conn.req_headers, %{})
302
303 if String.contains?(headers["signature"], params["actor"]) do
304 Logger.info(
305 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
306 )
307
308 Logger.info(inspect(conn.req_headers))
309 end
310
311 json(conn, dgettext("errors", "error"))
312 end
313
314 defp represent_service_actor(%User{} = user, conn) do
315 with {:ok, user} <- User.ensure_keys_present(user) do
316 conn
317 |> put_resp_content_type("application/activity+json")
318 |> put_view(UserView)
319 |> render("user.json", %{user: user})
320 else
321 nil -> {:error, :not_found}
322 end
323 end
324
325 defp represent_service_actor(nil, _), do: {:error, :not_found}
326
327 def relay(conn, _params) do
328 Relay.get_actor()
329 |> represent_service_actor(conn)
330 end
331
332 def internal_fetch(conn, _params) do
333 InternalFetchActor.get_actor()
334 |> represent_service_actor(conn)
335 end
336
337 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
338 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
339 conn
340 |> put_resp_content_type("application/activity+json")
341 |> put_view(UserView)
342 |> render("user.json", %{user: user})
343 end
344
345 def whoami(_conn, _params), do: {:error, :not_found}
346
347 def read_inbox(
348 %{assigns: %{user: %{nickname: nickname} = user}} = conn,
349 %{"nickname" => nickname, "page" => page?} = params
350 )
351 when page? in [true, "true"] do
352 activities =
353 if params["max_id"] do
354 ActivityPub.fetch_activities([user.ap_id | user.following], %{
355 "max_id" => params["max_id"],
356 "limit" => 10
357 })
358 else
359 ActivityPub.fetch_activities([user.ap_id | user.following], %{"limit" => 10})
360 end
361
362 conn
363 |> put_resp_content_type("application/activity+json")
364 |> put_view(UserView)
365 |> render("activity_collection_page.json", %{
366 activities: activities,
367 iri: "#{user.ap_id}/inbox"
368 })
369 end
370
371 def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
372 "nickname" => nickname
373 }) do
374 with {:ok, user} <- User.ensure_keys_present(user) do
375 conn
376 |> put_resp_content_type("application/activity+json")
377 |> put_view(UserView)
378 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
379 end
380 end
381
382 def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
383 err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
384
385 conn
386 |> put_status(:forbidden)
387 |> json(err)
388 end
389
390 def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
391 "nickname" => nickname
392 }) do
393 err =
394 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
395 nickname: nickname,
396 as_nickname: as_nickname
397 )
398
399 conn
400 |> put_status(:forbidden)
401 |> json(err)
402 end
403
404 def handle_user_activity(user, %{"type" => "Create"} = params) do
405 object =
406 params["object"]
407 |> Map.merge(Map.take(params, ["to", "cc"]))
408 |> Map.put("attributedTo", user.ap_id())
409 |> Transmogrifier.fix_object()
410
411 ActivityPub.create(%{
412 to: params["to"],
413 actor: user,
414 context: object["context"],
415 object: object,
416 additional: Map.take(params, ["cc"])
417 })
418 end
419
420 def handle_user_activity(user, %{"type" => "Delete"} = params) do
421 with %Object{} = object <- Object.normalize(params["object"]),
422 true <- user.info.is_moderator || user.ap_id == object.data["actor"],
423 {:ok, delete} <- ActivityPub.delete(object) do
424 {:ok, delete}
425 else
426 _ -> {:error, dgettext("errors", "Can't delete object")}
427 end
428 end
429
430 def handle_user_activity(user, %{"type" => "Like"} = params) do
431 with %Object{} = object <- Object.normalize(params["object"]),
432 {:ok, activity, _object} <- ActivityPub.like(user, object) do
433 {:ok, activity}
434 else
435 _ -> {:error, dgettext("errors", "Can't like object")}
436 end
437 end
438
439 def handle_user_activity(_, _) do
440 {:error, dgettext("errors", "Unhandled activity type")}
441 end
442
443 def update_outbox(
444 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
445 %{"nickname" => nickname} = params
446 ) do
447 actor = user.ap_id()
448
449 params =
450 params
451 |> Map.drop(["id"])
452 |> Map.put("actor", actor)
453 |> Transmogrifier.fix_addressing()
454
455 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
456 conn
457 |> put_status(:created)
458 |> put_resp_header("location", activity.data["id"])
459 |> json(activity.data)
460 else
461 {:error, message} ->
462 conn
463 |> put_status(:bad_request)
464 |> json(message)
465 end
466 end
467
468 def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
469 err =
470 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
471 nickname: nickname,
472 as_nickname: user.nickname
473 )
474
475 conn
476 |> put_status(:forbidden)
477 |> json(err)
478 end
479
480 def errors(conn, {:error, :not_found}) do
481 conn
482 |> put_status(:not_found)
483 |> json(dgettext("errors", "Not found"))
484 end
485
486 def errors(conn, _e) do
487 conn
488 |> put_status(:internal_server_error)
489 |> json(dgettext("errors", "error"))
490 end
491
492 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
493 with actor <- conn.params["actor"],
494 true <- is_binary(actor) do
495 Pleroma.Instances.set_reachable(actor)
496 end
497
498 conn
499 end
500
501 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
502 {:ok, new_user} = User.ensure_keys_present(user)
503
504 for_user =
505 if new_user != user and match?(%User{}, for_user) do
506 User.get_cached_by_nickname(for_user.nickname)
507 else
508 for_user
509 end
510
511 {new_user, for_user}
512 end
513
514 # TODO: Add support for "object" field
515 @doc """
516 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
517
518 Parameters:
519 - (required) `file`: data of the media
520 - (optionnal) `description`: description of the media, intended for accessibility
521
522 Response:
523 - HTTP Code: 201 Created
524 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
525 """
526 def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
527 with {:ok, object} <-
528 ActivityPub.upload(
529 file,
530 actor: User.ap_id(user),
531 description: Map.get(data, "description")
532 ) do
533 Logger.debug(inspect(object))
534
535 conn
536 |> put_status(:created)
537 |> json(object.data)
538 end
539 end
540 end