Merge branch 'hide-muted-reactions' into 'develop'
[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 with_muted: Map.get(params, :with_muted, false)
297 )
298 else
299 error -> user_visibility_error(conn, error)
300 end
301 end
302
303 defp user_visibility_error(conn, error) do
304 case error do
305 :restrict_unauthenticated ->
306 render_error(conn, :unauthorized, "This API requires an authenticated user")
307
308 _ ->
309 render_error(conn, :not_found, "Can't find user")
310 end
311 end
312
313 @doc "GET /api/v1/accounts/:id/followers"
314 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
315 params =
316 params
317 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
318 |> Enum.into(%{})
319
320 followers =
321 cond do
322 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
323 user.hide_followers -> []
324 true -> MastodonAPI.get_followers(user, params)
325 end
326
327 conn
328 |> add_link_headers(followers)
329 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
330 |> render("index.json",
331 for: for_user,
332 users: followers,
333 as: :user,
334 embed_relationships: embed_relationships?(params)
335 )
336 end
337
338 @doc "GET /api/v1/accounts/:id/following"
339 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
340 params =
341 params
342 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
343 |> Enum.into(%{})
344
345 followers =
346 cond do
347 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
348 user.hide_follows -> []
349 true -> MastodonAPI.get_friends(user, params)
350 end
351
352 conn
353 |> add_link_headers(followers)
354 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
355 |> render("index.json",
356 for: for_user,
357 users: followers,
358 as: :user,
359 embed_relationships: embed_relationships?(params)
360 )
361 end
362
363 @doc "GET /api/v1/accounts/:id/lists"
364 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
365 lists = Pleroma.List.get_lists_account_belongs(user, account)
366
367 conn
368 |> put_view(ListView)
369 |> render("index.json", lists: lists)
370 end
371
372 @doc "POST /api/v1/accounts/:id/follow"
373 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
374 {:error, "Can not follow yourself"}
375 end
376
377 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
378 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
379 render(conn, "relationship.json", user: follower, target: followed)
380 else
381 {:error, message} -> json_response(conn, :forbidden, %{error: message})
382 end
383 end
384
385 @doc "POST /api/v1/accounts/:id/unfollow"
386 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
387 {:error, "Can not unfollow yourself"}
388 end
389
390 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
391 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
392 render(conn, "relationship.json", user: follower, target: followed)
393 end
394 end
395
396 @doc "POST /api/v1/accounts/:id/mute"
397 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
398 with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
399 render(conn, "relationship.json", user: muter, target: muted)
400 else
401 {:error, message} -> json_response(conn, :forbidden, %{error: message})
402 end
403 end
404
405 @doc "POST /api/v1/accounts/:id/unmute"
406 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
407 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
408 render(conn, "relationship.json", user: muter, target: muted)
409 else
410 {:error, message} -> json_response(conn, :forbidden, %{error: message})
411 end
412 end
413
414 @doc "POST /api/v1/accounts/:id/block"
415 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
416 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
417 render(conn, "relationship.json", user: blocker, target: blocked)
418 else
419 {:error, message} -> json_response(conn, :forbidden, %{error: message})
420 end
421 end
422
423 @doc "POST /api/v1/accounts/:id/unblock"
424 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
425 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
426 render(conn, "relationship.json", user: blocker, target: blocked)
427 else
428 {:error, message} -> json_response(conn, :forbidden, %{error: message})
429 end
430 end
431
432 @doc "POST /api/v1/follows"
433 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
434 case User.get_cached_by_nickname(uri) do
435 %User{} = user ->
436 conn
437 |> assign(:account, user)
438 |> follow(%{})
439
440 nil ->
441 {:error, :not_found}
442 end
443 end
444
445 @doc "GET /api/v1/mutes"
446 def mutes(%{assigns: %{user: user}} = conn, params) do
447 users =
448 user
449 |> User.muted_users_relation(_restrict_deactivated = true)
450 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
451
452 conn
453 |> add_link_headers(users)
454 |> render("index.json", users: users, for: user, as: :user)
455 end
456
457 @doc "GET /api/v1/blocks"
458 def blocks(%{assigns: %{user: user}} = conn, params) do
459 users =
460 user
461 |> User.blocked_users_relation(_restrict_deactivated = true)
462 |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
463
464 conn
465 |> add_link_headers(users)
466 |> render("index.json", users: users, for: user, as: :user)
467 end
468
469 @doc "GET /api/v1/endorsements"
470 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
471
472 @doc "GET /api/v1/identity_proofs"
473 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
474 end