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