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.CommonAPI
24 alias Pleroma.Web.MastodonAPI.ListView
25 alias Pleroma.Web.MastodonAPI.MastodonAPI
26 alias Pleroma.Web.MastodonAPI.MastodonAPIController
27 alias Pleroma.Web.MastodonAPI.StatusView
28 alias Pleroma.Web.OAuth.Token
29 alias Pleroma.Web.TwitterAPI.TwitterAPI
31 plug(Pleroma.Web.ApiSpec.CastAndValidate)
33 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
35 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
39 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
40 when action in [:show, :followers, :following]
45 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
46 when action == :statuses
51 %{scopes: ["read:accounts"]}
52 when action in [:verify_credentials, :endorsements, :identity_proofs]
55 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
57 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
61 %{scopes: ["follow", "read:blocks"]} when action == :blocks
66 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
69 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
73 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
76 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
78 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
80 @relationship_actions [:follow, :unfollow]
81 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
85 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
88 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
89 plug(RateLimiter, [name: :app_account_creation] when action == :create)
90 plug(:assign_account_by_id when action in @needs_account)
92 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
94 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
96 @doc "POST /api/v1/accounts"
97 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
98 with :ok <- validate_email_param(params),
99 :ok <- TwitterAPI.validate_captcha(app, params),
100 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
101 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
103 token_type: "Bearer",
104 access_token: token.token,
106 created_at: Token.Utils.format_created_at(token)
109 {:error, error} -> json_response(conn, :bad_request, %{error: error})
113 def create(%{assigns: %{app: _app}} = conn, _) do
114 render_error(conn, :bad_request, "Missing parameters")
117 def create(conn, _) do
118 render_error(conn, :forbidden, "Invalid credentials")
121 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
123 defp validate_email_param(_) do
124 case Pleroma.Config.get([:instance, :account_activation_required]) do
125 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
130 @doc "GET /api/v1/accounts/verify_credentials"
131 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
132 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
134 render(conn, "show.json",
137 with_pleroma_settings: true,
138 with_chat_token: chat_token
142 @doc "PATCH /api/v1/accounts/update_credentials"
143 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
146 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
153 :hide_followers_count,
159 :skip_thread_containment,
160 :allow_following_move,
163 |> Enum.reduce(%{}, fn key, acc ->
164 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
166 |> Maps.put_if_present(:name, params[:display_name])
167 |> Maps.put_if_present(:bio, params[:note])
168 |> Maps.put_if_present(:avatar, params[:avatar])
169 |> Maps.put_if_present(:banner, params[:header])
170 |> Maps.put_if_present(:background, params[:pleroma_background_image])
171 |> Maps.put_if_present(
173 params[:fields_attributes],
174 &{:ok, normalize_fields_attributes(&1)}
176 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
177 |> Maps.put_if_present(:default_scope, params[:default_scope])
178 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
179 |> Maps.put_if_present(:actor_type, params[:actor_type])
181 changeset = User.update_changeset(user, user_params)
183 with {:ok, user} <- User.update_and_set_cache(changeset) do
185 |> build_update_activity_params()
186 |> ActivityPub.update()
188 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
190 _e -> render_error(conn, :forbidden, "Invalid request")
194 # Hotfix, handling will be redone with the pipeline
195 defp build_update_activity_params(user) do
197 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
198 |> Map.delete("@context")
202 to: [user.follower_address],
209 defp normalize_fields_attributes(fields) do
210 if Enum.all?(fields, &is_tuple/1) do
211 Enum.map(fields, fn {_, v} -> v end)
214 %{} = field -> %{"name" => field.name, "value" => field.value}
220 @doc "GET /api/v1/accounts/relationships"
221 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
222 targets = User.get_all_by_ids(List.wrap(id))
224 render(conn, "relationships.json", user: user, targets: targets)
227 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
228 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
230 @doc "GET /api/v1/accounts/:id"
231 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
232 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
233 true <- User.visible_for?(user, for_user) do
234 render(conn, "show.json", user: user, for: for_user)
236 _e -> render_error(conn, :not_found, "Can't find user")
240 @doc "GET /api/v1/accounts/:id/statuses"
241 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
242 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
243 true <- User.visible_for?(user, reading_user) do
246 |> Map.delete(:tagged)
247 |> Enum.filter(&(not is_nil(&1)))
248 |> Map.new(fn {key, value} -> {to_string(key), value} end)
249 |> Map.put("tag", params[:tagged])
251 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
254 |> add_link_headers(activities)
255 |> put_view(StatusView)
256 |> render("index.json",
257 activities: activities,
262 _e -> render_error(conn, :not_found, "Can't find user")
266 @doc "GET /api/v1/accounts/:id/followers"
267 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
270 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
275 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
276 user.hide_followers -> []
277 true -> MastodonAPI.get_followers(user, params)
281 |> add_link_headers(followers)
282 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
283 |> render("index.json",
287 embed_relationships: embed_relationships?(params)
291 @doc "GET /api/v1/accounts/:id/following"
292 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
295 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
300 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
301 user.hide_follows -> []
302 true -> MastodonAPI.get_friends(user, params)
306 |> add_link_headers(followers)
307 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
308 |> render("index.json",
312 embed_relationships: embed_relationships?(params)
316 @doc "GET /api/v1/accounts/:id/lists"
317 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
318 lists = Pleroma.List.get_lists_account_belongs(user, account)
321 |> put_view(ListView)
322 |> render("index.json", lists: lists)
325 @doc "POST /api/v1/accounts/:id/follow"
326 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
327 {:error, "Can not follow yourself"}
330 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
331 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
332 render(conn, "relationship.json", user: follower, target: followed)
334 {:error, message} -> json_response(conn, :forbidden, %{error: message})
338 @doc "POST /api/v1/accounts/:id/unfollow"
339 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
340 {:error, "Can not unfollow yourself"}
343 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
344 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
345 render(conn, "relationship.json", user: follower, target: followed)
349 @doc "POST /api/v1/accounts/:id/mute"
350 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
351 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
352 render(conn, "relationship.json", user: muter, target: muted)
354 {:error, message} -> json_response(conn, :forbidden, %{error: message})
358 @doc "POST /api/v1/accounts/:id/unmute"
359 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
360 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
361 render(conn, "relationship.json", user: muter, target: muted)
363 {:error, message} -> json_response(conn, :forbidden, %{error: message})
367 @doc "POST /api/v1/accounts/:id/block"
368 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
369 with {:ok, _user_block} <- User.block(blocker, blocked),
370 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
371 render(conn, "relationship.json", user: blocker, target: blocked)
373 {:error, message} -> json_response(conn, :forbidden, %{error: message})
377 @doc "POST /api/v1/accounts/:id/unblock"
378 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
379 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
380 render(conn, "relationship.json", user: blocker, target: blocked)
382 {:error, message} -> json_response(conn, :forbidden, %{error: message})
386 @doc "POST /api/v1/follows"
387 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
388 case User.get_cached_by_nickname(uri) do
391 |> assign(:account, user)
399 @doc "GET /api/v1/mutes"
400 def mutes(%{assigns: %{user: user}} = conn, _) do
401 users = User.muted_users(user, _restrict_deactivated = true)
402 render(conn, "index.json", users: users, for: user, as: :user)
405 @doc "GET /api/v1/blocks"
406 def blocks(%{assigns: %{user: user}} = conn, _) do
407 users = User.blocked_users(user, _restrict_deactivated = true)
408 render(conn, "index.json", users: users, for: user, as: :user)
411 @doc "GET /api/v1/endorsements"
412 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
414 @doc "GET /api/v1/identity_proofs"
415 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)