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.Plugs.EnsurePublicOrAuthenticatedPlug
30 alias Pleroma.Web.Plugs.OAuthScopesPlug
31 alias Pleroma.Web.Plugs.RateLimiter
32 alias Pleroma.Web.TwitterAPI.TwitterAPI
34 plug(Pleroma.Web.ApiSpec.CastAndValidate)
36 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
38 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
42 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
43 when action in [:show, :followers, :following]
48 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
49 when action == :statuses
54 %{scopes: ["read:accounts"]}
55 when action in [:verify_credentials, :endorsements, :identity_proofs]
58 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
60 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
64 %{scopes: ["follow", "read:blocks"]} when action == :blocks
69 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
72 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
76 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
79 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
81 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
83 @relationship_actions [:follow, :unfollow]
84 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
88 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
91 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
92 plug(RateLimiter, [name: :app_account_creation] when action == :create)
93 plug(:assign_account_by_id when action in @needs_account)
95 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
97 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
99 @doc "POST /api/v1/accounts"
100 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
101 with :ok <- validate_email_param(params),
102 :ok <- TwitterAPI.validate_captcha(app, params),
103 {:ok, user} <- TwitterAPI.register_user(params),
105 {:login, OAuthController.login(user, app, app.scopes)} do
106 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
108 {:login, {:account_status, :confirmation_pending}} ->
109 json_response(conn, :ok, %{
110 message: "You have been registered. Please check your email for further instructions.",
111 identifier: "missing_confirmed_email"
114 {:login, {:account_status, :approval_pending}} ->
115 json_response(conn, :ok, %{
117 "You have been registered. You'll be able to log in once your account is approved.",
118 identifier: "awaiting_approval"
122 json_response(conn, :ok, %{
124 "You have been registered. Some post-registration steps may be pending. " <>
125 "Please log in manually.",
126 identifier: "manual_login_required"
130 json_response(conn, :bad_request, %{error: error})
134 def create(%{assigns: %{app: _app}} = conn, _) do
135 render_error(conn, :bad_request, "Missing parameters")
138 def create(conn, _) do
139 render_error(conn, :forbidden, "Invalid credentials")
142 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
144 defp validate_email_param(_) do
145 case Pleroma.Config.get([:instance, :account_activation_required]) do
146 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
151 @doc "GET /api/v1/accounts/verify_credentials"
152 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
153 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
155 render(conn, "show.json",
158 with_pleroma_settings: true,
159 with_chat_token: chat_token
163 @doc "PATCH /api/v1/accounts/update_credentials"
164 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
167 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
170 # We use an empty string as a special value to reset
171 # avatars, banners, backgrounds
172 user_image_value = fn
174 value -> {:ok, value}
180 :hide_followers_count,
186 :skip_thread_containment,
187 :allow_following_move,
188 :accepts_chat_messages
190 |> Enum.reduce(%{}, fn key, acc ->
191 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
193 |> Maps.put_if_present(:name, params[:display_name])
194 |> Maps.put_if_present(:bio, params[:note])
195 |> Maps.put_if_present(:raw_bio, params[:note])
196 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
197 |> Maps.put_if_present(:banner, params[:header], user_image_value)
198 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
199 |> Maps.put_if_present(
201 params[:fields_attributes],
202 &{:ok, normalize_fields_attributes(&1)}
204 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
205 |> Maps.put_if_present(:default_scope, params[:default_scope])
206 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
207 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
208 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
210 |> Maps.put_if_present(:actor_type, params[:actor_type])
211 # Note: param name is indeed :locked (not an error)
212 |> Maps.put_if_present(:is_locked, params[:locked])
213 # Note: param name is indeed :discoverable (not an error)
214 |> Maps.put_if_present(:is_discoverable, params[:discoverable])
218 # We want to update the user through the pipeline, but the ActivityPub
219 # update information is not quite enough for this, because this also
220 # contains local settings that don't federate and don't even appear
221 # in the Update activity.
223 # So we first build the normal local changeset, then apply it to the
224 # user data, but don't persist it. With this, we generate the object
225 # data for our update activity. We feed this and the changeset as meta
226 # inforation into the pipeline, where they will be properly updated and
228 with changeset <- User.update_changeset(user, user_params),
229 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
231 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
232 |> Map.delete("@context"),
233 {:ok, update_data, []} <- Builder.update(user, updated_object),
235 Pipeline.common_pipeline(update_data,
237 user_update_changeset: changeset
239 render(conn, "show.json",
240 user: unpersisted_user,
241 for: unpersisted_user,
242 with_pleroma_settings: true
245 _e -> render_error(conn, :forbidden, "Invalid request")
249 defp normalize_fields_attributes(fields) do
250 if Enum.all?(fields, &is_tuple/1) do
251 Enum.map(fields, fn {_, v} -> v end)
254 %{} = field -> %{"name" => field.name, "value" => field.value}
260 @doc "GET /api/v1/accounts/relationships"
261 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
262 targets = User.get_all_by_ids(List.wrap(id))
264 render(conn, "relationships.json", user: user, targets: targets)
267 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
268 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
270 @doc "GET /api/v1/accounts/:id"
271 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
272 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
273 :visible <- User.visible_for(user, for_user) do
274 render(conn, "show.json", user: user, for: for_user)
276 error -> user_visibility_error(conn, error)
280 @doc "GET /api/v1/accounts/:id/statuses"
281 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
282 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
283 :visible <- User.visible_for(user, reading_user) do
286 |> Map.delete(:tagged)
287 |> Map.put(:tag, params[:tagged])
289 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
292 |> add_link_headers(activities)
293 |> put_view(StatusView)
294 |> render("index.json",
295 activities: activities,
298 with_muted: Map.get(params, :with_muted, false)
301 error -> user_visibility_error(conn, error)
305 defp user_visibility_error(conn, error) do
307 :restrict_unauthenticated ->
308 render_error(conn, :unauthorized, "This API requires an authenticated user")
311 render_error(conn, :not_found, "Can't find user")
315 @doc "GET /api/v1/accounts/:id/followers"
316 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
319 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
324 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
325 user.hide_followers -> []
326 true -> MastodonAPI.get_followers(user, params)
330 |> add_link_headers(followers)
331 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
332 |> render("index.json",
336 embed_relationships: embed_relationships?(params)
340 @doc "GET /api/v1/accounts/:id/following"
341 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
344 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
349 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
350 user.hide_follows -> []
351 true -> MastodonAPI.get_friends(user, params)
355 |> add_link_headers(followers)
356 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
357 |> render("index.json",
361 embed_relationships: embed_relationships?(params)
365 @doc "GET /api/v1/accounts/:id/lists"
366 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
367 lists = Pleroma.List.get_lists_account_belongs(user, account)
370 |> put_view(ListView)
371 |> render("index.json", lists: lists)
374 @doc "POST /api/v1/accounts/:id/follow"
375 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
376 {:error, "Can not follow yourself"}
379 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
380 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
381 render(conn, "relationship.json", user: follower, target: followed)
383 {:error, message} -> json_response(conn, :forbidden, %{error: message})
387 @doc "POST /api/v1/accounts/:id/unfollow"
388 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
389 {:error, "Can not unfollow yourself"}
392 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
393 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
394 render(conn, "relationship.json", user: follower, target: followed)
398 @doc "POST /api/v1/accounts/:id/mute"
399 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
400 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
401 render(conn, "relationship.json", user: muter, target: muted)
403 {:error, message} -> json_response(conn, :forbidden, %{error: message})
407 @doc "POST /api/v1/accounts/:id/unmute"
408 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
409 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
410 render(conn, "relationship.json", user: muter, target: muted)
412 {:error, message} -> json_response(conn, :forbidden, %{error: message})
416 @doc "POST /api/v1/accounts/:id/block"
417 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
418 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
419 render(conn, "relationship.json", user: blocker, target: blocked)
421 {:error, message} -> json_response(conn, :forbidden, %{error: message})
425 @doc "POST /api/v1/accounts/:id/unblock"
426 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
427 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
428 render(conn, "relationship.json", user: blocker, target: blocked)
430 {:error, message} -> json_response(conn, :forbidden, %{error: message})
434 @doc "POST /api/v1/follows"
435 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
436 case User.get_cached_by_nickname(uri) do
439 |> assign(:account, user)
447 @doc "GET /api/v1/mutes"
448 def mutes(%{assigns: %{user: user}} = conn, params) do
451 |> User.muted_users_relation(_restrict_deactivated = true)
452 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
455 |> add_link_headers(users)
456 |> render("index.json", users: users, for: user, as: :user)
459 @doc "GET /api/v1/blocks"
460 def blocks(%{assigns: %{user: user}} = conn, params) do
463 |> User.blocked_users_relation(_restrict_deactivated = true)
464 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
467 |> add_link_headers(users)
468 |> render("index.json", users: users, for: user, as: :user)
471 @doc "GET /api/v1/endorsements"
472 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
474 @doc "GET /api/v1/identity_proofs"
475 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)