1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 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, :lookup])
37 plug(:skip_public_check when action in [:show, :statuses])
41 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
42 when action in [:show, :followers, :following]
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, :endorse, :unendorse]
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]
89 followers following lists follow unfollow mute unmute block unblock
90 note endorse unendorse remove_from_followers
95 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
98 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
99 plug(RateLimiter, [name: :app_account_creation] when action == :create)
100 plug(:assign_account_by_id when action in @needs_account)
102 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
104 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
106 @doc "POST /api/v1/accounts"
107 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
108 with :ok <- validate_email_param(params),
109 :ok <- TwitterAPI.validate_captcha(app, params),
110 {:ok, user} <- TwitterAPI.register_user(params),
112 {:login, OAuthController.login(user, app, app.scopes)} do
113 OAuthController.after_token_exchange(conn, %{user: user, token: token})
115 {:login, {:account_status, :confirmation_pending}} ->
116 json_response(conn, :ok, %{
117 message: "You have been registered. Please check your email for further instructions.",
118 identifier: "missing_confirmed_email"
121 {:login, {:account_status, :approval_pending}} ->
122 json_response(conn, :ok, %{
124 "You have been registered. You'll be able to log in once your account is approved.",
125 identifier: "awaiting_approval"
129 json_response(conn, :ok, %{
131 "You have been registered. Some post-registration steps may be pending. " <>
132 "Please log in manually.",
133 identifier: "manual_login_required"
137 json_response(conn, :bad_request, %{error: error})
141 def create(%{assigns: %{app: _app}} = conn, _) do
142 render_error(conn, :bad_request, "Missing parameters")
145 def create(conn, _) do
146 render_error(conn, :forbidden, "Invalid credentials")
149 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
151 defp validate_email_param(_) do
152 case Pleroma.Config.get([:instance, :account_activation_required]) do
153 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
158 @doc "GET /api/v1/accounts/verify_credentials"
159 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
160 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
162 render(conn, "show.json",
165 with_pleroma_settings: true,
166 with_chat_token: chat_token
170 @doc "PATCH /api/v1/accounts/update_credentials"
171 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
174 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
177 # We use an empty string as a special value to reset
178 # avatars, banners, backgrounds
179 user_image_value = fn
181 value -> {:ok, value}
187 :hide_followers_count,
193 :skip_thread_containment,
194 :allow_following_move,
196 :accepts_chat_messages,
199 |> Enum.reduce(%{}, fn key, acc ->
200 Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
202 |> Maps.put_if_present(:name, params[:display_name])
203 |> Maps.put_if_present(:bio, params[:note])
204 |> Maps.put_if_present(:raw_bio, params[:note])
205 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
206 |> Maps.put_if_present(:banner, params[:header], user_image_value)
207 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
208 |> Maps.put_if_present(
210 params[:fields_attributes],
211 &{:ok, normalize_fields_attributes(&1)}
213 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
214 |> Maps.put_if_present(:default_scope, params[:default_scope])
215 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
216 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
217 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
219 |> Maps.put_if_present(:actor_type, params[:actor_type])
220 |> Maps.put_if_present(:also_known_as, params[:also_known_as])
221 # Note: param name is indeed :locked (not an error)
222 |> Maps.put_if_present(:is_locked, params[:locked])
223 # Note: param name is indeed :discoverable (not an error)
224 |> Maps.put_if_present(:is_discoverable, params[:discoverable])
225 |> Maps.put_if_present(:birthday, params[:birthday])
226 |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
230 # We want to update the user through the pipeline, but the ActivityPub
231 # update information is not quite enough for this, because this also
232 # contains local settings that don't federate and don't even appear
233 # in the Update activity.
235 # So we first build the normal local changeset, then apply it to the
236 # user data, but don't persist it. With this, we generate the object
237 # data for our update activity. We feed this and the changeset as meta
238 # inforation into the pipeline, where they will be properly updated and
240 with changeset <- User.update_changeset(user, user_params),
241 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
243 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
244 |> Map.delete("@context"),
245 {:ok, update_data, []} <- Builder.update(user, updated_object),
247 Pipeline.common_pipeline(update_data,
249 user_update_changeset: changeset
251 render(conn, "show.json",
252 user: unpersisted_user,
253 for: unpersisted_user,
254 with_pleroma_settings: true
257 {:error, %Ecto.Changeset{errors: [avatar: {"file is too large", _}]}} ->
258 render_error(conn, :request_entity_too_large, "File is too large")
260 {:error, %Ecto.Changeset{errors: [banner: {"file is too large", _}]}} ->
261 render_error(conn, :request_entity_too_large, "File is too large")
263 {:error, %Ecto.Changeset{errors: [background: {"file is too large", _}]}} ->
264 render_error(conn, :request_entity_too_large, "File is too large")
267 render_error(conn, :forbidden, "Invalid request")
271 defp normalize_fields_attributes(fields) do
272 if Enum.all?(fields, &is_tuple/1) do
273 Enum.map(fields, fn {_, v} -> v end)
276 %{} = field -> %{"name" => field.name, "value" => field.value}
282 @doc "GET /api/v1/accounts/relationships"
283 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
284 targets = User.get_all_by_ids(List.wrap(id))
286 render(conn, "relationships.json", user: user, targets: targets)
289 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
290 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
292 @doc "GET /api/v1/accounts/:id"
293 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
294 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
295 :visible <- User.visible_for(user, for_user) do
296 render(conn, "show.json",
299 embed_relationships: embed_relationships?(params)
302 error -> user_visibility_error(conn, error)
306 @doc "GET /api/v1/accounts/:id/statuses"
307 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
308 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
309 :visible <- User.visible_for(user, reading_user) do
312 |> Map.delete(:tagged)
313 |> Map.put(:tag, params[:tagged])
315 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
318 |> add_link_headers(activities)
319 |> put_view(StatusView)
320 |> render("index.json",
321 activities: activities,
324 with_muted: Map.get(params, :with_muted, false)
327 error -> user_visibility_error(conn, error)
331 defp user_visibility_error(conn, error) do
333 :restrict_unauthenticated ->
334 render_error(conn, :unauthorized, "This API requires an authenticated user")
337 render_error(conn, :not_found, "Can't find user")
341 @doc "GET /api/v1/accounts/:id/followers"
342 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
345 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
350 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
351 user.hide_followers -> []
352 true -> MastodonAPI.get_followers(user, params)
356 |> add_link_headers(followers)
357 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
358 |> render("index.json",
362 embed_relationships: embed_relationships?(params)
366 @doc "GET /api/v1/accounts/:id/following"
367 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
370 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
375 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
376 user.hide_follows -> []
377 true -> MastodonAPI.get_friends(user, params)
381 |> add_link_headers(followers)
382 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
383 |> render("index.json",
387 embed_relationships: embed_relationships?(params)
391 @doc "GET /api/v1/accounts/:id/lists"
392 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
393 lists = Pleroma.List.get_lists_account_belongs(user, account)
396 |> put_view(ListView)
397 |> render("index.json", lists: lists)
400 @doc "POST /api/v1/accounts/:id/follow"
401 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
402 {:error, "Can not follow yourself"}
405 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
406 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
407 render(conn, "relationship.json", user: follower, target: followed)
409 {:error, message} -> json_response(conn, :forbidden, %{error: message})
413 @doc "POST /api/v1/accounts/:id/unfollow"
414 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
415 {:error, "Can not unfollow yourself"}
418 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
419 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
420 render(conn, "relationship.json", user: follower, target: followed)
424 @doc "POST /api/v1/accounts/:id/mute"
425 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
428 |> Map.put_new(:duration, Map.get(params, :expires_in, 0))
430 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
431 render(conn, "relationship.json", user: muter, target: muted)
433 {:error, message} -> json_response(conn, :forbidden, %{error: message})
437 @doc "POST /api/v1/accounts/:id/unmute"
438 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
439 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
440 render(conn, "relationship.json", user: muter, target: muted)
442 {:error, message} -> json_response(conn, :forbidden, %{error: message})
446 @doc "POST /api/v1/accounts/:id/block"
447 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
448 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
449 render(conn, "relationship.json", user: blocker, target: blocked)
451 {:error, message} -> json_response(conn, :forbidden, %{error: message})
455 @doc "POST /api/v1/accounts/:id/unblock"
456 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
457 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
458 render(conn, "relationship.json", user: blocker, target: blocked)
460 {:error, message} -> json_response(conn, :forbidden, %{error: message})
464 @doc "POST /api/v1/accounts/:id/note"
466 %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
469 with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
470 render(conn, "relationship.json", user: noter, target: target)
474 @doc "POST /api/v1/accounts/:id/pin"
475 def endorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
476 with {:ok, _user_relationships} <- User.endorse(endorser, endorsed) do
477 render(conn, "relationship.json", user: endorser, target: endorsed)
479 {:error, message} -> json_response(conn, :bad_request, %{error: message})
483 @doc "POST /api/v1/accounts/:id/unpin"
484 def unendorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
485 with {:ok, _user_relationships} <- User.unendorse(endorser, endorsed) do
486 render(conn, "relationship.json", user: endorser, target: endorsed)
488 {:error, message} -> json_response(conn, :forbidden, %{error: message})
492 @doc "POST /api/v1/accounts/:id/remove_from_followers"
493 def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
494 {:error, "Can not unfollow yourself"}
497 def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do
498 with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
499 render(conn, "relationship.json", user: followed, target: follower)
502 render_error(conn, :not_found, "Record not found")
506 @doc "POST /api/v1/follows"
507 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
508 case User.get_cached_by_nickname(uri) do
511 |> assign(:account, user)
519 @doc "GET /api/v1/mutes"
520 def mutes(%{assigns: %{user: user}} = conn, params) do
523 |> User.muted_users_relation(_restrict_deactivated = true)
524 |> Pleroma.Pagination.fetch_paginated(params)
527 |> add_link_headers(users)
528 |> render("index.json",
532 embed_relationships: embed_relationships?(params),
537 @doc "GET /api/v1/blocks"
538 def blocks(%{assigns: %{user: user}} = conn, params) do
541 |> User.blocked_users_relation(_restrict_deactivated = true)
542 |> Pleroma.Pagination.fetch_paginated(params)
545 |> add_link_headers(users)
546 |> render("index.json", users: users, for: user, as: :user)
549 @doc "GET /api/v1/accounts/lookup"
550 def lookup(conn, %{acct: nickname} = _params) do
551 with %User{} = user <- User.get_by_nickname(nickname) do
552 render(conn, "show.json",
554 skip_visibility_check: true
557 error -> user_visibility_error(conn, error)
561 @doc "GET /api/v1/endorsements"
562 def endorsements(%{assigns: %{user: user}} = conn, params) do
565 |> User.endorsed_users_relation(_restrict_deactivated = true)
566 |> Pleroma.Repo.all()
569 |> render("index.json",
573 embed_relationships: embed_relationships?(params)
577 @doc "GET /api/v1/identity_proofs"
578 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)