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 not in [:create, :show, :statuses]
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),
232 true <- User.visible_for?(user, reading_user) do
235 |> Map.put("tag", params["tagged"])
236 |> Map.delete("godmode")
238 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
241 |> add_link_headers(activities)
242 |> put_view(StatusView)
243 |> render("index.json", activities: activities, for: reading_user, as: :activity)
245 _e -> render_error(conn, :not_found, "Can't find user")
249 @doc "GET /api/v1/accounts/:id/followers"
250 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
253 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
254 user.hide_followers -> []
255 true -> MastodonAPI.get_followers(user, params)
259 |> add_link_headers(followers)
260 |> render("index.json", for: for_user, users: followers, as: :user)
263 @doc "GET /api/v1/accounts/:id/following"
264 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
267 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
268 user.hide_follows -> []
269 true -> MastodonAPI.get_friends(user, params)
273 |> add_link_headers(followers)
274 |> render("index.json", for: for_user, users: followers, as: :user)
277 @doc "GET /api/v1/accounts/:id/lists"
278 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
279 lists = Pleroma.List.get_lists_account_belongs(user, account)
282 |> put_view(ListView)
283 |> render("index.json", lists: lists)
286 @doc "POST /api/v1/accounts/:id/follow"
287 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
291 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
292 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
293 render(conn, "relationship.json", user: follower, target: followed)
295 {:error, message} -> json_response(conn, :forbidden, %{error: message})
299 @doc "POST /api/v1/accounts/:id/unfollow"
300 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
304 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
305 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
306 render(conn, "relationship.json", user: follower, target: followed)
310 @doc "POST /api/v1/accounts/:id/mute"
311 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
312 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
314 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
315 render(conn, "relationship.json", user: muter, target: muted)
317 {:error, message} -> json_response(conn, :forbidden, %{error: message})
321 @doc "POST /api/v1/accounts/:id/unmute"
322 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
323 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
324 render(conn, "relationship.json", user: muter, target: muted)
326 {:error, message} -> json_response(conn, :forbidden, %{error: message})
330 @doc "POST /api/v1/accounts/:id/block"
331 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
332 with {:ok, _user_block} <- User.block(blocker, blocked),
333 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
334 render(conn, "relationship.json", user: blocker, target: blocked)
336 {:error, message} -> json_response(conn, :forbidden, %{error: message})
340 @doc "POST /api/v1/accounts/:id/unblock"
341 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
342 with {:ok, _user_block} <- User.unblock(blocker, blocked),
343 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
344 render(conn, "relationship.json", user: blocker, target: blocked)
346 {:error, message} -> json_response(conn, :forbidden, %{error: message})
350 @doc "POST /api/v1/follows"
351 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
352 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
353 {_, true} <- {:followed, follower.id != followed.id},
354 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
355 render(conn, "show.json", user: followed, for: follower)
357 {:followed, _} -> {:error, :not_found}
358 {:error, message} -> json_response(conn, :forbidden, %{error: message})
362 @doc "GET /api/v1/mutes"
363 def mutes(%{assigns: %{user: user}} = conn, _) do
364 users = User.muted_users(user, _restrict_deactivated = true)
365 render(conn, "index.json", users: users, for: user, as: :user)
368 @doc "GET /api/v1/blocks"
369 def blocks(%{assigns: %{user: user}} = conn, _) do
370 users = User.blocked_users(user, _restrict_deactivated = true)
371 render(conn, "index.json", users: users, for: user, as: :user)
374 @doc "GET /api/v1/endorsements"
375 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
377 @doc "GET /api/v1/identity_proofs"
378 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)