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