Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma 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.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 # "include_poll_votes" is a hack because postgres generates inefficient
235 # queries when filtering by 'Answer', poll votes will be hidden by the
236 # visibility filter in this case anyway
237 params =
238 params
239 |> Map.drop(["nickname", "page"])
240 |> Map.put("include_poll_votes", true)
241
242 activities = ActivityPub.fetch_user_activities(user, for_user, params)
243
244 conn
245 |> put_resp_content_type("application/activity+json")
246 |> put_view(UserView)
247 |> render("activity_collection_page.json", %{
248 activities: activities,
249 pagination: ControllerHelper.get_pagination_fields(conn, activities),
250 iri: "#{user.ap_id}/outbox"
251 })
252 end
253 end
254
255 def outbox(conn, %{"nickname" => nickname}) do
256 with %User{} = user <- User.get_cached_by_nickname(nickname),
257 {:ok, user} <- User.ensure_keys_present(user) do
258 conn
259 |> put_resp_content_type("application/activity+json")
260 |> put_view(UserView)
261 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
262 end
263 end
264
265 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
266 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
267 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
268 true <- Utils.recipient_in_message(recipient, actor, params),
269 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
270 Federator.incoming_ap_doc(params)
271 json(conn, "ok")
272 end
273 end
274
275 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
276 Federator.incoming_ap_doc(params)
277 json(conn, "ok")
278 end
279
280 # POST /relay/inbox -or- POST /internal/fetch/inbox
281 def inbox(conn, params) do
282 if params["type"] == "Create" && FederatingPlug.federating?() do
283 post_inbox_relayed_create(conn, params)
284 else
285 post_inbox_fallback(conn, params)
286 end
287 end
288
289 defp post_inbox_relayed_create(conn, params) do
290 Logger.debug(
291 "Signature missing or not from author, relayed Create message, fetching object from source"
292 )
293
294 Fetcher.fetch_object_from_id(params["object"]["id"])
295
296 json(conn, "ok")
297 end
298
299 defp post_inbox_fallback(conn, params) do
300 headers = Enum.into(conn.req_headers, %{})
301
302 if headers["signature"] && params["actor"] &&
303 String.contains?(headers["signature"], params["actor"]) do
304 Logger.debug(
305 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
306 )
307
308 Logger.debug(inspect(conn.req_headers))
309 end
310
311 conn
312 |> put_status(:bad_request)
313 |> json(dgettext("errors", "error"))
314 end
315
316 defp represent_service_actor(%User{} = user, conn) do
317 with {:ok, user} <- User.ensure_keys_present(user) do
318 conn
319 |> put_resp_content_type("application/activity+json")
320 |> put_view(UserView)
321 |> render("user.json", %{user: user})
322 else
323 nil -> {:error, :not_found}
324 end
325 end
326
327 defp represent_service_actor(nil, _), do: {:error, :not_found}
328
329 def relay(conn, _params) do
330 Relay.get_actor()
331 |> represent_service_actor(conn)
332 end
333
334 def internal_fetch(conn, _params) do
335 InternalFetchActor.get_actor()
336 |> represent_service_actor(conn)
337 end
338
339 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
340 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
341 conn
342 |> put_resp_content_type("application/activity+json")
343 |> put_view(UserView)
344 |> render("user.json", %{user: user})
345 end
346
347 def read_inbox(
348 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
349 %{"nickname" => nickname, "page" => page?} = params
350 )
351 when page? in [true, "true"] do
352 params =
353 params
354 |> Map.drop(["nickname", "page"])
355 |> Map.put("blocking_user", user)
356 |> Map.put("user", user)
357
358 activities =
359 [user.ap_id | User.following(user)]
360 |> ActivityPub.fetch_activities(params)
361 |> Enum.reverse()
362
363 conn
364 |> put_resp_content_type("application/activity+json")
365 |> put_view(UserView)
366 |> render("activity_collection_page.json", %{
367 activities: activities,
368 pagination: ControllerHelper.get_pagination_fields(conn, activities),
369 iri: "#{user.ap_id}/inbox"
370 })
371 end
372
373 def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
374 "nickname" => nickname
375 }) do
376 with {:ok, user} <- User.ensure_keys_present(user) do
377 conn
378 |> put_resp_content_type("application/activity+json")
379 |> put_view(UserView)
380 |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
381 end
382 end
383
384 def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
385 "nickname" => nickname
386 }) do
387 err =
388 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
389 nickname: nickname,
390 as_nickname: as_nickname
391 )
392
393 conn
394 |> put_status(:forbidden)
395 |> json(err)
396 end
397
398 defp handle_user_activity(
399 %User{} = user,
400 %{"type" => "Create", "object" => %{"type" => "Note"}} = params
401 ) do
402 object =
403 params["object"]
404 |> Map.merge(Map.take(params, ["to", "cc"]))
405 |> Map.put("attributedTo", user.ap_id())
406 |> Transmogrifier.fix_object()
407
408 ActivityPub.create(%{
409 to: params["to"],
410 actor: user,
411 context: object["context"],
412 object: object,
413 additional: Map.take(params, ["cc"])
414 })
415 end
416
417 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
418 with %Object{} = object <- Object.normalize(params["object"]),
419 true <- user.is_moderator || user.ap_id == object.data["actor"],
420 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
421 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
422 {:ok, delete}
423 else
424 _ -> {:error, dgettext("errors", "Can't delete object")}
425 end
426 end
427
428 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
429 with %Object{} = object <- Object.normalize(params["object"]),
430 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
431 {_, {:ok, %Activity{} = activity, _meta}} <-
432 {:common_pipeline,
433 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
434 {:ok, activity}
435 else
436 _ -> {:error, dgettext("errors", "Can't like object")}
437 end
438 end
439
440 defp handle_user_activity(_, _) do
441 {:error, dgettext("errors", "Unhandled activity type")}
442 end
443
444 def update_outbox(
445 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
446 %{"nickname" => nickname} = params
447 ) do
448 actor = user.ap_id()
449
450 params =
451 params
452 |> Map.drop(["id"])
453 |> Map.put("actor", actor)
454 |> Transmogrifier.fix_addressing()
455
456 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
457 conn
458 |> put_status(:created)
459 |> put_resp_header("location", activity.data["id"])
460 |> json(activity.data)
461 else
462 {:error, message} ->
463 conn
464 |> put_status(:bad_request)
465 |> json(message)
466 end
467 end
468
469 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
470 err =
471 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
472 nickname: nickname,
473 as_nickname: user.nickname
474 )
475
476 conn
477 |> put_status(:forbidden)
478 |> json(err)
479 end
480
481 defp errors(conn, {:error, :not_found}) do
482 conn
483 |> put_status(:not_found)
484 |> json(dgettext("errors", "Not found"))
485 end
486
487 defp errors(conn, _e) do
488 conn
489 |> put_status(:internal_server_error)
490 |> json(dgettext("errors", "error"))
491 end
492
493 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
494 with actor <- conn.params["actor"],
495 true <- is_binary(actor) do
496 Pleroma.Instances.set_reachable(actor)
497 end
498
499 conn
500 end
501
502 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
503 {:ok, new_user} = User.ensure_keys_present(user)
504
505 for_user =
506 if new_user != user and match?(%User{}, for_user) do
507 User.get_cached_by_nickname(for_user.nickname)
508 else
509 for_user
510 end
511
512 {new_user, for_user}
513 end
514
515 # TODO: Add support for "object" field
516 @doc """
517 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
518
519 Parameters:
520 - (required) `file`: data of the media
521 - (optionnal) `description`: description of the media, intended for accessibility
522
523 Response:
524 - HTTP Code: 201 Created
525 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
526 """
527 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
528 with {:ok, object} <-
529 ActivityPub.upload(
530 file,
531 actor: User.ap_id(user),
532 description: Map.get(data, "description")
533 ) do
534 Logger.debug(inspect(object))
535
536 conn
537 |> put_status(:created)
538 |> json(object.data)
539 end
540 end
541 end