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