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