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, params.fullname || params.username)
108 |> Map.put(:bio, params.bio || "")
109 |> Map.put(:confirm, params.password)
110 |> Map.put(:trusted_app, app.trusted)
112 with :ok <- validate_email_param(params),
113 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
114 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
116 token_type: "Bearer",
117 access_token: token.token,
119 created_at: Token.Utils.format_created_at(token)
122 {:error, errors} -> json_response(conn, :bad_request, errors)
126 def create(%{assigns: %{app: _app}} = conn, _) do
127 render_error(conn, :bad_request, "Missing parameters")
130 def create(conn, _) do
131 render_error(conn, :forbidden, "Invalid credentials")
134 defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
136 defp validate_email_param(_) do
137 case Pleroma.Config.get([:instance, :account_activation_required]) do
138 true -> {:error, %{"error" => "Missing parameters"}}
143 @doc "GET /api/v1/accounts/verify_credentials"
144 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
145 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
147 render(conn, "show.json",
150 with_pleroma_settings: true,
151 with_chat_token: chat_token
155 @doc "PATCH /api/v1/accounts/update_credentials"
156 def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
162 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
169 :hide_followers_count,
175 :skip_thread_containment,
176 :allow_following_move,
179 |> Enum.reduce(%{}, fn key, acc ->
180 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
182 |> add_if_present(params, :display_name, :name)
183 |> add_if_present(params, :note, :bio)
184 |> add_if_present(params, :avatar, :avatar)
185 |> add_if_present(params, :header, :banner)
186 |> add_if_present(params, :pleroma_background_image, :background)
191 &{:ok, normalize_fields_attributes(&1)}
193 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
194 |> add_if_present(params, :default_scope, :default_scope)
195 |> add_if_present(params, :actor_type, :actor_type)
197 changeset = User.update_changeset(user, user_params)
199 with {:ok, user} <- User.update_and_set_cache(changeset) do
200 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
202 _e -> render_error(conn, :forbidden, "Invalid request")
206 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
207 with true <- Map.has_key?(params, params_field),
208 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
209 Map.put(map, map_field, new_value)
215 defp normalize_fields_attributes(fields) do
216 if Enum.all?(fields, &is_tuple/1) do
217 Enum.map(fields, fn {_, v} -> v end)
220 %Pleroma.Web.ApiSpec.Schemas.AccountAttributeField{} = field ->
221 %{"name" => field.name, "value" => field.value}
229 @doc "GET /api/v1/accounts/relationships"
230 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
231 targets = User.get_all_by_ids(List.wrap(id))
233 render(conn, "relationships.json", user: user, targets: targets)
236 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
237 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
239 @doc "GET /api/v1/accounts/:id"
240 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
241 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
242 true <- User.visible_for?(user, for_user) do
243 render(conn, "show.json", user: user, for: for_user)
245 _e -> render_error(conn, :not_found, "Can't find user")
249 @doc "GET /api/v1/accounts/:id/statuses"
250 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
251 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
252 true <- User.visible_for?(user, reading_user) do
255 |> Map.delete(:tagged)
256 |> Enum.filter(&(not is_nil(&1)))
257 |> Map.new(fn {key, value} -> {to_string(key), value} end)
258 |> Map.put("tag", params[:tagged])
260 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
263 |> add_link_headers(activities)
264 |> put_view(StatusView)
265 |> render("index.json",
266 activities: activities,
269 skip_relationships: skip_relationships?(params)
272 _e -> render_error(conn, :not_found, "Can't find user")
276 @doc "GET /api/v1/accounts/:id/followers"
277 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
280 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
285 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
286 user.hide_followers -> []
287 true -> MastodonAPI.get_followers(user, params)
291 |> add_link_headers(followers)
292 |> render("index.json", for: for_user, users: followers, as: :user)
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 |> render("index.json", for: for_user, users: followers, as: :user)
314 @doc "GET /api/v1/accounts/:id/lists"
315 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
316 lists = Pleroma.List.get_lists_account_belongs(user, account)
319 |> put_view(ListView)
320 |> render("index.json", lists: lists)
323 @doc "POST /api/v1/accounts/:id/follow"
324 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
325 {:error, "Can not follow yourself"}
328 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
329 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
330 render(conn, "relationship.json", user: follower, target: followed)
332 {:error, message} -> json_response(conn, :forbidden, %{error: message})
336 @doc "POST /api/v1/accounts/:id/unfollow"
337 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
338 {:error, "Can not unfollow yourself"}
341 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
342 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
343 render(conn, "relationship.json", user: follower, target: followed)
347 @doc "POST /api/v1/accounts/:id/mute"
348 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
349 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
350 render(conn, "relationship.json", user: muter, target: muted)
352 {:error, message} -> json_response(conn, :forbidden, %{error: message})
356 @doc "POST /api/v1/accounts/:id/unmute"
357 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
358 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
359 render(conn, "relationship.json", user: muter, target: muted)
361 {:error, message} -> json_response(conn, :forbidden, %{error: message})
365 @doc "POST /api/v1/accounts/:id/block"
366 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
367 with {:ok, _user_block} <- User.block(blocker, blocked),
368 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
369 render(conn, "relationship.json", user: blocker, target: blocked)
371 {:error, message} -> json_response(conn, :forbidden, %{error: message})
375 @doc "POST /api/v1/accounts/:id/unblock"
376 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
377 with {:ok, _user_block} <- User.unblock(blocker, blocked),
378 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
379 render(conn, "relationship.json", user: blocker, target: blocked)
381 {:error, message} -> json_response(conn, :forbidden, %{error: message})
385 @doc "POST /api/v1/follows"
386 def follows(%{body_params: %{uri: uri}} = conn, _) do
387 case User.get_cached_by_nickname(uri) do
390 |> assign(:account, user)
398 @doc "GET /api/v1/mutes"
399 def mutes(%{assigns: %{user: user}} = conn, _) do
400 users = User.muted_users(user, _restrict_deactivated = true)
401 render(conn, "index.json", users: users, for: user, as: :user)
404 @doc "GET /api/v1/blocks"
405 def blocks(%{assigns: %{user: user}} = conn, _) do
406 users = User.blocked_users(user, _restrict_deactivated = true)
407 render(conn, "index.json", users: users, for: user, as: :user)
410 @doc "GET /api/v1/endorsements"
411 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
413 @doc "GET /api/v1/identity_proofs"
414 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)