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