f3b6b357147a3a5952b17b7fa8a0ede79a9ad942
[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.OAuth.Token
30 alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
31 alias Pleroma.Web.Plugs.OAuthScopesPlug
32 alias Pleroma.Web.Plugs.RateLimiter
33 alias Pleroma.Web.TwitterAPI.TwitterAPI
34
35 plug(Pleroma.Web.ApiSpec.CastAndValidate)
36
37 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
38
39 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
40
41 plug(
42 OAuthScopesPlug,
43 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
44 when action in [:show, :followers, :following]
45 )
46
47 plug(
48 OAuthScopesPlug,
49 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
50 when action == :statuses
51 )
52
53 plug(
54 OAuthScopesPlug,
55 %{scopes: ["read:accounts"]}
56 when action in [:verify_credentials, :endorsements, :identity_proofs]
57 )
58
59 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
60
61 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
62
63 plug(
64 OAuthScopesPlug,
65 %{scopes: ["follow", "read:blocks"]} when action == :blocks
66 )
67
68 plug(
69 OAuthScopesPlug,
70 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
71 )
72
73 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
74
75 plug(
76 OAuthScopesPlug,
77 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
78 )
79
80 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
81
82 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
83
84 @relationship_actions [:follow, :unfollow]
85 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
86
87 plug(
88 RateLimiter,
89 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
90 )
91
92 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
93 plug(RateLimiter, [name: :app_account_creation] when action == :create)
94 plug(:assign_account_by_id when action in @needs_account)
95
96 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
97
98 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
99
100 @doc "POST /api/v1/accounts"
101 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
102 with :ok <- validate_email_param(params),
103 :ok <- TwitterAPI.validate_captcha(app, params),
104 {:ok, user} <- TwitterAPI.register_user(params),
105 {_, {:ok, token}} <-
106 {:login, OAuthController.login(user, app, app.scopes)} do
107 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
108 else
109 {:login, {:account_status, :confirmation_pending}} ->
110 json_response(conn, :ok, %{
111 message: "You have been registered. Please check your email for further instructions.",
112 identifier: "missing_confirmed_email"
113 })
114
115 {:login, {:account_status, :approval_pending}} ->
116 json_response(conn, :ok, %{
117 message:
118 "You have been registered. You'll be able to log in once your account is approved.",
119 identifier: "awaiting_approval"
120 })
121
122 {:login, _} ->
123 json_response(conn, :ok, %{
124 message:
125 "You have been registered. Some post-registration steps may be pending. " <>
126 "Please log in manually.",
127 identifier: "manual_login_required"
128 })
129
130 {:error, error} ->
131 json_response(conn, :bad_request, %{error: error})
132 end
133 end
134
135 def create(%{assigns: %{app: _app}} = conn, _) do
136 render_error(conn, :bad_request, "Missing parameters")
137 end
138
139 def create(conn, _) do
140 render_error(conn, :forbidden, "Invalid credentials")
141 end
142
143 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
144
145 defp validate_email_param(_) do
146 case Pleroma.Config.get([:instance, :account_activation_required]) do
147 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
148 _ -> :ok
149 end
150 end
151
152 @doc "GET /api/v1/accounts/verify_credentials"
153 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
154 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
155
156 render(conn, "show.json",
157 user: user,
158 for: user,
159 with_pleroma_settings: true,
160 with_chat_token: chat_token
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 user_params =
179 [
180 :no_rich_text,
181 :locked,
182 :hide_followers_count,
183 :hide_follows_count,
184 :hide_followers,
185 :hide_follows,
186 :hide_favorites,
187 :show_role,
188 :skip_thread_containment,
189 :allow_following_move,
190 :discoverable,
191 :accepts_chat_messages
192 ]
193 |> Enum.reduce(%{}, fn key, acc ->
194 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
195 end)
196 |> Maps.put_if_present(:name, params[:display_name])
197 |> Maps.put_if_present(:bio, params[:note])
198 |> Maps.put_if_present(:raw_bio, params[:note])
199 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
200 |> Maps.put_if_present(:banner, params[:header], user_image_value)
201 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
202 |> Maps.put_if_present(
203 :raw_fields,
204 params[:fields_attributes],
205 &{:ok, normalize_fields_attributes(&1)}
206 )
207 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
208 |> Maps.put_if_present(:default_scope, params[:default_scope])
209 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
210 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
211 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
212 end)
213 |> Maps.put_if_present(:actor_type, params[:actor_type])
214
215 # What happens here:
216 #
217 # We want to update the user through the pipeline, but the ActivityPub
218 # update information is not quite enough for this, because this also
219 # contains local settings that don't federate and don't even appear
220 # in the Update activity.
221 #
222 # So we first build the normal local changeset, then apply it to the
223 # user data, but don't persist it. With this, we generate the object
224 # data for our update activity. We feed this and the changeset as meta
225 # inforation into the pipeline, where they will be properly updated and
226 # federated.
227 with changeset <- User.update_changeset(user, user_params),
228 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
229 updated_object <-
230 Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
231 |> Map.delete("@context"),
232 {:ok, update_data, []} <- Builder.update(user, updated_object),
233 {:ok, _update, _} <-
234 Pipeline.common_pipeline(update_data,
235 local: true,
236 user_update_changeset: changeset
237 ) do
238 render(conn, "show.json",
239 user: unpersisted_user,
240 for: unpersisted_user,
241 with_pleroma_settings: true
242 )
243 else
244 _e -> render_error(conn, :forbidden, "Invalid request")
245 end
246 end
247
248 defp normalize_fields_attributes(fields) do
249 if Enum.all?(fields, &is_tuple/1) do
250 Enum.map(fields, fn {_, v} -> v end)
251 else
252 Enum.map(fields, fn
253 %{} = field -> %{"name" => field.name, "value" => field.value}
254 field -> field
255 end)
256 end
257 end
258
259 @doc "GET /api/v1/accounts/relationships"
260 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
261 targets = User.get_all_by_ids(List.wrap(id))
262
263 render(conn, "relationships.json", user: user, targets: targets)
264 end
265
266 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
267 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
268
269 @doc "GET /api/v1/accounts/:id"
270 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
271 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
272 :visible <- User.visible_for(user, for_user) do
273 render(conn, "show.json", user: user, for: for_user)
274 else
275 error -> user_visibility_error(conn, error)
276 end
277 end
278
279 @doc "GET /api/v1/accounts/:id/statuses"
280 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
281 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
282 :visible <- User.visible_for(user, reading_user) do
283 params =
284 params
285 |> Map.delete(:tagged)
286 |> Map.put(:tag, params[:tagged])
287
288 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
289
290 conn
291 |> add_link_headers(activities)
292 |> put_view(StatusView)
293 |> render("index.json",
294 activities: activities,
295 for: reading_user,
296 as: :activity
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.notifications) 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, _) do
447 users = User.muted_users(user, _restrict_deactivated = true)
448 render(conn, "index.json", users: users, for: user, as: :user)
449 end
450
451 @doc "GET /api/v1/blocks"
452 def blocks(%{assigns: %{user: user}} = conn, _) do
453 users = User.blocked_users(user, _restrict_deactivated = true)
454 render(conn, "index.json", users: users, for: user, as: :user)
455 end
456
457 @doc "GET /api/v1/endorsements"
458 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
459
460 @doc "GET /api/v1/identity_proofs"
461 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
462 end