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(:raw_bio, params[:note])
169 |> Maps.put_if_present(:avatar, params[:avatar])
170 |> Maps.put_if_present(:banner, params[:header])
171 |> Maps.put_if_present(:background, params[:pleroma_background_image])
172 |> Maps.put_if_present(
174 params[:fields_attributes],
175 &{:ok, normalize_fields_attributes(&1)}
177 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
178 |> Maps.put_if_present(:default_scope, params[:default_scope])
179 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
180 |> Maps.put_if_present(:actor_type, params[:actor_type])
182 changeset = User.update_changeset(user, user_params)
184 with {:ok, user} <- User.update_and_set_cache(changeset) do
186 |> build_update_activity_params()
187 |> ActivityPub.update()
189 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
191 _e -> render_error(conn, :forbidden, "Invalid request")
195 # Hotfix, handling will be redone with the pipeline
196 defp build_update_activity_params(user) do
198 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
199 |> Map.delete("@context")
203 to: [user.follower_address],
210 defp normalize_fields_attributes(fields) do
211 if Enum.all?(fields, &is_tuple/1) do
212 Enum.map(fields, fn {_, v} -> v end)
215 %{} = field -> %{"name" => field.name, "value" => field.value}
221 @doc "GET /api/v1/accounts/relationships"
222 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
223 targets = User.get_all_by_ids(List.wrap(id))
225 render(conn, "relationships.json", user: user, targets: targets)
228 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
229 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
231 @doc "GET /api/v1/accounts/:id"
232 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
233 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
234 true <- User.visible_for?(user, for_user) do
235 render(conn, "show.json", user: user, for: for_user)
237 _e -> render_error(conn, :not_found, "Can't find user")
241 @doc "GET /api/v1/accounts/:id/statuses"
242 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
243 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
244 true <- User.visible_for?(user, reading_user) do
247 |> Map.delete(:tagged)
248 |> Map.put(:tag, params[:tagged])
250 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
253 |> add_link_headers(activities)
254 |> put_view(StatusView)
255 |> render("index.json",
256 activities: activities,
261 _e -> render_error(conn, :not_found, "Can't find user")
265 @doc "GET /api/v1/accounts/:id/followers"
266 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
269 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
274 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
275 user.hide_followers -> []
276 true -> MastodonAPI.get_followers(user, params)
280 |> add_link_headers(followers)
281 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
282 |> render("index.json",
286 embed_relationships: embed_relationships?(params)
290 @doc "GET /api/v1/accounts/:id/following"
291 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
294 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
299 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
300 user.hide_follows -> []
301 true -> MastodonAPI.get_friends(user, params)
305 |> add_link_headers(followers)
306 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
307 |> render("index.json",
311 embed_relationships: embed_relationships?(params)
315 @doc "GET /api/v1/accounts/:id/lists"
316 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
317 lists = Pleroma.List.get_lists_account_belongs(user, account)
320 |> put_view(ListView)
321 |> render("index.json", lists: lists)
324 @doc "POST /api/v1/accounts/:id/follow"
325 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
326 {:error, "Can not follow yourself"}
329 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
330 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
331 render(conn, "relationship.json", user: follower, target: followed)
333 {:error, message} -> json_response(conn, :forbidden, %{error: message})
337 @doc "POST /api/v1/accounts/:id/unfollow"
338 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
339 {:error, "Can not unfollow yourself"}
342 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
343 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
344 render(conn, "relationship.json", user: follower, target: followed)
348 @doc "POST /api/v1/accounts/:id/mute"
349 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
350 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
351 render(conn, "relationship.json", user: muter, target: muted)
353 {:error, message} -> json_response(conn, :forbidden, %{error: message})
357 @doc "POST /api/v1/accounts/:id/unmute"
358 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
359 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
360 render(conn, "relationship.json", user: muter, target: muted)
362 {:error, message} -> json_response(conn, :forbidden, %{error: message})
366 @doc "POST /api/v1/accounts/:id/block"
367 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
368 with {:ok, _user_block} <- User.block(blocker, blocked),
369 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
370 render(conn, "relationship.json", user: blocker, target: blocked)
372 {:error, message} -> json_response(conn, :forbidden, %{error: message})
376 @doc "POST /api/v1/accounts/:id/unblock"
377 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
378 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
379 render(conn, "relationship.json", user: blocker, target: blocked)
381 {:error, message} -> json_response(conn, :forbidden, %{error: message})
385 @doc "POST /api/v1/follows"
386 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
387 case User.get_cached_by_nickname(uri) do
390 |> assign(:account, user)
398 @doc "GET /api/v1/mutes"
399 def mutes(%{assigns: %{user: user}} = conn, _) do
400 users = User.muted_users(user, _restrict_deactivated = true)
401 render(conn, "index.json", users: users, for: user, as: :user)
404 @doc "GET /api/v1/blocks"
405 def blocks(%{assigns: %{user: user}} = conn, _) do
406 users = User.blocked_users(user, _restrict_deactivated = true)
407 render(conn, "index.json", users: users, for: user, as: :user)
410 @doc "GET /api/v1/endorsements"
411 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
413 @doc "GET /api/v1/identity_proofs"
414 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)