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.Web.ActivityPub.ActivityPub
19 alias Pleroma.Web.ActivityPub.Builder
20 alias Pleroma.Web.ActivityPub.Pipeline
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.MastodonAPI.ListView
23 alias Pleroma.Web.MastodonAPI.MastodonAPI
24 alias Pleroma.Web.MastodonAPI.MastodonAPIController
25 alias Pleroma.Web.MastodonAPI.StatusView
26 alias Pleroma.Web.OAuth.OAuthController
27 alias Pleroma.Web.Plugs.OAuthScopesPlug
28 alias Pleroma.Web.Plugs.RateLimiter
29 alias Pleroma.Web.TwitterAPI.TwitterAPI
30 alias Pleroma.Web.Utils.Params
32 plug(Pleroma.Web.ApiSpec.CastAndValidate)
34 plug(:skip_auth when action == :create)
36 plug(:skip_public_check when action in [:show, :statuses])
40 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
41 when action in [:show, :followers, :following]
46 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
47 when action == :statuses
52 %{scopes: ["read:accounts"]}
53 when action in [:verify_credentials, :endorsements, :identity_proofs]
56 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
58 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
62 %{scopes: ["follow", "read:blocks"]} when action == :blocks
67 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
70 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
74 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
77 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
79 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
81 @relationship_actions [:follow, :unfollow]
82 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
86 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
89 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
90 plug(RateLimiter, [name: :app_account_creation] when action == :create)
91 plug(:assign_account_by_id when action in @needs_account)
93 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
95 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
97 @doc "POST /api/v1/accounts"
98 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
99 with :ok <- validate_email_param(params),
100 :ok <- TwitterAPI.validate_captcha(app, params),
101 {:ok, user} <- TwitterAPI.register_user(params),
103 {:login, OAuthController.login(user, app, app.scopes)} do
104 OAuthController.after_token_exchange(conn, %{user: user, token: token})
106 {:login, {:account_status, :confirmation_pending}} ->
107 json_response(conn, :ok, %{
108 message: "You have been registered. Please check your email for further instructions.",
109 identifier: "missing_confirmed_email"
112 {:login, {:account_status, :approval_pending}} ->
113 json_response(conn, :ok, %{
115 "You have been registered. You'll be able to log in once your account is approved.",
116 identifier: "awaiting_approval"
120 json_response(conn, :ok, %{
122 "You have been registered. Some post-registration steps may be pending. " <>
123 "Please log in manually.",
124 identifier: "manual_login_required"
128 json_response(conn, :bad_request, %{error: error})
132 def create(%{assigns: %{app: _app}} = conn, _) do
133 render_error(conn, :bad_request, "Missing parameters")
136 def create(conn, _) do
137 render_error(conn, :forbidden, "Invalid credentials")
140 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
142 defp validate_email_param(_) do
143 case Pleroma.Config.get([:instance, :account_activation_required]) do
144 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
149 @doc "GET /api/v1/accounts/verify_credentials"
150 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
151 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
153 render(conn, "show.json",
156 with_pleroma_settings: true,
157 with_chat_token: chat_token
161 @doc "PATCH /api/v1/accounts/update_credentials"
162 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
165 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
168 # We use an empty string as a special value to reset
169 # avatars, banners, backgrounds
170 user_image_value = fn
172 value -> {:ok, value}
178 :hide_followers_count,
184 :skip_thread_containment,
185 :allow_following_move,
187 :accepts_chat_messages
189 |> Enum.reduce(%{}, fn key, acc ->
190 Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
192 |> Maps.put_if_present(:name, params[:display_name])
193 |> Maps.put_if_present(:bio, params[:note])
194 |> Maps.put_if_present(:raw_bio, params[:note])
195 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
196 |> Maps.put_if_present(:banner, params[:header], user_image_value)
197 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
198 |> Maps.put_if_present(
200 params[:fields_attributes],
201 &{:ok, normalize_fields_attributes(&1)}
203 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
204 |> Maps.put_if_present(:default_scope, params[:default_scope])
205 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
206 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
207 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
209 |> Maps.put_if_present(:actor_type, params[:actor_type])
210 |> Maps.put_if_present(:also_known_as, params[:also_known_as])
211 # Note: param name is indeed :locked (not an error)
212 |> Maps.put_if_present(:is_locked, params[:locked])
213 # Note: param name is indeed :discoverable (not an error)
214 |> Maps.put_if_present(:is_discoverable, params[:discoverable])
218 # We want to update the user through the pipeline, but the ActivityPub
219 # update information is not quite enough for this, because this also
220 # contains local settings that don't federate and don't even appear
221 # in the Update activity.
223 # So we first build the normal local changeset, then apply it to the
224 # user data, but don't persist it. With this, we generate the object
225 # data for our update activity. We feed this and the changeset as meta
226 # inforation into the pipeline, where they will be properly updated and
228 with changeset <- User.update_changeset(user, user_params),
229 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
231 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
232 |> Map.delete("@context"),
233 {:ok, update_data, []} <- Builder.update(user, updated_object),
235 Pipeline.common_pipeline(update_data,
237 user_update_changeset: changeset
239 render(conn, "show.json",
240 user: unpersisted_user,
241 for: unpersisted_user,
242 with_pleroma_settings: true
245 _e -> render_error(conn, :forbidden, "Invalid request")
249 defp normalize_fields_attributes(fields) do
250 if Enum.all?(fields, &is_tuple/1) do
251 Enum.map(fields, fn {_, v} -> v end)
254 %{} = field -> %{"name" => field.name, "value" => field.value}
260 @doc "GET /api/v1/accounts/relationships"
261 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
262 targets = User.get_all_by_ids(List.wrap(id))
264 render(conn, "relationships.json", user: user, targets: targets)
267 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
268 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
270 @doc "GET /api/v1/accounts/:id"
271 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
272 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
273 :visible <- User.visible_for(user, for_user) do
274 render(conn, "show.json",
277 embed_relationships: embed_relationships?(params)
280 error -> user_visibility_error(conn, error)
284 @doc "GET /api/v1/accounts/:id/statuses"
285 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
286 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
287 :visible <- User.visible_for(user, reading_user) do
290 |> Map.delete(:tagged)
291 |> Map.put(:tag, params[:tagged])
293 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
296 |> add_link_headers(activities)
297 |> put_view(StatusView)
298 |> render("index.json",
299 activities: activities,
302 with_muted: Map.get(params, :with_muted, false)
305 error -> user_visibility_error(conn, error)
309 defp user_visibility_error(conn, error) do
311 :restrict_unauthenticated ->
312 render_error(conn, :unauthorized, "This API requires an authenticated user")
315 render_error(conn, :not_found, "Can't find user")
319 @doc "GET /api/v1/accounts/:id/followers"
320 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
323 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
328 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
329 user.hide_followers -> []
330 true -> MastodonAPI.get_followers(user, params)
334 |> add_link_headers(followers)
335 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
336 |> render("index.json",
340 embed_relationships: embed_relationships?(params)
344 @doc "GET /api/v1/accounts/:id/following"
345 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
348 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
353 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
354 user.hide_follows -> []
355 true -> MastodonAPI.get_friends(user, params)
359 |> add_link_headers(followers)
360 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
361 |> render("index.json",
365 embed_relationships: embed_relationships?(params)
369 @doc "GET /api/v1/accounts/:id/lists"
370 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
371 lists = Pleroma.List.get_lists_account_belongs(user, account)
374 |> put_view(ListView)
375 |> render("index.json", lists: lists)
378 @doc "POST /api/v1/accounts/:id/follow"
379 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
380 {:error, "Can not follow yourself"}
383 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
384 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
385 render(conn, "relationship.json", user: follower, target: followed)
387 {:error, message} -> json_response(conn, :forbidden, %{error: message})
391 @doc "POST /api/v1/accounts/:id/unfollow"
392 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
393 {:error, "Can not unfollow yourself"}
396 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
397 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
398 render(conn, "relationship.json", user: follower, target: followed)
402 @doc "POST /api/v1/accounts/:id/mute"
403 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
404 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
405 render(conn, "relationship.json", user: muter, target: muted)
407 {:error, message} -> json_response(conn, :forbidden, %{error: message})
411 @doc "POST /api/v1/accounts/:id/unmute"
412 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
413 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
414 render(conn, "relationship.json", user: muter, target: muted)
416 {:error, message} -> json_response(conn, :forbidden, %{error: message})
420 @doc "POST /api/v1/accounts/:id/block"
421 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
422 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
423 render(conn, "relationship.json", user: blocker, target: blocked)
425 {:error, message} -> json_response(conn, :forbidden, %{error: message})
429 @doc "POST /api/v1/accounts/:id/unblock"
430 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
431 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
432 render(conn, "relationship.json", user: blocker, target: blocked)
434 {:error, message} -> json_response(conn, :forbidden, %{error: message})
438 @doc "POST /api/v1/follows"
439 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
440 case User.get_cached_by_nickname(uri) do
443 |> assign(:account, user)
451 @doc "GET /api/v1/mutes"
452 def mutes(%{assigns: %{user: user}} = conn, params) do
455 |> User.muted_users_relation(_restrict_deactivated = true)
456 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
459 |> add_link_headers(users)
460 |> render("index.json",
464 embed_relationships: embed_relationships?(params)
468 @doc "GET /api/v1/blocks"
469 def blocks(%{assigns: %{user: user}} = conn, params) do
472 |> User.blocked_users_relation(_restrict_deactivated = true)
473 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
476 |> add_link_headers(users)
477 |> render("index.json", users: users, for: user, as: :user)
480 @doc "GET /api/v1/endorsements"
481 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
483 @doc "GET /api/v1/identity_proofs"
484 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)