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