1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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,
12 assign_account_by_id: 2,
13 embed_relationships?: 1,
17 alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
18 alias Pleroma.Plugs.OAuthScopesPlug
19 alias Pleroma.Plugs.RateLimiter
21 alias Pleroma.Web.ActivityPub.ActivityPub
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.Token
28 alias Pleroma.Web.TwitterAPI.TwitterAPI
30 plug(Pleroma.Web.ApiSpec.CastAndValidate)
32 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
34 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
38 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
39 when action in [:show, :followers, :following]
44 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
45 when action == :statuses
50 %{scopes: ["read:accounts"]}
51 when action in [:verify_credentials, :endorsements, :identity_proofs]
54 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
56 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
60 %{scopes: ["follow", "read:blocks"]} when action == :blocks
65 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
68 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
72 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
75 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
77 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
79 @relationship_actions [:follow, :unfollow]
80 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
84 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
87 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
88 plug(RateLimiter, [name: :app_account_creation] when action == :create)
89 plug(:assign_account_by_id when action in @needs_account)
91 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
93 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
95 @doc "POST /api/v1/accounts"
96 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
97 with :ok <- validate_email_param(params),
98 :ok <- TwitterAPI.validate_captcha(app, params),
99 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
100 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
102 token_type: "Bearer",
103 access_token: token.token,
105 created_at: Token.Utils.format_created_at(token)
108 {:error, error} -> json_response(conn, :bad_request, %{error: error})
112 def create(%{assigns: %{app: _app}} = conn, _) do
113 render_error(conn, :bad_request, "Missing parameters")
116 def create(conn, _) do
117 render_error(conn, :forbidden, "Invalid credentials")
120 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
122 defp validate_email_param(_) do
123 case Pleroma.Config.get([:instance, :account_activation_required]) do
124 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
129 @doc "GET /api/v1/accounts/verify_credentials"
130 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
131 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
133 render(conn, "show.json",
136 with_pleroma_settings: true,
137 with_chat_token: chat_token
141 @doc "PATCH /api/v1/accounts/update_credentials"
142 def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
147 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
154 :hide_followers_count,
160 :skip_thread_containment,
161 :allow_following_move,
164 |> Enum.reduce(%{}, fn key, acc ->
165 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
167 |> add_if_present(params, :display_name, :name)
168 |> add_if_present(params, :note, :bio)
169 |> add_if_present(params, :avatar, :avatar)
170 |> add_if_present(params, :header, :banner)
171 |> add_if_present(params, :pleroma_background_image, :background)
176 &{:ok, normalize_fields_attributes(&1)}
178 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
179 |> add_if_present(params, :default_scope, :default_scope)
180 |> add_if_present(params, :actor_type, :actor_type)
182 changeset = User.update_changeset(user, user_params)
184 with {:ok, user} <- User.update_and_set_cache(changeset) do
185 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
187 _e -> render_error(conn, :forbidden, "Invalid request")
191 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
192 with true <- Map.has_key?(params, params_field),
193 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
194 Map.put(map, map_field, new_value)
200 defp normalize_fields_attributes(fields) do
201 if Enum.all?(fields, &is_tuple/1) do
202 Enum.map(fields, fn {_, v} -> v end)
205 %{} = field -> %{"name" => field.name, "value" => field.value}
211 @doc "GET /api/v1/accounts/relationships"
212 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
213 targets = User.get_all_by_ids(List.wrap(id))
215 render(conn, "relationships.json", user: user, targets: targets)
218 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
219 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
221 @doc "GET /api/v1/accounts/:id"
222 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
223 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
224 true <- User.visible_for(user, for_user) do
225 render(conn, "show.json", user: user, for: for_user)
227 error -> user_visibility_error(conn, error)
231 @doc "GET /api/v1/accounts/:id/statuses"
232 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
233 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
234 true <- User.visible_for(user, reading_user) do
237 |> Map.delete(:tagged)
238 |> Enum.filter(&(not is_nil(&1)))
239 |> Map.new(fn {key, value} -> {to_string(key), value} end)
240 |> Map.put("tag", params[:tagged])
242 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
245 |> add_link_headers(activities)
246 |> put_view(StatusView)
247 |> render("index.json",
248 activities: activities,
253 error -> user_visibility_error(conn, error)
257 defp user_visibility_error(conn, error) do
260 render_error(conn, :gone, "")
262 :restrict_unauthenticated ->
263 render_error(conn, :unauthorized, "This API requires an authenticated user")
266 render_error(conn, :not_found, "Can't find user")
270 @doc "GET /api/v1/accounts/:id/followers"
271 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
274 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
279 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
280 user.hide_followers -> []
281 true -> MastodonAPI.get_followers(user, params)
285 |> add_link_headers(followers)
286 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
287 |> render("index.json",
291 embed_relationships: embed_relationships?(params)
295 @doc "GET /api/v1/accounts/:id/following"
296 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
299 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
304 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
305 user.hide_follows -> []
306 true -> MastodonAPI.get_friends(user, params)
310 |> add_link_headers(followers)
311 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
312 |> render("index.json",
316 embed_relationships: embed_relationships?(params)
320 @doc "GET /api/v1/accounts/:id/lists"
321 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
322 lists = Pleroma.List.get_lists_account_belongs(user, account)
325 |> put_view(ListView)
326 |> render("index.json", lists: lists)
329 @doc "POST /api/v1/accounts/:id/follow"
330 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
331 {:error, "Can not follow yourself"}
334 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
335 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
336 render(conn, "relationship.json", user: follower, target: followed)
338 {:error, message} -> json_response(conn, :forbidden, %{error: message})
342 @doc "POST /api/v1/accounts/:id/unfollow"
343 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
344 {:error, "Can not unfollow yourself"}
347 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
348 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
349 render(conn, "relationship.json", user: follower, target: followed)
353 @doc "POST /api/v1/accounts/:id/mute"
354 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
355 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
356 render(conn, "relationship.json", user: muter, target: muted)
358 {:error, message} -> json_response(conn, :forbidden, %{error: message})
362 @doc "POST /api/v1/accounts/:id/unmute"
363 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
364 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
365 render(conn, "relationship.json", user: muter, target: muted)
367 {:error, message} -> json_response(conn, :forbidden, %{error: message})
371 @doc "POST /api/v1/accounts/:id/block"
372 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
373 with {:ok, _user_block} <- User.block(blocker, blocked),
374 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
375 render(conn, "relationship.json", user: blocker, target: blocked)
377 {:error, message} -> json_response(conn, :forbidden, %{error: message})
381 @doc "POST /api/v1/accounts/:id/unblock"
382 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
383 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
384 render(conn, "relationship.json", user: blocker, target: blocked)
386 {:error, message} -> json_response(conn, :forbidden, %{error: message})
390 @doc "POST /api/v1/follows"
391 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
392 case User.get_cached_by_nickname(uri) do
395 |> assign(:account, user)
403 @doc "GET /api/v1/mutes"
404 def mutes(%{assigns: %{user: user}} = conn, _) do
405 users = User.muted_users(user, _restrict_deactivated = true)
406 render(conn, "index.json", users: users, for: user, as: :user)
409 @doc "GET /api/v1/blocks"
410 def blocks(%{assigns: %{user: user}} = conn, _) do
411 users = User.blocked_users(user, _restrict_deactivated = true)
412 render(conn, "index.json", users: users, for: user, as: :user)
415 @doc "GET /api/v1/endorsements"
416 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
418 @doc "GET /api/v1/identity_proofs"
419 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)