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,
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Builder
21 alias Pleroma.Web.ActivityPub.Pipeline
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.OAuthController
28 alias Pleroma.Web.OAuth.OAuthView
29 alias Pleroma.Web.OAuth.Token
30 alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
31 alias Pleroma.Web.Plugs.OAuthScopesPlug
32 alias Pleroma.Web.Plugs.RateLimiter
33 alias Pleroma.Web.TwitterAPI.TwitterAPI
35 plug(Pleroma.Web.ApiSpec.CastAndValidate)
37 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
39 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
43 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
44 when action in [:show, :followers, :following]
49 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
50 when action == :statuses
55 %{scopes: ["read:accounts"]}
56 when action in [:verify_credentials, :endorsements, :identity_proofs]
59 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
61 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
65 %{scopes: ["follow", "read:blocks"]} when action == :blocks
70 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
73 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
77 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
80 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
82 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
84 @relationship_actions [:follow, :unfollow]
85 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
89 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
92 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
93 plug(RateLimiter, [name: :app_account_creation] when action == :create)
94 plug(:assign_account_by_id when action in @needs_account)
96 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
98 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
100 @doc "POST /api/v1/accounts"
101 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
102 with :ok <- validate_email_param(params),
103 :ok <- TwitterAPI.validate_captcha(app, params),
104 {:ok, user} <- TwitterAPI.register_user(params),
106 {:login, OAuthController.login(user, app, app.scopes)} do
107 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
109 {:login, {:account_status, :confirmation_pending}} ->
110 json_response(conn, :ok, %{
111 message: "You have been registered. Please check your email for further instructions.",
112 identifier: "missing_confirmed_email"
115 {:login, {:account_status, :approval_pending}} ->
116 json_response(conn, :ok, %{
118 "You have been registered. You'll be able to log in once your account is approved.",
119 identifier: "awaiting_approval"
123 json_response(conn, :ok, %{
125 "You have been registered. Some post-registration steps may be pending. " <>
126 "Please log in manually.",
127 identifier: "manual_login_required"
131 json_response(conn, :bad_request, %{error: error})
135 def create(%{assigns: %{app: _app}} = conn, _) do
136 render_error(conn, :bad_request, "Missing parameters")
139 def create(conn, _) do
140 render_error(conn, :forbidden, "Invalid credentials")
143 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
145 defp validate_email_param(_) do
146 case Pleroma.Config.get([:instance, :account_activation_required]) do
147 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
152 @doc "GET /api/v1/accounts/verify_credentials"
153 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
154 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
156 render(conn, "show.json",
159 with_pleroma_settings: true,
160 with_chat_token: chat_token
164 @doc "PATCH /api/v1/accounts/update_credentials"
165 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
168 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
171 # We use an empty string as a special value to reset
172 # avatars, banners, backgrounds
173 user_image_value = fn
175 value -> {:ok, value}
182 :hide_followers_count,
188 :skip_thread_containment,
189 :allow_following_move,
191 :accepts_chat_messages
193 |> Enum.reduce(%{}, fn key, acc ->
194 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
196 |> Maps.put_if_present(:name, params[:display_name])
197 |> Maps.put_if_present(:bio, params[:note])
198 |> Maps.put_if_present(:raw_bio, params[:note])
199 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
200 |> Maps.put_if_present(:banner, params[:header], user_image_value)
201 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
202 |> Maps.put_if_present(
204 params[:fields_attributes],
205 &{:ok, normalize_fields_attributes(&1)}
207 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
208 |> Maps.put_if_present(:default_scope, params[:default_scope])
209 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
210 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
211 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
213 |> Maps.put_if_present(:actor_type, params[:actor_type])
217 # We want to update the user through the pipeline, but the ActivityPub
218 # update information is not quite enough for this, because this also
219 # contains local settings that don't federate and don't even appear
220 # in the Update activity.
222 # So we first build the normal local changeset, then apply it to the
223 # user data, but don't persist it. With this, we generate the object
224 # data for our update activity. We feed this and the changeset as meta
225 # inforation into the pipeline, where they will be properly updated and
227 with changeset <- User.update_changeset(user, user_params),
228 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
230 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
231 |> Map.delete("@context"),
232 {:ok, update_data, []} <- Builder.update(user, updated_object),
234 Pipeline.common_pipeline(update_data,
236 user_update_changeset: changeset
238 render(conn, "show.json",
239 user: unpersisted_user,
240 for: unpersisted_user,
241 with_pleroma_settings: true
244 _e -> render_error(conn, :forbidden, "Invalid request")
248 defp normalize_fields_attributes(fields) do
249 if Enum.all?(fields, &is_tuple/1) do
250 Enum.map(fields, fn {_, v} -> v end)
253 %{} = field -> %{"name" => field.name, "value" => field.value}
259 @doc "GET /api/v1/accounts/relationships"
260 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
261 targets = User.get_all_by_ids(List.wrap(id))
263 render(conn, "relationships.json", user: user, targets: targets)
266 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
267 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
269 @doc "GET /api/v1/accounts/:id"
270 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
271 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
272 :visible <- User.visible_for(user, for_user) do
273 render(conn, "show.json", user: user, for: for_user)
275 error -> user_visibility_error(conn, error)
279 @doc "GET /api/v1/accounts/:id/statuses"
280 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
281 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
282 :visible <- User.visible_for(user, reading_user) do
285 |> Map.delete(:tagged)
286 |> Map.put(:tag, params[:tagged])
288 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
291 |> add_link_headers(activities)
292 |> put_view(StatusView)
293 |> render("index.json",
294 activities: activities,
299 error -> user_visibility_error(conn, error)
303 defp user_visibility_error(conn, error) do
305 :restrict_unauthenticated ->
306 render_error(conn, :unauthorized, "This API requires an authenticated user")
309 render_error(conn, :not_found, "Can't find user")
313 @doc "GET /api/v1/accounts/:id/followers"
314 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
317 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
322 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
323 user.hide_followers -> []
324 true -> MastodonAPI.get_followers(user, params)
328 |> add_link_headers(followers)
329 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
330 |> render("index.json",
334 embed_relationships: embed_relationships?(params)
338 @doc "GET /api/v1/accounts/:id/following"
339 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
342 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
347 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
348 user.hide_follows -> []
349 true -> MastodonAPI.get_friends(user, params)
353 |> add_link_headers(followers)
354 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
355 |> render("index.json",
359 embed_relationships: embed_relationships?(params)
363 @doc "GET /api/v1/accounts/:id/lists"
364 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
365 lists = Pleroma.List.get_lists_account_belongs(user, account)
368 |> put_view(ListView)
369 |> render("index.json", lists: lists)
372 @doc "POST /api/v1/accounts/:id/follow"
373 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
374 {:error, "Can not follow yourself"}
377 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
378 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
379 render(conn, "relationship.json", user: follower, target: followed)
381 {:error, message} -> json_response(conn, :forbidden, %{error: message})
385 @doc "POST /api/v1/accounts/:id/unfollow"
386 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
387 {:error, "Can not unfollow yourself"}
390 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
391 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
392 render(conn, "relationship.json", user: follower, target: followed)
396 @doc "POST /api/v1/accounts/:id/mute"
397 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
398 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
399 render(conn, "relationship.json", user: muter, target: muted)
401 {:error, message} -> json_response(conn, :forbidden, %{error: message})
405 @doc "POST /api/v1/accounts/:id/unmute"
406 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
407 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
408 render(conn, "relationship.json", user: muter, target: muted)
410 {:error, message} -> json_response(conn, :forbidden, %{error: message})
414 @doc "POST /api/v1/accounts/:id/block"
415 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
416 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
417 render(conn, "relationship.json", user: blocker, target: blocked)
419 {:error, message} -> json_response(conn, :forbidden, %{error: message})
423 @doc "POST /api/v1/accounts/:id/unblock"
424 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
425 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
426 render(conn, "relationship.json", user: blocker, target: blocked)
428 {:error, message} -> json_response(conn, :forbidden, %{error: message})
432 @doc "POST /api/v1/follows"
433 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
434 case User.get_cached_by_nickname(uri) do
437 |> assign(:account, user)
445 @doc "GET /api/v1/mutes"
446 def mutes(%{assigns: %{user: user}} = conn, _) do
447 users = User.muted_users(user, _restrict_deactivated = true)
448 render(conn, "index.json", users: users, for: user, as: :user)
451 @doc "GET /api/v1/blocks"
452 def blocks(%{assigns: %{user: user}} = conn, _) do
453 users = User.blocked_users(user, _restrict_deactivated = true)
454 render(conn, "index.json", users: users, for: user, as: :user)
457 @doc "GET /api/v1/endorsements"
458 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
460 @doc "GET /api/v1/identity_proofs"
461 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)