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,
18 alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
19 alias Pleroma.Plugs.OAuthScopesPlug
20 alias Pleroma.Plugs.RateLimiter
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.CommonAPI
24 alias Pleroma.Web.MastodonAPI.ListView
25 alias Pleroma.Web.MastodonAPI.MastodonAPI
26 alias Pleroma.Web.MastodonAPI.MastodonAPIController
27 alias Pleroma.Web.MastodonAPI.StatusView
28 alias Pleroma.Web.OAuth.Token
29 alias Pleroma.Web.TwitterAPI.TwitterAPI
31 plug(Pleroma.Web.ApiSpec.CastAndValidate)
33 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
35 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
39 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
40 when action in [:show, :followers, :following]
45 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
46 when action == :statuses
51 %{scopes: ["read:accounts"]}
52 when action in [:verify_credentials, :endorsements, :identity_proofs]
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 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
96 @doc "POST /api/v1/accounts"
97 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
98 with :ok <- validate_email_param(params),
99 :ok <- TwitterAPI.validate_captcha(app, params),
100 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
101 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
103 token_type: "Bearer",
104 access_token: token.token,
106 created_at: Token.Utils.format_created_at(token)
109 {:error, error} -> json_response(conn, :bad_request, %{error: error})
113 def create(%{assigns: %{app: _app}} = conn, _) do
114 render_error(conn, :bad_request, "Missing parameters")
117 def create(conn, _) do
118 render_error(conn, :forbidden, "Invalid credentials")
121 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
123 defp validate_email_param(_) do
124 case Pleroma.Config.get([:instance, :account_activation_required]) do
125 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
130 @doc "GET /api/v1/accounts/verify_credentials"
131 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
132 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
134 render(conn, "show.json",
137 with_pleroma_settings: true,
138 with_chat_token: chat_token
142 @doc "PATCH /api/v1/accounts/update_credentials"
143 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
146 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
153 :hide_followers_count,
159 :skip_thread_containment,
160 :allow_following_move,
163 |> Enum.reduce(%{}, fn key, acc ->
164 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
166 |> Maps.put_if_present(:name, params[:display_name])
167 |> Maps.put_if_present(:bio, params[:note])
168 |> Maps.put_if_present(:raw_bio, params[:note])
169 |> Maps.put_if_present(:avatar, params[:avatar])
170 |> Maps.put_if_present(:banner, params[:header])
171 |> Maps.put_if_present(:background, params[:pleroma_background_image])
172 |> Maps.put_if_present(
174 params[:fields_attributes],
175 &{:ok, normalize_fields_attributes(&1)}
177 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
178 |> Maps.put_if_present(:default_scope, params[:default_scope])
179 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
180 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
181 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
183 |> Maps.put_if_present(:actor_type, params[:actor_type])
185 changeset = User.update_changeset(user, user_params)
187 with {:ok, user} <- User.update_and_set_cache(changeset) do
189 |> build_update_activity_params()
190 |> ActivityPub.update()
192 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
194 _e -> render_error(conn, :forbidden, "Invalid request")
198 # Hotfix, handling will be redone with the pipeline
199 defp build_update_activity_params(user) do
201 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
202 |> Map.delete("@context")
206 to: [user.follower_address],
213 defp normalize_fields_attributes(fields) do
214 if Enum.all?(fields, &is_tuple/1) do
215 Enum.map(fields, fn {_, v} -> v end)
218 %{} = field -> %{"name" => field.name, "value" => field.value}
224 @doc "GET /api/v1/accounts/relationships"
225 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
226 targets = User.get_all_by_ids(List.wrap(id))
228 render(conn, "relationships.json", user: user, targets: targets)
231 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
232 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
234 @doc "GET /api/v1/accounts/:id"
235 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
236 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
237 :visible <- User.visible_for(user, for_user) do
238 render(conn, "show.json", user: user, for: for_user)
240 error -> user_visibility_error(conn, error)
244 @doc "GET /api/v1/accounts/:id/statuses"
245 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
246 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
247 :visible <- User.visible_for(user, reading_user) do
250 |> Map.delete(:tagged)
251 |> Map.put(:tag, params[:tagged])
253 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
256 |> add_link_headers(activities)
257 |> put_view(StatusView)
258 |> render("index.json",
259 activities: activities,
264 error -> user_visibility_error(conn, error)
268 defp user_visibility_error(conn, error) do
270 :restrict_unauthenticated ->
271 render_error(conn, :unauthorized, "This API requires an authenticated user")
274 render_error(conn, :not_found, "Can't find user")
278 @doc "GET /api/v1/accounts/:id/followers"
279 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
282 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
287 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
288 user.hide_followers -> []
289 true -> MastodonAPI.get_followers(user, params)
293 |> add_link_headers(followers)
294 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
295 |> render("index.json",
299 embed_relationships: embed_relationships?(params)
303 @doc "GET /api/v1/accounts/:id/following"
304 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
307 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
312 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
313 user.hide_follows -> []
314 true -> MastodonAPI.get_friends(user, params)
318 |> add_link_headers(followers)
319 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
320 |> render("index.json",
324 embed_relationships: embed_relationships?(params)
328 @doc "GET /api/v1/accounts/:id/lists"
329 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
330 lists = Pleroma.List.get_lists_account_belongs(user, account)
333 |> put_view(ListView)
334 |> render("index.json", lists: lists)
337 @doc "POST /api/v1/accounts/:id/follow"
338 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
339 {:error, "Can not follow yourself"}
342 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
343 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
344 render(conn, "relationship.json", user: follower, target: followed)
346 {:error, message} -> json_response(conn, :forbidden, %{error: message})
350 @doc "POST /api/v1/accounts/:id/unfollow"
351 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
352 {:error, "Can not unfollow yourself"}
355 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
356 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
357 render(conn, "relationship.json", user: follower, target: followed)
361 @doc "POST /api/v1/accounts/:id/mute"
362 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
363 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
364 render(conn, "relationship.json", user: muter, target: muted)
366 {:error, message} -> json_response(conn, :forbidden, %{error: message})
370 @doc "POST /api/v1/accounts/:id/unmute"
371 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
372 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
373 render(conn, "relationship.json", user: muter, target: muted)
375 {:error, message} -> json_response(conn, :forbidden, %{error: message})
379 @doc "POST /api/v1/accounts/:id/block"
380 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
381 with {:ok, _user_block} <- User.block(blocker, blocked),
382 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
383 render(conn, "relationship.json", user: blocker, target: blocked)
385 {:error, message} -> json_response(conn, :forbidden, %{error: message})
389 @doc "POST /api/v1/accounts/:id/unblock"
390 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
391 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
392 render(conn, "relationship.json", user: blocker, target: blocked)
394 {:error, message} -> json_response(conn, :forbidden, %{error: message})
398 @doc "POST /api/v1/follows"
399 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
400 case User.get_cached_by_nickname(uri) do
403 |> assign(:account, user)
411 @doc "GET /api/v1/mutes"
412 def mutes(%{assigns: %{user: user}} = conn, _) do
413 users = User.muted_users(user, _restrict_deactivated = true)
414 render(conn, "index.json", users: users, for: user, as: :user)
417 @doc "GET /api/v1/blocks"
418 def blocks(%{assigns: %{user: user}} = conn, _) do
419 users = User.blocked_users(user, _restrict_deactivated = true)
420 render(conn, "index.json", users: users, for: user, as: :user)
423 @doc "GET /api/v1/endorsements"
424 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
426 @doc "GET /api/v1/identity_proofs"
427 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)