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,
14 skip_relationships?: 1
17 alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
18 alias Pleroma.Plugs.OAuthScopesPlug
19 alias Pleroma.Plugs.RateLimiter
21 alias Pleroma.Web.ActivityPub.ActivityPub
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonAPI
25 alias Pleroma.Web.MastodonAPI.MastodonAPIController
26 alias Pleroma.Web.MastodonAPI.StatusView
27 alias Pleroma.Web.OAuth.Token
28 alias Pleroma.Web.TwitterAPI.TwitterAPI
30 plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
32 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
34 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
38 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
39 when action in [:show, :followers, :following]
44 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
45 when action == :statuses
50 %{scopes: ["read:accounts"]}
51 when action in [:verify_credentials, :endorsements, :identity_proofs]
54 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
56 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
60 %{scopes: ["follow", "read:blocks"]} when action == :blocks
65 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
68 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
72 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
75 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
77 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
79 @relationship_actions [:follow, :unfollow]
80 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
84 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
87 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
88 plug(RateLimiter, [name: :app_account_creation] when action == :create)
89 plug(:assign_account_by_id when action in @needs_account)
91 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
93 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
95 @doc "POST /api/v1/accounts"
96 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
104 :captcha_answer_data,
109 |> Map.put(:nickname, params.username)
110 |> Map.put(:fullname, Map.get(params, :fullname, params.username))
111 |> Map.put(:confirm, params.password)
112 |> Map.put(:trusted_app, app.trusted)
114 with :ok <- validate_email_param(params),
115 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
116 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
118 token_type: "Bearer",
119 access_token: token.token,
121 created_at: Token.Utils.format_created_at(token)
124 {:error, errors} -> json_response(conn, :bad_request, errors)
128 def create(%{assigns: %{app: _app}} = conn, _) do
129 render_error(conn, :bad_request, "Missing parameters")
132 def create(conn, _) do
133 render_error(conn, :forbidden, "Invalid credentials")
136 defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
138 defp validate_email_param(_) do
139 case Pleroma.Config.get([:instance, :account_activation_required]) do
140 true -> {:error, %{"error" => "Missing parameters"}}
145 @doc "GET /api/v1/accounts/verify_credentials"
146 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
147 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
149 render(conn, "show.json",
152 with_pleroma_settings: true,
153 with_chat_token: chat_token
157 @doc "PATCH /api/v1/accounts/update_credentials"
158 def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
163 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
170 :hide_followers_count,
176 :skip_thread_containment,
177 :allow_following_move,
180 |> Enum.reduce(%{}, fn key, acc ->
181 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
183 |> add_if_present(params, :display_name, :name)
184 |> add_if_present(params, :note, :bio)
185 |> add_if_present(params, :avatar, :avatar)
186 |> add_if_present(params, :header, :banner)
187 |> add_if_present(params, :pleroma_background_image, :background)
192 &{:ok, normalize_fields_attributes(&1)}
194 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
195 |> add_if_present(params, :default_scope, :default_scope)
196 |> add_if_present(params, :actor_type, :actor_type)
198 changeset = User.update_changeset(user, user_params)
200 with {:ok, user} <- User.update_and_set_cache(changeset) do
201 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
203 _e -> render_error(conn, :forbidden, "Invalid request")
207 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
208 with true <- Map.has_key?(params, params_field),
209 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
210 Map.put(map, map_field, new_value)
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 true <- User.visible_for?(user, for_user) do
241 render(conn, "show.json", user: user, for: for_user)
243 _e -> render_error(conn, :not_found, "Can't find user")
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 true <- User.visible_for?(user, reading_user) do
253 |> Map.delete(:tagged)
254 |> Enum.filter(&(not is_nil(&1)))
255 |> Map.new(fn {key, value} -> {to_string(key), value} end)
256 |> Map.put("tag", params[:tagged])
258 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
261 |> add_link_headers(activities)
262 |> put_view(StatusView)
263 |> render("index.json",
264 activities: activities,
267 skip_relationships: skip_relationships?(params)
270 _e -> render_error(conn, :not_found, "Can't find user")
274 @doc "GET /api/v1/accounts/:id/followers"
275 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
278 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
283 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
284 user.hide_followers -> []
285 true -> MastodonAPI.get_followers(user, params)
289 |> add_link_headers(followers)
290 |> render("index.json", for: for_user, users: followers, as: :user)
293 @doc "GET /api/v1/accounts/:id/following"
294 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
297 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
302 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
303 user.hide_follows -> []
304 true -> MastodonAPI.get_friends(user, params)
308 |> add_link_headers(followers)
309 |> render("index.json", for: for_user, users: followers, as: :user)
312 @doc "GET /api/v1/accounts/:id/lists"
313 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
314 lists = Pleroma.List.get_lists_account_belongs(user, account)
317 |> put_view(ListView)
318 |> render("index.json", lists: lists)
321 @doc "POST /api/v1/accounts/:id/follow"
322 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
323 {:error, "Can not follow yourself"}
326 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
327 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
328 render(conn, "relationship.json", user: follower, target: followed)
330 {:error, message} -> json_response(conn, :forbidden, %{error: message})
334 @doc "POST /api/v1/accounts/:id/unfollow"
335 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
336 {:error, "Can not unfollow yourself"}
339 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
340 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
341 render(conn, "relationship.json", user: follower, target: followed)
345 @doc "POST /api/v1/accounts/:id/mute"
346 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
347 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
348 render(conn, "relationship.json", user: muter, target: muted)
350 {:error, message} -> json_response(conn, :forbidden, %{error: message})
354 @doc "POST /api/v1/accounts/:id/unmute"
355 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
356 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
357 render(conn, "relationship.json", user: muter, target: muted)
359 {:error, message} -> json_response(conn, :forbidden, %{error: message})
363 @doc "POST /api/v1/accounts/:id/block"
364 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
365 with {:ok, _user_block} <- User.block(blocker, blocked),
366 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
367 render(conn, "relationship.json", user: blocker, target: blocked)
369 {:error, message} -> json_response(conn, :forbidden, %{error: message})
373 @doc "POST /api/v1/accounts/:id/unblock"
374 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
375 with {:ok, _user_block} <- User.unblock(blocker, blocked),
376 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
377 render(conn, "relationship.json", user: blocker, target: blocked)
379 {:error, message} -> json_response(conn, :forbidden, %{error: message})
383 @doc "POST /api/v1/follows"
384 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
385 case User.get_cached_by_nickname(uri) do
388 |> assign(:account, user)
396 @doc "GET /api/v1/mutes"
397 def mutes(%{assigns: %{user: user}} = conn, _) do
398 users = User.muted_users(user, _restrict_deactivated = true)
399 render(conn, "index.json", users: users, for: user, as: :user)
402 @doc "GET /api/v1/blocks"
403 def blocks(%{assigns: %{user: user}} = conn, _) do
404 users = User.blocked_users(user, _restrict_deactivated = true)
405 render(conn, "index.json", users: users, for: user, as: :user)
408 @doc "GET /api/v1/endorsements"
409 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
411 @doc "GET /api/v1/identity_proofs"
412 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)