total rebase
[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, replace_params: false)
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(
108         %{assigns: %{app: app}, private: %{open_api_spex: %{body_params: params}}} = conn,
109         _params
110       ) do
111     with :ok <- validate_email_param(params),
112          :ok <- TwitterAPI.validate_captcha(app, params),
113          {:ok, user} <- TwitterAPI.register_user(params),
114          {_, {:ok, token}} <-
115            {:login, OAuthController.login(user, app, app.scopes)} do
116       OAuthController.after_token_exchange(conn, %{user: user, token: token})
117     else
118       {:login, {:account_status, :confirmation_pending}} ->
119         json_response(conn, :ok, %{
120           message: "You have been registered. Please check your email for further instructions.",
121           identifier: "missing_confirmed_email"
122         })
123
124       {:login, {:account_status, :approval_pending}} ->
125         json_response(conn, :ok, %{
126           message:
127             "You have been registered. You'll be able to log in once your account is approved.",
128           identifier: "awaiting_approval"
129         })
130
131       {:login, _} ->
132         json_response(conn, :ok, %{
133           message:
134             "You have been registered. Some post-registration steps may be pending. " <>
135               "Please log in manually.",
136           identifier: "manual_login_required"
137         })
138
139       {:error, error} ->
140         json_response(conn, :bad_request, %{error: error})
141     end
142   end
143
144   def create(%{assigns: %{app: _app}} = conn, _) do
145     render_error(conn, :bad_request, "Missing parameters")
146   end
147
148   def create(conn, _) do
149     render_error(conn, :forbidden, "Invalid credentials")
150   end
151
152   defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
153
154   defp validate_email_param(_) do
155     case Pleroma.Config.get([:instance, :account_activation_required]) do
156       true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
157       _ -> :ok
158     end
159   end
160
161   @doc "GET /api/v1/accounts/verify_credentials"
162   def verify_credentials(%{assigns: %{user: user}} = conn, _) do
163     chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
164
165     render(conn, "show.json",
166       user: user,
167       for: user,
168       with_pleroma_settings: true,
169       with_chat_token: chat_token
170     )
171   end
172
173   @doc "PATCH /api/v1/accounts/update_credentials"
174   def update_credentials(
175         %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn,
176         _params
177       ) do
178     params =
179       params
180       |> Enum.filter(fn {_, value} -> not is_nil(value) end)
181       |> Enum.into(%{})
182
183     # We use an empty string as a special value to reset
184     # avatars, banners, backgrounds
185     user_image_value = fn
186       "" -> {:ok, nil}
187       value -> {:ok, value}
188     end
189
190     user_params =
191       [
192         :no_rich_text,
193         :hide_followers_count,
194         :hide_follows_count,
195         :hide_followers,
196         :hide_follows,
197         :hide_favorites,
198         :show_role,
199         :skip_thread_containment,
200         :allow_following_move,
201         :also_known_as,
202         :accepts_chat_messages,
203         :show_birthday
204       ]
205       |> Enum.reduce(%{}, fn key, acc ->
206         Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
207       end)
208       |> Maps.put_if_present(:name, params[:display_name])
209       |> Maps.put_if_present(:bio, params[:note])
210       |> Maps.put_if_present(:raw_bio, params[:note])
211       |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
212       |> Maps.put_if_present(:banner, params[:header], user_image_value)
213       |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
214       |> Maps.put_if_present(
215         :raw_fields,
216         params[:fields_attributes],
217         &{:ok, normalize_fields_attributes(&1)}
218       )
219       |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
220       |> Maps.put_if_present(:default_scope, params[:default_scope])
221       |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
222       |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
223         if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
224       end)
225       |> Maps.put_if_present(:actor_type, params[:actor_type])
226       |> Maps.put_if_present(:also_known_as, params[:also_known_as])
227       # Note: param name is indeed :locked (not an error)
228       |> Maps.put_if_present(:is_locked, params[:locked])
229       # Note: param name is indeed :discoverable (not an error)
230       |> Maps.put_if_present(:is_discoverable, params[:discoverable])
231       |> Maps.put_if_present(:birthday, params[:birthday])
232       |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
233
234     # What happens here:
235     #
236     # We want to update the user through the pipeline, but the ActivityPub
237     # update information is not quite enough for this, because this also
238     # contains local settings that don't federate and don't even appear
239     # in the Update activity.
240     #
241     # So we first build the normal local changeset, then apply it to the
242     # user data, but don't persist it. With this, we generate the object
243     # data for our update activity. We feed this and the changeset as meta
244     # information into the pipeline, where they will be properly updated and
245     # federated.
246     with changeset <- User.update_changeset(user, user_params),
247          {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
248          updated_object <-
249            Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
250            |> Map.delete("@context"),
251          {:ok, update_data, []} <- Builder.update(user, updated_object),
252          {:ok, _update, _} <-
253            Pipeline.common_pipeline(update_data,
254              local: true,
255              user_update_changeset: changeset
256            ) do
257       render(conn, "show.json",
258         user: unpersisted_user,
259         for: unpersisted_user,
260         with_pleroma_settings: true
261       )
262     else
263       {:error, %Ecto.Changeset{errors: [avatar: {"file is too large", _}]}} ->
264         render_error(conn, :request_entity_too_large, "File is too large")
265
266       {:error, %Ecto.Changeset{errors: [banner: {"file is too large", _}]}} ->
267         render_error(conn, :request_entity_too_large, "File is too large")
268
269       {:error, %Ecto.Changeset{errors: [background: {"file is too large", _}]}} ->
270         render_error(conn, :request_entity_too_large, "File is too large")
271
272       {:error, %Ecto.Changeset{errors: [{:bio, {_, _}} | _]}} ->
273         render_error(conn, :request_entity_too_large, "Bio is too long")
274
275       {:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} ->
276         render_error(conn, :request_entity_too_large, "Name is too long")
277
278       {:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} ->
279         render_error(conn, :request_entity_too_large, "One or more field entries are too long")
280
281       {:error, %Ecto.Changeset{errors: [{:fields, {_, _}} | _]}} ->
282         render_error(conn, :request_entity_too_large, "Too many field entries")
283
284       _e ->
285         render_error(conn, :forbidden, "Invalid request")
286     end
287   end
288
289   defp normalize_fields_attributes(fields) do
290     if(Enum.all?(fields, &is_tuple/1), do: Enum.map(fields, fn {_, v} -> v end), else: fields)
291     |> Enum.map(fn
292       %{} = field -> %{"name" => field.name, "value" => field.value}
293       field -> field
294     end)
295   end
296
297   @doc "GET /api/v1/accounts/relationships"
298   def relationships(
299         %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
300         _
301       ) do
302     targets = User.get_all_by_ids(List.wrap(id))
303
304     render(conn, "relationships.json", user: user, targets: targets)
305   end
306
307   # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
308   def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
309
310   @doc "GET /api/v1/accounts/:id"
311   def show(
312         %{
313           assigns: %{user: for_user},
314           private: %{open_api_spex: %{params: %{id: nickname_or_id} = params}}
315         } = conn,
316         _params
317       ) do
318     with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
319          :visible <- User.visible_for(user, for_user) do
320       render(conn, "show.json",
321         user: user,
322         for: for_user,
323         embed_relationships: embed_relationships?(params)
324       )
325     else
326       error -> user_visibility_error(conn, error)
327     end
328   end
329
330   @doc "GET /api/v1/accounts/:id/statuses"
331   def statuses(
332         %{assigns: %{user: reading_user}, private: %{open_api_spex: %{params: params}}} = conn,
333         _params
334       ) do
335     with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
336          :visible <- User.visible_for(user, reading_user) do
337       params =
338         params
339         |> Map.delete(:tagged)
340         |> Map.put(:tag, params[:tagged])
341
342       activities = ActivityPub.fetch_user_activities(user, reading_user, params)
343
344       conn
345       |> add_link_headers(activities)
346       |> put_view(StatusView)
347       |> render("index.json",
348         activities: activities,
349         for: reading_user,
350         as: :activity,
351         with_muted: Map.get(params, :with_muted, false)
352       )
353     else
354       error -> user_visibility_error(conn, error)
355     end
356   end
357
358   defp user_visibility_error(conn, error) do
359     case error do
360       :restrict_unauthenticated ->
361         render_error(conn, :unauthorized, "This API requires an authenticated user")
362
363       _ ->
364         render_error(conn, :not_found, "Can't find user")
365     end
366   end
367
368   @doc "GET /api/v1/accounts/:id/followers"
369   def followers(
370         %{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
371           conn,
372         _params
373       ) do
374     params =
375       params
376       |> Enum.map(fn {key, value} -> {to_string(key), value} end)
377       |> Enum.into(%{})
378
379     followers =
380       cond do
381         for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
382         user.hide_followers -> []
383         true -> MastodonAPI.get_followers(user, params)
384       end
385
386     conn
387     |> add_link_headers(followers)
388     # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
389     |> render("index.json",
390       for: for_user,
391       users: followers,
392       as: :user,
393       embed_relationships: embed_relationships?(params)
394     )
395   end
396
397   @doc "GET /api/v1/accounts/:id/following"
398   def following(
399         %{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
400           conn,
401         _params
402       ) do
403     params =
404       params
405       |> Enum.map(fn {key, value} -> {to_string(key), value} end)
406       |> Enum.into(%{})
407
408     followers =
409       cond do
410         for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
411         user.hide_follows -> []
412         true -> MastodonAPI.get_friends(user, params)
413       end
414
415     conn
416     |> add_link_headers(followers)
417     # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
418     |> render("index.json",
419       for: for_user,
420       users: followers,
421       as: :user,
422       embed_relationships: embed_relationships?(params)
423     )
424   end
425
426   @doc "GET /api/v1/accounts/:id/lists"
427   def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
428     lists = Pleroma.List.get_lists_account_belongs(user, account)
429
430     conn
431     |> put_view(ListView)
432     |> render("index.json", lists: lists)
433   end
434
435   @doc "POST /api/v1/accounts/:id/follow"
436   def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
437     {:error, "Can not follow yourself"}
438   end
439
440   def follow(
441         %{
442           assigns: %{user: follower, account: followed},
443           private: %{open_api_spex: %{body_params: params}}
444         } = conn,
445         _
446       ) do
447     with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
448       render(conn, "relationship.json", user: follower, target: followed)
449     else
450       {:error, message} -> json_response(conn, :forbidden, %{error: message})
451     end
452   end
453
454   @doc "POST /api/v1/accounts/:id/unfollow"
455   def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
456     {:error, "Can not unfollow yourself"}
457   end
458
459   def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
460     with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
461       render(conn, "relationship.json", user: follower, target: followed)
462     end
463   end
464
465   @doc "POST /api/v1/accounts/:id/mute"
466   def mute(
467         %{
468           assigns: %{user: muter, account: muted},
469           private: %{open_api_spex: %{body_params: params}}
470         } = conn,
471         _params
472       ) do
473     params =
474       params
475       |> Map.put_new(:duration, Map.get(params, :expires_in, 0))
476
477     with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
478       render(conn, "relationship.json", user: muter, target: muted)
479     else
480       {:error, message} -> json_response(conn, :forbidden, %{error: message})
481     end
482   end
483
484   @doc "POST /api/v1/accounts/:id/unmute"
485   def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
486     with {:ok, _user_relationships} <- User.unmute(muter, muted) do
487       render(conn, "relationship.json", user: muter, target: muted)
488     else
489       {:error, message} -> json_response(conn, :forbidden, %{error: message})
490     end
491   end
492
493   @doc "POST /api/v1/accounts/:id/block"
494   def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
495     with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
496       render(conn, "relationship.json", user: blocker, target: blocked)
497     else
498       {:error, message} -> json_response(conn, :forbidden, %{error: message})
499     end
500   end
501
502   @doc "POST /api/v1/accounts/:id/unblock"
503   def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
504     with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
505       render(conn, "relationship.json", user: blocker, target: blocked)
506     else
507       {:error, message} -> json_response(conn, :forbidden, %{error: message})
508     end
509   end
510
511   @doc "POST /api/v1/accounts/:id/note"
512   def note(
513         %{
514           assigns: %{user: noter, account: target},
515           private: %{open_api_spex: %{body_params: %{comment: comment}}}
516         } = conn,
517         _params
518       ) do
519     with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
520       render(conn, "relationship.json", user: noter, target: target)
521     end
522   end
523
524   @doc "POST /api/v1/accounts/:id/pin"
525   def endorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
526     with {:ok, _user_relationships} <- User.endorse(endorser, endorsed) do
527       render(conn, "relationship.json", user: endorser, target: endorsed)
528     else
529       {:error, message} -> json_response(conn, :bad_request, %{error: message})
530     end
531   end
532
533   @doc "POST /api/v1/accounts/:id/unpin"
534   def unendorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
535     with {:ok, _user_relationships} <- User.unendorse(endorser, endorsed) do
536       render(conn, "relationship.json", user: endorser, target: endorsed)
537     else
538       {:error, message} -> json_response(conn, :forbidden, %{error: message})
539     end
540   end
541
542   @doc "POST /api/v1/accounts/:id/remove_from_followers"
543   def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
544     {:error, "Can not unfollow yourself"}
545   end
546
547   def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do
548     with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
549       render(conn, "relationship.json", user: followed, target: follower)
550     else
551       nil ->
552         render_error(conn, :not_found, "Record not found")
553     end
554   end
555
556   @doc "POST /api/v1/follows"
557   def follow_by_uri(%{private: %{open_api_spex: %{body_params: %{uri: uri}}}} = conn, _) do
558     case User.get_cached_by_nickname(uri) do
559       %User{} = user ->
560         conn
561         |> assign(:account, user)
562         |> follow(%{})
563
564       nil ->
565         {:error, :not_found}
566     end
567   end
568
569   @doc "GET /api/v1/mutes"
570   def mutes(%{assigns: %{user: user}} = conn, params) do
571     users =
572       user
573       |> User.muted_users_relation(_restrict_deactivated = true)
574       |> Pleroma.Pagination.fetch_paginated(params)
575
576     conn
577     |> add_link_headers(users)
578     |> render("index.json",
579       users: users,
580       for: user,
581       as: :user,
582       embed_relationships: embed_relationships?(params),
583       mutes: true
584     )
585   end
586
587   @doc "GET /api/v1/blocks"
588   def blocks(%{assigns: %{user: user}} = conn, params) do
589     users =
590       user
591       |> User.blocked_users_relation(_restrict_deactivated = true)
592       |> Pleroma.Pagination.fetch_paginated(params)
593
594     conn
595     |> add_link_headers(users)
596     |> render("index.json",
597       users: users,
598       for: user,
599       as: :user,
600       embed_relationships: embed_relationships?(params)
601     )
602   end
603
604   @doc "GET /api/v1/accounts/lookup"
605   def lookup(%{private: %{open_api_spex: %{params: %{acct: nickname}}}} = conn, _params) do
606     with %User{} = user <- User.get_by_nickname(nickname) do
607       render(conn, "show.json",
608         user: user,
609         skip_visibility_check: true
610       )
611     else
612       error -> user_visibility_error(conn, error)
613     end
614   end
615
616   @doc "GET /api/v1/endorsements"
617   def endorsements(%{assigns: %{user: user}} = conn, params) do
618     users =
619       user
620       |> User.endorsed_users_relation(_restrict_deactivated = true)
621       |> Pleroma.Repo.all()
622
623     conn
624     |> render("index.json",
625       users: users,
626       for: user,
627       as: :user,
628       embed_relationships: embed_relationships?(params)
629     )
630   end
631
632   @doc "GET /api/v1/identity_proofs"
633   def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
634 end