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,
164 :accepts_chat_messages
166 |> Enum.reduce(%{}, fn key, acc ->
167 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
169 |> Maps.put_if_present(:name, params[:display_name])
170 |> Maps.put_if_present(:bio, params[:note])
171 |> Maps.put_if_present(:raw_bio, params[:note])
172 |> Maps.put_if_present(:avatar, params[:avatar])
173 |> Maps.put_if_present(:banner, params[:header])
174 |> Maps.put_if_present(:background, params[:pleroma_background_image])
175 |> Maps.put_if_present(
177 params[:fields_attributes],
178 &{:ok, normalize_fields_attributes(&1)}
180 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
181 |> Maps.put_if_present(:default_scope, params[:default_scope])
182 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
183 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
184 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
186 |> Maps.put_if_present(:actor_type, params[:actor_type])
190 # We want to update the user through the pipeline, but the ActivityPub
191 # update information is not quite enough for this, because this also
192 # contains local settings that don't federate and don't even appear
193 # in the Update activity.
195 # So we first build the normal local changeset, then apply it to the
196 # user data, but don't persist it. With this, we generate the object
197 # data for our update activity. We feed this and the changeset as meta
198 # inforation into the pipeline, where they will be properly updated and
200 with changeset <- User.update_changeset(user, user_params),
201 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
203 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
204 |> Map.delete("@context"),
205 {:ok, update_data, []} <- Builder.update(user, updated_object),
207 Pipeline.common_pipeline(update_data,
209 user_update_changeset: changeset
211 render(conn, "show.json",
212 user: unpersisted_user,
213 for: unpersisted_user,
214 with_pleroma_settings: true
217 _e -> render_error(conn, :forbidden, "Invalid request")
221 defp normalize_fields_attributes(fields) do
222 if Enum.all?(fields, &is_tuple/1) do
223 Enum.map(fields, fn {_, v} -> v end)
226 %{} = field -> %{"name" => field.name, "value" => field.value}
232 @doc "GET /api/v1/accounts/relationships"
233 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
234 targets = User.get_all_by_ids(List.wrap(id))
236 render(conn, "relationships.json", user: user, targets: targets)
239 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
240 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
242 @doc "GET /api/v1/accounts/:id"
243 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
244 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
245 :visible <- User.visible_for(user, for_user) do
246 render(conn, "show.json", user: user, for: for_user)
248 error -> user_visibility_error(conn, error)
252 @doc "GET /api/v1/accounts/:id/statuses"
253 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
254 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
255 :visible <- User.visible_for(user, reading_user) do
258 |> Map.delete(:tagged)
259 |> Map.put(:tag, params[:tagged])
261 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
264 |> add_link_headers(activities)
265 |> put_view(StatusView)
266 |> render("index.json",
267 activities: activities,
272 error -> user_visibility_error(conn, error)
276 defp user_visibility_error(conn, error) do
278 :restrict_unauthenticated ->
279 render_error(conn, :unauthorized, "This API requires an authenticated user")
282 render_error(conn, :not_found, "Can't find user")
286 @doc "GET /api/v1/accounts/:id/followers"
287 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
290 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
295 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
296 user.hide_followers -> []
297 true -> MastodonAPI.get_followers(user, params)
301 |> add_link_headers(followers)
302 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
303 |> render("index.json",
307 embed_relationships: embed_relationships?(params)
311 @doc "GET /api/v1/accounts/:id/following"
312 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
315 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
320 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
321 user.hide_follows -> []
322 true -> MastodonAPI.get_friends(user, params)
326 |> add_link_headers(followers)
327 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
328 |> render("index.json",
332 embed_relationships: embed_relationships?(params)
336 @doc "GET /api/v1/accounts/:id/lists"
337 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
338 lists = Pleroma.List.get_lists_account_belongs(user, account)
341 |> put_view(ListView)
342 |> render("index.json", lists: lists)
345 @doc "POST /api/v1/accounts/:id/follow"
346 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
347 {:error, "Can not follow yourself"}
350 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
351 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
352 render(conn, "relationship.json", user: follower, target: followed)
354 {:error, message} -> json_response(conn, :forbidden, %{error: message})
358 @doc "POST /api/v1/accounts/:id/unfollow"
359 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
360 {:error, "Can not unfollow yourself"}
363 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
364 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
365 render(conn, "relationship.json", user: follower, target: followed)
369 @doc "POST /api/v1/accounts/:id/mute"
370 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
371 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
372 render(conn, "relationship.json", user: muter, target: muted)
374 {:error, message} -> json_response(conn, :forbidden, %{error: message})
378 @doc "POST /api/v1/accounts/:id/unmute"
379 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
380 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
381 render(conn, "relationship.json", user: muter, target: muted)
383 {:error, message} -> json_response(conn, :forbidden, %{error: message})
387 @doc "POST /api/v1/accounts/:id/block"
388 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
389 with {:ok, _activity} <- CommonAPI.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)