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