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.ActivityPub.Builder
24 alias Pleroma.Web.ActivityPub.Pipeline
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.ListView
27 alias Pleroma.Web.MastodonAPI.MastodonAPI
28 alias Pleroma.Web.MastodonAPI.MastodonAPIController
29 alias Pleroma.Web.MastodonAPI.StatusView
30 alias Pleroma.Web.OAuth.Token
31 alias Pleroma.Web.TwitterAPI.TwitterAPI
33 plug(Pleroma.Web.ApiSpec.CastAndValidate)
35 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
37 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
41 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
42 when action in [:show, :followers, :following]
47 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
48 when action == :statuses
53 %{scopes: ["read:accounts"]}
54 when action in [:verify_credentials, :endorsements, :identity_proofs]
57 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
59 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
63 %{scopes: ["follow", "read:blocks"]} when action == :blocks
68 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
71 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
75 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
78 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
80 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
82 @relationship_actions [:follow, :unfollow]
83 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
87 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
90 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
91 plug(RateLimiter, [name: :app_account_creation] when action == :create)
92 plug(:assign_account_by_id when action in @needs_account)
94 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
96 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
98 @doc "POST /api/v1/accounts"
99 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
100 with :ok <- validate_email_param(params),
101 :ok <- TwitterAPI.validate_captcha(app, params),
102 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
103 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
105 token_type: "Bearer",
106 access_token: token.token,
108 created_at: Token.Utils.format_created_at(token)
111 {:error, error} -> json_response(conn, :bad_request, %{error: error})
115 def create(%{assigns: %{app: _app}} = conn, _) do
116 render_error(conn, :bad_request, "Missing parameters")
119 def create(conn, _) do
120 render_error(conn, :forbidden, "Invalid credentials")
123 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
125 defp validate_email_param(_) do
126 case Pleroma.Config.get([:instance, :account_activation_required]) do
127 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
132 @doc "GET /api/v1/accounts/verify_credentials"
133 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
134 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
136 render(conn, "show.json",
139 with_pleroma_settings: true,
140 with_chat_token: chat_token
144 @doc "PATCH /api/v1/accounts/update_credentials"
145 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
148 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
151 # We use an empty string as a special value to reset
152 # avatars, banners, backgrounds
153 user_image_value = fn
155 value -> {:ok, value}
162 :hide_followers_count,
168 :skip_thread_containment,
169 :allow_following_move,
172 |> Enum.reduce(%{}, fn key, acc ->
173 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
175 |> Maps.put_if_present(:name, params[:display_name])
176 |> Maps.put_if_present(:bio, params[:note])
177 |> Maps.put_if_present(:raw_bio, params[:note])
178 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
179 |> Maps.put_if_present(:banner, params[:header], user_image_value)
180 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
181 |> Maps.put_if_present(
183 params[:fields_attributes],
184 &{:ok, normalize_fields_attributes(&1)}
186 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
187 |> Maps.put_if_present(:default_scope, params[:default_scope])
188 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
189 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
190 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
192 |> Maps.put_if_present(:actor_type, params[:actor_type])
196 # We want to update the user through the pipeline, but the ActivityPub
197 # update information is not quite enough for this, because this also
198 # contains local settings that don't federate and don't even appear
199 # in the Update activity.
201 # So we first build the normal local changeset, then apply it to the
202 # user data, but don't persist it. With this, we generate the object
203 # data for our update activity. We feed this and the changeset as meta
204 # inforation into the pipeline, where they will be properly updated and
206 with changeset <- User.update_changeset(user, user_params),
207 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
209 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
210 |> Map.delete("@context"),
211 {:ok, update_data, []} <- Builder.update(user, updated_object),
213 Pipeline.common_pipeline(update_data,
215 user_update_changeset: changeset
217 render(conn, "show.json",
218 user: unpersisted_user,
219 for: unpersisted_user,
220 with_pleroma_settings: true
223 _e -> render_error(conn, :forbidden, "Invalid request")
227 defp normalize_fields_attributes(fields) do
228 if Enum.all?(fields, &is_tuple/1) do
229 Enum.map(fields, fn {_, v} -> v end)
232 %{} = field -> %{"name" => field.name, "value" => field.value}
238 @doc "GET /api/v1/accounts/relationships"
239 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
240 targets = User.get_all_by_ids(List.wrap(id))
242 render(conn, "relationships.json", user: user, targets: targets)
245 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
246 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
248 @doc "GET /api/v1/accounts/:id"
249 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
250 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
251 :visible <- User.visible_for(user, for_user) do
252 render(conn, "show.json", user: user, for: for_user)
254 error -> user_visibility_error(conn, error)
258 @doc "GET /api/v1/accounts/:id/statuses"
259 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
260 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
261 :visible <- User.visible_for(user, reading_user) do
264 |> Map.delete(:tagged)
265 |> Map.put(:tag, params[:tagged])
267 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
270 |> add_link_headers(activities)
271 |> put_view(StatusView)
272 |> render("index.json",
273 activities: activities,
278 error -> user_visibility_error(conn, error)
282 defp user_visibility_error(conn, error) do
284 :restrict_unauthenticated ->
285 render_error(conn, :unauthorized, "This API requires an authenticated user")
288 render_error(conn, :not_found, "Can't find user")
292 @doc "GET /api/v1/accounts/:id/followers"
293 def followers(%{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_followers(user, params)
302 user.hide_followers -> []
303 true -> MastodonAPI.get_followers(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/following"
318 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
321 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
326 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
327 user.hide_follows -> []
328 true -> MastodonAPI.get_friends(user, params)
332 |> add_link_headers(followers)
333 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
334 |> render("index.json",
338 embed_relationships: embed_relationships?(params)
342 @doc "GET /api/v1/accounts/:id/lists"
343 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
344 lists = Pleroma.List.get_lists_account_belongs(user, account)
347 |> put_view(ListView)
348 |> render("index.json", lists: lists)
351 @doc "POST /api/v1/accounts/:id/follow"
352 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
353 {:error, "Can not follow yourself"}
356 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
357 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
358 render(conn, "relationship.json", user: follower, target: followed)
360 {:error, message} -> json_response(conn, :forbidden, %{error: message})
364 @doc "POST /api/v1/accounts/:id/unfollow"
365 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
366 {:error, "Can not unfollow yourself"}
369 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
370 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
371 render(conn, "relationship.json", user: follower, target: followed)
375 @doc "POST /api/v1/accounts/:id/mute"
376 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
377 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
378 render(conn, "relationship.json", user: muter, target: muted)
380 {:error, message} -> json_response(conn, :forbidden, %{error: message})
384 @doc "POST /api/v1/accounts/:id/unmute"
385 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
386 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
387 render(conn, "relationship.json", user: muter, target: muted)
389 {:error, message} -> json_response(conn, :forbidden, %{error: message})
393 @doc "POST /api/v1/accounts/:id/block"
394 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
395 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
396 render(conn, "relationship.json", user: blocker, target: blocked)
398 {:error, message} -> json_response(conn, :forbidden, %{error: message})
402 @doc "POST /api/v1/accounts/:id/unblock"
403 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
404 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
405 render(conn, "relationship.json", user: blocker, target: blocked)
407 {:error, message} -> json_response(conn, :forbidden, %{error: message})
411 @doc "POST /api/v1/follows"
412 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
413 case User.get_cached_by_nickname(uri) do
416 |> assign(:account, user)
424 @doc "GET /api/v1/mutes"
425 def mutes(%{assigns: %{user: user}} = conn, _) do
426 users = User.muted_users(user, _restrict_deactivated = true)
427 render(conn, "index.json", users: users, for: user, as: :user)
430 @doc "GET /api/v1/blocks"
431 def blocks(%{assigns: %{user: user}} = conn, _) do
432 users = User.blocked_users(user, _restrict_deactivated = true)
433 render(conn, "index.json", users: users, for: user, as: :user)
436 @doc "GET /api/v1/endorsements"
437 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
439 @doc "GET /api/v1/identity_proofs"
440 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)