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