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: user}, body_params: params} = conn, _params) do
145 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
152 :hide_followers_count,
158 :skip_thread_containment,
159 :allow_following_move,
162 |> Enum.reduce(%{}, fn key, acc ->
163 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
165 |> add_if_present(params, :display_name, :name)
166 |> add_if_present(params, :note, :bio)
167 |> add_if_present(params, :avatar, :avatar)
168 |> add_if_present(params, :header, :banner)
169 |> add_if_present(params, :pleroma_background_image, :background)
174 &{:ok, normalize_fields_attributes(&1)}
176 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
177 |> add_if_present(params, :default_scope, :default_scope)
178 |> add_if_present(params["source"], "privacy", :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
185 |> build_update_activity_params()
186 |> ActivityPub.update()
188 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
190 _e -> render_error(conn, :forbidden, "Invalid request")
194 # Hotfix, handling will be redone with the pipeline
195 defp build_update_activity_params(user) do
197 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
198 |> Map.delete("@context")
202 to: [user.follower_address],
209 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
210 with true <- is_map(params),
211 true <- Map.has_key?(params, params_field),
212 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
213 Map.put(map, map_field, new_value)
219 defp normalize_fields_attributes(fields) do
220 if Enum.all?(fields, &is_tuple/1) do
221 Enum.map(fields, fn {_, v} -> v end)
224 %{} = field -> %{"name" => field.name, "value" => field.value}
230 @doc "GET /api/v1/accounts/relationships"
231 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
232 targets = User.get_all_by_ids(List.wrap(id))
234 render(conn, "relationships.json", user: user, targets: targets)
237 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
238 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
240 @doc "GET /api/v1/accounts/:id"
241 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
242 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
243 true <- User.visible_for?(user, for_user) do
244 render(conn, "show.json", user: user, for: for_user)
246 _e -> render_error(conn, :not_found, "Can't find user")
250 @doc "GET /api/v1/accounts/:id/statuses"
251 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
252 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
253 true <- User.visible_for?(user, reading_user) do
256 |> Map.delete(:tagged)
257 |> Enum.filter(&(not is_nil(&1)))
258 |> Map.new(fn {key, value} -> {to_string(key), value} end)
259 |> Map.put("tag", params[:tagged])
261 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
264 |> add_link_headers(activities)
265 |> put_view(StatusView)
266 |> render("index.json",
267 activities: activities,
272 _e -> render_error(conn, :not_found, "Can't find user")
276 @doc "GET /api/v1/accounts/:id/followers"
277 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
280 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
285 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
286 user.hide_followers -> []
287 true -> MastodonAPI.get_followers(user, params)
291 |> add_link_headers(followers)
292 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
293 |> render("index.json",
297 embed_relationships: embed_relationships?(params)
301 @doc "GET /api/v1/accounts/:id/following"
302 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
305 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
310 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
311 user.hide_follows -> []
312 true -> MastodonAPI.get_friends(user, params)
316 |> add_link_headers(followers)
317 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
318 |> render("index.json",
322 embed_relationships: embed_relationships?(params)
326 @doc "GET /api/v1/accounts/:id/lists"
327 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
328 lists = Pleroma.List.get_lists_account_belongs(user, account)
331 |> put_view(ListView)
332 |> render("index.json", lists: lists)
335 @doc "POST /api/v1/accounts/:id/follow"
336 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
337 {:error, "Can not follow yourself"}
340 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
341 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
342 render(conn, "relationship.json", user: follower, target: followed)
344 {:error, message} -> json_response(conn, :forbidden, %{error: message})
348 @doc "POST /api/v1/accounts/:id/unfollow"
349 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
350 {:error, "Can not unfollow yourself"}
353 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
354 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
355 render(conn, "relationship.json", user: follower, target: followed)
359 @doc "POST /api/v1/accounts/:id/mute"
360 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
361 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
362 render(conn, "relationship.json", user: muter, target: muted)
364 {:error, message} -> json_response(conn, :forbidden, %{error: message})
368 @doc "POST /api/v1/accounts/:id/unmute"
369 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
370 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
371 render(conn, "relationship.json", user: muter, target: muted)
373 {:error, message} -> json_response(conn, :forbidden, %{error: message})
377 @doc "POST /api/v1/accounts/:id/block"
378 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
379 with {:ok, _user_block} <- User.block(blocker, blocked),
380 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
381 render(conn, "relationship.json", user: blocker, target: blocked)
383 {:error, message} -> json_response(conn, :forbidden, %{error: message})
387 @doc "POST /api/v1/accounts/:id/unblock"
388 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
389 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
390 render(conn, "relationship.json", user: blocker, target: blocked)
392 {:error, message} -> json_response(conn, :forbidden, %{error: message})
396 @doc "POST /api/v1/follows"
397 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
398 case User.get_cached_by_nickname(uri) do
401 |> assign(:account, user)
409 @doc "GET /api/v1/mutes"
410 def mutes(%{assigns: %{user: user}} = conn, _) do
411 users = User.muted_users(user, _restrict_deactivated = true)
412 render(conn, "index.json", users: users, for: user, as: :user)
415 @doc "GET /api/v1/blocks"
416 def blocks(%{assigns: %{user: user}} = conn, _) do
417 users = User.blocked_users(user, _restrict_deactivated = true)
418 render(conn, "index.json", users: users, for: user, as: :user)
421 @doc "GET /api/v1/endorsements"
422 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
424 @doc "GET /api/v1/identity_proofs"
425 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)