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 render(conn, "show.json",
159 with_pleroma_settings: true
163 @doc "PATCH /api/v1/accounts/update_credentials"
164 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
167 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
170 # We use an empty string as a special value to reset
171 # avatars, banners, backgrounds
172 user_image_value = fn
174 value -> {:ok, value}
180 :hide_followers_count,
186 :skip_thread_containment,
187 :allow_following_move,
190 |> Enum.reduce(%{}, fn key, acc ->
191 Maps.put_if_present(acc, key, params[key], &{:ok, Params.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])
216 |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
220 # We want to update the user through the pipeline, but the ActivityPub
221 # update information is not quite enough for this, because this also
222 # contains local settings that don't federate and don't even appear
223 # in the Update activity.
225 # So we first build the normal local changeset, then apply it to the
226 # user data, but don't persist it. With this, we generate the object
227 # data for our update activity. We feed this and the changeset as meta
228 # inforation into the pipeline, where they will be properly updated and
230 with changeset <- User.update_changeset(user, user_params),
231 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
233 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
234 |> Map.delete("@context"),
235 {:ok, update_data, []} <- Builder.update(user, updated_object),
237 Pipeline.common_pipeline(update_data,
239 user_update_changeset: changeset
241 render(conn, "show.json",
242 user: unpersisted_user,
243 for: unpersisted_user,
244 with_pleroma_settings: true
247 _e -> render_error(conn, :forbidden, "Invalid request")
251 defp normalize_fields_attributes(fields) do
252 if Enum.all?(fields, &is_tuple/1) do
253 Enum.map(fields, fn {_, v} -> v end)
256 %{} = field -> %{"name" => field.name, "value" => field.value}
262 @doc "GET /api/v1/accounts/relationships"
263 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
264 targets = User.get_all_by_ids(List.wrap(id))
266 render(conn, "relationships.json", user: user, targets: targets)
269 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
270 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
272 @doc "GET /api/v1/accounts/:id"
273 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
274 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
275 :visible <- User.visible_for(user, for_user) do
276 render(conn, "show.json",
279 embed_relationships: embed_relationships?(params)
282 error -> user_visibility_error(conn, error)
286 @doc "GET /api/v1/accounts/:id/statuses"
287 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
288 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
289 :visible <- User.visible_for(user, reading_user) do
292 |> Map.delete(:tagged)
293 |> Map.put(:tag, params[:tagged])
295 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
298 |> add_link_headers(activities)
299 |> put_view(StatusView)
300 |> render("index.json",
301 activities: activities,
304 with_muted: Map.get(params, :with_muted, false)
307 error -> user_visibility_error(conn, error)
311 defp user_visibility_error(conn, error) do
313 :restrict_unauthenticated ->
314 render_error(conn, :unauthorized, "This API requires an authenticated user")
317 render_error(conn, :not_found, "Can't find user")
321 @doc "GET /api/v1/accounts/:id/followers"
322 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
325 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
330 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
331 user.hide_followers -> []
332 true -> MastodonAPI.get_followers(user, params)
336 |> add_link_headers(followers)
337 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
338 |> render("index.json",
342 embed_relationships: embed_relationships?(params)
346 @doc "GET /api/v1/accounts/:id/following"
347 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
350 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
355 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
356 user.hide_follows -> []
357 true -> MastodonAPI.get_friends(user, params)
361 |> add_link_headers(followers)
362 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
363 |> render("index.json",
367 embed_relationships: embed_relationships?(params)
371 @doc "GET /api/v1/accounts/:id/lists"
372 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
373 lists = Pleroma.List.get_lists_account_belongs(user, account)
376 |> put_view(ListView)
377 |> render("index.json", lists: lists)
380 @doc "POST /api/v1/accounts/:id/follow"
381 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
382 {:error, "Can not follow yourself"}
385 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
386 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
387 render(conn, "relationship.json", user: follower, target: followed)
389 {:error, message} -> json_response(conn, :forbidden, %{error: message})
393 @doc "POST /api/v1/accounts/:id/unfollow"
394 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
395 {:error, "Can not unfollow yourself"}
398 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
399 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
400 render(conn, "relationship.json", user: follower, target: followed)
404 @doc "POST /api/v1/accounts/:id/mute"
405 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
406 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
407 render(conn, "relationship.json", user: muter, target: muted)
409 {:error, message} -> json_response(conn, :forbidden, %{error: message})
413 @doc "POST /api/v1/accounts/:id/unmute"
414 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
415 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
416 render(conn, "relationship.json", user: muter, target: muted)
418 {:error, message} -> json_response(conn, :forbidden, %{error: message})
422 @doc "POST /api/v1/accounts/:id/block"
423 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
424 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
425 render(conn, "relationship.json", user: blocker, target: blocked)
427 {:error, message} -> json_response(conn, :forbidden, %{error: message})
431 @doc "POST /api/v1/accounts/:id/unblock"
432 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
433 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
434 render(conn, "relationship.json", user: blocker, target: blocked)
436 {:error, message} -> json_response(conn, :forbidden, %{error: message})
440 @doc "POST /api/v1/accounts/:id/note"
442 %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
445 with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
446 render(conn, "relationship.json", user: noter, target: target)
450 @doc "POST /api/v1/follows"
451 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
452 case User.get_cached_by_nickname(uri) do
455 |> assign(:account, user)
463 @doc "GET /api/v1/mutes"
464 def mutes(%{assigns: %{user: user}} = conn, params) do
467 |> User.muted_users_relation(_restrict_deactivated = true)
468 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
471 |> add_link_headers(users)
472 |> render("index.json",
476 embed_relationships: embed_relationships?(params)
480 @doc "GET /api/v1/blocks"
481 def blocks(%{assigns: %{user: user}} = conn, params) do
484 |> User.blocked_users_relation(_restrict_deactivated = true)
485 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
488 |> add_link_headers(users)
489 |> render("index.json", users: users, for: user, as: :user)
492 @doc "GET /api/v1/accounts/lookup"
493 def lookup(conn, %{acct: nickname} = _params) do
494 with %User{} = user <- User.get_by_nickname(nickname) do
495 render(conn, "show.json",
497 skip_visibility_check: true
500 error -> user_visibility_error(conn, error)
504 @doc "GET /api/v1/endorsements"
505 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
507 @doc "GET /api/v1/identity_proofs"
508 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)