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 in [:create, :identity_proofs])
33 Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
34 when action in [:create, :show, :statuses]
39 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
40 when action in [:show, :followers, :following, :endorsements]
45 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
46 when action == :statuses
51 %{scopes: ["read:accounts"]}
52 when action in [:endorsements, :verify_credentials]
55 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
57 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
61 %{scopes: ["follow", "read:blocks"]} when action == :blocks
66 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
69 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
73 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
76 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
78 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
80 @relationship_actions [:follow, :unfollow]
81 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
85 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
88 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
89 plug(RateLimiter, [name: :app_account_creation] when action == :create)
90 plug(:assign_account_by_id when action in @needs_account)
92 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
94 @doc "POST /api/v1/accounts"
96 %{assigns: %{app: app}} = conn,
97 %{"username" => nickname, "password" => _, "agreement" => true} = params
105 "captcha_answer_data",
109 |> Map.put("nickname", nickname)
110 |> Map.put("fullname", params["fullname"] || nickname)
111 |> Map.put("bio", params["bio"] || "")
112 |> Map.put("confirm", params["password"])
113 |> Map.put("trusted_app", app.trusted)
115 with :ok <- validate_email_param(params),
116 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
117 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
119 token_type: "Bearer",
120 access_token: token.token,
122 created_at: Token.Utils.format_created_at(token)
125 {:error, errors} -> json_response(conn, :bad_request, errors)
129 def create(%{assigns: %{app: _app}} = conn, _) do
130 render_error(conn, :bad_request, "Missing parameters")
133 def create(conn, _) do
134 render_error(conn, :forbidden, "Invalid credentials")
137 defp validate_email_param(%{"email" => _}), do: :ok
139 defp validate_email_param(_) do
140 case Pleroma.Config.get([:instance, :account_activation_required]) do
141 true -> {:error, %{"error" => "Missing parameters"}}
146 @doc "GET /api/v1/accounts/verify_credentials"
147 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
148 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
150 render(conn, "show.json",
153 with_pleroma_settings: true,
154 with_chat_token: chat_token
158 @doc "PATCH /api/v1/accounts/update_credentials"
159 def update_credentials(%{assigns: %{user: user}} = conn, params) do
164 :hide_followers_count,
170 :skip_thread_containment,
171 :allow_following_move,
174 |> Enum.reduce(%{}, fn key, acc ->
175 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
177 |> add_if_present(params, "display_name", :name)
178 |> add_if_present(params, "note", :bio)
179 |> add_if_present(params, "avatar", :avatar)
180 |> add_if_present(params, "header", :banner)
181 |> add_if_present(params, "pleroma_background_image", :background)
186 &{:ok, normalize_fields_attributes(&1)}
188 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
189 |> add_if_present(params, "default_scope", :default_scope)
190 |> add_if_present(params, "actor_type", :actor_type)
192 changeset = User.update_changeset(user, user_params)
194 with {:ok, user} <- User.update_and_set_cache(changeset) do
195 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
197 _e -> render_error(conn, :forbidden, "Invalid request")
201 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
202 with true <- Map.has_key?(params, params_field),
203 {:ok, new_value} <- value_function.(params[params_field]) do
204 Map.put(map, map_field, new_value)
210 defp normalize_fields_attributes(fields) do
211 if Enum.all?(fields, &is_tuple/1) do
212 Enum.map(fields, fn {_, v} -> v end)
218 @doc "GET /api/v1/accounts/relationships"
219 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
220 targets = User.get_all_by_ids(List.wrap(id))
222 render(conn, "relationships.json", user: user, targets: targets)
225 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
226 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
228 @doc "GET /api/v1/accounts/:id"
229 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
230 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
231 true <- User.visible_for?(user, for_user) do
232 render(conn, "show.json", user: user, for: for_user)
234 _e -> render_error(conn, :not_found, "Can't find user")
238 @doc "GET /api/v1/accounts/:id/statuses"
239 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
240 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
241 true <- User.visible_for?(user, reading_user) do
244 |> Map.put("tag", params["tagged"])
245 |> Map.delete("godmode")
247 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
250 |> add_link_headers(activities)
251 |> put_view(StatusView)
252 |> render("index.json",
253 activities: activities,
256 skip_relationships: skip_relationships?(params)
259 _e -> render_error(conn, :not_found, "Can't find user")
263 @doc "GET /api/v1/accounts/:id/followers"
264 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
267 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
268 user.hide_followers -> []
269 true -> MastodonAPI.get_followers(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/following"
278 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
281 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
282 user.hide_follows -> []
283 true -> MastodonAPI.get_friends(user, params)
287 |> add_link_headers(followers)
288 |> render("index.json", for: for_user, users: followers, as: :user)
291 @doc "GET /api/v1/accounts/:id/lists"
292 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
293 lists = Pleroma.List.get_lists_account_belongs(user, account)
296 |> put_view(ListView)
297 |> render("index.json", lists: lists)
300 @doc "POST /api/v1/accounts/:id/follow"
301 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
305 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
306 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
307 render(conn, "relationship.json", user: follower, target: followed)
309 {:error, message} -> json_response(conn, :forbidden, %{error: message})
313 @doc "POST /api/v1/accounts/:id/unfollow"
314 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
318 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
319 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
320 render(conn, "relationship.json", user: follower, target: followed)
324 @doc "POST /api/v1/accounts/:id/mute"
325 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
326 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
328 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
329 render(conn, "relationship.json", user: muter, target: muted)
331 {:error, message} -> json_response(conn, :forbidden, %{error: message})
335 @doc "POST /api/v1/accounts/:id/unmute"
336 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
337 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
338 render(conn, "relationship.json", user: muter, target: muted)
340 {:error, message} -> json_response(conn, :forbidden, %{error: message})
344 @doc "POST /api/v1/accounts/:id/block"
345 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
346 with {:ok, _user_block} <- User.block(blocker, blocked),
347 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
348 render(conn, "relationship.json", user: blocker, target: blocked)
350 {:error, message} -> json_response(conn, :forbidden, %{error: message})
354 @doc "POST /api/v1/accounts/:id/unblock"
355 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
356 with {:ok, _user_block} <- User.unblock(blocker, blocked),
357 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
358 render(conn, "relationship.json", user: blocker, target: blocked)
360 {:error, message} -> json_response(conn, :forbidden, %{error: message})
364 @doc "POST /api/v1/follows"
365 def follow_by_uri(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
366 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
367 {_, true} <- {:followed, follower.id != followed.id},
368 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
369 render(conn, "show.json", user: followed, for: follower)
371 {:followed, _} -> {:error, :not_found}
372 {:error, message} -> json_response(conn, :forbidden, %{error: message})
376 @doc "GET /api/v1/mutes"
377 def mutes(%{assigns: %{user: user}} = conn, _) do
378 users = User.muted_users(user, _restrict_deactivated = true)
379 render(conn, "index.json", users: users, for: user, as: :user)
382 @doc "GET /api/v1/blocks"
383 def blocks(%{assigns: %{user: user}} = conn, _) do
384 users = User.blocked_users(user, _restrict_deactivated = true)
385 render(conn, "index.json", users: users, for: user, as: :user)
388 @doc "GET /api/v1/endorsements"
389 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
391 @doc "GET /api/v1/identity_proofs"
392 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)