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,
13 embed_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(Pleroma.Web.ApiSpec.CastAndValidate)
32 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
34 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
38 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
39 when action in [:show, :followers, :following]
44 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
45 when action == :statuses
50 %{scopes: ["read:accounts"]}
51 when action in [:verify_credentials, :endorsements, :identity_proofs]
54 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
56 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
60 %{scopes: ["follow", "read:blocks"]} when action == :blocks
65 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
68 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
72 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
75 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
77 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
79 @relationship_actions [:follow, :unfollow]
80 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
84 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
87 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
88 plug(RateLimiter, [name: :app_account_creation] when action == :create)
89 plug(:assign_account_by_id when action in @needs_account)
91 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
93 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
95 @doc "POST /api/v1/accounts"
96 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
97 with :ok <- validate_email_param(params),
98 :ok <- TwitterAPI.validate_captcha(app, params),
99 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
100 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
102 token_type: "Bearer",
103 access_token: token.token,
105 created_at: Token.Utils.format_created_at(token)
108 {:error, error} -> json_response(conn, :bad_request, %{error: error})
112 def create(%{assigns: %{app: _app}} = conn, _) do
113 render_error(conn, :bad_request, "Missing parameters")
116 def create(conn, _) do
117 render_error(conn, :forbidden, "Invalid credentials")
120 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
122 defp validate_email_param(_) do
123 case Pleroma.Config.get([:instance, :account_activation_required]) do
124 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
129 @doc "GET /api/v1/accounts/verify_credentials"
130 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
131 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
133 render(conn, "show.json",
136 with_pleroma_settings: true,
137 with_chat_token: chat_token
141 @doc "PATCH /api/v1/accounts/update_credentials"
142 def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
147 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
154 :hide_followers_count,
160 :skip_thread_containment,
161 :allow_following_move,
164 |> Enum.reduce(%{}, fn key, acc ->
165 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
167 |> add_if_present(params, :display_name, :name)
168 |> add_if_present(params, :note, :bio)
169 |> add_if_present(params, :avatar, :avatar)
170 |> add_if_present(params, :header, :banner)
171 |> add_if_present(params, :pleroma_background_image, :background)
176 &{:ok, normalize_fields_attributes(&1)}
178 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
179 |> add_if_present(params, :default_scope, :default_scope)
180 |> add_if_present(params["source"], "privacy", :default_scope)
181 |> add_if_present(params, :actor_type, :actor_type)
183 changeset = User.update_changeset(user, user_params)
185 with {:ok, user} <- User.update_and_set_cache(changeset) do
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 <- is_map(params),
194 true <- Map.has_key?(params, params_field),
195 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
196 Map.put(map, map_field, new_value)
202 defp normalize_fields_attributes(fields) do
203 if Enum.all?(fields, &is_tuple/1) do
204 Enum.map(fields, fn {_, v} -> v end)
207 %{} = field -> %{"name" => field.name, "value" => field.value}
213 @doc "GET /api/v1/accounts/relationships"
214 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
215 targets = User.get_all_by_ids(List.wrap(id))
217 render(conn, "relationships.json", user: user, targets: targets)
220 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
221 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
223 @doc "GET /api/v1/accounts/:id"
224 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
225 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
226 true <- User.visible_for?(user, for_user) do
227 render(conn, "show.json", user: user, for: for_user)
229 _e -> render_error(conn, :not_found, "Can't find user")
233 @doc "GET /api/v1/accounts/:id/statuses"
234 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
235 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
236 true <- User.visible_for?(user, reading_user) do
239 |> Map.delete(:tagged)
240 |> Enum.filter(&(not is_nil(&1)))
241 |> Map.new(fn {key, value} -> {to_string(key), value} end)
242 |> Map.put("tag", params[:tagged])
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,
255 _e -> render_error(conn, :not_found, "Can't find user")
259 @doc "GET /api/v1/accounts/:id/followers"
260 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
263 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
268 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
269 user.hide_followers -> []
270 true -> MastodonAPI.get_followers(user, params)
274 |> add_link_headers(followers)
275 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
276 |> render("index.json",
280 embed_relationships: embed_relationships?(params)
284 @doc "GET /api/v1/accounts/:id/following"
285 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
288 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
293 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
294 user.hide_follows -> []
295 true -> MastodonAPI.get_friends(user, params)
299 |> add_link_headers(followers)
300 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
301 |> render("index.json",
305 embed_relationships: embed_relationships?(params)
309 @doc "GET /api/v1/accounts/:id/lists"
310 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
311 lists = Pleroma.List.get_lists_account_belongs(user, account)
314 |> put_view(ListView)
315 |> render("index.json", lists: lists)
318 @doc "POST /api/v1/accounts/:id/follow"
319 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
320 {:error, "Can not follow yourself"}
323 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
324 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
325 render(conn, "relationship.json", user: follower, target: followed)
327 {:error, message} -> json_response(conn, :forbidden, %{error: message})
331 @doc "POST /api/v1/accounts/:id/unfollow"
332 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
333 {:error, "Can not unfollow yourself"}
336 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
337 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
338 render(conn, "relationship.json", user: follower, target: followed)
342 @doc "POST /api/v1/accounts/:id/mute"
343 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
344 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
345 render(conn, "relationship.json", user: muter, target: muted)
347 {:error, message} -> json_response(conn, :forbidden, %{error: message})
351 @doc "POST /api/v1/accounts/:id/unmute"
352 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
353 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
354 render(conn, "relationship.json", user: muter, target: muted)
356 {:error, message} -> json_response(conn, :forbidden, %{error: message})
360 @doc "POST /api/v1/accounts/:id/block"
361 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
362 with {:ok, _user_block} <- User.block(blocker, blocked),
363 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
364 render(conn, "relationship.json", user: blocker, target: blocked)
366 {:error, message} -> json_response(conn, :forbidden, %{error: message})
370 @doc "POST /api/v1/accounts/:id/unblock"
371 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
372 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
373 render(conn, "relationship.json", user: blocker, target: blocked)
375 {:error, message} -> json_response(conn, :forbidden, %{error: message})
379 @doc "POST /api/v1/follows"
380 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
381 case User.get_cached_by_nickname(uri) do
384 |> assign(:account, user)
392 @doc "GET /api/v1/mutes"
393 def mutes(%{assigns: %{user: user}} = conn, _) do
394 users = User.muted_users(user, _restrict_deactivated = true)
395 render(conn, "index.json", users: users, for: user, as: :user)
398 @doc "GET /api/v1/blocks"
399 def blocks(%{assigns: %{user: user}} = conn, _) do
400 users = User.blocked_users(user, _restrict_deactivated = true)
401 render(conn, "index.json", users: users, for: user, as: :user)
404 @doc "GET /api/v1/endorsements"
405 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
407 @doc "GET /api/v1/identity_proofs"
408 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)