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.ActivityPub.Builder
24 alias Pleroma.Web.ActivityPub.Pipeline
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.ListView
27 alias Pleroma.Web.MastodonAPI.MastodonAPI
28 alias Pleroma.Web.MastodonAPI.MastodonAPIController
29 alias Pleroma.Web.MastodonAPI.StatusView
30 alias Pleroma.Web.OAuth.OAuthController
31 alias Pleroma.Web.OAuth.OAuthView
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}
181 :hide_followers_count,
187 :skip_thread_containment,
188 :allow_following_move,
190 :accepts_chat_messages
192 |> Enum.reduce(%{}, fn key, acc ->
193 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
195 |> Maps.put_if_present(:name, params[:display_name])
196 |> Maps.put_if_present(:bio, params[:note])
197 |> Maps.put_if_present(:raw_bio, params[:note])
198 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
199 |> Maps.put_if_present(:banner, params[:header], user_image_value)
200 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
201 |> Maps.put_if_present(
203 params[:fields_attributes],
204 &{:ok, normalize_fields_attributes(&1)}
206 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
207 |> Maps.put_if_present(:default_scope, params[:default_scope])
208 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
209 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
210 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
212 |> Maps.put_if_present(:actor_type, params[:actor_type])
216 # We want to update the user through the pipeline, but the ActivityPub
217 # update information is not quite enough for this, because this also
218 # contains local settings that don't federate and don't even appear
219 # in the Update activity.
221 # So we first build the normal local changeset, then apply it to the
222 # user data, but don't persist it. With this, we generate the object
223 # data for our update activity. We feed this and the changeset as meta
224 # inforation into the pipeline, where they will be properly updated and
226 with changeset <- User.update_changeset(user, user_params),
227 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
229 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
230 |> Map.delete("@context"),
231 {:ok, update_data, []} <- Builder.update(user, updated_object),
233 Pipeline.common_pipeline(update_data,
235 user_update_changeset: changeset
237 render(conn, "show.json",
238 user: unpersisted_user,
239 for: unpersisted_user,
240 with_pleroma_settings: true
243 _e -> render_error(conn, :forbidden, "Invalid request")
247 defp normalize_fields_attributes(fields) do
248 if Enum.all?(fields, &is_tuple/1) do
249 Enum.map(fields, fn {_, v} -> v end)
252 %{} = field -> %{"name" => field.name, "value" => field.value}
258 @doc "GET /api/v1/accounts/relationships"
259 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
260 targets = User.get_all_by_ids(List.wrap(id))
262 render(conn, "relationships.json", user: user, targets: targets)
265 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
266 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
268 @doc "GET /api/v1/accounts/:id"
269 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
270 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
271 :visible <- User.visible_for(user, for_user) do
272 render(conn, "show.json", user: user, for: for_user)
274 error -> user_visibility_error(conn, error)
278 @doc "GET /api/v1/accounts/:id/statuses"
279 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
280 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
281 :visible <- User.visible_for(user, reading_user) do
284 |> Map.delete(:tagged)
285 |> Map.put(:tag, params[:tagged])
287 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
290 |> add_link_headers(activities)
291 |> put_view(StatusView)
292 |> render("index.json",
293 activities: activities,
298 error -> user_visibility_error(conn, error)
302 defp user_visibility_error(conn, error) do
304 :restrict_unauthenticated ->
305 render_error(conn, :unauthorized, "This API requires an authenticated user")
308 render_error(conn, :not_found, "Can't find user")
312 @doc "GET /api/v1/accounts/:id/followers"
313 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
316 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
321 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
322 user.hide_followers -> []
323 true -> MastodonAPI.get_followers(user, params)
327 |> add_link_headers(followers)
328 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
329 |> render("index.json",
333 embed_relationships: embed_relationships?(params)
337 @doc "GET /api/v1/accounts/:id/following"
338 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
341 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
346 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
347 user.hide_follows -> []
348 true -> MastodonAPI.get_friends(user, params)
352 |> add_link_headers(followers)
353 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
354 |> render("index.json",
358 embed_relationships: embed_relationships?(params)
362 @doc "GET /api/v1/accounts/:id/lists"
363 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
364 lists = Pleroma.List.get_lists_account_belongs(user, account)
367 |> put_view(ListView)
368 |> render("index.json", lists: lists)
371 @doc "POST /api/v1/accounts/:id/follow"
372 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
373 {:error, "Can not follow yourself"}
376 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
377 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
378 render(conn, "relationship.json", user: follower, target: followed)
380 {:error, message} -> json_response(conn, :forbidden, %{error: message})
384 @doc "POST /api/v1/accounts/:id/unfollow"
385 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
386 {:error, "Can not unfollow yourself"}
389 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
390 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
391 render(conn, "relationship.json", user: follower, target: followed)
395 @doc "POST /api/v1/accounts/:id/mute"
396 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
397 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
398 render(conn, "relationship.json", user: muter, target: muted)
400 {:error, message} -> json_response(conn, :forbidden, %{error: message})
404 @doc "POST /api/v1/accounts/:id/unmute"
405 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
406 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
407 render(conn, "relationship.json", user: muter, target: muted)
409 {:error, message} -> json_response(conn, :forbidden, %{error: message})
413 @doc "POST /api/v1/accounts/:id/block"
414 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
415 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
416 render(conn, "relationship.json", user: blocker, target: blocked)
418 {:error, message} -> json_response(conn, :forbidden, %{error: message})
422 @doc "POST /api/v1/accounts/:id/unblock"
423 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
424 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
425 render(conn, "relationship.json", user: blocker, target: blocked)
427 {:error, message} -> json_response(conn, :forbidden, %{error: message})
431 @doc "POST /api/v1/follows"
432 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
433 case User.get_cached_by_nickname(uri) do
436 |> assign(:account, user)
444 @doc "GET /api/v1/mutes"
445 def mutes(%{assigns: %{user: user}} = conn, _) do
446 users = User.muted_users(user, _restrict_deactivated = true)
447 render(conn, "index.json", users: users, for: user, as: :user)
450 @doc "GET /api/v1/blocks"
451 def blocks(%{assigns: %{user: user}} = conn, _) do
452 users = User.blocked_users(user, _restrict_deactivated = true)
453 render(conn, "index.json", users: users, for: user, as: :user)
456 @doc "GET /api/v1/endorsements"
457 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
459 @doc "GET /api/v1/identity_proofs"
460 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)