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