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, :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]
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"]} when action in [:follow_by_uri, :follow, :unfollow]
82 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
84 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
86 @relationship_actions [:follow, :unfollow]
87 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock note)a
91 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
94 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
95 plug(RateLimiter, [name: :app_account_creation] when action == :create)
96 plug(:assign_account_by_id when action in @needs_account)
98 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
100 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
102 @doc "POST /api/v1/accounts"
103 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
104 with :ok <- validate_email_param(params),
105 :ok <- TwitterAPI.validate_captcha(app, params),
106 {:ok, user} <- TwitterAPI.register_user(params),
108 {:login, OAuthController.login(user, app, app.scopes)} do
109 OAuthController.after_token_exchange(conn, %{user: user, token: token})
111 {:login, {:account_status, :confirmation_pending}} ->
112 json_response(conn, :ok, %{
113 message: "You have been registered. Please check your email for further instructions.",
114 identifier: "missing_confirmed_email"
117 {:login, {:account_status, :approval_pending}} ->
118 json_response(conn, :ok, %{
120 "You have been registered. You'll be able to log in once your account is approved.",
121 identifier: "awaiting_approval"
125 json_response(conn, :ok, %{
127 "You have been registered. Some post-registration steps may be pending. " <>
128 "Please log in manually.",
129 identifier: "manual_login_required"
133 json_response(conn, :bad_request, %{error: error})
137 def create(%{assigns: %{app: _app}} = conn, _) do
138 render_error(conn, :bad_request, "Missing parameters")
141 def create(conn, _) do
142 render_error(conn, :forbidden, "Invalid credentials")
145 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
147 defp validate_email_param(_) do
148 case Pleroma.Config.get([:instance, :account_activation_required]) do
149 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
154 @doc "GET /api/v1/accounts/verify_credentials"
155 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
156 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
158 render(conn, "show.json",
161 with_pleroma_settings: true,
162 with_chat_token: chat_token
166 @doc "PATCH /api/v1/accounts/update_credentials"
167 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
170 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
173 # We use an empty string as a special value to reset
174 # avatars, banners, backgrounds
175 user_image_value = fn
177 value -> {:ok, value}
183 :hide_followers_count,
189 :skip_thread_containment,
190 :allow_following_move,
192 :accepts_chat_messages
194 |> Enum.reduce(%{}, fn key, acc ->
195 Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
197 |> Maps.put_if_present(:name, params[:display_name])
198 |> Maps.put_if_present(:bio, params[:note])
199 |> Maps.put_if_present(:raw_bio, params[:note])
200 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
201 |> Maps.put_if_present(:banner, params[:header], user_image_value)
202 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
203 |> Maps.put_if_present(
205 params[:fields_attributes],
206 &{:ok, normalize_fields_attributes(&1)}
208 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
209 |> Maps.put_if_present(:default_scope, params[:default_scope])
210 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
211 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
212 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
214 |> Maps.put_if_present(:actor_type, params[:actor_type])
215 |> Maps.put_if_present(:also_known_as, params[:also_known_as])
216 # Note: param name is indeed :locked (not an error)
217 |> Maps.put_if_present(:is_locked, params[:locked])
218 # Note: param name is indeed :discoverable (not an error)
219 |> Maps.put_if_present(:is_discoverable, params[:discoverable])
220 |> Maps.put_if_present(:language, params[:language])
224 # We want to update the user through the pipeline, but the ActivityPub
225 # update information is not quite enough for this, because this also
226 # contains local settings that don't federate and don't even appear
227 # in the Update activity.
229 # So we first build the normal local changeset, then apply it to the
230 # user data, but don't persist it. With this, we generate the object
231 # data for our update activity. We feed this and the changeset as meta
232 # inforation into the pipeline, where they will be properly updated and
234 with changeset <- User.update_changeset(user, user_params),
235 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
237 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
238 |> Map.delete("@context"),
239 {:ok, update_data, []} <- Builder.update(user, updated_object),
241 Pipeline.common_pipeline(update_data,
243 user_update_changeset: changeset
245 render(conn, "show.json",
246 user: unpersisted_user,
247 for: unpersisted_user,
248 with_pleroma_settings: true
251 _e -> render_error(conn, :forbidden, "Invalid request")
255 defp normalize_fields_attributes(fields) do
256 if Enum.all?(fields, &is_tuple/1) do
257 Enum.map(fields, fn {_, v} -> v end)
260 %{} = field -> %{"name" => field.name, "value" => field.value}
266 @doc "GET /api/v1/accounts/relationships"
267 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
268 targets = User.get_all_by_ids(List.wrap(id))
270 render(conn, "relationships.json", user: user, targets: targets)
273 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
274 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
276 @doc "GET /api/v1/accounts/:id"
277 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
278 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
279 :visible <- User.visible_for(user, for_user) do
280 render(conn, "show.json",
283 embed_relationships: embed_relationships?(params)
286 error -> user_visibility_error(conn, error)
290 @doc "GET /api/v1/accounts/:id/statuses"
291 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
292 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
293 :visible <- User.visible_for(user, reading_user) do
296 |> Map.delete(:tagged)
297 |> Map.put(:tag, params[:tagged])
299 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
302 |> add_link_headers(activities)
303 |> put_view(StatusView)
304 |> render("index.json",
305 activities: activities,
308 with_muted: Map.get(params, :with_muted, false)
311 error -> user_visibility_error(conn, error)
315 defp user_visibility_error(conn, error) do
317 :restrict_unauthenticated ->
318 render_error(conn, :unauthorized, "This API requires an authenticated user")
321 render_error(conn, :not_found, "Can't find user")
325 @doc "GET /api/v1/accounts/:id/followers"
326 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
329 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
334 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
335 user.hide_followers -> []
336 true -> MastodonAPI.get_followers(user, params)
340 |> add_link_headers(followers)
341 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
342 |> render("index.json",
346 embed_relationships: embed_relationships?(params)
350 @doc "GET /api/v1/accounts/:id/following"
351 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
354 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
359 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
360 user.hide_follows -> []
361 true -> MastodonAPI.get_friends(user, params)
365 |> add_link_headers(followers)
366 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
367 |> render("index.json",
371 embed_relationships: embed_relationships?(params)
375 @doc "GET /api/v1/accounts/:id/lists"
376 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
377 lists = Pleroma.List.get_lists_account_belongs(user, account)
380 |> put_view(ListView)
381 |> render("index.json", lists: lists)
384 @doc "POST /api/v1/accounts/:id/follow"
385 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
386 {:error, "Can not follow yourself"}
389 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
390 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
391 render(conn, "relationship.json", user: follower, target: followed)
393 {:error, message} -> json_response(conn, :forbidden, %{error: message})
397 @doc "POST /api/v1/accounts/:id/unfollow"
398 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
399 {:error, "Can not unfollow yourself"}
402 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
403 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
404 render(conn, "relationship.json", user: follower, target: followed)
408 @doc "POST /api/v1/accounts/:id/mute"
409 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
410 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
411 render(conn, "relationship.json", user: muter, target: muted)
413 {:error, message} -> json_response(conn, :forbidden, %{error: message})
417 @doc "POST /api/v1/accounts/:id/unmute"
418 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
419 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
420 render(conn, "relationship.json", user: muter, target: muted)
422 {:error, message} -> json_response(conn, :forbidden, %{error: message})
426 @doc "POST /api/v1/accounts/:id/block"
427 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
428 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
429 render(conn, "relationship.json", user: blocker, target: blocked)
431 {:error, message} -> json_response(conn, :forbidden, %{error: message})
435 @doc "POST /api/v1/accounts/:id/unblock"
436 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
437 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
438 render(conn, "relationship.json", user: blocker, target: blocked)
440 {:error, message} -> json_response(conn, :forbidden, %{error: message})
444 @doc "POST /api/v1/accounts/:id/note"
446 %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
449 with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
450 render(conn, "relationship.json", user: noter, target: target)
454 @doc "POST /api/v1/follows"
455 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
456 case User.get_cached_by_nickname(uri) do
459 |> assign(:account, user)
467 @doc "GET /api/v1/mutes"
468 def mutes(%{assigns: %{user: user}} = conn, params) do
471 |> User.muted_users_relation(_restrict_deactivated = true)
472 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
475 |> add_link_headers(users)
476 |> render("index.json",
480 embed_relationships: embed_relationships?(params)
484 @doc "GET /api/v1/blocks"
485 def blocks(%{assigns: %{user: user}} = conn, params) do
488 |> User.blocked_users_relation(_restrict_deactivated = true)
489 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
492 |> add_link_headers(users)
493 |> render("index.json", users: users, for: user, as: :user)
496 @doc "GET /api/v1/accounts/lookup"
497 def lookup(conn, %{acct: nickname} = _params) do
498 with %User{} = user <- User.get_by_nickname(nickname) do
499 render(conn, "show.json",
501 skip_visibility_check: true
504 error -> user_visibility_error(conn, error)
508 @doc "GET /api/v1/endorsements"
509 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
511 @doc "GET /api/v1/identity_proofs"
512 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)