1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.AccountController do
6 use Pleroma.Web, :controller
8 import Pleroma.Web.ControllerHelper,
11 assign_account_by_id: 2,
12 embed_relationships?: 1,
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
33 plug(Pleroma.Web.ApiSpec.CastAndValidate)
35 plug(:skip_auth when action in [:create])
37 plug(:skip_public_check when action in [:show, :statuses])
41 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
42 when action in [:show, :followers, :following, :lookup]
47 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
48 when action == :statuses
53 %{scopes: ["read:accounts"]}
54 when action in [:verify_credentials, :endorsements, :identity_proofs]
59 %{scopes: ["write:accounts"]}
60 when action in [:update_credentials, :note]
63 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
67 %{scopes: ["follow", "read:blocks"]} when action == :blocks
72 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
75 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
79 %{scopes: ["follow", "write:follows"]}
80 when action in [:follow_by_uri, :follow, :unfollow, :remove_from_followers]
83 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
85 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
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
92 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
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)
99 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
101 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
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),
109 {:login, OAuthController.login(user, app, app.scopes)} do
110 OAuthController.after_token_exchange(conn, %{user: user, token: token})
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"
118 {:login, {:account_status, :approval_pending}} ->
119 json_response(conn, :ok, %{
121 "You have been registered. You'll be able to log in once your account is approved.",
122 identifier: "awaiting_approval"
126 json_response(conn, :ok, %{
128 "You have been registered. Some post-registration steps may be pending. " <>
129 "Please log in manually.",
130 identifier: "manual_login_required"
134 json_response(conn, :bad_request, %{error: error})
138 def create(%{assigns: %{app: _app}} = conn, _) do
139 render_error(conn, :bad_request, "Missing parameters")
142 def create(conn, _) do
143 render_error(conn, :forbidden, "Invalid credentials")
146 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
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")}
155 @doc "GET /api/v1/accounts/verify_credentials"
156 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
157 render(conn, "show.json",
160 with_pleroma_settings: true
164 @doc "PATCH /api/v1/accounts/update_credentials"
165 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
168 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
171 # We use an empty string as a special value to reset
172 # avatars, banners, backgrounds
173 user_image_value = fn
175 value -> {:ok, value}
178 status_ttl_days_value = fn
180 value -> {:ok, value}
186 :hide_followers_count,
192 :skip_thread_containment,
193 :allow_following_move,
196 |> Enum.reduce(%{}, fn key, acc ->
197 Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
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(
207 params[:fields_attributes],
208 &{:ok, normalize_fields_attributes(&1)}
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"}
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)
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.
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
237 with changeset <- User.update_changeset(user, user_params),
238 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
240 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
241 |> Map.delete("@context"),
242 {:ok, update_data, []} <- Builder.update(user, updated_object),
244 Pipeline.common_pipeline(update_data,
246 user_update_changeset: changeset
248 render(conn, "show.json",
249 user: unpersisted_user,
250 for: unpersisted_user,
251 with_pleroma_settings: true
254 {:error, %Ecto.Changeset{errors: [avatar: {"file is too large", _}]}} ->
255 render_error(conn, :request_entity_too_large, "File is too large")
257 {:error, %Ecto.Changeset{errors: [banner: {"file is too large", _}]}} ->
258 render_error(conn, :request_entity_too_large, "File is too large")
260 {:error, %Ecto.Changeset{errors: [background: {"file is too large", _}]}} ->
261 render_error(conn, :request_entity_too_large, "File is too large")
264 render_error(conn, :forbidden, "Invalid request")
268 defp normalize_fields_attributes(fields) do
269 if Enum.all?(fields, &is_tuple/1) do
270 Enum.map(fields, fn {_, v} -> v end)
273 %{} = field -> %{"name" => field.name, "value" => field.value}
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))
283 render(conn, "relationships.json", user: user, targets: targets)
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, [])
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",
296 embed_relationships: embed_relationships?(params)
299 error -> user_visibility_error(conn, error)
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
309 |> Map.delete(:tagged)
310 |> Map.put(:tag, params[:tagged])
312 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
315 |> add_link_headers(activities)
316 |> put_view(StatusView)
317 |> render("index.json",
318 activities: activities,
321 with_muted: Map.get(params, :with_muted, false)
324 error -> user_visibility_error(conn, error)
328 defp user_visibility_error(conn, error) do
330 :restrict_unauthenticated ->
331 render_error(conn, :unauthorized, "This API requires an authenticated user")
334 render_error(conn, :not_found, "Can't find user")
338 @doc "GET /api/v1/accounts/:id/followers"
339 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
342 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
347 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
348 user.hide_followers -> []
349 true -> MastodonAPI.get_followers(user, params)
353 |> add_link_headers(followers)
354 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
355 |> render("index.json",
359 embed_relationships: embed_relationships?(params)
363 @doc "GET /api/v1/accounts/:id/following"
364 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
367 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
372 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
373 user.hide_follows -> []
374 true -> MastodonAPI.get_friends(user, params)
378 |> add_link_headers(followers)
379 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
380 |> render("index.json",
384 embed_relationships: embed_relationships?(params)
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)
393 |> put_view(ListView)
394 |> render("index.json", lists: lists)
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"}
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)
406 {:error, message} -> json_response(conn, :forbidden, %{error: message})
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"}
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)
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)
426 {:error, message} -> json_response(conn, :forbidden, %{error: message})
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)
435 {:error, message} -> json_response(conn, :forbidden, %{error: message})
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)
444 {:error, message} -> json_response(conn, :forbidden, %{error: message})
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)
453 {:error, message} -> json_response(conn, :forbidden, %{error: message})
457 @doc "POST /api/v1/accounts/:id/note"
459 %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
462 with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
463 render(conn, "relationship.json", user: noter, target: target)
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"}
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)
477 render_error(conn, :not_found, "Record not found")
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
486 |> assign(:account, user)
494 @doc "GET /api/v1/mutes"
495 def mutes(%{assigns: %{user: user}} = conn, params) do
498 |> User.muted_users_relation(_restrict_deactivated = true)
499 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
502 |> add_link_headers(users)
503 |> render("index.json",
507 embed_relationships: embed_relationships?(params)
511 @doc "GET /api/v1/blocks"
512 def blocks(%{assigns: %{user: user}} = conn, params) do
515 |> User.blocked_users_relation(_restrict_deactivated = true)
516 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
519 |> add_link_headers(users)
520 |> render("index.json", users: users, for: user, as: :user)
523 @doc "GET /api/v1/accounts/lookup"
524 def lookup(%{assigns: %{user: for_user}} = conn, %{acct: nickname} = _params) do
525 with %User{} = user <- User.get_by_nickname(nickname),
526 :visible <- User.visible_for(user, for_user) do
527 render(conn, "show.json",
529 skip_visibility_check: true
532 error -> user_visibility_error(conn, error)
536 @doc "GET /api/v1/endorsements"
537 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
539 @doc "GET /api/v1/identity_proofs"
540 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)