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"]}
80 when action in [:follow_by_uri, :follow, :unfollow, :remove_from_followers]
83 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
85 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
87 @relationship_actions [:follow, :unfollow, :remove_from_followers]
88 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock note remove_from_followers)a
92 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
95 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
96 plug(RateLimiter, [name: :app_account_creation] when action == :create)
97 plug(:assign_account_by_id when action in @needs_account)
99 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
101 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
103 @doc "POST /api/v1/accounts"
104 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
105 with :ok <- validate_email_param(params),
106 :ok <- TwitterAPI.validate_captcha(app, params),
107 {:ok, user} <- TwitterAPI.register_user(params),
109 {:login, OAuthController.login(user, app, app.scopes)} do
110 OAuthController.after_token_exchange(conn, %{user: user, token: token})
112 {:login, {:account_status, :confirmation_pending}} ->
113 json_response(conn, :ok, %{
114 message: "You have been registered. Please check your email for further instructions.",
115 identifier: "missing_confirmed_email"
118 {:login, {:account_status, :approval_pending}} ->
119 json_response(conn, :ok, %{
121 "You have been registered. You'll be able to log in once your account is approved.",
122 identifier: "awaiting_approval"
126 json_response(conn, :ok, %{
128 "You have been registered. Some post-registration steps may be pending. " <>
129 "Please log in manually.",
130 identifier: "manual_login_required"
134 json_response(conn, :bad_request, %{error: error})
138 def create(%{assigns: %{app: _app}} = conn, _) do
139 render_error(conn, :bad_request, "Missing parameters")
142 def create(conn, _) do
143 render_error(conn, :forbidden, "Invalid credentials")
146 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
148 defp validate_email_param(_) do
149 case Pleroma.Config.get([:instance, :account_activation_required]) do
150 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
155 @doc "GET /api/v1/accounts/verify_credentials"
156 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
157 render(conn, "show.json",
160 with_pleroma_settings: true
164 @doc "PATCH /api/v1/accounts/update_credentials"
165 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
168 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
171 # We use an empty string as a special value to reset
172 # avatars, banners, backgrounds
173 user_image_value = fn
175 value -> {:ok, value}
181 :hide_followers_count,
187 :skip_thread_containment,
188 :allow_following_move,
191 |> Enum.reduce(%{}, fn key, acc ->
192 Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
194 |> Maps.put_if_present(:name, params[:display_name])
195 |> Maps.put_if_present(:bio, params[:note])
196 |> Maps.put_if_present(:raw_bio, params[:note])
197 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
198 |> Maps.put_if_present(:banner, params[:header], user_image_value)
199 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
200 |> Maps.put_if_present(
202 params[:fields_attributes],
203 &{:ok, normalize_fields_attributes(&1)}
205 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
206 |> Maps.put_if_present(:default_scope, params[:default_scope])
207 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
208 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
209 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
211 |> Maps.put_if_present(:actor_type, params[:actor_type])
212 |> Maps.put_if_present(:also_known_as, params[:also_known_as])
213 # Note: param name is indeed :locked (not an error)
214 |> Maps.put_if_present(:is_locked, params[:locked])
215 # Note: param name is indeed :discoverable (not an error)
216 |> Maps.put_if_present(:is_discoverable, params[:discoverable])
217 |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
221 # We want to update the user through the pipeline, but the ActivityPub
222 # update information is not quite enough for this, because this also
223 # contains local settings that don't federate and don't even appear
224 # in the Update activity.
226 # So we first build the normal local changeset, then apply it to the
227 # user data, but don't persist it. With this, we generate the object
228 # data for our update activity. We feed this and the changeset as meta
229 # inforation into the pipeline, where they will be properly updated and
231 with changeset <- User.update_changeset(user, user_params),
232 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
234 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
235 |> Map.delete("@context"),
236 {:ok, update_data, []} <- Builder.update(user, updated_object),
238 Pipeline.common_pipeline(update_data,
240 user_update_changeset: changeset
242 render(conn, "show.json",
243 user: unpersisted_user,
244 for: unpersisted_user,
245 with_pleroma_settings: true
248 _e -> render_error(conn, :forbidden, "Invalid request")
252 defp normalize_fields_attributes(fields) do
253 if Enum.all?(fields, &is_tuple/1) do
254 Enum.map(fields, fn {_, v} -> v end)
257 %{} = field -> %{"name" => field.name, "value" => field.value}
263 @doc "GET /api/v1/accounts/relationships"
264 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
265 targets = User.get_all_by_ids(List.wrap(id))
267 render(conn, "relationships.json", user: user, targets: targets)
270 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
271 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
273 @doc "GET /api/v1/accounts/:id"
274 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
275 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
276 :visible <- User.visible_for(user, for_user) do
277 render(conn, "show.json",
280 embed_relationships: embed_relationships?(params)
283 error -> user_visibility_error(conn, error)
287 @doc "GET /api/v1/accounts/:id/statuses"
288 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
289 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
290 :visible <- User.visible_for(user, reading_user) do
293 |> Map.delete(:tagged)
294 |> Map.put(:tag, params[:tagged])
296 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
299 |> add_link_headers(activities)
300 |> put_view(StatusView)
301 |> render("index.json",
302 activities: activities,
305 with_muted: Map.get(params, :with_muted, false)
308 error -> user_visibility_error(conn, error)
312 defp user_visibility_error(conn, error) do
314 :restrict_unauthenticated ->
315 render_error(conn, :unauthorized, "This API requires an authenticated user")
318 render_error(conn, :not_found, "Can't find user")
322 @doc "GET /api/v1/accounts/:id/followers"
323 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
326 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
331 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
332 user.hide_followers -> []
333 true -> MastodonAPI.get_followers(user, params)
337 |> add_link_headers(followers)
338 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
339 |> render("index.json",
343 embed_relationships: embed_relationships?(params)
347 @doc "GET /api/v1/accounts/:id/following"
348 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
351 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
356 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
357 user.hide_follows -> []
358 true -> MastodonAPI.get_friends(user, params)
362 |> add_link_headers(followers)
363 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
364 |> render("index.json",
368 embed_relationships: embed_relationships?(params)
372 @doc "GET /api/v1/accounts/:id/lists"
373 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
374 lists = Pleroma.List.get_lists_account_belongs(user, account)
377 |> put_view(ListView)
378 |> render("index.json", lists: lists)
381 @doc "POST /api/v1/accounts/:id/follow"
382 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
383 {:error, "Can not follow yourself"}
386 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
387 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
388 render(conn, "relationship.json", user: follower, target: followed)
390 {:error, message} -> json_response(conn, :forbidden, %{error: message})
394 @doc "POST /api/v1/accounts/:id/unfollow"
395 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
396 {:error, "Can not unfollow yourself"}
399 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
400 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
401 render(conn, "relationship.json", user: follower, target: followed)
405 @doc "POST /api/v1/accounts/:id/mute"
406 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
407 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
408 render(conn, "relationship.json", user: muter, target: muted)
410 {:error, message} -> json_response(conn, :forbidden, %{error: message})
414 @doc "POST /api/v1/accounts/:id/unmute"
415 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
416 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
417 render(conn, "relationship.json", user: muter, target: muted)
419 {:error, message} -> json_response(conn, :forbidden, %{error: message})
423 @doc "POST /api/v1/accounts/:id/block"
424 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
425 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
426 render(conn, "relationship.json", user: blocker, target: blocked)
428 {:error, message} -> json_response(conn, :forbidden, %{error: message})
432 @doc "POST /api/v1/accounts/:id/unblock"
433 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
434 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
435 render(conn, "relationship.json", user: blocker, target: blocked)
437 {:error, message} -> json_response(conn, :forbidden, %{error: message})
441 @doc "POST /api/v1/accounts/:id/note"
443 %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
446 with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
447 render(conn, "relationship.json", user: noter, target: target)
451 @doc "POST /api/v1/accounts/:id/remove_from_followers"
452 def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
453 {:error, "Can not unfollow yourself"}
456 def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do
457 with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
458 render(conn, "relationship.json", user: followed, target: follower)
461 render_error(conn, :not_found, "Record not found")
465 @doc "POST /api/v1/follows"
466 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
467 case User.get_cached_by_nickname(uri) do
470 |> assign(:account, user)
478 @doc "GET /api/v1/mutes"
479 def mutes(%{assigns: %{user: user}} = conn, params) do
482 |> User.muted_users_relation(_restrict_deactivated = true)
483 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
486 |> add_link_headers(users)
487 |> render("index.json",
491 embed_relationships: embed_relationships?(params)
495 @doc "GET /api/v1/blocks"
496 def blocks(%{assigns: %{user: user}} = conn, params) do
499 |> User.blocked_users_relation(_restrict_deactivated = true)
500 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
503 |> add_link_headers(users)
504 |> render("index.json", users: users, for: user, as: :user)
507 @doc "GET /api/v1/accounts/lookup"
508 def lookup(conn, %{acct: nickname} = _params) do
509 with %User{} = user <- User.get_by_nickname(nickname) do
510 render(conn, "show.json",
512 skip_visibility_check: true
515 error -> user_visibility_error(conn, error)
519 @doc "GET /api/v1/endorsements"
520 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
522 @doc "GET /api/v1/identity_proofs"
523 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)