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,
16 alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
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(Pleroma.Web.ApiSpec.CastAndValidate)
31 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
33 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
37 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
38 when action in [:show, :followers, :following]
43 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
44 when action == :statuses
49 %{scopes: ["read:accounts"]}
50 when action in [:verify_credentials, :endorsements, :identity_proofs]
53 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
55 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
59 %{scopes: ["follow", "read:blocks"]} when action == :blocks
64 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
67 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
71 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
74 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
76 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
78 @relationship_actions [:follow, :unfollow]
79 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
83 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
86 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
87 plug(RateLimiter, [name: :app_account_creation] when action == :create)
88 plug(:assign_account_by_id when action in @needs_account)
90 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
92 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
94 @doc "POST /api/v1/accounts"
95 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
96 with :ok <- validate_email_param(params),
97 :ok <- TwitterAPI.validate_captcha(app, params),
98 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
99 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
101 token_type: "Bearer",
102 access_token: token.token,
104 created_at: Token.Utils.format_created_at(token)
107 {:error, error} -> json_response(conn, :bad_request, %{error: error})
111 def create(%{assigns: %{app: _app}} = conn, _) do
112 render_error(conn, :bad_request, "Missing parameters")
115 def create(conn, _) do
116 render_error(conn, :forbidden, "Invalid credentials")
119 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
121 defp validate_email_param(_) do
122 case Pleroma.Config.get([:instance, :account_activation_required]) do
123 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
128 @doc "GET /api/v1/accounts/verify_credentials"
129 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
130 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
132 render(conn, "show.json",
135 with_pleroma_settings: true,
136 with_chat_token: chat_token
140 @doc "PATCH /api/v1/accounts/update_credentials"
141 def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
146 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
153 :hide_followers_count,
159 :skip_thread_containment,
160 :allow_following_move,
163 |> Enum.reduce(%{}, fn key, acc ->
164 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
166 |> add_if_present(params, :display_name, :name)
167 |> add_if_present(params, :note, :bio)
168 |> add_if_present(params, :avatar, :avatar)
169 |> add_if_present(params, :header, :banner)
170 |> add_if_present(params, :pleroma_background_image, :background)
175 &{:ok, normalize_fields_attributes(&1)}
177 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
178 |> add_if_present(params, :default_scope, :default_scope)
179 |> add_if_present(params, :actor_type, :actor_type)
181 changeset = User.update_changeset(user, user_params)
183 with {:ok, user} <- User.update_and_set_cache(changeset) do
184 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
186 _e -> render_error(conn, :forbidden, "Invalid request")
190 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
191 with true <- Map.has_key?(params, params_field),
192 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
193 Map.put(map, map_field, new_value)
199 defp normalize_fields_attributes(fields) do
200 if Enum.all?(fields, &is_tuple/1) do
201 Enum.map(fields, fn {_, v} -> v end)
204 %{} = field -> %{"name" => field.name, "value" => field.value}
210 @doc "GET /api/v1/accounts/relationships"
211 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
212 targets = User.get_all_by_ids(List.wrap(id))
214 render(conn, "relationships.json", user: user, targets: targets)
217 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
218 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
220 @doc "GET /api/v1/accounts/:id"
221 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
222 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
223 true <- User.visible_for?(user, for_user) do
224 render(conn, "show.json", user: user, for: for_user)
226 _e -> render_error(conn, :not_found, "Can't find user")
230 @doc "GET /api/v1/accounts/:id/statuses"
231 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
232 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
233 true <- User.visible_for?(user, reading_user) do
236 |> Map.delete(:tagged)
237 |> Enum.filter(&(not is_nil(&1)))
238 |> Map.new(fn {key, value} -> {to_string(key), value} end)
239 |> Map.put("tag", params[:tagged])
241 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
244 |> add_link_headers(activities)
245 |> put_view(StatusView)
246 |> render("index.json",
247 activities: activities,
252 _e -> render_error(conn, :not_found, "Can't find user")
256 @doc "GET /api/v1/accounts/:id/followers"
257 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
260 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
265 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
266 user.hide_followers -> []
267 true -> MastodonAPI.get_followers(user, params)
271 |> add_link_headers(followers)
272 |> render("index.json", for: for_user, users: followers, as: :user)
275 @doc "GET /api/v1/accounts/:id/following"
276 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
279 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
284 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
285 user.hide_follows -> []
286 true -> MastodonAPI.get_friends(user, params)
290 |> add_link_headers(followers)
291 |> render("index.json", for: for_user, users: followers, as: :user)
294 @doc "GET /api/v1/accounts/:id/lists"
295 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
296 lists = Pleroma.List.get_lists_account_belongs(user, account)
299 |> put_view(ListView)
300 |> render("index.json", lists: lists)
303 @doc "POST /api/v1/accounts/:id/follow"
304 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
305 {:error, "Can not follow yourself"}
308 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
309 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
310 render(conn, "relationship.json", user: follower, target: followed)
312 {:error, message} -> json_response(conn, :forbidden, %{error: message})
316 @doc "POST /api/v1/accounts/:id/unfollow"
317 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
318 {:error, "Can not unfollow yourself"}
321 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
322 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
323 render(conn, "relationship.json", user: follower, target: followed)
327 @doc "POST /api/v1/accounts/:id/mute"
328 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
329 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
330 render(conn, "relationship.json", user: muter, target: muted)
332 {:error, message} -> json_response(conn, :forbidden, %{error: message})
336 @doc "POST /api/v1/accounts/:id/unmute"
337 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
338 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
339 render(conn, "relationship.json", user: muter, target: muted)
341 {:error, message} -> json_response(conn, :forbidden, %{error: message})
345 @doc "POST /api/v1/accounts/:id/block"
346 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
347 with {:ok, _user_block} <- User.block(blocker, blocked),
348 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
349 render(conn, "relationship.json", user: blocker, target: blocked)
351 {:error, message} -> json_response(conn, :forbidden, %{error: message})
355 @doc "POST /api/v1/accounts/:id/unblock"
356 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
357 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
358 render(conn, "relationship.json", user: blocker, target: blocked)
360 {:error, message} -> json_response(conn, :forbidden, %{error: message})
364 @doc "POST /api/v1/follows"
365 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
366 case User.get_cached_by_nickname(uri) do
369 |> assign(:account, user)
377 @doc "GET /api/v1/mutes"
378 def mutes(%{assigns: %{user: user}} = conn, _) do
379 users = User.muted_users(user, _restrict_deactivated = true)
380 render(conn, "index.json", users: users, for: user, as: :user)
383 @doc "GET /api/v1/blocks"
384 def blocks(%{assigns: %{user: user}} = conn, _) do
385 users = User.blocked_users(user, _restrict_deactivated = true)
386 render(conn, "index.json", users: users, for: user, as: :user)
389 @doc "GET /api/v1/endorsements"
390 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
392 @doc "GET /api/v1/identity_proofs"
393 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)