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.OAuthView
31 alias Pleroma.Web.OAuth.Token
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, need_confirmation: true),
104 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
105 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
107 {:error, error} -> json_response(conn, :bad_request, %{error: error})
111 def create(%{assigns: %{app: _app}} = conn, _) do
112 render_error(conn, :bad_request, "Missing parameters")
115 def create(conn, _) do
116 render_error(conn, :forbidden, "Invalid credentials")
119 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
121 defp validate_email_param(_) do
122 case Pleroma.Config.get([:instance, :account_activation_required]) do
123 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
128 @doc "GET /api/v1/accounts/verify_credentials"
129 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
130 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
132 render(conn, "show.json",
135 with_pleroma_settings: true,
136 with_chat_token: chat_token
140 @doc "PATCH /api/v1/accounts/update_credentials"
141 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
144 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
147 # We use an empty string as a special value to reset
148 # avatars, banners, backgrounds
149 user_image_value = fn
151 value -> {:ok, value}
158 :hide_followers_count,
164 :skip_thread_containment,
165 :allow_following_move,
168 |> Enum.reduce(%{}, fn key, acc ->
169 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
171 |> Maps.put_if_present(:name, params[:display_name])
172 |> Maps.put_if_present(:bio, params[:note])
173 |> Maps.put_if_present(:raw_bio, params[:note])
174 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
175 |> Maps.put_if_present(:banner, params[:header], user_image_value)
176 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
177 |> Maps.put_if_present(
179 params[:fields_attributes],
180 &{:ok, normalize_fields_attributes(&1)}
182 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
183 |> Maps.put_if_present(:default_scope, params[:default_scope])
184 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
185 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
186 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
188 |> Maps.put_if_present(:actor_type, params[:actor_type])
192 # We want to update the user through the pipeline, but the ActivityPub
193 # update information is not quite enough for this, because this also
194 # contains local settings that don't federate and don't even appear
195 # in the Update activity.
197 # So we first build the normal local changeset, then apply it to the
198 # user data, but don't persist it. With this, we generate the object
199 # data for our update activity. We feed this and the changeset as meta
200 # inforation into the pipeline, where they will be properly updated and
202 with changeset <- User.update_changeset(user, user_params),
203 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
205 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
206 |> Map.delete("@context"),
207 {:ok, update_data, []} <- Builder.update(user, updated_object),
209 Pipeline.common_pipeline(update_data,
211 user_update_changeset: changeset
213 render(conn, "show.json",
214 user: unpersisted_user,
215 for: unpersisted_user,
216 with_pleroma_settings: true
219 _e -> render_error(conn, :forbidden, "Invalid request")
223 defp normalize_fields_attributes(fields) do
224 if Enum.all?(fields, &is_tuple/1) do
225 Enum.map(fields, fn {_, v} -> v end)
228 %{} = field -> %{"name" => field.name, "value" => field.value}
234 @doc "GET /api/v1/accounts/relationships"
235 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
236 targets = User.get_all_by_ids(List.wrap(id))
238 render(conn, "relationships.json", user: user, targets: targets)
241 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
242 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
244 @doc "GET /api/v1/accounts/:id"
245 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
246 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
247 :visible <- User.visible_for(user, for_user) do
248 render(conn, "show.json", user: user, for: for_user)
250 error -> user_visibility_error(conn, error)
254 @doc "GET /api/v1/accounts/:id/statuses"
255 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
256 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
257 :visible <- User.visible_for(user, reading_user) do
260 |> Map.delete(:tagged)
261 |> Map.put(:tag, params[:tagged])
263 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
266 |> add_link_headers(activities)
267 |> put_view(StatusView)
268 |> render("index.json",
269 activities: activities,
274 error -> user_visibility_error(conn, error)
278 defp user_visibility_error(conn, error) do
280 :restrict_unauthenticated ->
281 render_error(conn, :unauthorized, "This API requires an authenticated user")
284 render_error(conn, :not_found, "Can't find user")
288 @doc "GET /api/v1/accounts/:id/followers"
289 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
292 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
297 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
298 user.hide_followers -> []
299 true -> MastodonAPI.get_followers(user, params)
303 |> add_link_headers(followers)
304 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
305 |> render("index.json",
309 embed_relationships: embed_relationships?(params)
313 @doc "GET /api/v1/accounts/:id/following"
314 def following(%{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_friends(user, params)
323 user.hide_follows -> []
324 true -> MastodonAPI.get_friends(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/lists"
339 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
340 lists = Pleroma.List.get_lists_account_belongs(user, account)
343 |> put_view(ListView)
344 |> render("index.json", lists: lists)
347 @doc "POST /api/v1/accounts/:id/follow"
348 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
349 {:error, "Can not follow yourself"}
352 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
353 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
354 render(conn, "relationship.json", user: follower, target: followed)
356 {:error, message} -> json_response(conn, :forbidden, %{error: message})
360 @doc "POST /api/v1/accounts/:id/unfollow"
361 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
362 {:error, "Can not unfollow yourself"}
365 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
366 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
367 render(conn, "relationship.json", user: follower, target: followed)
371 @doc "POST /api/v1/accounts/:id/mute"
372 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
373 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
374 render(conn, "relationship.json", user: muter, target: muted)
376 {:error, message} -> json_response(conn, :forbidden, %{error: message})
380 @doc "POST /api/v1/accounts/:id/unmute"
381 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
382 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
383 render(conn, "relationship.json", user: muter, target: muted)
385 {:error, message} -> json_response(conn, :forbidden, %{error: message})
389 @doc "POST /api/v1/accounts/:id/block"
390 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
391 with {:ok, _activity} <- CommonAPI.block(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/accounts/:id/unblock"
399 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
400 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
401 render(conn, "relationship.json", user: blocker, target: blocked)
403 {:error, message} -> json_response(conn, :forbidden, %{error: message})
407 @doc "POST /api/v1/follows"
408 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
409 case User.get_cached_by_nickname(uri) do
412 |> assign(:account, user)
420 @doc "GET /api/v1/mutes"
421 def mutes(%{assigns: %{user: user}} = conn, _) do
422 users = User.muted_users(user, _restrict_deactivated = true)
423 render(conn, "index.json", users: users, for: user, as: :user)
426 @doc "GET /api/v1/blocks"
427 def blocks(%{assigns: %{user: user}} = conn, _) do
428 users = User.blocked_users(user, _restrict_deactivated = true)
429 render(conn, "index.json", users: users, for: user, as: :user)
432 @doc "GET /api/v1/endorsements"
433 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
435 @doc "GET /api/v1/identity_proofs"
436 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)