Applied relationships preloading to GET /api/v1/accounts/relationships. Refactoring...
[akkoma] / lib / pleroma / web / mastodon_api / controllers / account_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.MastodonAPI.AccountController do
6 use Pleroma.Web, :controller
7
8 import Pleroma.Web.ControllerHelper,
9 only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]
10
11 alias Pleroma.Plugs.OAuthScopesPlug
12 alias Pleroma.Plugs.RateLimiter
13 alias Pleroma.User
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.CommonAPI
16 alias Pleroma.Web.MastodonAPI.ListView
17 alias Pleroma.Web.MastodonAPI.MastodonAPI
18 alias Pleroma.Web.MastodonAPI.StatusView
19 alias Pleroma.Web.OAuth.Token
20 alias Pleroma.Web.TwitterAPI.TwitterAPI
21
22 plug(
23 OAuthScopesPlug,
24 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
25 when action == :show
26 )
27
28 plug(
29 OAuthScopesPlug,
30 %{scopes: ["read:accounts"]}
31 when action in [:endorsements, :verify_credentials, :followers, :following]
32 )
33
34 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
35
36 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
37
38 plug(
39 OAuthScopesPlug,
40 %{scopes: ["follow", "read:blocks"]} when action == :blocks
41 )
42
43 plug(
44 OAuthScopesPlug,
45 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
46 )
47
48 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
49
50 # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
51 plug(
52 OAuthScopesPlug,
53 %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
54 )
55
56 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
57
58 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
59
60 plug(
61 Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
62 when action not in [:create, :show, :statuses]
63 )
64
65 @relations [:follow, :unfollow]
66 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
67
68 plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
69 plug(RateLimiter, [name: :relations_actions] when action in @relations)
70 plug(RateLimiter, [name: :app_account_creation] when action == :create)
71 plug(:assign_account_by_id when action in @needs_account)
72
73 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
74
75 @doc "POST /api/v1/accounts"
76 def create(
77 %{assigns: %{app: app}} = conn,
78 %{"username" => nickname, "password" => _, "agreement" => true} = params
79 ) do
80 params =
81 params
82 |> Map.take([
83 "email",
84 "captcha_solution",
85 "captcha_token",
86 "captcha_answer_data",
87 "token",
88 "password"
89 ])
90 |> Map.put("nickname", nickname)
91 |> Map.put("fullname", params["fullname"] || nickname)
92 |> Map.put("bio", params["bio"] || "")
93 |> Map.put("confirm", params["password"])
94
95 with :ok <- validate_email_param(params),
96 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
97 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
98 json(conn, %{
99 token_type: "Bearer",
100 access_token: token.token,
101 scope: app.scopes,
102 created_at: Token.Utils.format_created_at(token)
103 })
104 else
105 {:error, errors} -> json_response(conn, :bad_request, errors)
106 end
107 end
108
109 def create(%{assigns: %{app: _app}} = conn, _) do
110 render_error(conn, :bad_request, "Missing parameters")
111 end
112
113 def create(conn, _) do
114 render_error(conn, :forbidden, "Invalid credentials")
115 end
116
117 defp validate_email_param(%{"email" => _}), do: :ok
118
119 defp validate_email_param(_) do
120 case Pleroma.Config.get([:instance, :account_activation_required]) do
121 true -> {:error, %{"error" => "Missing parameters"}}
122 _ -> :ok
123 end
124 end
125
126 @doc "GET /api/v1/accounts/verify_credentials"
127 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
128 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
129
130 render(conn, "show.json",
131 user: user,
132 for: user,
133 with_pleroma_settings: true,
134 with_chat_token: chat_token
135 )
136 end
137
138 @doc "PATCH /api/v1/accounts/update_credentials"
139 def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
140 user = original_user
141
142 user_params =
143 [
144 :no_rich_text,
145 :locked,
146 :hide_followers_count,
147 :hide_follows_count,
148 :hide_followers,
149 :hide_follows,
150 :hide_favorites,
151 :show_role,
152 :skip_thread_containment,
153 :allow_following_move,
154 :discoverable
155 ]
156 |> Enum.reduce(%{}, fn key, acc ->
157 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
158 end)
159 |> add_if_present(params, "display_name", :name)
160 |> add_if_present(params, "note", :bio)
161 |> add_if_present(params, "avatar", :avatar)
162 |> add_if_present(params, "header", :banner)
163 |> add_if_present(params, "pleroma_background_image", :background)
164 |> add_if_present(
165 params,
166 "fields_attributes",
167 :raw_fields,
168 &{:ok, normalize_fields_attributes(&1)}
169 )
170 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
171 |> add_if_present(params, "default_scope", :default_scope)
172 |> add_if_present(params, "actor_type", :actor_type)
173
174 changeset = User.update_changeset(user, user_params)
175
176 with {:ok, user} <- User.update_and_set_cache(changeset) do
177 if original_user != user, do: CommonAPI.update(user)
178
179 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
180 else
181 _e -> render_error(conn, :forbidden, "Invalid request")
182 end
183 end
184
185 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
186 with true <- Map.has_key?(params, params_field),
187 {:ok, new_value} <- value_function.(params[params_field]) do
188 Map.put(map, map_field, new_value)
189 else
190 _ -> map
191 end
192 end
193
194 defp normalize_fields_attributes(fields) do
195 if Enum.all?(fields, &is_tuple/1) do
196 Enum.map(fields, fn {_, v} -> v end)
197 else
198 fields
199 end
200 end
201
202 @doc "GET /api/v1/accounts/relationships"
203 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
204 targets = User.get_all_by_ids(List.wrap(id))
205
206 render(conn, "relationships.json", user: user, targets: targets)
207 end
208
209 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
210 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
211
212 @doc "GET /api/v1/accounts/:id"
213 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
214 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
215 true <- User.visible_for?(user, for_user) do
216 render(conn, "show.json", user: user, for: for_user)
217 else
218 _e -> render_error(conn, :not_found, "Can't find user")
219 end
220 end
221
222 @doc "GET /api/v1/accounts/:id/statuses"
223 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
224 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
225 true <- User.visible_for?(user, reading_user) do
226 params =
227 params
228 |> Map.put("tag", params["tagged"])
229 |> Map.delete("godmode")
230
231 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
232
233 conn
234 |> add_link_headers(activities)
235 |> put_view(StatusView)
236 |> render("index.json", activities: activities, for: reading_user, as: :activity)
237 else
238 _e -> render_error(conn, :not_found, "Can't find user")
239 end
240 end
241
242 @doc "GET /api/v1/accounts/:id/followers"
243 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
244 followers =
245 cond do
246 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
247 user.hide_followers -> []
248 true -> MastodonAPI.get_followers(user, params)
249 end
250
251 conn
252 |> add_link_headers(followers)
253 |> render("index.json", for: for_user, users: followers, as: :user)
254 end
255
256 @doc "GET /api/v1/accounts/:id/following"
257 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
258 followers =
259 cond do
260 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
261 user.hide_follows -> []
262 true -> MastodonAPI.get_friends(user, params)
263 end
264
265 conn
266 |> add_link_headers(followers)
267 |> render("index.json", for: for_user, users: followers, as: :user)
268 end
269
270 @doc "GET /api/v1/accounts/:id/lists"
271 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
272 lists = Pleroma.List.get_lists_account_belongs(user, account)
273
274 conn
275 |> put_view(ListView)
276 |> render("index.json", lists: lists)
277 end
278
279 @doc "POST /api/v1/accounts/:id/follow"
280 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
281 {:error, :not_found}
282 end
283
284 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
285 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
286 render(conn, "relationship.json", user: follower, target: followed)
287 else
288 {:error, message} -> json_response(conn, :forbidden, %{error: message})
289 end
290 end
291
292 @doc "POST /api/v1/accounts/:id/unfollow"
293 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
294 {:error, :not_found}
295 end
296
297 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
298 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
299 render(conn, "relationship.json", user: follower, target: followed)
300 end
301 end
302
303 @doc "POST /api/v1/accounts/:id/mute"
304 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
305 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
306
307 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
308 render(conn, "relationship.json", user: muter, target: muted)
309 else
310 {:error, message} -> json_response(conn, :forbidden, %{error: message})
311 end
312 end
313
314 @doc "POST /api/v1/accounts/:id/unmute"
315 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
316 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
317 render(conn, "relationship.json", user: muter, target: muted)
318 else
319 {:error, message} -> json_response(conn, :forbidden, %{error: message})
320 end
321 end
322
323 @doc "POST /api/v1/accounts/:id/block"
324 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
325 with {:ok, _user_block} <- User.block(blocker, blocked),
326 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
327 render(conn, "relationship.json", user: blocker, target: blocked)
328 else
329 {:error, message} -> json_response(conn, :forbidden, %{error: message})
330 end
331 end
332
333 @doc "POST /api/v1/accounts/:id/unblock"
334 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
335 with {:ok, _user_block} <- User.unblock(blocker, blocked),
336 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
337 render(conn, "relationship.json", user: blocker, target: blocked)
338 else
339 {:error, message} -> json_response(conn, :forbidden, %{error: message})
340 end
341 end
342
343 @doc "POST /api/v1/follows"
344 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
345 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
346 {_, true} <- {:followed, follower.id != followed.id},
347 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
348 render(conn, "show.json", user: followed, for: follower)
349 else
350 {:followed, _} -> {:error, :not_found}
351 {:error, message} -> json_response(conn, :forbidden, %{error: message})
352 end
353 end
354
355 @doc "GET /api/v1/mutes"
356 def mutes(%{assigns: %{user: user}} = conn, _) do
357 users = User.muted_users(user, _restrict_deactivated = true)
358 render(conn, "index.json", users: users, for: user, as: :user)
359 end
360
361 @doc "GET /api/v1/blocks"
362 def blocks(%{assigns: %{user: user}} = conn, _) do
363 users = User.blocked_users(user, _restrict_deactivated = true)
364 render(conn, "index.json", users: users, for: user, as: :user)
365 end
366
367 @doc "GET /api/v1/endorsements"
368 def endorsements(conn, params),
369 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
370 end