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)
155 :hide_followers_count,
161 :skip_thread_containment,
162 :allow_following_move,
165 |> Enum.reduce(%{}, fn key, acc ->
166 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
168 |> Maps.put_if_present(:name, params[:display_name])
169 |> Maps.put_if_present(:bio, params[:note])
170 |> Maps.put_if_present(:raw_bio, params[:note])
171 |> Maps.put_if_present(:avatar, params[:avatar])
172 |> Maps.put_if_present(:banner, params[:header])
173 |> Maps.put_if_present(:background, params[:pleroma_background_image])
174 |> Maps.put_if_present(
176 params[:fields_attributes],
177 &{:ok, normalize_fields_attributes(&1)}
179 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
180 |> Maps.put_if_present(:default_scope, params[:default_scope])
181 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
182 |> Maps.put_if_present(:actor_type, params[:actor_type])
186 # We want to update the user through the pipeline, but the ActivityPub
187 # update information is not quite enough for this, because this also
188 # contains local settings that don't federate and don't even appear
189 # in the Update activity.
191 # So we first build the normal local changeset, then apply it to the
192 # user data, but don't persist it. With this, we generate the object
193 # data for our update activity. We feed this and the changeset as meta
194 # inforation into the pipeline, where they will be properly updated and
196 with changeset <- User.update_changeset(user, user_params),
197 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
199 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
200 |> Map.delete("@context"),
201 {:ok, update_data, []} <- Builder.update(user, updated_object),
203 Pipeline.common_pipeline(update_data,
205 user_update_changeset: changeset
207 render(conn, "show.json",
208 user: unpersisted_user,
209 for: unpersisted_user,
210 with_pleroma_settings: true
213 _e -> render_error(conn, :forbidden, "Invalid request")
217 defp normalize_fields_attributes(fields) do
218 if Enum.all?(fields, &is_tuple/1) do
219 Enum.map(fields, fn {_, v} -> v end)
222 %{} = field -> %{"name" => field.name, "value" => field.value}
228 @doc "GET /api/v1/accounts/relationships"
229 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
230 targets = User.get_all_by_ids(List.wrap(id))
232 render(conn, "relationships.json", user: user, targets: targets)
235 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
236 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
238 @doc "GET /api/v1/accounts/:id"
239 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
240 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
241 true <- User.visible_for?(user, for_user) do
242 render(conn, "show.json", user: user, for: for_user)
244 _e -> render_error(conn, :not_found, "Can't find user")
248 @doc "GET /api/v1/accounts/:id/statuses"
249 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
250 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
251 true <- User.visible_for?(user, reading_user) do
254 |> Map.delete(:tagged)
255 |> Map.put(:tag, params[:tagged])
257 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
260 |> add_link_headers(activities)
261 |> put_view(StatusView)
262 |> render("index.json",
263 activities: activities,
268 _e -> render_error(conn, :not_found, "Can't find user")
272 @doc "GET /api/v1/accounts/:id/followers"
273 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
276 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
281 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
282 user.hide_followers -> []
283 true -> MastodonAPI.get_followers(user, params)
287 |> add_link_headers(followers)
288 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
289 |> render("index.json",
293 embed_relationships: embed_relationships?(params)
297 @doc "GET /api/v1/accounts/:id/following"
298 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
301 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
306 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
307 user.hide_follows -> []
308 true -> MastodonAPI.get_friends(user, params)
312 |> add_link_headers(followers)
313 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
314 |> render("index.json",
318 embed_relationships: embed_relationships?(params)
322 @doc "GET /api/v1/accounts/:id/lists"
323 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
324 lists = Pleroma.List.get_lists_account_belongs(user, account)
327 |> put_view(ListView)
328 |> render("index.json", lists: lists)
331 @doc "POST /api/v1/accounts/:id/follow"
332 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
333 {:error, "Can not follow yourself"}
336 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
337 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
338 render(conn, "relationship.json", user: follower, target: followed)
340 {:error, message} -> json_response(conn, :forbidden, %{error: message})
344 @doc "POST /api/v1/accounts/:id/unfollow"
345 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
346 {:error, "Can not unfollow yourself"}
349 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
350 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
351 render(conn, "relationship.json", user: follower, target: followed)
355 @doc "POST /api/v1/accounts/:id/mute"
356 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
357 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
358 render(conn, "relationship.json", user: muter, target: muted)
360 {:error, message} -> json_response(conn, :forbidden, %{error: message})
364 @doc "POST /api/v1/accounts/:id/unmute"
365 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
366 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
367 render(conn, "relationship.json", user: muter, target: muted)
369 {:error, message} -> json_response(conn, :forbidden, %{error: message})
373 @doc "POST /api/v1/accounts/:id/block"
374 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
375 with {:ok, _user_block} <- User.block(blocker, blocked),
376 {:ok, _activity} <- ActivityPub.block(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/accounts/:id/unblock"
384 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
385 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
386 render(conn, "relationship.json", user: blocker, target: blocked)
388 {:error, message} -> json_response(conn, :forbidden, %{error: message})
392 @doc "POST /api/v1/follows"
393 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
394 case User.get_cached_by_nickname(uri) do
397 |> assign(:account, user)
405 @doc "GET /api/v1/mutes"
406 def mutes(%{assigns: %{user: user}} = conn, _) do
407 users = User.muted_users(user, _restrict_deactivated = true)
408 render(conn, "index.json", users: users, for: user, as: :user)
411 @doc "GET /api/v1/blocks"
412 def blocks(%{assigns: %{user: user}} = conn, _) do
413 users = User.blocked_users(user, _restrict_deactivated = true)
414 render(conn, "index.json", users: users, for: user, as: :user)
417 @doc "GET /api/v1/endorsements"
418 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
420 @doc "GET /api/v1/identity_proofs"
421 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)