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)
151 :hide_followers_count,
157 :skip_thread_containment,
158 :allow_following_move,
161 |> Enum.reduce(%{}, fn key, acc ->
162 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
164 |> Maps.put_if_present(:name, params[:display_name])
165 |> Maps.put_if_present(:bio, params[:note])
166 |> Maps.put_if_present(:raw_bio, params[:note])
167 |> Maps.put_if_present(:avatar, params[:avatar])
168 |> Maps.put_if_present(:banner, params[:header])
169 |> Maps.put_if_present(:background, params[:pleroma_background_image])
170 |> Maps.put_if_present(
172 params[:fields_attributes],
173 &{:ok, normalize_fields_attributes(&1)}
175 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
176 |> Maps.put_if_present(:default_scope, params[:default_scope])
177 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
178 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
179 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
181 |> Maps.put_if_present(:actor_type, params[:actor_type])
185 # We want to update the user through the pipeline, but the ActivityPub
186 # update information is not quite enough for this, because this also
187 # contains local settings that don't federate and don't even appear
188 # in the Update activity.
190 # So we first build the normal local changeset, then apply it to the
191 # user data, but don't persist it. With this, we generate the object
192 # data for our update activity. We feed this and the changeset as meta
193 # inforation into the pipeline, where they will be properly updated and
195 with changeset <- User.update_changeset(user, user_params),
196 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
198 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
199 |> Map.delete("@context"),
200 {:ok, update_data, []} <- Builder.update(user, updated_object),
202 Pipeline.common_pipeline(update_data,
204 user_update_changeset: changeset
206 render(conn, "show.json",
207 user: unpersisted_user,
208 for: unpersisted_user,
209 with_pleroma_settings: true
212 _e -> render_error(conn, :forbidden, "Invalid request")
216 defp normalize_fields_attributes(fields) do
217 if Enum.all?(fields, &is_tuple/1) do
218 Enum.map(fields, fn {_, v} -> v end)
221 %{} = field -> %{"name" => field.name, "value" => field.value}
227 @doc "GET /api/v1/accounts/relationships"
228 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
229 targets = User.get_all_by_ids(List.wrap(id))
231 render(conn, "relationships.json", user: user, targets: targets)
234 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
235 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
237 @doc "GET /api/v1/accounts/:id"
238 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
239 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
240 :visible <- User.visible_for(user, for_user) do
241 render(conn, "show.json", user: user, for: for_user)
243 error -> user_visibility_error(conn, error)
247 @doc "GET /api/v1/accounts/:id/statuses"
248 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
249 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
250 :visible <- User.visible_for(user, reading_user) do
253 |> Map.delete(:tagged)
254 |> Map.put(:tag, params[:tagged])
256 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
259 |> add_link_headers(activities)
260 |> put_view(StatusView)
261 |> render("index.json",
262 activities: activities,
267 error -> user_visibility_error(conn, error)
271 defp user_visibility_error(conn, error) do
273 :restrict_unauthenticated ->
274 render_error(conn, :unauthorized, "This API requires an authenticated user")
277 render_error(conn, :not_found, "Can't find user")
281 @doc "GET /api/v1/accounts/:id/followers"
282 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
285 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
290 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
291 user.hide_followers -> []
292 true -> MastodonAPI.get_followers(user, params)
296 |> add_link_headers(followers)
297 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
298 |> render("index.json",
302 embed_relationships: embed_relationships?(params)
306 @doc "GET /api/v1/accounts/:id/following"
307 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
310 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
315 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
316 user.hide_follows -> []
317 true -> MastodonAPI.get_friends(user, params)
321 |> add_link_headers(followers)
322 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
323 |> render("index.json",
327 embed_relationships: embed_relationships?(params)
331 @doc "GET /api/v1/accounts/:id/lists"
332 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
333 lists = Pleroma.List.get_lists_account_belongs(user, account)
336 |> put_view(ListView)
337 |> render("index.json", lists: lists)
340 @doc "POST /api/v1/accounts/:id/follow"
341 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
342 {:error, "Can not follow yourself"}
345 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
346 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
347 render(conn, "relationship.json", user: follower, target: followed)
349 {:error, message} -> json_response(conn, :forbidden, %{error: message})
353 @doc "POST /api/v1/accounts/:id/unfollow"
354 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
355 {:error, "Can not unfollow yourself"}
358 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
359 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
360 render(conn, "relationship.json", user: follower, target: followed)
364 @doc "POST /api/v1/accounts/:id/mute"
365 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
366 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
367 render(conn, "relationship.json", user: muter, target: muted)
369 {:error, message} -> json_response(conn, :forbidden, %{error: message})
373 @doc "POST /api/v1/accounts/:id/unmute"
374 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
375 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
376 render(conn, "relationship.json", user: muter, target: muted)
378 {:error, message} -> json_response(conn, :forbidden, %{error: message})
382 @doc "POST /api/v1/accounts/:id/block"
383 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
384 with {:ok, _user_block} <- User.block(blocker, blocked),
385 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
386 render(conn, "relationship.json", user: blocker, target: blocked)
388 {:error, message} -> json_response(conn, :forbidden, %{error: message})
392 @doc "POST /api/v1/accounts/:id/unblock"
393 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
394 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
395 render(conn, "relationship.json", user: blocker, target: blocked)
397 {:error, message} -> json_response(conn, :forbidden, %{error: message})
401 @doc "POST /api/v1/follows"
402 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
403 case User.get_cached_by_nickname(uri) do
406 |> assign(:account, user)
414 @doc "GET /api/v1/mutes"
415 def mutes(%{assigns: %{user: user}} = conn, _) do
416 users = User.muted_users(user, _restrict_deactivated = true)
417 render(conn, "index.json", users: users, for: user, as: :user)
420 @doc "GET /api/v1/blocks"
421 def blocks(%{assigns: %{user: user}} = conn, _) do
422 users = User.blocked_users(user, _restrict_deactivated = true)
423 render(conn, "index.json", users: users, for: user, as: :user)
426 @doc "GET /api/v1/endorsements"
427 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
429 @doc "GET /api/v1/identity_proofs"
430 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)