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: user}} = conn, params) do
157 :hide_followers_count,
163 :skip_thread_containment,
164 :allow_following_move,
167 |> Enum.reduce(%{}, fn key, acc ->
168 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
170 |> add_if_present(params, "display_name", :name)
171 |> add_if_present(params, "note", :bio)
172 |> add_if_present(params, "avatar", :avatar)
173 |> add_if_present(params, "header", :banner)
174 |> add_if_present(params, "pleroma_background_image", :background)
179 &{:ok, normalize_fields_attributes(&1)}
181 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
182 |> add_if_present(params, "default_scope", :default_scope)
183 |> add_if_present(params, "actor_type", :actor_type)
185 changeset = User.update_changeset(user, user_params)
187 with {:ok, user} <- User.update_and_set_cache(changeset) do
188 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
190 _e -> render_error(conn, :forbidden, "Invalid request")
194 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
195 with true <- Map.has_key?(params, params_field),
196 {:ok, new_value} <- value_function.(params[params_field]) do
197 Map.put(map, map_field, new_value)
203 defp normalize_fields_attributes(fields) do
204 if Enum.all?(fields, &is_tuple/1) do
205 Enum.map(fields, fn {_, v} -> v end)
211 @doc "GET /api/v1/accounts/relationships"
212 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
213 targets = User.get_all_by_ids(List.wrap(id))
215 render(conn, "relationships.json", user: user, targets: targets)
218 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
219 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
221 @doc "GET /api/v1/accounts/:id"
222 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
223 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
224 true <- User.visible_for?(user, for_user) do
225 render(conn, "show.json", user: user, for: for_user)
227 _e -> render_error(conn, :not_found, "Can't find user")
231 @doc "GET /api/v1/accounts/:id/statuses"
232 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
233 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
234 true <- User.visible_for?(user, reading_user) do
237 |> Map.put("tag", params["tagged"])
238 |> Map.delete("godmode")
240 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
243 |> add_link_headers(activities)
244 |> put_view(StatusView)
245 |> render("index.json",
246 activities: activities,
249 skip_relationships: skip_relationships?(params)
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 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
261 user.hide_followers -> []
262 true -> MastodonAPI.get_followers(user, params)
266 |> add_link_headers(followers)
267 |> render("index.json", for: for_user, users: followers, as: :user)
270 @doc "GET /api/v1/accounts/:id/following"
271 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
274 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
275 user.hide_follows -> []
276 true -> MastodonAPI.get_friends(user, params)
280 |> add_link_headers(followers)
281 |> render("index.json", for: for_user, users: followers, as: :user)
284 @doc "GET /api/v1/accounts/:id/lists"
285 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
286 lists = Pleroma.List.get_lists_account_belongs(user, account)
289 |> put_view(ListView)
290 |> render("index.json", lists: lists)
293 @doc "POST /api/v1/accounts/:id/follow"
294 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
298 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
299 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
300 render(conn, "relationship.json", user: follower, target: followed)
302 {:error, message} -> json_response(conn, :forbidden, %{error: message})
306 @doc "POST /api/v1/accounts/:id/unfollow"
307 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
311 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
312 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
313 render(conn, "relationship.json", user: follower, target: followed)
317 @doc "POST /api/v1/accounts/:id/mute"
318 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
319 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
321 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
322 render(conn, "relationship.json", user: muter, target: muted)
324 {:error, message} -> json_response(conn, :forbidden, %{error: message})
328 @doc "POST /api/v1/accounts/:id/unmute"
329 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
330 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
331 render(conn, "relationship.json", user: muter, target: muted)
333 {:error, message} -> json_response(conn, :forbidden, %{error: message})
337 @doc "POST /api/v1/accounts/:id/block"
338 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
339 with {:ok, _user_block} <- User.block(blocker, blocked),
340 {:ok, _activity} <- ActivityPub.block(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/accounts/:id/unblock"
348 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
349 with {:ok, _user_block} <- User.unblock(blocker, blocked),
350 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
351 render(conn, "relationship.json", user: blocker, target: blocked)
353 {:error, message} -> json_response(conn, :forbidden, %{error: message})
357 @doc "POST /api/v1/follows"
358 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
359 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
360 {_, true} <- {:followed, follower.id != followed.id},
361 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
362 render(conn, "show.json", user: followed, for: follower)
364 {:followed, _} -> {:error, :not_found}
365 {:error, message} -> json_response(conn, :forbidden, %{error: message})
369 @doc "GET /api/v1/mutes"
370 def mutes(%{assigns: %{user: user}} = conn, _) do
371 users = User.muted_users(user, _restrict_deactivated = true)
372 render(conn, "index.json", users: users, for: user, as: :user)
375 @doc "GET /api/v1/blocks"
376 def blocks(%{assigns: %{user: user}} = conn, _) do
377 users = User.blocked_users(user, _restrict_deactivated = true)
378 render(conn, "index.json", users: users, for: user, as: :user)
381 @doc "GET /api/v1/endorsements"
382 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
384 @doc "GET /api/v1/identity_proofs"
385 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)