Uploading an avatar media exceeding max size returns a 413
[akkoma] / lib / pleroma / web / mastodon_api / controllers / account_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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 assign_account_by_id: 2,
12 embed_relationships?: 1,
13 json_response: 3
14 ]
15
16 alias Pleroma.Maps
17 alias Pleroma.User
18 alias Pleroma.UserNote
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Builder
21 alias Pleroma.Web.ActivityPub.Pipeline
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonAPI
25 alias Pleroma.Web.MastodonAPI.MastodonAPIController
26 alias Pleroma.Web.MastodonAPI.StatusView
27 alias Pleroma.Web.OAuth.OAuthController
28 alias Pleroma.Web.Plugs.OAuthScopesPlug
29 alias Pleroma.Web.Plugs.RateLimiter
30 alias Pleroma.Web.TwitterAPI.TwitterAPI
31 alias Pleroma.Web.Utils.Params
32
33 plug(Pleroma.Web.ApiSpec.CastAndValidate)
34
35 plug(:skip_auth when action in [:create, :lookup])
36
37 plug(:skip_public_check when action in [:show, :statuses])
38
39 plug(
40 OAuthScopesPlug,
41 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
42 when action in [:show, :followers, :following]
43 )
44
45 plug(
46 OAuthScopesPlug,
47 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
48 when action == :statuses
49 )
50
51 plug(
52 OAuthScopesPlug,
53 %{scopes: ["read:accounts"]}
54 when action in [:verify_credentials, :endorsements, :identity_proofs]
55 )
56
57 plug(
58 OAuthScopesPlug,
59 %{scopes: ["write:accounts"]}
60 when action in [:update_credentials, :note]
61 )
62
63 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
64
65 plug(
66 OAuthScopesPlug,
67 %{scopes: ["follow", "read:blocks"]} when action == :blocks
68 )
69
70 plug(
71 OAuthScopesPlug,
72 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
73 )
74
75 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
76
77 plug(
78 OAuthScopesPlug,
79 %{scopes: ["follow", "write:follows"]}
80 when action in [:follow_by_uri, :follow, :unfollow, :remove_from_followers]
81 )
82
83 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
84
85 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
86
87 @relationship_actions [:follow, :unfollow, :remove_from_followers]
88 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock note remove_from_followers)a
89
90 plug(
91 RateLimiter,
92 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
93 )
94
95 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
96 plug(RateLimiter, [name: :app_account_creation] when action == :create)
97 plug(:assign_account_by_id when action in @needs_account)
98
99 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
100
101 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
102
103 @doc "POST /api/v1/accounts"
104 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
105 with :ok <- validate_email_param(params),
106 :ok <- TwitterAPI.validate_captcha(app, params),
107 {:ok, user} <- TwitterAPI.register_user(params),
108 {_, {:ok, token}} <-
109 {:login, OAuthController.login(user, app, app.scopes)} do
110 OAuthController.after_token_exchange(conn, %{user: user, token: token})
111 else
112 {:login, {:account_status, :confirmation_pending}} ->
113 json_response(conn, :ok, %{
114 message: "You have been registered. Please check your email for further instructions.",
115 identifier: "missing_confirmed_email"
116 })
117
118 {:login, {:account_status, :approval_pending}} ->
119 json_response(conn, :ok, %{
120 message:
121 "You have been registered. You'll be able to log in once your account is approved.",
122 identifier: "awaiting_approval"
123 })
124
125 {:login, _} ->
126 json_response(conn, :ok, %{
127 message:
128 "You have been registered. Some post-registration steps may be pending. " <>
129 "Please log in manually.",
130 identifier: "manual_login_required"
131 })
132
133 {:error, error} ->
134 json_response(conn, :bad_request, %{error: error})
135 end
136 end
137
138 def create(%{assigns: %{app: _app}} = conn, _) do
139 render_error(conn, :bad_request, "Missing parameters")
140 end
141
142 def create(conn, _) do
143 render_error(conn, :forbidden, "Invalid credentials")
144 end
145
146 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
147
148 defp validate_email_param(_) do
149 case Pleroma.Config.get([:instance, :account_activation_required]) do
150 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
151 _ -> :ok
152 end
153 end
154
155 @doc "GET /api/v1/accounts/verify_credentials"
156 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
157 render(conn, "show.json",
158 user: user,
159 for: user,
160 with_pleroma_settings: true
161 )
162 end
163
164 @doc "PATCH /api/v1/accounts/update_credentials"
165 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
166 params =
167 params
168 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
169 |> Enum.into(%{})
170
171 # We use an empty string as a special value to reset
172 # avatars, banners, backgrounds
173 user_image_value = fn
174 "" -> {:ok, nil}
175 value -> {:ok, value}
176 end
177
178 status_ttl_days_value = fn
179 -1 -> {:ok, nil}
180 value -> {:ok, value}
181 end
182
183 user_params =
184 [
185 :no_rich_text,
186 :hide_followers_count,
187 :hide_follows_count,
188 :hide_followers,
189 :hide_follows,
190 :hide_favorites,
191 :show_role,
192 :skip_thread_containment,
193 :allow_following_move,
194 :also_known_as
195 ]
196 |> Enum.reduce(%{}, fn key, acc ->
197 Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
198 end)
199 |> Maps.put_if_present(:name, params[:display_name])
200 |> Maps.put_if_present(:bio, params[:note])
201 |> Maps.put_if_present(:raw_bio, params[:note])
202 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
203 |> Maps.put_if_present(:banner, params[:header], user_image_value)
204 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
205 |> Maps.put_if_present(
206 :raw_fields,
207 params[:fields_attributes],
208 &{:ok, normalize_fields_attributes(&1)}
209 )
210 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
211 |> Maps.put_if_present(:default_scope, params[:default_scope])
212 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
213 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
214 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
215 end)
216 |> Maps.put_if_present(:actor_type, params[:actor_type])
217 |> Maps.put_if_present(:also_known_as, params[:also_known_as])
218 # Note: param name is indeed :locked (not an error)
219 |> Maps.put_if_present(:is_locked, params[:locked])
220 # Note: param name is indeed :discoverable (not an error)
221 |> Maps.put_if_present(:is_discoverable, params[:discoverable])
222 |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
223 |> Maps.put_if_present(:status_ttl_days, params[:status_ttl_days], status_ttl_days_value)
224
225 # What happens here:
226 #
227 # We want to update the user through the pipeline, but the ActivityPub
228 # update information is not quite enough for this, because this also
229 # contains local settings that don't federate and don't even appear
230 # in the Update activity.
231 #
232 # So we first build the normal local changeset, then apply it to the
233 # user data, but don't persist it. With this, we generate the object
234 # data for our update activity. We feed this and the changeset as meta
235 # inforation into the pipeline, where they will be properly updated and
236 # federated.
237 with changeset <- User.update_changeset(user, user_params),
238 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
239 updated_object <-
240 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
241 |> Map.delete("@context"),
242 {:ok, update_data, []} <- Builder.update(user, updated_object),
243 {:ok, _update, _} <-
244 Pipeline.common_pipeline(update_data,
245 local: true,
246 user_update_changeset: changeset
247 ) do
248 render(conn, "show.json",
249 user: unpersisted_user,
250 for: unpersisted_user,
251 with_pleroma_settings: true
252 )
253 else
254 {:error, %Ecto.Changeset{errors: [avatar: {"file is too large", _}]}} ->
255 render_error(conn, :request_entity_too_large, "File is too large")
256
257 _e ->
258 render_error(conn, :forbidden, "Invalid request")
259 end
260 end
261
262 defp normalize_fields_attributes(fields) do
263 if Enum.all?(fields, &is_tuple/1) do
264 Enum.map(fields, fn {_, v} -> v end)
265 else
266 Enum.map(fields, fn
267 %{} = field -> %{"name" => field.name, "value" => field.value}
268 field -> field
269 end)
270 end
271 end
272
273 @doc "GET /api/v1/accounts/relationships"
274 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
275 targets = User.get_all_by_ids(List.wrap(id))
276
277 render(conn, "relationships.json", user: user, targets: targets)
278 end
279
280 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
281 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
282
283 @doc "GET /api/v1/accounts/:id"
284 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
285 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
286 :visible <- User.visible_for(user, for_user) do
287 render(conn, "show.json",
288 user: user,
289 for: for_user,
290 embed_relationships: embed_relationships?(params)
291 )
292 else
293 error -> user_visibility_error(conn, error)
294 end
295 end
296
297 @doc "GET /api/v1/accounts/:id/statuses"
298 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
299 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
300 :visible <- User.visible_for(user, reading_user) do
301 params =
302 params
303 |> Map.delete(:tagged)
304 |> Map.put(:tag, params[:tagged])
305
306 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
307
308 conn
309 |> add_link_headers(activities)
310 |> put_view(StatusView)
311 |> render("index.json",
312 activities: activities,
313 for: reading_user,
314 as: :activity,
315 with_muted: Map.get(params, :with_muted, false)
316 )
317 else
318 error -> user_visibility_error(conn, error)
319 end
320 end
321
322 defp user_visibility_error(conn, error) do
323 case error do
324 :restrict_unauthenticated ->
325 render_error(conn, :unauthorized, "This API requires an authenticated user")
326
327 _ ->
328 render_error(conn, :not_found, "Can't find user")
329 end
330 end
331
332 @doc "GET /api/v1/accounts/:id/followers"
333 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
334 params =
335 params
336 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
337 |> Enum.into(%{})
338
339 followers =
340 cond do
341 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
342 user.hide_followers -> []
343 true -> MastodonAPI.get_followers(user, params)
344 end
345
346 conn
347 |> add_link_headers(followers)
348 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
349 |> render("index.json",
350 for: for_user,
351 users: followers,
352 as: :user,
353 embed_relationships: embed_relationships?(params)
354 )
355 end
356
357 @doc "GET /api/v1/accounts/:id/following"
358 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
359 params =
360 params
361 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
362 |> Enum.into(%{})
363
364 followers =
365 cond do
366 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
367 user.hide_follows -> []
368 true -> MastodonAPI.get_friends(user, params)
369 end
370
371 conn
372 |> add_link_headers(followers)
373 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
374 |> render("index.json",
375 for: for_user,
376 users: followers,
377 as: :user,
378 embed_relationships: embed_relationships?(params)
379 )
380 end
381
382 @doc "GET /api/v1/accounts/:id/lists"
383 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
384 lists = Pleroma.List.get_lists_account_belongs(user, account)
385
386 conn
387 |> put_view(ListView)
388 |> render("index.json", lists: lists)
389 end
390
391 @doc "POST /api/v1/accounts/:id/follow"
392 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
393 {:error, "Can not follow yourself"}
394 end
395
396 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
397 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
398 render(conn, "relationship.json", user: follower, target: followed)
399 else
400 {:error, message} -> json_response(conn, :forbidden, %{error: message})
401 end
402 end
403
404 @doc "POST /api/v1/accounts/:id/unfollow"
405 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
406 {:error, "Can not unfollow yourself"}
407 end
408
409 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
410 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
411 render(conn, "relationship.json", user: follower, target: followed)
412 end
413 end
414
415 @doc "POST /api/v1/accounts/:id/mute"
416 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
417 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
418 render(conn, "relationship.json", user: muter, target: muted)
419 else
420 {:error, message} -> json_response(conn, :forbidden, %{error: message})
421 end
422 end
423
424 @doc "POST /api/v1/accounts/:id/unmute"
425 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
426 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
427 render(conn, "relationship.json", user: muter, target: muted)
428 else
429 {:error, message} -> json_response(conn, :forbidden, %{error: message})
430 end
431 end
432
433 @doc "POST /api/v1/accounts/:id/block"
434 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
435 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
436 render(conn, "relationship.json", user: blocker, target: blocked)
437 else
438 {:error, message} -> json_response(conn, :forbidden, %{error: message})
439 end
440 end
441
442 @doc "POST /api/v1/accounts/:id/unblock"
443 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
444 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
445 render(conn, "relationship.json", user: blocker, target: blocked)
446 else
447 {:error, message} -> json_response(conn, :forbidden, %{error: message})
448 end
449 end
450
451 @doc "POST /api/v1/accounts/:id/note"
452 def note(
453 %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
454 _params
455 ) do
456 with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
457 render(conn, "relationship.json", user: noter, target: target)
458 end
459 end
460
461 @doc "POST /api/v1/accounts/:id/remove_from_followers"
462 def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
463 {:error, "Can not unfollow yourself"}
464 end
465
466 def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do
467 with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
468 render(conn, "relationship.json", user: followed, target: follower)
469 else
470 nil ->
471 render_error(conn, :not_found, "Record not found")
472 end
473 end
474
475 @doc "POST /api/v1/follows"
476 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
477 case User.get_cached_by_nickname(uri) do
478 %User{} = user ->
479 conn
480 |> assign(:account, user)
481 |> follow(%{})
482
483 nil ->
484 {:error, :not_found}
485 end
486 end
487
488 @doc "GET /api/v1/mutes"
489 def mutes(%{assigns: %{user: user}} = conn, params) do
490 users =
491 user
492 |> User.muted_users_relation(_restrict_deactivated = true)
493 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
494
495 conn
496 |> add_link_headers(users)
497 |> render("index.json",
498 users: users,
499 for: user,
500 as: :user,
501 embed_relationships: embed_relationships?(params)
502 )
503 end
504
505 @doc "GET /api/v1/blocks"
506 def blocks(%{assigns: %{user: user}} = conn, params) do
507 users =
508 user
509 |> User.blocked_users_relation(_restrict_deactivated = true)
510 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
511
512 conn
513 |> add_link_headers(users)
514 |> render("index.json", users: users, for: user, as: :user)
515 end
516
517 @doc "GET /api/v1/accounts/lookup"
518 def lookup(conn, %{acct: nickname} = _params) do
519 with %User{} = user <- User.get_by_nickname(nickname) do
520 render(conn, "show.json",
521 user: user,
522 skip_visibility_check: true
523 )
524 else
525 error -> user_visibility_error(conn, error)
526 end
527 end
528
529 @doc "GET /api/v1/endorsements"
530 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
531
532 @doc "GET /api/v1/identity_proofs"
533 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
534 end