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,
12 assign_account_by_id: 2,
13 embed_relationships?: 1,
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.EnsurePublicOrAuthenticatedPlug
29 alias Pleroma.Web.Plugs.OAuthScopesPlug
30 alias Pleroma.Web.Plugs.RateLimiter
31 alias Pleroma.Web.TwitterAPI.TwitterAPI
33 plug(Pleroma.Web.ApiSpec.CastAndValidate)
35 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
37 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug 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]
57 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
59 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
63 %{scopes: ["follow", "read:blocks"]} when action == :blocks
68 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
71 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
75 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
78 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
80 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
82 @relationship_actions [:follow, :unfollow]
83 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
87 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
90 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
91 plug(RateLimiter, [name: :app_account_creation] when action == :create)
92 plug(:assign_account_by_id when action in @needs_account)
94 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
96 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
98 @doc "POST /api/v1/accounts"
99 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
100 with :ok <- validate_email_param(params),
101 :ok <- TwitterAPI.validate_captcha(app, params),
102 {:ok, user} <- TwitterAPI.register_user(params),
104 {:login, OAuthController.login(user, app, app.scopes)} do
105 OAuthController.after_token_exchange(conn, %{user: user, token: token})
107 {:login, {:account_status, :confirmation_pending}} ->
108 json_response(conn, :ok, %{
109 message: "You have been registered. Please check your email for further instructions.",
110 identifier: "missing_confirmed_email"
113 {:login, {:account_status, :approval_pending}} ->
114 json_response(conn, :ok, %{
116 "You have been registered. You'll be able to log in once your account is approved.",
117 identifier: "awaiting_approval"
121 json_response(conn, :ok, %{
123 "You have been registered. Some post-registration steps may be pending. " <>
124 "Please log in manually.",
125 identifier: "manual_login_required"
129 json_response(conn, :bad_request, %{error: error})
133 def create(%{assigns: %{app: _app}} = conn, _) do
134 render_error(conn, :bad_request, "Missing parameters")
137 def create(conn, _) do
138 render_error(conn, :forbidden, "Invalid credentials")
141 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
143 defp validate_email_param(_) do
144 case Pleroma.Config.get([:instance, :account_activation_required]) do
145 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
150 @doc "GET /api/v1/accounts/verify_credentials"
151 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
152 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
154 render(conn, "show.json",
157 with_pleroma_settings: true,
158 with_chat_token: chat_token
162 @doc "PATCH /api/v1/accounts/update_credentials"
163 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
166 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
169 # We use an empty string as a special value to reset
170 # avatars, banners, backgrounds
171 user_image_value = fn
173 value -> {:ok, value}
179 :hide_followers_count,
185 :skip_thread_containment,
186 :allow_following_move,
188 :accepts_chat_messages
190 |> Enum.reduce(%{}, fn key, acc ->
191 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
193 |> Maps.put_if_present(:name, params[:display_name])
194 |> Maps.put_if_present(:bio, params[:note])
195 |> Maps.put_if_present(:raw_bio, params[:note])
196 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
197 |> Maps.put_if_present(:banner, params[:header], user_image_value)
198 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
199 |> Maps.put_if_present(
201 params[:fields_attributes],
202 &{:ok, normalize_fields_attributes(&1)}
204 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
205 |> Maps.put_if_present(:default_scope, params[:default_scope])
206 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
207 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
208 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
210 |> Maps.put_if_present(:actor_type, params[:actor_type])
211 |> Maps.put_if_present(:also_known_as, params[:also_known_as])
212 # Note: param name is indeed :locked (not an error)
213 |> Maps.put_if_present(:is_locked, params[:locked])
214 # Note: param name is indeed :discoverable (not an error)
215 |> Maps.put_if_present(:is_discoverable, params[:discoverable])
219 # We want to update the user through the pipeline, but the ActivityPub
220 # update information is not quite enough for this, because this also
221 # contains local settings that don't federate and don't even appear
222 # in the Update activity.
224 # So we first build the normal local changeset, then apply it to the
225 # user data, but don't persist it. With this, we generate the object
226 # data for our update activity. We feed this and the changeset as meta
227 # inforation into the pipeline, where they will be properly updated and
229 with changeset <- User.update_changeset(user, user_params),
230 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
232 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
233 |> Map.delete("@context"),
234 {:ok, update_data, []} <- Builder.update(user, updated_object),
236 Pipeline.common_pipeline(update_data,
238 user_update_changeset: changeset
240 render(conn, "show.json",
241 user: unpersisted_user,
242 for: unpersisted_user,
243 with_pleroma_settings: true
246 _e -> render_error(conn, :forbidden, "Invalid request")
250 defp normalize_fields_attributes(fields) do
251 if Enum.all?(fields, &is_tuple/1) do
252 Enum.map(fields, fn {_, v} -> v end)
255 %{} = field -> %{"name" => field.name, "value" => field.value}
261 @doc "GET /api/v1/accounts/relationships"
262 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
263 targets = User.get_all_by_ids(List.wrap(id))
265 render(conn, "relationships.json", user: user, targets: targets)
268 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
269 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
271 @doc "GET /api/v1/accounts/:id"
272 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
273 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
274 :visible <- User.visible_for(user, for_user) do
275 render(conn, "show.json",
278 embed_relationships: embed_relationships?(params)
281 error -> user_visibility_error(conn, error)
285 @doc "GET /api/v1/accounts/:id/statuses"
286 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
287 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
288 :visible <- User.visible_for(user, reading_user) do
291 |> Map.delete(:tagged)
292 |> Map.put(:tag, params[:tagged])
294 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
297 |> add_link_headers(activities)
298 |> put_view(StatusView)
299 |> render("index.json",
300 activities: activities,
303 with_muted: Map.get(params, :with_muted, false)
306 error -> user_visibility_error(conn, error)
310 defp user_visibility_error(conn, error) do
312 :restrict_unauthenticated ->
313 render_error(conn, :unauthorized, "This API requires an authenticated user")
316 render_error(conn, :not_found, "Can't find user")
320 @doc "GET /api/v1/accounts/:id/followers"
321 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
324 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
329 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
330 user.hide_followers -> []
331 true -> MastodonAPI.get_followers(user, params)
335 |> add_link_headers(followers)
336 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
337 |> render("index.json",
341 embed_relationships: embed_relationships?(params)
345 @doc "GET /api/v1/accounts/:id/following"
346 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
349 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
354 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
355 user.hide_follows -> []
356 true -> MastodonAPI.get_friends(user, params)
360 |> add_link_headers(followers)
361 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
362 |> render("index.json",
366 embed_relationships: embed_relationships?(params)
370 @doc "GET /api/v1/accounts/:id/lists"
371 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
372 lists = Pleroma.List.get_lists_account_belongs(user, account)
375 |> put_view(ListView)
376 |> render("index.json", lists: lists)
379 @doc "POST /api/v1/accounts/:id/follow"
380 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
381 {:error, "Can not follow yourself"}
384 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
385 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
386 render(conn, "relationship.json", user: follower, target: followed)
388 {:error, message} -> json_response(conn, :forbidden, %{error: message})
392 @doc "POST /api/v1/accounts/:id/unfollow"
393 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
394 {:error, "Can not unfollow yourself"}
397 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
398 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
399 render(conn, "relationship.json", user: follower, target: followed)
403 @doc "POST /api/v1/accounts/:id/mute"
404 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
405 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
406 render(conn, "relationship.json", user: muter, target: muted)
408 {:error, message} -> json_response(conn, :forbidden, %{error: message})
412 @doc "POST /api/v1/accounts/:id/unmute"
413 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
414 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
415 render(conn, "relationship.json", user: muter, target: muted)
417 {:error, message} -> json_response(conn, :forbidden, %{error: message})
421 @doc "POST /api/v1/accounts/:id/block"
422 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
423 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
424 render(conn, "relationship.json", user: blocker, target: blocked)
426 {:error, message} -> json_response(conn, :forbidden, %{error: message})
430 @doc "POST /api/v1/accounts/:id/unblock"
431 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
432 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
433 render(conn, "relationship.json", user: blocker, target: blocked)
435 {:error, message} -> json_response(conn, :forbidden, %{error: message})
439 @doc "POST /api/v1/follows"
440 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
441 case User.get_cached_by_nickname(uri) do
444 |> assign(:account, user)
452 @doc "GET /api/v1/mutes"
453 def mutes(%{assigns: %{user: user}} = conn, params) do
456 |> User.muted_users_relation(_restrict_deactivated = true)
457 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
460 |> add_link_headers(users)
461 |> render("index.json",
465 embed_relationships: embed_relationships?(params)
469 @doc "GET /api/v1/blocks"
470 def blocks(%{assigns: %{user: user}} = conn, params) do
473 |> User.blocked_users_relation(_restrict_deactivated = true)
474 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
477 |> add_link_headers(users)
478 |> render("index.json", users: users, for: user, as: :user)
481 @doc "GET /api/v1/endorsements"
482 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
484 @doc "GET /api/v1/identity_proofs"
485 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)