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.EnsurePublicOrAuthenticatedPlug
18 alias Pleroma.Plugs.OAuthScopesPlug
19 alias Pleroma.Plugs.RateLimiter
21 alias Pleroma.Web.ActivityPub.ActivityPub
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonAPI
25 alias Pleroma.Web.MastodonAPI.MastodonAPIController
26 alias Pleroma.Web.MastodonAPI.StatusView
27 alias Pleroma.Web.OAuth.Token
28 alias Pleroma.Web.TwitterAPI.TwitterAPI
30 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
32 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
36 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
37 when action in [:show, :followers, :following]
42 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
43 when action == :statuses
48 %{scopes: ["read:accounts"]}
49 when action in [:verify_credentials, :endorsements, :identity_proofs]
52 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
54 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
58 %{scopes: ["follow", "read:blocks"]} when action == :blocks
63 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
66 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
70 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
73 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
75 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
77 @relationship_actions [:follow, :unfollow]
78 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
82 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
85 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
86 plug(RateLimiter, [name: :app_account_creation] when action == :create)
87 plug(:assign_account_by_id when action in @needs_account)
89 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
91 @doc "POST /api/v1/accounts"
93 %{assigns: %{app: app}} = conn,
94 %{"username" => nickname, "password" => _, "agreement" => true} = params
102 "captcha_answer_data",
106 |> Map.put("nickname", nickname)
107 |> Map.put("fullname", params["fullname"] || nickname)
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" => _}), 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: user}} = conn, params) do
161 :hide_followers_count,
167 :skip_thread_containment,
168 :allow_following_move,
171 |> Enum.reduce(%{}, fn key, acc ->
172 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
174 |> add_if_present(params, "display_name", :name)
175 |> add_if_present(params, "note", :bio)
176 |> add_if_present(params, "avatar", :avatar)
177 |> add_if_present(params, "header", :banner)
178 |> add_if_present(params, "pleroma_background_image", :background)
183 &{:ok, normalize_fields_attributes(&1)}
185 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
186 |> add_if_present(params, "default_scope", :default_scope)
187 |> add_if_present(params, "actor_type", :actor_type)
189 changeset = User.update_changeset(user, user_params)
191 with {:ok, user} <- User.update_and_set_cache(changeset) do
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
299 {:error, "Can not follow yourself"}
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
312 {:error, "Can not unfollow yourself"}
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 follow_by_uri(conn, %{"uri" => uri}) do
363 case User.get_cached_by_nickname(uri) do
366 |> assign(:account, user)
374 @doc "GET /api/v1/mutes"
375 def mutes(%{assigns: %{user: user}} = conn, _) do
376 users = User.muted_users(user, _restrict_deactivated = true)
377 render(conn, "index.json", users: users, for: user, as: :user)
380 @doc "GET /api/v1/blocks"
381 def blocks(%{assigns: %{user: user}} = conn, _) do
382 users = User.blocked_users(user, _restrict_deactivated = true)
383 render(conn, "index.json", users: users, for: user, as: :user)
386 @doc "GET /api/v1/endorsements"
387 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
389 @doc "GET /api/v1/identity_proofs"
390 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)