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,
167 :accepts_chat_messages
169 |> Enum.reduce(%{}, fn key, acc ->
170 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
172 |> Maps.put_if_present(:name, params[:display_name])
173 |> Maps.put_if_present(:bio, params[:note])
174 |> Maps.put_if_present(:raw_bio, params[:note])
175 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
176 |> Maps.put_if_present(:banner, params[:header], user_image_value)
177 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
178 |> Maps.put_if_present(
180 params[:fields_attributes],
181 &{:ok, normalize_fields_attributes(&1)}
183 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
184 |> Maps.put_if_present(:default_scope, params[:default_scope])
185 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
186 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
187 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
189 |> Maps.put_if_present(:actor_type, params[:actor_type])
193 # We want to update the user through the pipeline, but the ActivityPub
194 # update information is not quite enough for this, because this also
195 # contains local settings that don't federate and don't even appear
196 # in the Update activity.
198 # So we first build the normal local changeset, then apply it to the
199 # user data, but don't persist it. With this, we generate the object
200 # data for our update activity. We feed this and the changeset as meta
201 # inforation into the pipeline, where they will be properly updated and
203 with changeset <- User.update_changeset(user, user_params),
204 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
206 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
207 |> Map.delete("@context"),
208 {:ok, update_data, []} <- Builder.update(user, updated_object),
210 Pipeline.common_pipeline(update_data,
212 user_update_changeset: changeset
214 render(conn, "show.json",
215 user: unpersisted_user,
216 for: unpersisted_user,
217 with_pleroma_settings: true
220 _e -> render_error(conn, :forbidden, "Invalid request")
224 defp normalize_fields_attributes(fields) do
225 if Enum.all?(fields, &is_tuple/1) do
226 Enum.map(fields, fn {_, v} -> v end)
229 %{} = field -> %{"name" => field.name, "value" => field.value}
235 @doc "GET /api/v1/accounts/relationships"
236 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
237 targets = User.get_all_by_ids(List.wrap(id))
239 render(conn, "relationships.json", user: user, targets: targets)
242 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
243 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
245 @doc "GET /api/v1/accounts/:id"
246 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
247 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
248 :visible <- User.visible_for(user, for_user) do
249 render(conn, "show.json", user: user, for: for_user)
251 error -> user_visibility_error(conn, error)
255 @doc "GET /api/v1/accounts/:id/statuses"
256 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
257 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
258 :visible <- User.visible_for(user, reading_user) do
261 |> Map.delete(:tagged)
262 |> Map.put(:tag, params[:tagged])
264 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
267 |> add_link_headers(activities)
268 |> put_view(StatusView)
269 |> render("index.json",
270 activities: activities,
275 error -> user_visibility_error(conn, error)
279 defp user_visibility_error(conn, error) do
281 :restrict_unauthenticated ->
282 render_error(conn, :unauthorized, "This API requires an authenticated user")
285 render_error(conn, :not_found, "Can't find user")
289 @doc "GET /api/v1/accounts/:id/followers"
290 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
293 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
298 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
299 user.hide_followers -> []
300 true -> MastodonAPI.get_followers(user, params)
304 |> add_link_headers(followers)
305 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
306 |> render("index.json",
310 embed_relationships: embed_relationships?(params)
314 @doc "GET /api/v1/accounts/:id/following"
315 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
318 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
323 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
324 user.hide_follows -> []
325 true -> MastodonAPI.get_friends(user, params)
329 |> add_link_headers(followers)
330 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
331 |> render("index.json",
335 embed_relationships: embed_relationships?(params)
339 @doc "GET /api/v1/accounts/:id/lists"
340 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
341 lists = Pleroma.List.get_lists_account_belongs(user, account)
344 |> put_view(ListView)
345 |> render("index.json", lists: lists)
348 @doc "POST /api/v1/accounts/:id/follow"
349 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
350 {:error, "Can not follow yourself"}
353 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
354 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
355 render(conn, "relationship.json", user: follower, target: followed)
357 {:error, message} -> json_response(conn, :forbidden, %{error: message})
361 @doc "POST /api/v1/accounts/:id/unfollow"
362 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
363 {:error, "Can not unfollow yourself"}
366 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
367 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
368 render(conn, "relationship.json", user: follower, target: followed)
372 @doc "POST /api/v1/accounts/:id/mute"
373 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
374 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
375 render(conn, "relationship.json", user: muter, target: muted)
377 {:error, message} -> json_response(conn, :forbidden, %{error: message})
381 @doc "POST /api/v1/accounts/:id/unmute"
382 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
383 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
384 render(conn, "relationship.json", user: muter, target: muted)
386 {:error, message} -> json_response(conn, :forbidden, %{error: message})
390 @doc "POST /api/v1/accounts/:id/block"
391 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
392 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
393 render(conn, "relationship.json", user: blocker, target: blocked)
395 {:error, message} -> json_response(conn, :forbidden, %{error: message})
399 @doc "POST /api/v1/accounts/:id/unblock"
400 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
401 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
402 render(conn, "relationship.json", user: blocker, target: blocked)
404 {:error, message} -> json_response(conn, :forbidden, %{error: message})
408 @doc "POST /api/v1/follows"
409 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
410 case User.get_cached_by_nickname(uri) do
413 |> assign(:account, user)
421 @doc "GET /api/v1/mutes"
422 def mutes(%{assigns: %{user: user}} = conn, _) do
423 users = User.muted_users(user, _restrict_deactivated = true)
424 render(conn, "index.json", users: users, for: user, as: :user)
427 @doc "GET /api/v1/blocks"
428 def blocks(%{assigns: %{user: user}} = conn, _) do
429 users = User.blocked_users(user, _restrict_deactivated = true)
430 render(conn, "index.json", users: users, for: user, as: :user)
433 @doc "GET /api/v1/endorsements"
434 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
436 @doc "GET /api/v1/identity_proofs"
437 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)