Merge pull request 'Merge search behaviour change, and refactor elasticsearch' (...
[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 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
157
158 render(conn, "show.json",
159 user: user,
160 for: user,
161 with_pleroma_settings: true,
162 with_chat_token: chat_token
163 )
164 end
165
166 @doc "PATCH /api/v1/accounts/update_credentials"
167 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
168 params =
169 params
170 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
171 |> Enum.into(%{})
172
173 # We use an empty string as a special value to reset
174 # avatars, banners, backgrounds
175 user_image_value = fn
176 "" -> {:ok, nil}
177 value -> {:ok, value}
178 end
179
180 user_params =
181 [
182 :no_rich_text,
183 :hide_followers_count,
184 :hide_follows_count,
185 :hide_followers,
186 :hide_follows,
187 :hide_favorites,
188 :show_role,
189 :skip_thread_containment,
190 :allow_following_move,
191 :also_known_as,
192 :accepts_chat_messages
193 ]
194 |> Enum.reduce(%{}, fn key, acc ->
195 Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
196 end)
197 |> Maps.put_if_present(:name, params[:display_name])
198 |> Maps.put_if_present(:bio, params[:note])
199 |> Maps.put_if_present(:raw_bio, params[:note])
200 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
201 |> Maps.put_if_present(:banner, params[:header], user_image_value)
202 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
203 |> Maps.put_if_present(
204 :raw_fields,
205 params[:fields_attributes],
206 &{:ok, normalize_fields_attributes(&1)}
207 )
208 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
209 |> Maps.put_if_present(:default_scope, params[:default_scope])
210 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
211 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
212 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
213 end)
214 |> Maps.put_if_present(:actor_type, params[:actor_type])
215 |> Maps.put_if_present(:also_known_as, params[:also_known_as])
216 # Note: param name is indeed :locked (not an error)
217 |> Maps.put_if_present(:is_locked, params[:locked])
218 # Note: param name is indeed :discoverable (not an error)
219 |> Maps.put_if_present(:is_discoverable, params[:discoverable])
220 |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
221
222 # What happens here:
223 #
224 # We want to update the user through the pipeline, but the ActivityPub
225 # update information is not quite enough for this, because this also
226 # contains local settings that don't federate and don't even appear
227 # in the Update activity.
228 #
229 # So we first build the normal local changeset, then apply it to the
230 # user data, but don't persist it. With this, we generate the object
231 # data for our update activity. We feed this and the changeset as meta
232 # inforation into the pipeline, where they will be properly updated and
233 # federated.
234 with changeset <- User.update_changeset(user, user_params),
235 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
236 updated_object <-
237 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
238 |> Map.delete("@context"),
239 {:ok, update_data, []} <- Builder.update(user, updated_object),
240 {:ok, _update, _} <-
241 Pipeline.common_pipeline(update_data,
242 local: true,
243 user_update_changeset: changeset
244 ) do
245 render(conn, "show.json",
246 user: unpersisted_user,
247 for: unpersisted_user,
248 with_pleroma_settings: true
249 )
250 else
251 _e -> render_error(conn, :forbidden, "Invalid request")
252 end
253 end
254
255 defp normalize_fields_attributes(fields) do
256 if Enum.all?(fields, &is_tuple/1) do
257 Enum.map(fields, fn {_, v} -> v end)
258 else
259 Enum.map(fields, fn
260 %{} = field -> %{"name" => field.name, "value" => field.value}
261 field -> field
262 end)
263 end
264 end
265
266 @doc "GET /api/v1/accounts/relationships"
267 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
268 targets = User.get_all_by_ids(List.wrap(id))
269
270 render(conn, "relationships.json", user: user, targets: targets)
271 end
272
273 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
274 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
275
276 @doc "GET /api/v1/accounts/:id"
277 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
278 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
279 :visible <- User.visible_for(user, for_user) do
280 render(conn, "show.json",
281 user: user,
282 for: for_user,
283 embed_relationships: embed_relationships?(params)
284 )
285 else
286 error -> user_visibility_error(conn, error)
287 end
288 end
289
290 @doc "GET /api/v1/accounts/:id/statuses"
291 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
292 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
293 :visible <- User.visible_for(user, reading_user) do
294 params =
295 params
296 |> Map.delete(:tagged)
297 |> Map.put(:tag, params[:tagged])
298
299 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
300
301 conn
302 |> add_link_headers(activities)
303 |> put_view(StatusView)
304 |> render("index.json",
305 activities: activities,
306 for: reading_user,
307 as: :activity,
308 with_muted: Map.get(params, :with_muted, false)
309 )
310 else
311 error -> user_visibility_error(conn, error)
312 end
313 end
314
315 defp user_visibility_error(conn, error) do
316 case error do
317 :restrict_unauthenticated ->
318 render_error(conn, :unauthorized, "This API requires an authenticated user")
319
320 _ ->
321 render_error(conn, :not_found, "Can't find user")
322 end
323 end
324
325 @doc "GET /api/v1/accounts/:id/followers"
326 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
327 params =
328 params
329 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
330 |> Enum.into(%{})
331
332 followers =
333 cond do
334 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
335 user.hide_followers -> []
336 true -> MastodonAPI.get_followers(user, params)
337 end
338
339 conn
340 |> add_link_headers(followers)
341 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
342 |> render("index.json",
343 for: for_user,
344 users: followers,
345 as: :user,
346 embed_relationships: embed_relationships?(params)
347 )
348 end
349
350 @doc "GET /api/v1/accounts/:id/following"
351 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
352 params =
353 params
354 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
355 |> Enum.into(%{})
356
357 followers =
358 cond do
359 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
360 user.hide_follows -> []
361 true -> MastodonAPI.get_friends(user, params)
362 end
363
364 conn
365 |> add_link_headers(followers)
366 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
367 |> render("index.json",
368 for: for_user,
369 users: followers,
370 as: :user,
371 embed_relationships: embed_relationships?(params)
372 )
373 end
374
375 @doc "GET /api/v1/accounts/:id/lists"
376 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
377 lists = Pleroma.List.get_lists_account_belongs(user, account)
378
379 conn
380 |> put_view(ListView)
381 |> render("index.json", lists: lists)
382 end
383
384 @doc "POST /api/v1/accounts/:id/follow"
385 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
386 {:error, "Can not follow yourself"}
387 end
388
389 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
390 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
391 render(conn, "relationship.json", user: follower, target: followed)
392 else
393 {:error, message} -> json_response(conn, :forbidden, %{error: message})
394 end
395 end
396
397 @doc "POST /api/v1/accounts/:id/unfollow"
398 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
399 {:error, "Can not unfollow yourself"}
400 end
401
402 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
403 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
404 render(conn, "relationship.json", user: follower, target: followed)
405 end
406 end
407
408 @doc "POST /api/v1/accounts/:id/mute"
409 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
410 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
411 render(conn, "relationship.json", user: muter, target: muted)
412 else
413 {:error, message} -> json_response(conn, :forbidden, %{error: message})
414 end
415 end
416
417 @doc "POST /api/v1/accounts/:id/unmute"
418 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
419 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
420 render(conn, "relationship.json", user: muter, target: muted)
421 else
422 {:error, message} -> json_response(conn, :forbidden, %{error: message})
423 end
424 end
425
426 @doc "POST /api/v1/accounts/:id/block"
427 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
428 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
429 render(conn, "relationship.json", user: blocker, target: blocked)
430 else
431 {:error, message} -> json_response(conn, :forbidden, %{error: message})
432 end
433 end
434
435 @doc "POST /api/v1/accounts/:id/unblock"
436 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
437 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
438 render(conn, "relationship.json", user: blocker, target: blocked)
439 else
440 {:error, message} -> json_response(conn, :forbidden, %{error: message})
441 end
442 end
443
444 @doc "POST /api/v1/accounts/:id/note"
445 def note(
446 %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
447 _params
448 ) do
449 with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
450 render(conn, "relationship.json", user: noter, target: target)
451 end
452 end
453
454 @doc "POST /api/v1/follows"
455 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
456 case User.get_cached_by_nickname(uri) do
457 %User{} = user ->
458 conn
459 |> assign(:account, user)
460 |> follow(%{})
461
462 nil ->
463 {:error, :not_found}
464 end
465 end
466
467 @doc "GET /api/v1/mutes"
468 def mutes(%{assigns: %{user: user}} = conn, params) do
469 users =
470 user
471 |> User.muted_users_relation(_restrict_deactivated = true)
472 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
473
474 conn
475 |> add_link_headers(users)
476 |> render("index.json",
477 users: users,
478 for: user,
479 as: :user,
480 embed_relationships: embed_relationships?(params)
481 )
482 end
483
484 @doc "GET /api/v1/blocks"
485 def blocks(%{assigns: %{user: user}} = conn, params) do
486 users =
487 user
488 |> User.blocked_users_relation(_restrict_deactivated = true)
489 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
490
491 conn
492 |> add_link_headers(users)
493 |> render("index.json", users: users, for: user, as: :user)
494 end
495
496 @doc "GET /api/v1/accounts/lookup"
497 def lookup(conn, %{acct: nickname} = _params) do
498 with %User{} = user <- User.get_by_nickname(nickname) do
499 render(conn, "show.json",
500 user: user,
501 skip_visibility_check: true
502 )
503 else
504 error -> user_visibility_error(conn, error)
505 end
506 end
507
508 @doc "GET /api/v1/endorsements"
509 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
510
511 @doc "GET /api/v1/identity_proofs"
512 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
513 end