Return 413 when an actor's banner or background exceeds the size limit
[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 {:error, %Ecto.Changeset{errors: [banner: {"file is too large", _}]}} ->
258 render_error(conn, :request_entity_too_large, "File is too large")
259
260 {:error, %Ecto.Changeset{errors: [background: {"file is too large", _}]}} ->
261 render_error(conn, :request_entity_too_large, "File is too large")
262
263 _e ->
264 render_error(conn, :forbidden, "Invalid request")
265 end
266 end
267
268 defp normalize_fields_attributes(fields) do
269 if Enum.all?(fields, &is_tuple/1) do
270 Enum.map(fields, fn {_, v} -> v end)
271 else
272 Enum.map(fields, fn
273 %{} = field -> %{"name" => field.name, "value" => field.value}
274 field -> field
275 end)
276 end
277 end
278
279 @doc "GET /api/v1/accounts/relationships"
280 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
281 targets = User.get_all_by_ids(List.wrap(id))
282
283 render(conn, "relationships.json", user: user, targets: targets)
284 end
285
286 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
287 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
288
289 @doc "GET /api/v1/accounts/:id"
290 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
291 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
292 :visible <- User.visible_for(user, for_user) do
293 render(conn, "show.json",
294 user: user,
295 for: for_user,
296 embed_relationships: embed_relationships?(params)
297 )
298 else
299 error -> user_visibility_error(conn, error)
300 end
301 end
302
303 @doc "GET /api/v1/accounts/:id/statuses"
304 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
305 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
306 :visible <- User.visible_for(user, reading_user) do
307 params =
308 params
309 |> Map.delete(:tagged)
310 |> Map.put(:tag, params[:tagged])
311
312 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
313
314 conn
315 |> add_link_headers(activities)
316 |> put_view(StatusView)
317 |> render("index.json",
318 activities: activities,
319 for: reading_user,
320 as: :activity,
321 with_muted: Map.get(params, :with_muted, false)
322 )
323 else
324 error -> user_visibility_error(conn, error)
325 end
326 end
327
328 defp user_visibility_error(conn, error) do
329 case error do
330 :restrict_unauthenticated ->
331 render_error(conn, :unauthorized, "This API requires an authenticated user")
332
333 _ ->
334 render_error(conn, :not_found, "Can't find user")
335 end
336 end
337
338 @doc "GET /api/v1/accounts/:id/followers"
339 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
340 params =
341 params
342 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
343 |> Enum.into(%{})
344
345 followers =
346 cond do
347 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
348 user.hide_followers -> []
349 true -> MastodonAPI.get_followers(user, params)
350 end
351
352 conn
353 |> add_link_headers(followers)
354 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
355 |> render("index.json",
356 for: for_user,
357 users: followers,
358 as: :user,
359 embed_relationships: embed_relationships?(params)
360 )
361 end
362
363 @doc "GET /api/v1/accounts/:id/following"
364 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
365 params =
366 params
367 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
368 |> Enum.into(%{})
369
370 followers =
371 cond do
372 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
373 user.hide_follows -> []
374 true -> MastodonAPI.get_friends(user, params)
375 end
376
377 conn
378 |> add_link_headers(followers)
379 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
380 |> render("index.json",
381 for: for_user,
382 users: followers,
383 as: :user,
384 embed_relationships: embed_relationships?(params)
385 )
386 end
387
388 @doc "GET /api/v1/accounts/:id/lists"
389 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
390 lists = Pleroma.List.get_lists_account_belongs(user, account)
391
392 conn
393 |> put_view(ListView)
394 |> render("index.json", lists: lists)
395 end
396
397 @doc "POST /api/v1/accounts/:id/follow"
398 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
399 {:error, "Can not follow yourself"}
400 end
401
402 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
403 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
404 render(conn, "relationship.json", user: follower, target: followed)
405 else
406 {:error, message} -> json_response(conn, :forbidden, %{error: message})
407 end
408 end
409
410 @doc "POST /api/v1/accounts/:id/unfollow"
411 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
412 {:error, "Can not unfollow yourself"}
413 end
414
415 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
416 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
417 render(conn, "relationship.json", user: follower, target: followed)
418 end
419 end
420
421 @doc "POST /api/v1/accounts/:id/mute"
422 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
423 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
424 render(conn, "relationship.json", user: muter, target: muted)
425 else
426 {:error, message} -> json_response(conn, :forbidden, %{error: message})
427 end
428 end
429
430 @doc "POST /api/v1/accounts/:id/unmute"
431 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
432 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
433 render(conn, "relationship.json", user: muter, target: muted)
434 else
435 {:error, message} -> json_response(conn, :forbidden, %{error: message})
436 end
437 end
438
439 @doc "POST /api/v1/accounts/:id/block"
440 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
441 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
442 render(conn, "relationship.json", user: blocker, target: blocked)
443 else
444 {:error, message} -> json_response(conn, :forbidden, %{error: message})
445 end
446 end
447
448 @doc "POST /api/v1/accounts/:id/unblock"
449 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
450 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
451 render(conn, "relationship.json", user: blocker, target: blocked)
452 else
453 {:error, message} -> json_response(conn, :forbidden, %{error: message})
454 end
455 end
456
457 @doc "POST /api/v1/accounts/:id/note"
458 def note(
459 %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
460 _params
461 ) do
462 with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
463 render(conn, "relationship.json", user: noter, target: target)
464 end
465 end
466
467 @doc "POST /api/v1/accounts/:id/remove_from_followers"
468 def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
469 {:error, "Can not unfollow yourself"}
470 end
471
472 def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do
473 with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
474 render(conn, "relationship.json", user: followed, target: follower)
475 else
476 nil ->
477 render_error(conn, :not_found, "Record not found")
478 end
479 end
480
481 @doc "POST /api/v1/follows"
482 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
483 case User.get_cached_by_nickname(uri) do
484 %User{} = user ->
485 conn
486 |> assign(:account, user)
487 |> follow(%{})
488
489 nil ->
490 {:error, :not_found}
491 end
492 end
493
494 @doc "GET /api/v1/mutes"
495 def mutes(%{assigns: %{user: user}} = conn, params) do
496 users =
497 user
498 |> User.muted_users_relation(_restrict_deactivated = true)
499 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
500
501 conn
502 |> add_link_headers(users)
503 |> render("index.json",
504 users: users,
505 for: user,
506 as: :user,
507 embed_relationships: embed_relationships?(params)
508 )
509 end
510
511 @doc "GET /api/v1/blocks"
512 def blocks(%{assigns: %{user: user}} = conn, params) do
513 users =
514 user
515 |> User.blocked_users_relation(_restrict_deactivated = true)
516 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
517
518 conn
519 |> add_link_headers(users)
520 |> render("index.json", users: users, for: user, as: :user)
521 end
522
523 @doc "GET /api/v1/accounts/lookup"
524 def lookup(conn, %{acct: nickname} = _params) do
525 with %User{} = user <- User.get_by_nickname(nickname) do
526 render(conn, "show.json",
527 user: user,
528 skip_visibility_check: true
529 )
530 else
531 error -> user_visibility_error(conn, error)
532 end
533 end
534
535 @doc "GET /api/v1/endorsements"
536 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
537
538 @doc "GET /api/v1/identity_proofs"
539 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
540 end