e68d0763e99140da64af86f0736d66354b6fbf8a
[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?/0] when action not in @federating_only_actions
38 )
39
40 plug(
41 EnsureAuthenticatedPlug
42 when action in [:read_inbox, :update_outbox, :whoami, :upload_media, :following, :followers]
43 )
44
45 plug(
46 Pleroma.Plugs.Cache,
47 [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
48 when action in [:activity, :object]
49 )
50
51 plug(:set_requester_reachable when action in [:inbox])
52 plug(:relay_active? when action in [:relay])
53
54 defp relay_active?(conn, _) do
55 if Pleroma.Config.get([:instance, :allow_relay]) do
56 conn
57 else
58 conn
59 |> render_error(:not_found, "not found")
60 |> halt()
61 end
62 end
63
64 def user(conn, %{"nickname" => nickname}) do
65 with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
66 {:ok, user} <- User.ensure_keys_present(user) do
67 conn
68 |> put_resp_content_type("application/activity+json")
69 |> put_view(UserView)
70 |> render("user.json", %{user: user})
71 else
72 nil -> {:error, :not_found}
73 %{local: false} -> {:error, :not_found}
74 end
75 end
76
77 def object(conn, %{"uuid" => uuid}) do
78 with ap_id <- o_status_url(conn, :object, uuid),
79 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
80 {_, true} <- {:public?, Visibility.is_public?(object)} do
81 conn
82 |> assign(:tracking_fun_data, object.id)
83 |> set_cache_ttl_for(object)
84 |> put_resp_content_type("application/activity+json")
85 |> put_view(ObjectView)
86 |> render("object.json", object: object)
87 else
88 {:public?, false} ->
89 {:error, :not_found}
90 end
91 end
92
93 def track_object_fetch(conn, nil), do: conn
94
95 def track_object_fetch(conn, object_id) do
96 with %{assigns: %{user: %User{id: user_id}}} <- conn do
97 Delivery.create(object_id, user_id)
98 end
99
100 conn
101 end
102
103 def activity(conn, %{"uuid" => uuid}) do
104 with ap_id <- o_status_url(conn, :activity, uuid),
105 %Activity{} = activity <- Activity.normalize(ap_id),
106 {_, true} <- {:public?, Visibility.is_public?(activity)} do
107 conn
108 |> maybe_set_tracking_data(activity)
109 |> set_cache_ttl_for(activity)
110 |> put_resp_content_type("application/activity+json")
111 |> put_view(ObjectView)
112 |> render("object.json", object: activity)
113 else
114 {:public?, false} -> {:error, :not_found}
115 nil -> {:error, :not_found}
116 end
117 end
118
119 defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
120 object_id = Object.normalize(activity).id
121 assign(conn, :tracking_fun_data, object_id)
122 end
123
124 defp maybe_set_tracking_data(conn, _activity), do: conn
125
126 defp set_cache_ttl_for(conn, %Activity{object: object}) do
127 set_cache_ttl_for(conn, object)
128 end
129
130 defp set_cache_ttl_for(conn, entity) do
131 ttl =
132 case entity do
133 %Object{data: %{"type" => "Question"}} ->
134 Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
135
136 %Object{} ->
137 Pleroma.Config.get([:web_cache_ttl, :activity_pub])
138
139 _ ->
140 nil
141 end
142
143 assign(conn, :cache_ttl, ttl)
144 end
145
146 # GET /relay/following
147 def relay_following(conn, _params) do
148 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
149 conn
150 |> put_resp_content_type("application/activity+json")
151 |> put_view(UserView)
152 |> render("following.json", %{user: Relay.get_actor()})
153 end
154 end
155
156 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
157 with %User{} = user <- User.get_cached_by_nickname(nickname),
158 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
159 {:show_follows, true} <-
160 {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
161 {page, _} = Integer.parse(page)
162
163 conn
164 |> put_resp_content_type("application/activity+json")
165 |> put_view(UserView)
166 |> render("following.json", %{user: user, page: page, for: for_user})
167 else
168 {:show_follows, _} ->
169 conn
170 |> put_resp_content_type("application/activity+json")
171 |> send_resp(403, "")
172 end
173 end
174
175 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
176 with %User{} = user <- User.get_cached_by_nickname(nickname),
177 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
178 conn
179 |> put_resp_content_type("application/activity+json")
180 |> put_view(UserView)
181 |> render("following.json", %{user: user, for: for_user})
182 end
183 end
184
185 # GET /relay/followers
186 def relay_followers(conn, _params) do
187 with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
188 conn
189 |> put_resp_content_type("application/activity+json")
190 |> put_view(UserView)
191 |> render("followers.json", %{user: Relay.get_actor()})
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(
225 %{assigns: %{user: for_user}} = conn,
226 %{"nickname" => nickname, "page" => page?} = params
227 )
228 when page? in [true, "true"] do
229 with %User{} = user <- User.get_cached_by_nickname(nickname),
230 {:ok, user} <- User.ensure_keys_present(user) do
231 activities =
232 if params["max_id"] do
233 ActivityPub.fetch_user_activities(user, for_user, %{
234 "max_id" => params["max_id"],
235 # This is a hack because postgres generates inefficient queries when filtering by
236 # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
237 "include_poll_votes" => true,
238 "limit" => 10
239 })
240 else
241 ActivityPub.fetch_user_activities(user, for_user, %{
242 "limit" => 10,
243 "include_poll_votes" => true
244 })
245 end
246
247 conn
248 |> put_resp_content_type("application/activity+json")
249 |> put_view(UserView)
250 |> render("activity_collection_page.json", %{
251 activities: activities,
252 iri: "#{user.ap_id}/outbox"
253 })
254 end
255 end
256
257 def outbox(conn, %{"nickname" => nickname}) do
258 with %User{} = user <- User.get_cached_by_nickname(nickname),
259 {:ok, user} <- User.ensure_keys_present(user) do
260 conn
261 |> put_resp_content_type("application/activity+json")
262 |> put_view(UserView)
263 |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
264 end
265 end
266
267 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
268 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
269 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
270 true <- Utils.recipient_in_message(recipient, actor, params),
271 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
272 Federator.incoming_ap_doc(params)
273 json(conn, "ok")
274 end
275 end
276
277 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
278 Federator.incoming_ap_doc(params)
279 json(conn, "ok")
280 end
281
282 # POST /relay/inbox -or- POST /internal/fetch/inbox
283 def inbox(conn, params) do
284 if params["type"] == "Create" && FederatingPlug.federating?() do
285 post_inbox_relayed_create(conn, params)
286 else
287 post_inbox_fallback(conn, params)
288 end
289 end
290
291 defp post_inbox_relayed_create(conn, params) do
292 Logger.debug(
293 "Signature missing or not from author, relayed Create message, fetching object from source"
294 )
295
296 Fetcher.fetch_object_from_id(params["object"]["id"])
297
298 json(conn, "ok")
299 end
300
301 defp post_inbox_fallback(conn, params) do
302 headers = Enum.into(conn.req_headers, %{})
303
304 if headers["signature"] && params["actor"] &&
305 String.contains?(headers["signature"], params["actor"]) do
306 Logger.debug(
307 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
308 )
309
310 Logger.debug(inspect(conn.req_headers))
311 end
312
313 conn
314 |> put_status(:bad_request)
315 |> json(dgettext("errors", "error"))
316 end
317
318 defp represent_service_actor(%User{} = user, conn) do
319 with {:ok, user} <- User.ensure_keys_present(user) do
320 conn
321 |> put_resp_content_type("application/activity+json")
322 |> put_view(UserView)
323 |> render("user.json", %{user: user})
324 else
325 nil -> {:error, :not_found}
326 end
327 end
328
329 defp represent_service_actor(nil, _), do: {:error, :not_found}
330
331 def relay(conn, _params) do
332 Relay.get_actor()
333 |> represent_service_actor(conn)
334 end
335
336 def internal_fetch(conn, _params) do
337 InternalFetchActor.get_actor()
338 |> represent_service_actor(conn)
339 end
340
341 @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
342 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
343 conn
344 |> put_resp_content_type("application/activity+json")
345 |> put_view(UserView)
346 |> render("user.json", %{user: user})
347 end
348
349 def read_inbox(
350 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
351 %{"nickname" => nickname, "page" => page?} = params
352 )
353 when page? in [true, "true"] do
354 activities =
355 if params["max_id"] do
356 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
357 "max_id" => params["max_id"],
358 "limit" => 10
359 })
360 else
361 ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
362 end
363
364 conn
365 |> put_resp_content_type("application/activity+json")
366 |> put_view(UserView)
367 |> render("activity_collection_page.json", %{
368 activities: 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(%User{} = user, %{"type" => "Create"} = params) do
399 object =
400 params["object"]
401 |> Map.merge(Map.take(params, ["to", "cc"]))
402 |> Map.put("attributedTo", user.ap_id())
403 |> Transmogrifier.fix_object()
404
405 ActivityPub.create(%{
406 to: params["to"],
407 actor: user,
408 context: object["context"],
409 object: object,
410 additional: Map.take(params, ["cc"])
411 })
412 end
413
414 defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
415 with %Object{} = object <- Object.normalize(params["object"]),
416 true <- user.is_moderator || user.ap_id == object.data["actor"],
417 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
418 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
419 {:ok, delete}
420 else
421 _ -> {:error, dgettext("errors", "Can't delete object")}
422 end
423 end
424
425 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
426 with %Object{} = object <- Object.normalize(params["object"]),
427 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
428 {_, {:ok, %Activity{} = activity, _meta}} <-
429 {:common_pipeline,
430 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
431 {:ok, activity}
432 else
433 _ -> {:error, dgettext("errors", "Can't like object")}
434 end
435 end
436
437 defp 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 defp errors(conn, {:error, :not_found}) do
479 conn
480 |> put_status(:not_found)
481 |> json(dgettext("errors", "Not found"))
482 end
483
484 defp 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