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