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 |> Enum.filter(&(not is_nil(&1)))
249 |> Map.new(fn {key, value} -> {to_string(key), value} end)
250 |> Map.put("tag", params[:tagged])
252 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
255 |> add_link_headers(activities)
256 |> put_view(StatusView)
257 |> render("index.json",
258 activities: activities,
263 _e -> render_error(conn, :not_found, "Can't find user")
267 @doc "GET /api/v1/accounts/:id/followers"
268 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
271 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
276 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
277 user.hide_followers -> []
278 true -> MastodonAPI.get_followers(user, params)
282 |> add_link_headers(followers)
283 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
284 |> render("index.json",
288 embed_relationships: embed_relationships?(params)
292 @doc "GET /api/v1/accounts/:id/following"
293 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
296 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
301 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
302 user.hide_follows -> []
303 true -> MastodonAPI.get_friends(user, params)
307 |> add_link_headers(followers)
308 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
309 |> render("index.json",
313 embed_relationships: embed_relationships?(params)
317 @doc "GET /api/v1/accounts/:id/lists"
318 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
319 lists = Pleroma.List.get_lists_account_belongs(user, account)
322 |> put_view(ListView)
323 |> render("index.json", lists: lists)
326 @doc "POST /api/v1/accounts/:id/follow"
327 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
328 {:error, "Can not follow yourself"}
331 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
332 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
333 render(conn, "relationship.json", user: follower, target: followed)
335 {:error, message} -> json_response(conn, :forbidden, %{error: message})
339 @doc "POST /api/v1/accounts/:id/unfollow"
340 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
341 {:error, "Can not unfollow yourself"}
344 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
345 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
346 render(conn, "relationship.json", user: follower, target: followed)
350 @doc "POST /api/v1/accounts/:id/mute"
351 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
352 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
353 render(conn, "relationship.json", user: muter, target: muted)
355 {:error, message} -> json_response(conn, :forbidden, %{error: message})
359 @doc "POST /api/v1/accounts/:id/unmute"
360 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
361 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
362 render(conn, "relationship.json", user: muter, target: muted)
364 {:error, message} -> json_response(conn, :forbidden, %{error: message})
368 @doc "POST /api/v1/accounts/:id/block"
369 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
370 with {:ok, _user_block} <- User.block(blocker, blocked),
371 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
372 render(conn, "relationship.json", user: blocker, target: blocked)
374 {:error, message} -> json_response(conn, :forbidden, %{error: message})
378 @doc "POST /api/v1/accounts/:id/unblock"
379 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
380 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
381 render(conn, "relationship.json", user: blocker, target: blocked)
383 {:error, message} -> json_response(conn, :forbidden, %{error: message})
387 @doc "POST /api/v1/follows"
388 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
389 case User.get_cached_by_nickname(uri) do
392 |> assign(:account, user)
400 @doc "GET /api/v1/mutes"
401 def mutes(%{assigns: %{user: user}} = conn, _) do
402 users = User.muted_users(user, _restrict_deactivated = true)
403 render(conn, "index.json", users: users, for: user, as: :user)
406 @doc "GET /api/v1/blocks"
407 def blocks(%{assigns: %{user: user}} = conn, _) do
408 users = User.blocked_users(user, _restrict_deactivated = true)
409 render(conn, "index.json", users: users, for: user, as: :user)
412 @doc "GET /api/v1/endorsements"
413 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
415 @doc "GET /api/v1/identity_proofs"
416 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)