Merge branch 'develop' into feature/database-compaction
[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.ObjectView
14 alias Pleroma.Web.ActivityPub.Relay
15 alias Pleroma.Web.ActivityPub.Transmogrifier
16 alias Pleroma.Web.ActivityPub.UserView
17 alias Pleroma.Web.ActivityPub.Utils
18 alias Pleroma.Web.ActivityPub.Visibility
19 alias Pleroma.Web.Federator
20
21 require Logger
22
23 action_fallback(:errors)
24
25 plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
26 plug(:set_requester_reachable when action in [:inbox])
27 plug(:relay_active? when action in [:relay])
28
29 def relay_active?(conn, _) do
30 if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do
31 conn
32 else
33 conn
34 |> put_status(404)
35 |> json(%{error: "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} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
43 conn
44 |> put_resp_header("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_header("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_header("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_header("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_header("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 def following(conn, %{"nickname" => nickname, "page" => page}) do
108 with %User{} = user <- User.get_cached_by_nickname(nickname),
109 {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
110 {page, _} = Integer.parse(page)
111
112 conn
113 |> put_resp_header("content-type", "application/activity+json")
114 |> json(UserView.render("following.json", %{user: user, page: page}))
115 end
116 end
117
118 def following(conn, %{"nickname" => nickname}) do
119 with %User{} = user <- User.get_cached_by_nickname(nickname),
120 {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
121 conn
122 |> put_resp_header("content-type", "application/activity+json")
123 |> json(UserView.render("following.json", %{user: user}))
124 end
125 end
126
127 def followers(conn, %{"nickname" => nickname, "page" => page}) do
128 with %User{} = user <- User.get_cached_by_nickname(nickname),
129 {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
130 {page, _} = Integer.parse(page)
131
132 conn
133 |> put_resp_header("content-type", "application/activity+json")
134 |> json(UserView.render("followers.json", %{user: user, page: page}))
135 end
136 end
137
138 def followers(conn, %{"nickname" => nickname}) do
139 with %User{} = user <- User.get_cached_by_nickname(nickname),
140 {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
141 conn
142 |> put_resp_header("content-type", "application/activity+json")
143 |> json(UserView.render("followers.json", %{user: user}))
144 end
145 end
146
147 def outbox(conn, %{"nickname" => nickname} = params) do
148 with %User{} = user <- User.get_cached_by_nickname(nickname),
149 {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
150 conn
151 |> put_resp_header("content-type", "application/activity+json")
152 |> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
153 end
154 end
155
156 def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
157 with %User{} = recipient <- User.get_cached_by_nickname(nickname),
158 %User{} = actor <- User.get_or_fetch_by_ap_id(params["actor"]),
159 true <- Utils.recipient_in_message(recipient, actor, params),
160 params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
161 Federator.incoming_ap_doc(params)
162 json(conn, "ok")
163 end
164 end
165
166 def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
167 Federator.incoming_ap_doc(params)
168 json(conn, "ok")
169 end
170
171 # only accept relayed Creates
172 def inbox(conn, %{"type" => "Create"} = params) do
173 Logger.info(
174 "Signature missing or not from author, relayed Create message, fetching object from source"
175 )
176
177 Fetcher.fetch_object_from_id(params["object"]["id"])
178
179 json(conn, "ok")
180 end
181
182 def inbox(conn, params) do
183 headers = Enum.into(conn.req_headers, %{})
184
185 if String.contains?(headers["signature"], params["actor"]) do
186 Logger.info(
187 "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
188 )
189
190 Logger.info(inspect(conn.req_headers))
191 end
192
193 json(conn, "error")
194 end
195
196 def relay(conn, _params) do
197 with %User{} = user <- Relay.get_actor(),
198 {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
199 conn
200 |> put_resp_header("content-type", "application/activity+json")
201 |> json(UserView.render("user.json", %{user: user}))
202 else
203 nil -> {:error, :not_found}
204 end
205 end
206
207 def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
208 conn
209 |> put_resp_header("content-type", "application/activity+json")
210 |> json(UserView.render("user.json", %{user: user}))
211 end
212
213 def whoami(_conn, _params), do: {:error, :not_found}
214
215 def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do
216 if nickname == user.nickname do
217 conn
218 |> put_resp_header("content-type", "application/activity+json")
219 |> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]}))
220 else
221 conn
222 |> put_status(:forbidden)
223 |> json("can't read inbox of #{nickname} as #{user.nickname}")
224 end
225 end
226
227 def handle_user_activity(user, %{"type" => "Create"} = params) do
228 object =
229 params["object"]
230 |> Map.merge(Map.take(params, ["to", "cc"]))
231 |> Map.put("attributedTo", user.ap_id())
232 |> Transmogrifier.fix_object()
233
234 ActivityPub.create(%{
235 to: params["to"],
236 actor: user,
237 context: object["context"],
238 object: object,
239 additional: Map.take(params, ["cc"])
240 })
241 end
242
243 def handle_user_activity(user, %{"type" => "Delete"} = params) do
244 with %Object{} = object <- Object.normalize(params["object"]),
245 true <- user.info.is_moderator || user.ap_id == object.data["actor"],
246 {:ok, delete} <- ActivityPub.delete(object) do
247 {:ok, delete}
248 else
249 _ -> {:error, "Can't delete object"}
250 end
251 end
252
253 def handle_user_activity(user, %{"type" => "Like"} = params) do
254 with %Object{} = object <- Object.normalize(params["object"]),
255 {:ok, activity, _object} <- ActivityPub.like(user, object) do
256 {:ok, activity}
257 else
258 _ -> {:error, "Can't like object"}
259 end
260 end
261
262 def handle_user_activity(_, _) do
263 {:error, "Unhandled activity type"}
264 end
265
266 def update_outbox(
267 %{assigns: %{user: user}} = conn,
268 %{"nickname" => nickname} = params
269 ) do
270 if nickname == user.nickname do
271 actor = user.ap_id()
272
273 params =
274 params
275 |> Map.drop(["id"])
276 |> Map.put("actor", actor)
277 |> Transmogrifier.fix_addressing()
278
279 with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
280 conn
281 |> put_status(:created)
282 |> put_resp_header("location", activity.data["id"])
283 |> json(activity.data)
284 else
285 {:error, message} ->
286 conn
287 |> put_status(:bad_request)
288 |> json(message)
289 end
290 else
291 conn
292 |> put_status(:forbidden)
293 |> json("can't update outbox of #{nickname} as #{user.nickname}")
294 end
295 end
296
297 def errors(conn, {:error, :not_found}) do
298 conn
299 |> put_status(404)
300 |> json("Not found")
301 end
302
303 def errors(conn, _e) do
304 conn
305 |> put_status(500)
306 |> json("error")
307 end
308
309 defp set_requester_reachable(%Plug.Conn{} = conn, _) do
310 with actor <- conn.params["actor"],
311 true <- is_binary(actor) do
312 Pleroma.Instances.set_reachable(actor)
313 end
314
315 conn
316 end
317 end