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,
14 skip_relationships?: 1
17 alias Pleroma.Plugs.OAuthScopesPlug
18 alias Pleroma.Plugs.RateLimiter
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.MastodonAPI.ListView
23 alias Pleroma.Web.MastodonAPI.MastodonAPI
24 alias Pleroma.Web.MastodonAPI.MastodonAPIController
25 alias Pleroma.Web.MastodonAPI.StatusView
26 alias Pleroma.Web.OAuth.Token
27 alias Pleroma.Web.TwitterAPI.TwitterAPI
29 plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
31 plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs)
35 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
41 %{scopes: ["read:accounts"]}
42 when action in [:endorsements, :verify_credentials, :followers, :following]
45 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
47 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
51 %{scopes: ["follow", "read:blocks"]} when action == :blocks
56 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
59 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
61 # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
64 %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
67 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
69 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
72 Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
73 when action not in [:create, :show, :statuses]
76 @relationship_actions [:follow, :unfollow]
77 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
81 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
84 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
85 plug(RateLimiter, [name: :app_account_creation] when action == :create)
86 plug(:assign_account_by_id when action in @needs_account)
88 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
90 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
92 @doc "POST /api/v1/accounts"
93 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
101 :captcha_answer_data,
106 |> Map.put(:nickname, params.username)
107 |> Map.put(:fullname, Map.get(params, :fullname, params.username))
108 |> Map.put(:confirm, params.password)
109 |> Map.put(:trusted_app, app.trusted)
111 with :ok <- validate_email_param(params),
112 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
113 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
115 token_type: "Bearer",
116 access_token: token.token,
118 created_at: Token.Utils.format_created_at(token)
121 {:error, errors} -> json_response(conn, :bad_request, errors)
125 def create(%{assigns: %{app: _app}} = conn, _) do
126 render_error(conn, :bad_request, "Missing parameters")
129 def create(conn, _) do
130 render_error(conn, :forbidden, "Invalid credentials")
133 defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
135 defp validate_email_param(_) do
136 case Pleroma.Config.get([:instance, :account_activation_required]) do
137 true -> {:error, %{"error" => "Missing parameters"}}
142 @doc "GET /api/v1/accounts/verify_credentials"
143 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
144 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
146 render(conn, "show.json",
149 with_pleroma_settings: true,
150 with_chat_token: chat_token
154 @doc "PATCH /api/v1/accounts/update_credentials"
155 def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
160 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
167 :hide_followers_count,
173 :skip_thread_containment,
174 :allow_following_move,
177 |> Enum.reduce(%{}, fn key, acc ->
178 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
180 |> add_if_present(params, :display_name, :name)
181 |> add_if_present(params, :note, :bio)
182 |> add_if_present(params, :avatar, :avatar)
183 |> add_if_present(params, :header, :banner)
184 |> add_if_present(params, :pleroma_background_image, :background)
189 &{:ok, normalize_fields_attributes(&1)}
191 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
192 |> add_if_present(params, :default_scope, :default_scope)
193 |> add_if_present(params, :actor_type, :actor_type)
195 changeset = User.update_changeset(user, user_params)
197 with {:ok, user} <- User.update_and_set_cache(changeset) do
198 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
200 _e -> render_error(conn, :forbidden, "Invalid request")
204 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
205 with true <- Map.has_key?(params, params_field),
206 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
207 Map.put(map, map_field, new_value)
213 defp normalize_fields_attributes(fields) do
214 if Enum.all?(fields, &is_tuple/1) do
215 Enum.map(fields, fn {_, v} -> v end)
218 %{} = field -> %{"name" => field.name, "value" => field.value}
224 @doc "GET /api/v1/accounts/relationships"
225 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
226 targets = User.get_all_by_ids(List.wrap(id))
228 render(conn, "relationships.json", user: user, targets: targets)
231 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
232 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
234 @doc "GET /api/v1/accounts/:id"
235 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
236 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
237 true <- User.visible_for?(user, for_user) do
238 render(conn, "show.json", user: user, for: for_user)
240 _e -> render_error(conn, :not_found, "Can't find user")
244 @doc "GET /api/v1/accounts/:id/statuses"
245 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
246 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
247 true <- User.visible_for?(user, reading_user) do
250 |> Map.delete(:tagged)
251 |> Enum.filter(&(not is_nil(&1)))
252 |> Map.new(fn {key, value} -> {to_string(key), value} end)
253 |> Map.put("tag", params[:tagged])
255 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
258 |> add_link_headers(activities)
259 |> put_view(StatusView)
260 |> render("index.json",
261 activities: activities,
264 skip_relationships: skip_relationships?(params)
267 _e -> render_error(conn, :not_found, "Can't find user")
271 @doc "GET /api/v1/accounts/:id/followers"
272 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
275 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
280 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
281 user.hide_followers -> []
282 true -> MastodonAPI.get_followers(user, params)
286 |> add_link_headers(followers)
287 |> render("index.json", for: for_user, users: followers, as: :user)
290 @doc "GET /api/v1/accounts/:id/following"
291 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
294 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
299 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
300 user.hide_follows -> []
301 true -> MastodonAPI.get_friends(user, params)
305 |> add_link_headers(followers)
306 |> render("index.json", for: for_user, users: followers, as: :user)
309 @doc "GET /api/v1/accounts/:id/lists"
310 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
311 lists = Pleroma.List.get_lists_account_belongs(user, account)
314 |> put_view(ListView)
315 |> render("index.json", lists: lists)
318 @doc "POST /api/v1/accounts/:id/follow"
319 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
320 {:error, "Can not follow yourself"}
323 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
324 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
325 render(conn, "relationship.json", user: follower, target: followed)
327 {:error, message} -> json_response(conn, :forbidden, %{error: message})
331 @doc "POST /api/v1/accounts/:id/unfollow"
332 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
333 {:error, "Can not unfollow yourself"}
336 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
337 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
338 render(conn, "relationship.json", user: follower, target: followed)
342 @doc "POST /api/v1/accounts/:id/mute"
343 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
344 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
345 render(conn, "relationship.json", user: muter, target: muted)
347 {:error, message} -> json_response(conn, :forbidden, %{error: message})
351 @doc "POST /api/v1/accounts/:id/unmute"
352 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
353 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
354 render(conn, "relationship.json", user: muter, target: muted)
356 {:error, message} -> json_response(conn, :forbidden, %{error: message})
360 @doc "POST /api/v1/accounts/:id/block"
361 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
362 with {:ok, _user_block} <- User.block(blocker, blocked),
363 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
364 render(conn, "relationship.json", user: blocker, target: blocked)
366 {:error, message} -> json_response(conn, :forbidden, %{error: message})
370 @doc "POST /api/v1/accounts/:id/unblock"
371 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
372 with {:ok, _user_block} <- User.unblock(blocker, blocked),
373 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
374 render(conn, "relationship.json", user: blocker, target: blocked)
376 {:error, message} -> json_response(conn, :forbidden, %{error: message})
380 @doc "POST /api/v1/follows"
381 def follows(%{body_params: %{uri: uri}} = conn, _) do
382 case User.get_cached_by_nickname(uri) do
385 |> assign(:account, user)
393 @doc "GET /api/v1/mutes"
394 def mutes(%{assigns: %{user: user}} = conn, _) do
395 users = User.muted_users(user, _restrict_deactivated = true)
396 render(conn, "index.json", users: users, for: user, as: :user)
399 @doc "GET /api/v1/blocks"
400 def blocks(%{assigns: %{user: user}} = conn, _) do
401 users = User.blocked_users(user, _restrict_deactivated = true)
402 render(conn, "index.json", users: users, for: user, as: :user)
405 @doc "GET /api/v1/endorsements"
406 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
408 @doc "GET /api/v1/identity_proofs"
409 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)