docs: Remove quarantine section
[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 user_params =
179 [
180 :no_rich_text,
181 :hide_followers_count,
182 :hide_follows_count,
183 :hide_followers,
184 :hide_follows,
185 :hide_favorites,
186 :show_role,
187 :skip_thread_containment,
188 :allow_following_move,
189 :also_known_as
190 ]
191 |> Enum.reduce(%{}, fn key, acc ->
192 Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
193 end)
194 |> Maps.put_if_present(:name, params[:display_name])
195 |> Maps.put_if_present(:bio, params[:note])
196 |> Maps.put_if_present(:raw_bio, params[:note])
197 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
198 |> Maps.put_if_present(:banner, params[:header], user_image_value)
199 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
200 |> Maps.put_if_present(
201 :raw_fields,
202 params[:fields_attributes],
203 &{:ok, normalize_fields_attributes(&1)}
204 )
205 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
206 |> Maps.put_if_present(:default_scope, params[:default_scope])
207 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
208 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
209 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
210 end)
211 |> Maps.put_if_present(:actor_type, params[:actor_type])
212 |> Maps.put_if_present(:also_known_as, params[:also_known_as])
213 # Note: param name is indeed :locked (not an error)
214 |> Maps.put_if_present(:is_locked, params[:locked])
215 # Note: param name is indeed :discoverable (not an error)
216 |> Maps.put_if_present(:is_discoverable, params[:discoverable])
217 |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
218
219 # What happens here:
220 #
221 # We want to update the user through the pipeline, but the ActivityPub
222 # update information is not quite enough for this, because this also
223 # contains local settings that don't federate and don't even appear
224 # in the Update activity.
225 #
226 # So we first build the normal local changeset, then apply it to the
227 # user data, but don't persist it. With this, we generate the object
228 # data for our update activity. We feed this and the changeset as meta
229 # inforation into the pipeline, where they will be properly updated and
230 # federated.
231 with changeset <- User.update_changeset(user, user_params),
232 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
233 updated_object <-
234 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
235 |> Map.delete("@context"),
236 {:ok, update_data, []} <- Builder.update(user, updated_object),
237 {:ok, _update, _} <-
238 Pipeline.common_pipeline(update_data,
239 local: true,
240 user_update_changeset: changeset
241 ) do
242 render(conn, "show.json",
243 user: unpersisted_user,
244 for: unpersisted_user,
245 with_pleroma_settings: true
246 )
247 else
248 _e -> render_error(conn, :forbidden, "Invalid request")
249 end
250 end
251
252 defp normalize_fields_attributes(fields) do
253 if Enum.all?(fields, &is_tuple/1) do
254 Enum.map(fields, fn {_, v} -> v end)
255 else
256 Enum.map(fields, fn
257 %{} = field -> %{"name" => field.name, "value" => field.value}
258 field -> field
259 end)
260 end
261 end
262
263 @doc "GET /api/v1/accounts/relationships"
264 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
265 targets = User.get_all_by_ids(List.wrap(id))
266
267 render(conn, "relationships.json", user: user, targets: targets)
268 end
269
270 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
271 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
272
273 @doc "GET /api/v1/accounts/:id"
274 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
275 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
276 :visible <- User.visible_for(user, for_user) do
277 render(conn, "show.json",
278 user: user,
279 for: for_user,
280 embed_relationships: embed_relationships?(params)
281 )
282 else
283 error -> user_visibility_error(conn, error)
284 end
285 end
286
287 @doc "GET /api/v1/accounts/:id/statuses"
288 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
289 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
290 :visible <- User.visible_for(user, reading_user) do
291 params =
292 params
293 |> Map.delete(:tagged)
294 |> Map.put(:tag, params[:tagged])
295
296 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
297
298 conn
299 |> add_link_headers(activities)
300 |> put_view(StatusView)
301 |> render("index.json",
302 activities: activities,
303 for: reading_user,
304 as: :activity,
305 with_muted: Map.get(params, :with_muted, false)
306 )
307 else
308 error -> user_visibility_error(conn, error)
309 end
310 end
311
312 defp user_visibility_error(conn, error) do
313 case error do
314 :restrict_unauthenticated ->
315 render_error(conn, :unauthorized, "This API requires an authenticated user")
316
317 _ ->
318 render_error(conn, :not_found, "Can't find user")
319 end
320 end
321
322 @doc "GET /api/v1/accounts/:id/followers"
323 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
324 params =
325 params
326 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
327 |> Enum.into(%{})
328
329 followers =
330 cond do
331 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
332 user.hide_followers -> []
333 true -> MastodonAPI.get_followers(user, params)
334 end
335
336 conn
337 |> add_link_headers(followers)
338 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
339 |> render("index.json",
340 for: for_user,
341 users: followers,
342 as: :user,
343 embed_relationships: embed_relationships?(params)
344 )
345 end
346
347 @doc "GET /api/v1/accounts/:id/following"
348 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
349 params =
350 params
351 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
352 |> Enum.into(%{})
353
354 followers =
355 cond do
356 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
357 user.hide_follows -> []
358 true -> MastodonAPI.get_friends(user, params)
359 end
360
361 conn
362 |> add_link_headers(followers)
363 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
364 |> render("index.json",
365 for: for_user,
366 users: followers,
367 as: :user,
368 embed_relationships: embed_relationships?(params)
369 )
370 end
371
372 @doc "GET /api/v1/accounts/:id/lists"
373 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
374 lists = Pleroma.List.get_lists_account_belongs(user, account)
375
376 conn
377 |> put_view(ListView)
378 |> render("index.json", lists: lists)
379 end
380
381 @doc "POST /api/v1/accounts/:id/follow"
382 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
383 {:error, "Can not follow yourself"}
384 end
385
386 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
387 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
388 render(conn, "relationship.json", user: follower, target: followed)
389 else
390 {:error, message} -> json_response(conn, :forbidden, %{error: message})
391 end
392 end
393
394 @doc "POST /api/v1/accounts/:id/unfollow"
395 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
396 {:error, "Can not unfollow yourself"}
397 end
398
399 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
400 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
401 render(conn, "relationship.json", user: follower, target: followed)
402 end
403 end
404
405 @doc "POST /api/v1/accounts/:id/mute"
406 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
407 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
408 render(conn, "relationship.json", user: muter, target: muted)
409 else
410 {:error, message} -> json_response(conn, :forbidden, %{error: message})
411 end
412 end
413
414 @doc "POST /api/v1/accounts/:id/unmute"
415 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
416 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
417 render(conn, "relationship.json", user: muter, target: muted)
418 else
419 {:error, message} -> json_response(conn, :forbidden, %{error: message})
420 end
421 end
422
423 @doc "POST /api/v1/accounts/:id/block"
424 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
425 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
426 render(conn, "relationship.json", user: blocker, target: blocked)
427 else
428 {:error, message} -> json_response(conn, :forbidden, %{error: message})
429 end
430 end
431
432 @doc "POST /api/v1/accounts/:id/unblock"
433 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
434 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
435 render(conn, "relationship.json", user: blocker, target: blocked)
436 else
437 {:error, message} -> json_response(conn, :forbidden, %{error: message})
438 end
439 end
440
441 @doc "POST /api/v1/accounts/:id/note"
442 def note(
443 %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
444 _params
445 ) do
446 with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
447 render(conn, "relationship.json", user: noter, target: target)
448 end
449 end
450
451 @doc "POST /api/v1/accounts/:id/remove_from_followers"
452 def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
453 {:error, "Can not unfollow yourself"}
454 end
455
456 def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do
457 with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
458 render(conn, "relationship.json", user: followed, target: follower)
459 else
460 nil ->
461 render_error(conn, :not_found, "Record not found")
462 end
463 end
464
465 @doc "POST /api/v1/follows"
466 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
467 case User.get_cached_by_nickname(uri) do
468 %User{} = user ->
469 conn
470 |> assign(:account, user)
471 |> follow(%{})
472
473 nil ->
474 {:error, :not_found}
475 end
476 end
477
478 @doc "GET /api/v1/mutes"
479 def mutes(%{assigns: %{user: user}} = conn, params) do
480 users =
481 user
482 |> User.muted_users_relation(_restrict_deactivated = true)
483 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
484
485 conn
486 |> add_link_headers(users)
487 |> render("index.json",
488 users: users,
489 for: user,
490 as: :user,
491 embed_relationships: embed_relationships?(params)
492 )
493 end
494
495 @doc "GET /api/v1/blocks"
496 def blocks(%{assigns: %{user: user}} = conn, params) do
497 users =
498 user
499 |> User.blocked_users_relation(_restrict_deactivated = true)
500 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
501
502 conn
503 |> add_link_headers(users)
504 |> render("index.json", users: users, for: user, as: :user)
505 end
506
507 @doc "GET /api/v1/accounts/lookup"
508 def lookup(conn, %{acct: nickname} = _params) do
509 with %User{} = user <- User.get_by_nickname(nickname) do
510 render(conn, "show.json",
511 user: user,
512 skip_visibility_check: true
513 )
514 else
515 error -> user_visibility_error(conn, error)
516 end
517 end
518
519 @doc "GET /api/v1/endorsements"
520 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
521
522 @doc "GET /api/v1/identity_proofs"
523 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
524 end