Merge branch 'develop' into 'remove-twitter-api'
[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(
400 %User{} = user,
401 %{"type" => "Create", "object" => %{"type" => "Note"}} = params
402 ) 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 defp 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_data, _} <- Builder.delete(user, object.data["id"]),
422 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
423 {:ok, delete}
424 else
425 _ -> {:error, dgettext("errors", "Can't delete object")}
426 end
427 end
428
429 defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
430 with %Object{} = object <- Object.normalize(params["object"]),
431 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
432 {_, {:ok, %Activity{} = activity, _meta}} <-
433 {:common_pipeline,
434 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
435 {:ok, activity}
436 else
437 _ -> {:error, dgettext("errors", "Can't like object")}
438 end
439 end
440
441 defp handle_user_activity(_, _) do
442 {:error, dgettext("errors", "Unhandled activity type")}
443 end
444
445 def update_outbox(
446 %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
447 %{"nickname" => nickname} = params
448 ) do
449 actor = user.ap_id()
450
451 params =
452 params
453 |> Map.drop(["id"])
454 |> Map.put("actor", actor)
455 |> Transmogrifier.fix_addressing()
456
457 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
458 conn
459 |> put_status(:created)
460 |> put_resp_header("location", activity.data["id"])
461 |> json(activity.data)
462 else
463 {:error, message} ->
464 conn
465 |> put_status(:bad_request)
466 |> json(message)
467 end
468 end
469
470 def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
471 err =
472 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
473 nickname: nickname,
474 as_nickname: user.nickname
475 )
476
477 conn
478 |> put_status(:forbidden)
479 |> json(err)
480 end
481
482 defp errors(conn, {:error, :not_found}) do
483 conn
484 |> put_status(:not_found)
485 |> json(dgettext("errors", "Not found"))
486 end
487
488 defp errors(conn, _e) do
489 conn
490 |> put_status(:internal_server_error)
491 |> json(dgettext("errors", "error"))
492 end
493
494 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
495 with actor <- conn.params["actor"],
496 true <- is_binary(actor) do
497 Pleroma.Instances.set_reachable(actor)
498 end
499
500 conn
501 end
502
503 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
504 {:ok, new_user} = User.ensure_keys_present(user)
505
506 for_user =
507 if new_user != user and match?(%User{}, for_user) do
508 User.get_cached_by_nickname(for_user.nickname)
509 else
510 for_user
511 end
512
513 {new_user, for_user}
514 end
515
516 # TODO: Add support for "object" field
517 @doc """
518 Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
519
520 Parameters:
521 - (required) `file`: data of the media
522 - (optionnal) `description`: description of the media, intended for accessibility
523
524 Response:
525 - HTTP Code: 201 Created
526 - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
527 """
528 def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
529 with {:ok, object} <-
530 ActivityPub.upload(
531 file,
532 actor: User.ap_id(user),
533 description: Map.get(data, "description")
534 ) do
535 Logger.debug(inspect(object))
536
537 conn
538 |> put_status(:created)
539 |> json(object.data)
540 end
541 end
542 end