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