First
[anni] / lib / pleroma / web / mastodon_api / controllers / account_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.AccountController do
6   use Pleroma.Web, :controller
7
8   import Pleroma.Web.ControllerHelper,
9     only: [
10       add_link_headers: 2,
11       assign_account_by_id: 2,
12       embed_relationships?: 1,
13       json_response: 3
14     ]
15
16   alias Pleroma.Maps
17   alias Pleroma.User
18   alias Pleroma.UserNote
19   alias Pleroma.Web.ActivityPub.ActivityPub
20   alias Pleroma.Web.ActivityPub.Builder
21   alias Pleroma.Web.ActivityPub.Pipeline
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.OAuthController
28   alias Pleroma.Web.Plugs.OAuthScopesPlug
29   alias Pleroma.Web.Plugs.RateLimiter
30   alias Pleroma.Web.TwitterAPI.TwitterAPI
31   alias Pleroma.Web.Utils.Params
32
33   plug(Pleroma.Web.ApiSpec.CastAndValidate)
34
35   plug(:skip_auth when action in [:create, :lookup])
36
37   plug(:skip_public_check when action in [:show, :statuses])
38
39   plug(
40     OAuthScopesPlug,
41     %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
42     when action in [:show, :followers, :following]
43   )
44
45   plug(
46     OAuthScopesPlug,
47     %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
48     when action == :statuses
49   )
50
51   plug(
52     OAuthScopesPlug,
53     %{scopes: ["read:accounts"]}
54     when action in [:verify_credentials, :endorsements, :identity_proofs]
55   )
56
57   plug(
58     OAuthScopesPlug,
59     %{scopes: ["write:accounts"]}
60     when action in [:update_credentials, :note, :endorse, :unendorse]
61   )
62
63   plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
64
65   plug(
66     OAuthScopesPlug,
67     %{scopes: ["follow", "read:blocks"]} when action == :blocks
68   )
69
70   plug(
71     OAuthScopesPlug,
72     %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
73   )
74
75   plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
76
77   plug(
78     OAuthScopesPlug,
79     %{scopes: ["follow", "write:follows"]}
80     when action in [:follow_by_uri, :follow, :unfollow, :remove_from_followers]
81   )
82
83   plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
84
85   plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
86
87   @relationship_actions [:follow, :unfollow, :remove_from_followers]
88   @needs_account ~W(
89     followers following lists follow unfollow mute unmute block unblock
90     note endorse unendorse remove_from_followers
91   )a
92
93   plug(
94     RateLimiter,
95     [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
96   )
97
98   plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
99   plug(RateLimiter, [name: :app_account_creation] when action == :create)
100   plug(:assign_account_by_id when action in @needs_account)
101
102   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
103
104   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
105
106   @doc "POST /api/v1/accounts"
107   def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
108     with :ok <- validate_email_param(params),
109          :ok <- TwitterAPI.validate_captcha(app, params),
110          {:ok, user} <- TwitterAPI.register_user(params),
111          {_, {:ok, token}} <-
112            {:login, OAuthController.login(user, app, app.scopes)} do
113       OAuthController.after_token_exchange(conn, %{user: user, token: token})
114     else
115       {:login, {:account_status, :confirmation_pending}} ->
116         json_response(conn, :ok, %{
117           message: "You have been registered. Please check your email for further instructions.",
118           identifier: "missing_confirmed_email"
119         })
120
121       {:login, {:account_status, :approval_pending}} ->
122         json_response(conn, :ok, %{
123           message:
124             "You have been registered. You'll be able to log in once your account is approved.",
125           identifier: "awaiting_approval"
126         })
127
128       {:login, _} ->
129         json_response(conn, :ok, %{
130           message:
131             "You have been registered. Some post-registration steps may be pending. " <>
132               "Please log in manually.",
133           identifier: "manual_login_required"
134         })
135
136       {:error, error} ->
137         json_response(conn, :bad_request, %{error: error})
138     end
139   end
140
141   def create(%{assigns: %{app: _app}} = conn, _) do
142     render_error(conn, :bad_request, "Missing parameters")
143   end
144
145   def create(conn, _) do
146     render_error(conn, :forbidden, "Invalid credentials")
147   end
148
149   defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
150
151   defp validate_email_param(_) do
152     case Pleroma.Config.get([:instance, :account_activation_required]) do
153       true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
154       _ -> :ok
155     end
156   end
157
158   @doc "GET /api/v1/accounts/verify_credentials"
159   def verify_credentials(%{assigns: %{user: user}} = conn, _) do
160     chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
161
162     render(conn, "show.json",
163       user: user,
164       for: user,
165       with_pleroma_settings: true,
166       with_chat_token: chat_token
167     )
168   end
169
170   @doc "PATCH /api/v1/accounts/update_credentials"
171   def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
172     params =
173       params
174       |> Enum.filter(fn {_, value} -> not is_nil(value) end)
175       |> Enum.into(%{})
176
177     # We use an empty string as a special value to reset
178     # avatars, banners, backgrounds
179     user_image_value = fn
180       "" -> {:ok, nil}
181       value -> {:ok, value}
182     end
183
184     user_params =
185       [
186         :no_rich_text,
187         :hide_followers_count,
188         :hide_follows_count,
189         :hide_followers,
190         :hide_follows,
191         :hide_favorites,
192         :show_role,
193         :skip_thread_containment,
194         :allow_following_move,
195         :also_known_as,
196         :accepts_chat_messages,
197         :show_birthday
198       ]
199       |> Enum.reduce(%{}, fn key, acc ->
200         Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
201       end)
202       |> Maps.put_if_present(:name, params[:display_name])
203       |> Maps.put_if_present(:bio, params[:note])
204       |> Maps.put_if_present(:raw_bio, params[:note])
205       |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
206       |> Maps.put_if_present(:banner, params[:header], user_image_value)
207       |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
208       |> Maps.put_if_present(
209         :raw_fields,
210         params[:fields_attributes],
211         &{:ok, normalize_fields_attributes(&1)}
212       )
213       |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
214       |> Maps.put_if_present(:default_scope, params[:default_scope])
215       |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
216       |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
217         if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
218       end)
219       |> Maps.put_if_present(:actor_type, params[:actor_type])
220       |> Maps.put_if_present(:also_known_as, params[:also_known_as])
221       # Note: param name is indeed :locked (not an error)
222       |> Maps.put_if_present(:is_locked, params[:locked])
223       # Note: param name is indeed :discoverable (not an error)
224       |> Maps.put_if_present(:is_discoverable, params[:discoverable])
225       |> Maps.put_if_present(:birthday, params[:birthday])
226       |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
227
228     # What happens here:
229     #
230     # We want to update the user through the pipeline, but the ActivityPub
231     # update information is not quite enough for this, because this also
232     # contains local settings that don't federate and don't even appear
233     # in the Update activity.
234     #
235     # So we first build the normal local changeset, then apply it to the
236     # user data, but don't persist it. With this, we generate the object
237     # data for our update activity. We feed this and the changeset as meta
238     # inforation into the pipeline, where they will be properly updated and
239     # federated.
240     with changeset <- User.update_changeset(user, user_params),
241          {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
242          updated_object <-
243            Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
244            |> Map.delete("@context"),
245          {:ok, update_data, []} <- Builder.update(user, updated_object),
246          {:ok, _update, _} <-
247            Pipeline.common_pipeline(update_data,
248              local: true,
249              user_update_changeset: changeset
250            ) do
251       render(conn, "show.json",
252         user: unpersisted_user,
253         for: unpersisted_user,
254         with_pleroma_settings: true
255       )
256     else
257       {:error, %Ecto.Changeset{errors: [avatar: {"file is too large", _}]}} ->
258         render_error(conn, :request_entity_too_large, "File is too large")
259
260       {:error, %Ecto.Changeset{errors: [banner: {"file is too large", _}]}} ->
261         render_error(conn, :request_entity_too_large, "File is too large")
262
263       {:error, %Ecto.Changeset{errors: [background: {"file is too large", _}]}} ->
264         render_error(conn, :request_entity_too_large, "File is too large")
265
266       _e ->
267         render_error(conn, :forbidden, "Invalid request")
268     end
269   end
270
271   defp normalize_fields_attributes(fields) do
272     if Enum.all?(fields, &is_tuple/1) do
273       Enum.map(fields, fn {_, v} -> v end)
274     else
275       Enum.map(fields, fn
276         %{} = field -> %{"name" => field.name, "value" => field.value}
277         field -> field
278       end)
279     end
280   end
281
282   @doc "GET /api/v1/accounts/relationships"
283   def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
284     targets = User.get_all_by_ids(List.wrap(id))
285
286     render(conn, "relationships.json", user: user, targets: targets)
287   end
288
289   # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
290   def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
291
292   @doc "GET /api/v1/accounts/:id"
293   def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
294     with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
295          :visible <- User.visible_for(user, for_user) do
296       render(conn, "show.json",
297         user: user,
298         for: for_user,
299         embed_relationships: embed_relationships?(params)
300       )
301     else
302       error -> user_visibility_error(conn, error)
303     end
304   end
305
306   @doc "GET /api/v1/accounts/:id/statuses"
307   def statuses(%{assigns: %{user: reading_user}} = conn, params) do
308     with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
309          :visible <- User.visible_for(user, reading_user) do
310       params =
311         params
312         |> Map.delete(:tagged)
313         |> Map.put(:tag, params[:tagged])
314
315       activities = ActivityPub.fetch_user_activities(user, reading_user, params)
316
317       conn
318       |> add_link_headers(activities)
319       |> put_view(StatusView)
320       |> render("index.json",
321         activities: activities,
322         for: reading_user,
323         as: :activity,
324         with_muted: Map.get(params, :with_muted, false)
325       )
326     else
327       error -> user_visibility_error(conn, error)
328     end
329   end
330
331   defp user_visibility_error(conn, error) do
332     case error do
333       :restrict_unauthenticated ->
334         render_error(conn, :unauthorized, "This API requires an authenticated user")
335
336       _ ->
337         render_error(conn, :not_found, "Can't find user")
338     end
339   end
340
341   @doc "GET /api/v1/accounts/:id/followers"
342   def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
343     params =
344       params
345       |> Enum.map(fn {key, value} -> {to_string(key), value} end)
346       |> Enum.into(%{})
347
348     followers =
349       cond do
350         for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
351         user.hide_followers -> []
352         true -> MastodonAPI.get_followers(user, params)
353       end
354
355     conn
356     |> add_link_headers(followers)
357     # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
358     |> render("index.json",
359       for: for_user,
360       users: followers,
361       as: :user,
362       embed_relationships: embed_relationships?(params)
363     )
364   end
365
366   @doc "GET /api/v1/accounts/:id/following"
367   def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
368     params =
369       params
370       |> Enum.map(fn {key, value} -> {to_string(key), value} end)
371       |> Enum.into(%{})
372
373     followers =
374       cond do
375         for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
376         user.hide_follows -> []
377         true -> MastodonAPI.get_friends(user, params)
378       end
379
380     conn
381     |> add_link_headers(followers)
382     # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
383     |> render("index.json",
384       for: for_user,
385       users: followers,
386       as: :user,
387       embed_relationships: embed_relationships?(params)
388     )
389   end
390
391   @doc "GET /api/v1/accounts/:id/lists"
392   def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
393     lists = Pleroma.List.get_lists_account_belongs(user, account)
394
395     conn
396     |> put_view(ListView)
397     |> render("index.json", lists: lists)
398   end
399
400   @doc "POST /api/v1/accounts/:id/follow"
401   def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
402     {:error, "Can not follow yourself"}
403   end
404
405   def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
406     with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
407       render(conn, "relationship.json", user: follower, target: followed)
408     else
409       {:error, message} -> json_response(conn, :forbidden, %{error: message})
410     end
411   end
412
413   @doc "POST /api/v1/accounts/:id/unfollow"
414   def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
415     {:error, "Can not unfollow yourself"}
416   end
417
418   def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
419     with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
420       render(conn, "relationship.json", user: follower, target: followed)
421     end
422   end
423
424   @doc "POST /api/v1/accounts/:id/mute"
425   def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
426     params =
427       params
428       |> Map.put_new(:duration, Map.get(params, :expires_in, 0))
429
430     with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
431       render(conn, "relationship.json", user: muter, target: muted)
432     else
433       {:error, message} -> json_response(conn, :forbidden, %{error: message})
434     end
435   end
436
437   @doc "POST /api/v1/accounts/:id/unmute"
438   def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
439     with {:ok, _user_relationships} <- User.unmute(muter, muted) do
440       render(conn, "relationship.json", user: muter, target: muted)
441     else
442       {:error, message} -> json_response(conn, :forbidden, %{error: message})
443     end
444   end
445
446   @doc "POST /api/v1/accounts/:id/block"
447   def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
448     with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
449       render(conn, "relationship.json", user: blocker, target: blocked)
450     else
451       {:error, message} -> json_response(conn, :forbidden, %{error: message})
452     end
453   end
454
455   @doc "POST /api/v1/accounts/:id/unblock"
456   def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
457     with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
458       render(conn, "relationship.json", user: blocker, target: blocked)
459     else
460       {:error, message} -> json_response(conn, :forbidden, %{error: message})
461     end
462   end
463
464   @doc "POST /api/v1/accounts/:id/note"
465   def note(
466         %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
467         _params
468       ) do
469     with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
470       render(conn, "relationship.json", user: noter, target: target)
471     end
472   end
473
474   @doc "POST /api/v1/accounts/:id/pin"
475   def endorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
476     with {:ok, _user_relationships} <- User.endorse(endorser, endorsed) do
477       render(conn, "relationship.json", user: endorser, target: endorsed)
478     else
479       {:error, message} -> json_response(conn, :bad_request, %{error: message})
480     end
481   end
482
483   @doc "POST /api/v1/accounts/:id/unpin"
484   def unendorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
485     with {:ok, _user_relationships} <- User.unendorse(endorser, endorsed) do
486       render(conn, "relationship.json", user: endorser, target: endorsed)
487     else
488       {:error, message} -> json_response(conn, :forbidden, %{error: message})
489     end
490   end
491
492   @doc "POST /api/v1/accounts/:id/remove_from_followers"
493   def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
494     {:error, "Can not unfollow yourself"}
495   end
496
497   def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do
498     with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
499       render(conn, "relationship.json", user: followed, target: follower)
500     else
501       nil ->
502         render_error(conn, :not_found, "Record not found")
503     end
504   end
505
506   @doc "POST /api/v1/follows"
507   def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
508     case User.get_cached_by_nickname(uri) do
509       %User{} = user ->
510         conn
511         |> assign(:account, user)
512         |> follow(%{})
513
514       nil ->
515         {:error, :not_found}
516     end
517   end
518
519   @doc "GET /api/v1/mutes"
520   def mutes(%{assigns: %{user: user}} = conn, params) do
521     users =
522       user
523       |> User.muted_users_relation(_restrict_deactivated = true)
524       |> Pleroma.Pagination.fetch_paginated(params)
525
526     conn
527     |> add_link_headers(users)
528     |> render("index.json",
529       users: users,
530       for: user,
531       as: :user,
532       embed_relationships: embed_relationships?(params),
533       mutes: true
534     )
535   end
536
537   @doc "GET /api/v1/blocks"
538   def blocks(%{assigns: %{user: user}} = conn, params) do
539     users =
540       user
541       |> User.blocked_users_relation(_restrict_deactivated = true)
542       |> Pleroma.Pagination.fetch_paginated(params)
543
544     conn
545     |> add_link_headers(users)
546     |> render("index.json", users: users, for: user, as: :user)
547   end
548
549   @doc "GET /api/v1/accounts/lookup"
550   def lookup(conn, %{acct: nickname} = _params) do
551     with %User{} = user <- User.get_by_nickname(nickname) do
552       render(conn, "show.json",
553         user: user,
554         skip_visibility_check: true
555       )
556     else
557       error -> user_visibility_error(conn, error)
558     end
559   end
560
561   @doc "GET /api/v1/endorsements"
562   def endorsements(%{assigns: %{user: user}} = conn, params) do
563     users =
564       user
565       |> User.endorsed_users_relation(_restrict_deactivated = true)
566       |> Pleroma.Repo.all()
567
568     conn
569     |> render("index.json",
570       users: users,
571       for: user,
572       as: :user,
573       embed_relationships: embed_relationships?(params)
574     )
575   end
576
577   @doc "GET /api/v1/identity_proofs"
578   def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
579 end