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