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(:skip_plug, OAuthScopesPlug when action == :identity_proofs)
33 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
39 %{scopes: ["read:accounts"]}
40 when action in [:endorsements, :verify_credentials, :followers, :following]
43 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
45 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
49 %{scopes: ["follow", "read:blocks"]} when action == :blocks
54 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
57 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
59 # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
62 %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
65 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
67 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
70 Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
71 when action not in [:create, :show, :statuses]
74 @relationship_actions [:follow, :unfollow]
75 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
79 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
82 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
83 plug(RateLimiter, [name: :app_account_creation] when action == :create)
84 plug(:assign_account_by_id when action in @needs_account)
86 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
88 @doc "POST /api/v1/accounts"
90 %{assigns: %{app: app}} = conn,
91 %{"username" => nickname, "password" => _, "agreement" => true} = params
99 "captcha_answer_data",
103 |> Map.put("nickname", nickname)
104 |> Map.put("fullname", params["fullname"] || nickname)
105 |> Map.put("bio", params["bio"] || "")
106 |> Map.put("confirm", params["password"])
108 with :ok <- validate_email_param(params),
109 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
110 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
112 token_type: "Bearer",
113 access_token: token.token,
115 created_at: Token.Utils.format_created_at(token)
118 {:error, errors} -> json_response(conn, :bad_request, errors)
122 def create(%{assigns: %{app: _app}} = conn, _) do
123 render_error(conn, :bad_request, "Missing parameters")
126 def create(conn, _) do
127 render_error(conn, :forbidden, "Invalid credentials")
130 defp validate_email_param(%{"email" => _}), do: :ok
132 defp validate_email_param(_) do
133 case Pleroma.Config.get([:instance, :account_activation_required]) do
134 true -> {:error, %{"error" => "Missing parameters"}}
139 @doc "GET /api/v1/accounts/verify_credentials"
140 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
141 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
143 render(conn, "show.json",
146 with_pleroma_settings: true,
147 with_chat_token: chat_token
151 @doc "PATCH /api/v1/accounts/update_credentials"
152 def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
159 :hide_followers_count,
165 :skip_thread_containment,
166 :allow_following_move,
169 |> Enum.reduce(%{}, fn key, acc ->
170 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
172 |> add_if_present(params, "display_name", :name)
173 |> add_if_present(params, "note", :bio)
174 |> add_if_present(params, "avatar", :avatar)
175 |> add_if_present(params, "header", :banner)
176 |> add_if_present(params, "pleroma_background_image", :background)
181 &{:ok, normalize_fields_attributes(&1)}
183 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
184 |> add_if_present(params, "default_scope", :default_scope)
185 |> add_if_present(params, "actor_type", :actor_type)
187 changeset = User.update_changeset(user, user_params)
189 with {:ok, user} <- User.update_and_set_cache(changeset) do
190 if original_user != user, do: CommonAPI.update(user)
192 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
194 _e -> render_error(conn, :forbidden, "Invalid request")
198 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
199 with true <- Map.has_key?(params, params_field),
200 {:ok, new_value} <- value_function.(params[params_field]) do
201 Map.put(map, map_field, new_value)
207 defp normalize_fields_attributes(fields) do
208 if Enum.all?(fields, &is_tuple/1) do
209 Enum.map(fields, fn {_, v} -> v end)
215 @doc "GET /api/v1/accounts/relationships"
216 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
217 targets = User.get_all_by_ids(List.wrap(id))
219 render(conn, "relationships.json", user: user, targets: targets)
222 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
223 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
225 @doc "GET /api/v1/accounts/:id"
226 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
227 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
228 true <- User.visible_for?(user, for_user) do
229 render(conn, "show.json", user: user, for: for_user)
231 _e -> render_error(conn, :not_found, "Can't find user")
235 @doc "GET /api/v1/accounts/:id/statuses"
236 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
237 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
238 true <- User.visible_for?(user, reading_user) do
241 |> Map.put("tag", params["tagged"])
242 |> Map.delete("godmode")
244 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
247 |> add_link_headers(activities)
248 |> put_view(StatusView)
249 |> render("index.json",
250 activities: activities,
253 skip_relationships: skip_relationships?(params)
256 _e -> render_error(conn, :not_found, "Can't find user")
260 @doc "GET /api/v1/accounts/:id/followers"
261 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
264 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
265 user.hide_followers -> []
266 true -> MastodonAPI.get_followers(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/following"
275 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
278 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
279 user.hide_follows -> []
280 true -> MastodonAPI.get_friends(user, params)
284 |> add_link_headers(followers)
285 |> render("index.json", for: for_user, users: followers, as: :user)
288 @doc "GET /api/v1/accounts/:id/lists"
289 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
290 lists = Pleroma.List.get_lists_account_belongs(user, account)
293 |> put_view(ListView)
294 |> render("index.json", lists: lists)
297 @doc "POST /api/v1/accounts/:id/follow"
298 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
302 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
303 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
304 render(conn, "relationship.json", user: follower, target: followed)
306 {:error, message} -> json_response(conn, :forbidden, %{error: message})
310 @doc "POST /api/v1/accounts/:id/unfollow"
311 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
315 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
316 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
317 render(conn, "relationship.json", user: follower, target: followed)
321 @doc "POST /api/v1/accounts/:id/mute"
322 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
323 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
325 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
326 render(conn, "relationship.json", user: muter, target: muted)
328 {:error, message} -> json_response(conn, :forbidden, %{error: message})
332 @doc "POST /api/v1/accounts/:id/unmute"
333 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
334 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
335 render(conn, "relationship.json", user: muter, target: muted)
337 {:error, message} -> json_response(conn, :forbidden, %{error: message})
341 @doc "POST /api/v1/accounts/:id/block"
342 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
343 with {:ok, _user_block} <- User.block(blocker, blocked),
344 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
345 render(conn, "relationship.json", user: blocker, target: blocked)
347 {:error, message} -> json_response(conn, :forbidden, %{error: message})
351 @doc "POST /api/v1/accounts/:id/unblock"
352 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
353 with {:ok, _user_block} <- User.unblock(blocker, blocked),
354 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
355 render(conn, "relationship.json", user: blocker, target: blocked)
357 {:error, message} -> json_response(conn, :forbidden, %{error: message})
361 @doc "POST /api/v1/follows"
362 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
363 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
364 {_, true} <- {:followed, follower.id != followed.id},
365 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
366 render(conn, "show.json", user: followed, for: follower)
368 {:followed, _} -> {:error, :not_found}
369 {:error, message} -> json_response(conn, :forbidden, %{error: message})
373 @doc "GET /api/v1/mutes"
374 def mutes(%{assigns: %{user: user}} = conn, _) do
375 users = User.muted_users(user, _restrict_deactivated = true)
376 render(conn, "index.json", users: users, for: user, as: :user)
379 @doc "GET /api/v1/blocks"
380 def blocks(%{assigns: %{user: user}} = conn, _) do
381 users = User.blocked_users(user, _restrict_deactivated = true)
382 render(conn, "index.json", users: users, for: user, as: :user)
385 @doc "GET /api/v1/endorsements"
386 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
388 @doc "GET /api/v1/identity_proofs"
389 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)