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,
9 only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]
11 alias Pleroma.Plugs.OAuthScopesPlug
12 alias Pleroma.Plugs.RateLimiter
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.CommonAPI
16 alias Pleroma.Web.MastodonAPI.ListView
17 alias Pleroma.Web.MastodonAPI.MastodonAPI
18 alias Pleroma.Web.MastodonAPI.MastodonAPIController
19 alias Pleroma.Web.MastodonAPI.StatusView
20 alias Pleroma.Web.OAuth.Token
21 alias Pleroma.Web.TwitterAPI.TwitterAPI
23 plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs)
27 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
33 %{scopes: ["read:accounts"]}
34 when action in [:endorsements, :verify_credentials, :followers, :following]
37 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
39 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
43 %{scopes: ["follow", "read:blocks"]} when action == :blocks
48 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
51 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
53 # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
56 %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
59 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
61 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
64 Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
65 when action != :create
68 @relationship_actions [:follow, :unfollow]
69 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
73 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
76 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
77 plug(RateLimiter, [name: :app_account_creation] when action == :create)
78 plug(:assign_account_by_id when action in @needs_account)
80 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
82 @doc "POST /api/v1/accounts"
84 %{assigns: %{app: app}} = conn,
85 %{"username" => nickname, "password" => _, "agreement" => true} = params
93 "captcha_answer_data",
97 |> Map.put("nickname", nickname)
98 |> Map.put("fullname", params["fullname"] || nickname)
99 |> Map.put("bio", params["bio"] || "")
100 |> Map.put("confirm", params["password"])
102 with :ok <- validate_email_param(params),
103 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
104 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
106 token_type: "Bearer",
107 access_token: token.token,
109 created_at: Token.Utils.format_created_at(token)
112 {:error, errors} -> json_response(conn, :bad_request, errors)
116 def create(%{assigns: %{app: _app}} = conn, _) do
117 render_error(conn, :bad_request, "Missing parameters")
120 def create(conn, _) do
121 render_error(conn, :forbidden, "Invalid credentials")
124 defp validate_email_param(%{"email" => _}), do: :ok
126 defp validate_email_param(_) do
127 case Pleroma.Config.get([:instance, :account_activation_required]) do
128 true -> {:error, %{"error" => "Missing parameters"}}
133 @doc "GET /api/v1/accounts/verify_credentials"
134 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
135 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
137 render(conn, "show.json",
140 with_pleroma_settings: true,
141 with_chat_token: chat_token
145 @doc "PATCH /api/v1/accounts/update_credentials"
146 def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
153 :hide_followers_count,
159 :skip_thread_containment,
160 :allow_following_move,
163 |> Enum.reduce(%{}, fn key, acc ->
164 add_if_present(acc, params, to_string(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 if original_user != user, do: CommonAPI.update(user)
186 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
188 _e -> render_error(conn, :forbidden, "Invalid request")
192 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
193 with true <- Map.has_key?(params, params_field),
194 {:ok, new_value} <- value_function.(params[params_field]) do
195 Map.put(map, map_field, new_value)
201 defp normalize_fields_attributes(fields) do
202 if Enum.all?(fields, &is_tuple/1) do
203 Enum.map(fields, fn {_, v} -> v end)
209 @doc "GET /api/v1/accounts/relationships"
210 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
211 targets = User.get_all_by_ids(List.wrap(id))
213 render(conn, "relationships.json", user: user, targets: targets)
216 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
217 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
219 @doc "GET /api/v1/accounts/:id"
220 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
221 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
222 true <- User.visible_for?(user, for_user) do
223 render(conn, "show.json", user: user, for: for_user)
225 _e -> render_error(conn, :not_found, "Can't find user")
229 @doc "GET /api/v1/accounts/:id/statuses"
230 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
231 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
234 |> Map.put("tag", params["tagged"])
235 |> Map.delete("godmode")
237 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
240 |> add_link_headers(activities)
241 |> put_view(StatusView)
242 |> render("index.json", activities: activities, for: reading_user, as: :activity)
246 @doc "GET /api/v1/accounts/:id/followers"
247 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
250 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
251 user.hide_followers -> []
252 true -> MastodonAPI.get_followers(user, params)
256 |> add_link_headers(followers)
257 |> render("index.json", for: for_user, users: followers, as: :user)
260 @doc "GET /api/v1/accounts/:id/following"
261 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
264 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
265 user.hide_follows -> []
266 true -> MastodonAPI.get_friends(user, params)
270 |> add_link_headers(followers)
271 |> render("index.json", for: for_user, users: followers, as: :user)
274 @doc "GET /api/v1/accounts/:id/lists"
275 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
276 lists = Pleroma.List.get_lists_account_belongs(user, account)
279 |> put_view(ListView)
280 |> render("index.json", lists: lists)
283 @doc "POST /api/v1/accounts/:id/follow"
284 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
288 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
289 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
290 render(conn, "relationship.json", user: follower, target: followed)
292 {:error, message} -> json_response(conn, :forbidden, %{error: message})
296 @doc "POST /api/v1/accounts/:id/unfollow"
297 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
301 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
302 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
303 render(conn, "relationship.json", user: follower, target: followed)
307 @doc "POST /api/v1/accounts/:id/mute"
308 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
309 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
311 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
312 render(conn, "relationship.json", user: muter, target: muted)
314 {:error, message} -> json_response(conn, :forbidden, %{error: message})
318 @doc "POST /api/v1/accounts/:id/unmute"
319 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
320 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
321 render(conn, "relationship.json", user: muter, target: muted)
323 {:error, message} -> json_response(conn, :forbidden, %{error: message})
327 @doc "POST /api/v1/accounts/:id/block"
328 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
329 with {:ok, _user_block} <- User.block(blocker, blocked),
330 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
331 render(conn, "relationship.json", user: blocker, target: blocked)
333 {:error, message} -> json_response(conn, :forbidden, %{error: message})
337 @doc "POST /api/v1/accounts/:id/unblock"
338 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
339 with {:ok, _user_block} <- User.unblock(blocker, blocked),
340 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
341 render(conn, "relationship.json", user: blocker, target: blocked)
343 {:error, message} -> json_response(conn, :forbidden, %{error: message})
347 @doc "POST /api/v1/follows"
348 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
349 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
350 {_, true} <- {:followed, follower.id != followed.id},
351 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
352 render(conn, "show.json", user: followed, for: follower)
354 {:followed, _} -> {:error, :not_found}
355 {:error, message} -> json_response(conn, :forbidden, %{error: message})
359 @doc "GET /api/v1/mutes"
360 def mutes(%{assigns: %{user: user}} = conn, _) do
361 users = User.muted_users(user, _restrict_deactivated = true)
362 render(conn, "index.json", users: users, for: user, as: :user)
365 @doc "GET /api/v1/blocks"
366 def blocks(%{assigns: %{user: user}} = conn, _) do
367 users = User.blocked_users(user, _restrict_deactivated = true)
368 render(conn, "index.json", users: users, for: user, as: :user)
371 @doc "GET /api/v1/endorsements"
372 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
374 @doc "GET /api/v1/identity_proofs"
375 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)