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