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.OAuthScopesPlug
18 alias Pleroma.Plugs.RateLimiter
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.MastodonAPI.ListView
23 alias Pleroma.Web.MastodonAPI.MastodonAPI
24 alias Pleroma.Web.MastodonAPI.MastodonAPIController
25 alias Pleroma.Web.MastodonAPI.StatusView
26 alias Pleroma.Web.OAuth.Token
27 alias Pleroma.Web.TwitterAPI.TwitterAPI
29 plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs)
33 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
39 %{scopes: ["read:accounts"]}
40 when action in [:endorsements, :verify_credentials, :followers, :following]
43 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
45 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
49 %{scopes: ["follow", "read:blocks"]} when action == :blocks
54 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
57 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
59 # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
62 %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
65 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
67 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
70 Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
71 when action not in [:create, :show, :statuses]
74 @relationship_actions [:follow, :unfollow]
75 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
79 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
82 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
83 plug(RateLimiter, [name: :app_account_creation] when action == :create)
84 plug(:assign_account_by_id when action in @needs_account)
86 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
88 @doc "POST /api/v1/accounts"
90 %{assigns: %{app: app}} = conn,
91 %{"username" => nickname, "password" => _, "agreement" => true} = params
99 "captcha_answer_data",
103 |> Map.put("nickname", nickname)
104 |> Map.put("fullname", params["fullname"] || nickname)
105 |> Map.put("bio", params["bio"] || "")
106 |> Map.put("confirm", params["password"])
107 |> Map.put("trusted_app", app.trusted)
109 with :ok <- validate_email_param(params),
110 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
111 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
113 token_type: "Bearer",
114 access_token: token.token,
116 created_at: Token.Utils.format_created_at(token)
119 {:error, errors} -> json_response(conn, :bad_request, errors)
123 def create(%{assigns: %{app: _app}} = conn, _) do
124 render_error(conn, :bad_request, "Missing parameters")
127 def create(conn, _) do
128 render_error(conn, :forbidden, "Invalid credentials")
131 defp validate_email_param(%{"email" => _}), do: :ok
133 defp validate_email_param(_) do
134 case Pleroma.Config.get([:instance, :account_activation_required]) do
135 true -> {:error, %{"error" => "Missing parameters"}}
140 @doc "GET /api/v1/accounts/verify_credentials"
141 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
142 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
144 render(conn, "show.json",
147 with_pleroma_settings: true,
148 with_chat_token: chat_token
152 @doc "PATCH /api/v1/accounts/update_credentials"
153 def update_credentials(%{assigns: %{user: user}} = conn, params) do
158 :hide_followers_count,
164 :skip_thread_containment,
165 :allow_following_move,
168 |> Enum.reduce(%{}, fn key, acc ->
169 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
171 |> add_if_present(params, "display_name", :name)
172 |> add_if_present(params, "note", :bio)
173 |> add_if_present(params, "avatar", :avatar)
174 |> add_if_present(params, "header", :banner)
175 |> add_if_present(params, "pleroma_background_image", :background)
180 &{:ok, normalize_fields_attributes(&1)}
182 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
183 |> add_if_present(params, "default_scope", :default_scope)
184 |> add_if_present(params, "actor_type", :actor_type)
186 changeset = User.update_changeset(user, user_params)
188 with {:ok, user} <- User.update_and_set_cache(changeset) do
189 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
191 _e -> render_error(conn, :forbidden, "Invalid request")
195 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
196 with true <- Map.has_key?(params, params_field),
197 {:ok, new_value} <- value_function.(params[params_field]) do
198 Map.put(map, map_field, new_value)
204 defp normalize_fields_attributes(fields) do
205 if Enum.all?(fields, &is_tuple/1) do
206 Enum.map(fields, fn {_, v} -> v end)
212 @doc "GET /api/v1/accounts/relationships"
213 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
214 targets = User.get_all_by_ids(List.wrap(id))
216 render(conn, "relationships.json", user: user, targets: targets)
219 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
220 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
222 @doc "GET /api/v1/accounts/:id"
223 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
224 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
225 true <- User.visible_for?(user, for_user) do
226 render(conn, "show.json", user: user, for: for_user)
228 _e -> render_error(conn, :not_found, "Can't find user")
232 @doc "GET /api/v1/accounts/:id/statuses"
233 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
234 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
235 true <- User.visible_for?(user, reading_user) do
238 |> Map.put("tag", params["tagged"])
239 |> Map.delete("godmode")
241 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
244 |> add_link_headers(activities)
245 |> put_view(StatusView)
246 |> render("index.json",
247 activities: activities,
250 skip_relationships: skip_relationships?(params)
253 _e -> render_error(conn, :not_found, "Can't find user")
257 @doc "GET /api/v1/accounts/:id/followers"
258 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
261 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
262 user.hide_followers -> []
263 true -> MastodonAPI.get_followers(user, params)
267 |> add_link_headers(followers)
268 |> render("index.json", for: for_user, users: followers, as: :user)
271 @doc "GET /api/v1/accounts/:id/following"
272 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
275 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
276 user.hide_follows -> []
277 true -> MastodonAPI.get_friends(user, params)
281 |> add_link_headers(followers)
282 |> render("index.json", for: for_user, users: followers, as: :user)
285 @doc "GET /api/v1/accounts/:id/lists"
286 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
287 lists = Pleroma.List.get_lists_account_belongs(user, account)
290 |> put_view(ListView)
291 |> render("index.json", lists: lists)
294 @doc "POST /api/v1/accounts/:id/follow"
295 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
296 {:error, "Can not follow yourself"}
299 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
300 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
301 render(conn, "relationship.json", user: follower, target: followed)
303 {:error, message} -> json_response(conn, :forbidden, %{error: message})
307 @doc "POST /api/v1/accounts/:id/unfollow"
308 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
309 {:error, "Can not unfollow yourself"}
312 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
313 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
314 render(conn, "relationship.json", user: follower, target: followed)
318 @doc "POST /api/v1/accounts/:id/mute"
319 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
320 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
322 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
323 render(conn, "relationship.json", user: muter, target: muted)
325 {:error, message} -> json_response(conn, :forbidden, %{error: message})
329 @doc "POST /api/v1/accounts/:id/unmute"
330 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
331 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
332 render(conn, "relationship.json", user: muter, target: muted)
334 {:error, message} -> json_response(conn, :forbidden, %{error: message})
338 @doc "POST /api/v1/accounts/:id/block"
339 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
340 with {:ok, _user_block} <- User.block(blocker, blocked),
341 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
342 render(conn, "relationship.json", user: blocker, target: blocked)
344 {:error, message} -> json_response(conn, :forbidden, %{error: message})
348 @doc "POST /api/v1/accounts/:id/unblock"
349 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
350 with {:ok, _user_block} <- User.unblock(blocker, blocked),
351 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
352 render(conn, "relationship.json", user: blocker, target: blocked)
354 {:error, message} -> json_response(conn, :forbidden, %{error: message})
358 @doc "POST /api/v1/follows"
359 def follows(conn, %{"uri" => uri}) do
360 case User.get_cached_by_nickname(uri) do
363 |> assign(:account, user)
371 @doc "GET /api/v1/mutes"
372 def mutes(%{assigns: %{user: user}} = conn, _) do
373 users = User.muted_users(user, _restrict_deactivated = true)
374 render(conn, "index.json", users: users, for: user, as: :user)
377 @doc "GET /api/v1/blocks"
378 def blocks(%{assigns: %{user: user}} = conn, _) do
379 users = User.blocked_users(user, _restrict_deactivated = true)
380 render(conn, "index.json", users: users, for: user, as: :user)
383 @doc "GET /api/v1/endorsements"
384 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
386 @doc "GET /api/v1/identity_proofs"
387 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)