added tests for ActivityPub.like\unlike
[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.Object
10 alias Pleroma.Object.Fetcher
11 alias Pleroma.User
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.InternalFetchActor
14 alias Pleroma.Web.ActivityPub.ObjectView
15 alias Pleroma.Web.ActivityPub.Relay
16 alias Pleroma.Web.ActivityPub.Transmogrifier
17 alias Pleroma.Web.ActivityPub.UserView
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.ActivityPub.Visibility
20 alias Pleroma.Web.Federator
21
22 require Logger
23
24 action_fallback(:errors)
25
26 plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
27 plug(:set_requester_reachable when action in [:inbox])
28 plug(:relay_active? when action in [:relay])
29
30 def relay_active?(conn, _) do
31 if Pleroma.Config.get([:instance, :allow_relay]) do
32 conn
33 else
34 conn
35 |> render_error(:not_found, "not found")
36 |> halt()
37 end
38 end
39
40 def user(conn, %{"nickname" => nickname}) do
41 with %User{} = user <- User.get_cached_by_nickname(nickname),
42 {:ok, user} <- User.ensure_keys_present(user) do
43 conn
44 |> put_resp_content_type("application/activity+json")
45 |> json(UserView.render("user.json", %{user: user}))
46 else
47 nil -> {:error, :not_found}
48 end
49 end
50
51 def object(conn, %{"uuid" => uuid}) do
52 with ap_id <- o_status_url(conn, :object, uuid),
53 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
54 {_, true} <- {:public?, Visibility.is_public?(object)} do
55 conn
56 |> put_resp_content_type("application/activity+json")
57 |> json(ObjectView.render("object.json", %{object: object}))
58 else
59 {:public?, false} ->
60 {:error, :not_found}
61 end
62 end
63
64 def object_likes(conn, %{"uuid" => uuid, "page" => page}) 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)},
68 likes <- Utils.get_object_likes(object) do
69 {page, _} = Integer.parse(page)
70
71 conn
72 |> put_resp_content_type("application/activity+json")
73 |> json(ObjectView.render("likes.json", ap_id, likes, page))
74 else
75 {:public?, false} ->
76 {:error, :not_found}
77 end
78 end
79
80 def object_likes(conn, %{"uuid" => uuid}) do
81 with ap_id <- o_status_url(conn, :object, uuid),
82 %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
83 {_, true} <- {:public?, Visibility.is_public?(object)},
84 likes <- Utils.get_object_likes(object) do
85 conn
86 |> put_resp_content_type("application/activity+json")
87 |> json(ObjectView.render("likes.json", ap_id, likes))
88 else
89 {:public?, false} ->
90 {:error, :not_found}
91 end
92 end
93
94 def activity(conn, %{"uuid" => uuid}) do
95 with ap_id <- o_status_url(conn, :activity, uuid),
96 %Activity{} = activity <- Activity.normalize(ap_id),
97 {_, true} <- {:public?, Visibility.is_public?(activity)} do
98 conn
99 |> put_resp_content_type("application/activity+json")
100 |> json(ObjectView.render("object.json", %{object: activity}))
101 else
102 {:public?, false} ->
103 {:error, :not_found}
104 end
105 end
106
107 # GET /relay/following
108 def following(%{assigns: %{relay: true}} = conn, _params) do
109 conn
110 |> put_resp_content_type("application/activity+json")
111 |> json(UserView.render("following.json", %{user: Relay.get_actor()}))
112 end
113
114 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
115 with %User{} = user <- User.get_cached_by_nickname(nickname),
116 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
117 {:show_follows, true} <-
118 {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do
119 {page, _} = Integer.parse(page)
120
121 conn
122 |> put_resp_content_type("application/activity+json")
123 |> json(UserView.render("following.json", %{user: user, page: page, for: for_user}))
124 else
125 {:show_follows, _} ->
126 conn
127 |> put_resp_content_type("application/activity+json")
128 |> send_resp(403, "")
129 end
130 end
131
132 def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
133 with %User{} = user <- User.get_cached_by_nickname(nickname),
134 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
135 conn
136 |> put_resp_content_type("application/activity+json")
137 |> json(UserView.render("following.json", %{user: user, for: for_user}))
138 end
139 end
140
141 # GET /relay/followers
142 def followers(%{assigns: %{relay: true}} = conn, _params) do
143 conn
144 |> put_resp_content_type("application/activity+json")
145 |> json(UserView.render("followers.json", %{user: Relay.get_actor()}))
146 end
147
148 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
149 with %User{} = user <- User.get_cached_by_nickname(nickname),
150 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
151 {:show_followers, true} <-
152 {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do
153 {page, _} = Integer.parse(page)
154
155 conn
156 |> put_resp_content_type("application/activity+json")
157 |> json(UserView.render("followers.json", %{user: user, page: page, for: for_user}))
158 else
159 {:show_followers, _} ->
160 conn
161 |> put_resp_content_type("application/activity+json")
162 |> send_resp(403, "")
163 end
164 end
165
166 def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
167 with %User{} = user <- User.get_cached_by_nickname(nickname),
168 {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
169 conn
170 |> put_resp_content_type("application/activity+json")
171 |> json(UserView.render("followers.json", %{user: user, for: for_user}))
172 end
173 end
174
175 def outbox(conn, %{"nickname" => nickname} = params) do
176 with %User{} = user <- User.get_cached_by_nickname(nickname),
177 {:ok, user} <- User.ensure_keys_present(user) do
178 conn
179 |> put_resp_content_type("application/activity+json")
180 |> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
181 end
182 end
183
184 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
185 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
186 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
187 true <- Utils.recipient_in_message(recipient, actor, params),
188 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
189 Federator.incoming_ap_doc(params)
190 json(conn, "ok")
191 end
192 end
193
194 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
195 Federator.incoming_ap_doc(params)
196 json(conn, "ok")
197 end
198
199 # only accept relayed Creates
200 def inbox(conn, %{"type" => "Create"} = params) do
201 Logger.info(
202 "Signature missing or not from author, relayed Create message, fetching object from source"
203 )
204
205 Fetcher.fetch_object_from_id(params["object"]["id"])
206
207 json(conn, "ok")
208 end
209
210 def inbox(conn, params) do
211 headers = Enum.into(conn.req_headers, %{})
212
213 if String.contains?(headers["signature"], params["actor"]) do
214 Logger.info(
215 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
216 )
217
218 Logger.info(inspect(conn.req_headers))
219 end
220
221 json(conn, dgettext("errors", "error"))
222 end
223
224 defp represent_service_actor(%User{} = user, conn) do
225 with {:ok, user} <- User.ensure_keys_present(user) do
226 conn
227 |> put_resp_content_type("application/activity+json")
228 |> json(UserView.render("user.json", %{user: user}))
229 else
230 nil -> {:error, :not_found}
231 end
232 end
233
234 defp represent_service_actor(nil, _), do: {:error, :not_found}
235
236 def relay(conn, _params) do
237 Relay.get_actor()
238 |> represent_service_actor(conn)
239 end
240
241 def internal_fetch(conn, _params) do
242 InternalFetchActor.get_actor()
243 |> represent_service_actor(conn)
244 end
245
246 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
247 conn
248 |> put_resp_content_type("application/activity+json")
249 |> json(UserView.render("user.json", %{user: user}))
250 end
251
252 def whoami(_conn, _params), do: {:error, :not_found}
253
254 def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do
255 if nickname == user.nickname do
256 conn
257 |> put_resp_content_type("application/activity+json")
258 |> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]}))
259 else
260 err =
261 dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
262 nickname: nickname,
263 as_nickname: user.nickname
264 )
265
266 conn
267 |> put_status(:forbidden)
268 |> json(err)
269 end
270 end
271
272 def handle_user_activity(user, %{"type" => "Create"} = params) do
273 object =
274 params["object"]
275 |> Map.merge(Map.take(params, ["to", "cc"]))
276 |> Map.put("attributedTo", user.ap_id())
277 |> Transmogrifier.fix_object()
278
279 ActivityPub.create(%{
280 to: params["to"],
281 actor: user,
282 context: object["context"],
283 object: object,
284 additional: Map.take(params, ["cc"])
285 })
286 end
287
288 def handle_user_activity(user, %{"type" => "Delete"} = params) do
289 with %Object{} = object <- Object.normalize(params["object"]),
290 true <- user.info.is_moderator || user.ap_id == object.data["actor"],
291 {:ok, delete} <- ActivityPub.delete(object) do
292 {:ok, delete}
293 else
294 _ -> {:error, dgettext("errors", "Can't delete object")}
295 end
296 end
297
298 def handle_user_activity(user, %{"type" => "Like"} = params) do
299 with %Object{} = object <- Object.normalize(params["object"]),
300 {:ok, activity, _object} <- ActivityPub.like(user, object) do
301 {:ok, activity}
302 else
303 _ -> {:error, dgettext("errors", "Can't like object")}
304 end
305 end
306
307 def handle_user_activity(_, _) do
308 {:error, dgettext("errors", "Unhandled activity type")}
309 end
310
311 def update_outbox(
312 %{assigns: %{user: %User{nickname: user_nickname} = user}} = conn,
313 %{"nickname" => nickname} = params
314 )
315 when user_nickname == nickname do
316 actor = user.ap_id()
317
318 params =
319 params
320 |> Map.drop(["id"])
321 |> Map.put("actor", actor)
322 |> Transmogrifier.fix_addressing()
323
324 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
325 conn
326 |> put_status(:created)
327 |> put_resp_header("location", activity.data["id"])
328 |> json(activity.data)
329 else
330 {:error, message} ->
331 conn
332 |> put_status(:bad_request)
333 |> json(message)
334 end
335 end
336
337 def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
338 err =
339 dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
340 nickname: nickname,
341 as_nickname: user.nickname
342 )
343
344 conn
345 |> put_status(:forbidden)
346 |> json(err)
347 end
348
349 def errors(conn, {:error, :not_found}) do
350 conn
351 |> put_status(:not_found)
352 |> json(dgettext("errors", "Not found"))
353 end
354
355 def errors(conn, _e) do
356 conn
357 |> put_status(:internal_server_error)
358 |> json(dgettext("errors", "error"))
359 end
360
361 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
362 with actor <- conn.params["actor"],
363 true <- is_binary(actor) do
364 Pleroma.Instances.set_reachable(actor)
365 end
366
367 conn
368 end
369
370 defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
371 {:ok, new_user} = User.ensure_keys_present(user)
372
373 for_user =
374 if new_user != user and match?(%User{}, for_user) do
375 User.get_cached_by_nickname(for_user.nickname)
376 else
377 for_user
378 end
379
380 {new_user, for_user}
381 end
382 end