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.Token
31 alias Pleroma.Web.TwitterAPI.TwitterAPI
33 plug(Pleroma.Web.ApiSpec.CastAndValidate)
35 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
37 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
41 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
42 when action in [:show, :followers, :following]
47 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
48 when action == :statuses
53 %{scopes: ["read:accounts"]}
54 when action in [:verify_credentials, :endorsements, :identity_proofs]
57 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
59 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
63 %{scopes: ["follow", "read:blocks"]} when action == :blocks
68 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
71 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
75 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
78 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
80 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
82 @relationship_actions [:follow, :unfollow]
83 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
87 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
90 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
91 plug(RateLimiter, [name: :app_account_creation] when action == :create)
92 plug(:assign_account_by_id when action in @needs_account)
94 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
96 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
98 @doc "POST /api/v1/accounts"
99 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
100 with :ok <- validate_email_param(params),
101 :ok <- TwitterAPI.validate_captcha(app, params),
102 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
103 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
105 token_type: "Bearer",
106 access_token: token.token,
108 created_at: Token.Utils.format_created_at(token)
111 {:error, error} -> json_response(conn, :bad_request, %{error: error})
115 def create(%{assigns: %{app: _app}} = conn, _) do
116 render_error(conn, :bad_request, "Missing parameters")
119 def create(conn, _) do
120 render_error(conn, :forbidden, "Invalid credentials")
123 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
125 defp validate_email_param(_) do
126 case Pleroma.Config.get([:instance, :account_activation_required]) do
127 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
132 @doc "GET /api/v1/accounts/verify_credentials"
133 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
134 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
136 render(conn, "show.json",
139 with_pleroma_settings: true,
140 with_chat_token: chat_token
144 @doc "PATCH /api/v1/accounts/update_credentials"
145 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
148 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
155 :hide_followers_count,
161 :skip_thread_containment,
162 :allow_following_move,
165 |> Enum.reduce(%{}, fn key, acc ->
166 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
168 |> Maps.put_if_present(:name, params[:display_name])
169 |> Maps.put_if_present(:bio, params[:note])
170 |> Maps.put_if_present(:raw_bio, params[:note])
171 |> Maps.put_if_present(:avatar, params[:avatar])
172 |> Maps.put_if_present(:banner, params[:header])
173 |> Maps.put_if_present(:background, params[:pleroma_background_image])
174 |> Maps.put_if_present(
176 params[:fields_attributes],
177 &{:ok, normalize_fields_attributes(&1)}
179 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
180 |> Maps.put_if_present(:default_scope, params[:default_scope])
181 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
182 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
183 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
185 |> Maps.put_if_present(:actor_type, params[:actor_type])
189 # We want to update the user through the pipeline, but the ActivityPub
190 # update information is not quite enough for this, because this also
191 # contains local settings that don't federate and don't even appear
192 # in the Update activity.
194 # So we first build the normal local changeset, then apply it to the
195 # user data, but don't persist it. With this, we generate the object
196 # data for our update activity. We feed this and the changeset as meta
197 # inforation into the pipeline, where they will be properly updated and
199 with changeset <- User.update_changeset(user, user_params),
200 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
202 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
203 |> Map.delete("@context"),
204 {:ok, update_data, []} <- Builder.update(user, updated_object),
206 Pipeline.common_pipeline(update_data,
208 user_update_changeset: changeset
210 render(conn, "show.json",
211 user: unpersisted_user,
212 for: unpersisted_user,
213 with_pleroma_settings: true
216 _e -> render_error(conn, :forbidden, "Invalid request")
220 defp normalize_fields_attributes(fields) do
221 if Enum.all?(fields, &is_tuple/1) do
222 Enum.map(fields, fn {_, v} -> v end)
225 %{} = field -> %{"name" => field.name, "value" => field.value}
231 @doc "GET /api/v1/accounts/relationships"
232 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
233 targets = User.get_all_by_ids(List.wrap(id))
235 render(conn, "relationships.json", user: user, targets: targets)
238 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
239 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
241 @doc "GET /api/v1/accounts/:id"
242 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
243 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
244 :visible <- User.visible_for(user, for_user) do
245 render(conn, "show.json", user: user, for: for_user)
247 error -> user_visibility_error(conn, error)
251 @doc "GET /api/v1/accounts/:id/statuses"
252 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
253 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
254 :visible <- User.visible_for(user, reading_user) do
257 |> Map.delete(:tagged)
258 |> Map.put(:tag, params[:tagged])
260 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
263 |> add_link_headers(activities)
264 |> put_view(StatusView)
265 |> render("index.json",
266 activities: activities,
271 error -> user_visibility_error(conn, error)
275 defp user_visibility_error(conn, error) do
277 :restrict_unauthenticated ->
278 render_error(conn, :unauthorized, "This API requires an authenticated user")
281 render_error(conn, :not_found, "Can't find user")
285 @doc "GET /api/v1/accounts/:id/followers"
286 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
289 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
294 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
295 user.hide_followers -> []
296 true -> MastodonAPI.get_followers(user, params)
300 |> add_link_headers(followers)
301 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
302 |> render("index.json",
306 embed_relationships: embed_relationships?(params)
310 @doc "GET /api/v1/accounts/:id/following"
311 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
314 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
319 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
320 user.hide_follows -> []
321 true -> MastodonAPI.get_friends(user, params)
325 |> add_link_headers(followers)
326 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
327 |> render("index.json",
331 embed_relationships: embed_relationships?(params)
335 @doc "GET /api/v1/accounts/:id/lists"
336 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
337 lists = Pleroma.List.get_lists_account_belongs(user, account)
340 |> put_view(ListView)
341 |> render("index.json", lists: lists)
344 @doc "POST /api/v1/accounts/:id/follow"
345 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
346 {:error, "Can not follow yourself"}
349 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
350 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
351 render(conn, "relationship.json", user: follower, target: followed)
353 {:error, message} -> json_response(conn, :forbidden, %{error: message})
357 @doc "POST /api/v1/accounts/:id/unfollow"
358 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
359 {:error, "Can not unfollow yourself"}
362 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
363 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
364 render(conn, "relationship.json", user: follower, target: followed)
368 @doc "POST /api/v1/accounts/:id/mute"
369 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
370 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
371 render(conn, "relationship.json", user: muter, target: muted)
373 {:error, message} -> json_response(conn, :forbidden, %{error: message})
377 @doc "POST /api/v1/accounts/:id/unmute"
378 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
379 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
380 render(conn, "relationship.json", user: muter, target: muted)
382 {:error, message} -> json_response(conn, :forbidden, %{error: message})
386 @doc "POST /api/v1/accounts/:id/block"
387 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
388 with {:ok, _user_block} <- User.block(blocker, blocked),
389 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
390 render(conn, "relationship.json", user: blocker, target: blocked)
392 {:error, message} -> json_response(conn, :forbidden, %{error: message})
396 @doc "POST /api/v1/accounts/:id/unblock"
397 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
398 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
399 render(conn, "relationship.json", user: blocker, target: blocked)
401 {:error, message} -> json_response(conn, :forbidden, %{error: message})
405 @doc "POST /api/v1/follows"
406 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
407 case User.get_cached_by_nickname(uri) do
410 |> assign(:account, user)
418 @doc "GET /api/v1/mutes"
419 def mutes(%{assigns: %{user: user}} = conn, _) do
420 users = User.muted_users(user, _restrict_deactivated = true)
421 render(conn, "index.json", users: users, for: user, as: :user)
424 @doc "GET /api/v1/blocks"
425 def blocks(%{assigns: %{user: user}} = conn, _) do
426 users = User.blocked_users(user, _restrict_deactivated = true)
427 render(conn, "index.json", users: users, for: user, as: :user)
430 @doc "GET /api/v1/endorsements"
431 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
433 @doc "GET /api/v1/identity_proofs"
434 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)