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])
223 # We want to update the user through the pipeline, but the ActivityPub
224 # update information is not quite enough for this, because this also
225 # contains local settings that don't federate and don't even appear
226 # in the Update activity.
228 # So we first build the normal local changeset, then apply it to the
229 # user data, but don't persist it. With this, we generate the object
230 # data for our update activity. We feed this and the changeset as meta
231 # inforation into the pipeline, where they will be properly updated and
233 with changeset <- User.update_changeset(user, user_params),
234 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
236 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
237 |> Map.delete("@context"),
238 {:ok, update_data, []} <- Builder.update(user, updated_object),
240 Pipeline.common_pipeline(update_data,
242 user_update_changeset: changeset
244 render(conn, "show.json",
245 user: unpersisted_user,
246 for: unpersisted_user,
247 with_pleroma_settings: true
250 _e -> render_error(conn, :forbidden, "Invalid request")
254 defp normalize_fields_attributes(fields) do
255 if Enum.all?(fields, &is_tuple/1) do
256 Enum.map(fields, fn {_, v} -> v end)
259 %{} = field -> %{"name" => field.name, "value" => field.value}
265 @doc "GET /api/v1/accounts/relationships"
266 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
267 targets = User.get_all_by_ids(List.wrap(id))
269 render(conn, "relationships.json", user: user, targets: targets)
272 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
273 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
275 @doc "GET /api/v1/accounts/:id"
276 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
277 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
278 :visible <- User.visible_for(user, for_user) do
279 render(conn, "show.json",
282 embed_relationships: embed_relationships?(params)
285 error -> user_visibility_error(conn, error)
289 @doc "GET /api/v1/accounts/:id/statuses"
290 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
291 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
292 :visible <- User.visible_for(user, reading_user) do
295 |> Map.delete(:tagged)
296 |> Map.put(:tag, params[:tagged])
298 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
301 |> add_link_headers(activities)
302 |> put_view(StatusView)
303 |> render("index.json",
304 activities: activities,
307 with_muted: Map.get(params, :with_muted, false)
310 error -> user_visibility_error(conn, error)
314 defp user_visibility_error(conn, error) do
316 :restrict_unauthenticated ->
317 render_error(conn, :unauthorized, "This API requires an authenticated user")
320 render_error(conn, :not_found, "Can't find user")
324 @doc "GET /api/v1/accounts/:id/followers"
325 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
328 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
333 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
334 user.hide_followers -> []
335 true -> MastodonAPI.get_followers(user, params)
339 |> add_link_headers(followers)
340 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
341 |> render("index.json",
345 embed_relationships: embed_relationships?(params)
349 @doc "GET /api/v1/accounts/:id/following"
350 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
353 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
358 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
359 user.hide_follows -> []
360 true -> MastodonAPI.get_friends(user, params)
364 |> add_link_headers(followers)
365 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
366 |> render("index.json",
370 embed_relationships: embed_relationships?(params)
374 @doc "GET /api/v1/accounts/:id/lists"
375 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
376 lists = Pleroma.List.get_lists_account_belongs(user, account)
379 |> put_view(ListView)
380 |> render("index.json", lists: lists)
383 @doc "POST /api/v1/accounts/:id/follow"
384 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
385 {:error, "Can not follow yourself"}
388 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
389 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
390 render(conn, "relationship.json", user: follower, target: followed)
392 {:error, message} -> json_response(conn, :forbidden, %{error: message})
396 @doc "POST /api/v1/accounts/:id/unfollow"
397 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
398 {:error, "Can not unfollow yourself"}
401 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
402 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
403 render(conn, "relationship.json", user: follower, target: followed)
407 @doc "POST /api/v1/accounts/:id/mute"
408 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
409 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
410 render(conn, "relationship.json", user: muter, target: muted)
412 {:error, message} -> json_response(conn, :forbidden, %{error: message})
416 @doc "POST /api/v1/accounts/:id/unmute"
417 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
418 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
419 render(conn, "relationship.json", user: muter, target: muted)
421 {:error, message} -> json_response(conn, :forbidden, %{error: message})
425 @doc "POST /api/v1/accounts/:id/block"
426 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
427 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
428 render(conn, "relationship.json", user: blocker, target: blocked)
430 {:error, message} -> json_response(conn, :forbidden, %{error: message})
434 @doc "POST /api/v1/accounts/:id/unblock"
435 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
436 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
437 render(conn, "relationship.json", user: blocker, target: blocked)
439 {:error, message} -> json_response(conn, :forbidden, %{error: message})
443 @doc "POST /api/v1/accounts/:id/note"
445 %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
448 with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
449 render(conn, "relationship.json", user: noter, target: target)
453 @doc "POST /api/v1/follows"
454 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
455 case User.get_cached_by_nickname(uri) do
458 |> assign(:account, user)
466 @doc "GET /api/v1/mutes"
467 def mutes(%{assigns: %{user: user}} = conn, params) do
470 |> User.muted_users_relation(_restrict_deactivated = true)
471 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
474 |> add_link_headers(users)
475 |> render("index.json",
479 embed_relationships: embed_relationships?(params)
483 @doc "GET /api/v1/blocks"
484 def blocks(%{assigns: %{user: user}} = conn, params) do
487 |> User.blocked_users_relation(_restrict_deactivated = true)
488 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
491 |> add_link_headers(users)
492 |> render("index.json", users: users, for: user, as: :user)
495 @doc "GET /api/v1/accounts/lookup"
496 def lookup(conn, %{acct: nickname} = _params) do
497 with %User{} = user <- User.get_by_nickname(nickname) do
498 render(conn, "show.json",
500 skip_visibility_check: true
503 error -> user_visibility_error(conn, error)
507 @doc "GET /api/v1/endorsements"
508 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
510 @doc "GET /api/v1/identity_proofs"
511 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)