Merge branch 'develop' into fix/csp-for-captcha
[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.Plugs.EnsurePublicOrAuthenticatedPlug
19 alias Pleroma.Plugs.OAuthScopesPlug
20 alias Pleroma.Plugs.RateLimiter
21 alias Pleroma.User
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Builder
24 alias Pleroma.Web.ActivityPub.Pipeline
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.ListView
27 alias Pleroma.Web.MastodonAPI.MastodonAPI
28 alias Pleroma.Web.MastodonAPI.MastodonAPIController
29 alias Pleroma.Web.MastodonAPI.StatusView
30 alias Pleroma.Web.OAuth.OAuthView
31 alias Pleroma.Web.OAuth.Token
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, need_confirmation: true),
104 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
105 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
106 else
107 {:error, error} -> json_response(conn, :bad_request, %{error: error})
108 end
109 end
110
111 def create(%{assigns: %{app: _app}} = conn, _) do
112 render_error(conn, :bad_request, "Missing parameters")
113 end
114
115 def create(conn, _) do
116 render_error(conn, :forbidden, "Invalid credentials")
117 end
118
119 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
120
121 defp validate_email_param(_) do
122 case Pleroma.Config.get([:instance, :account_activation_required]) do
123 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
124 _ -> :ok
125 end
126 end
127
128 @doc "GET /api/v1/accounts/verify_credentials"
129 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
130 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
131
132 render(conn, "show.json",
133 user: user,
134 for: user,
135 with_pleroma_settings: true,
136 with_chat_token: chat_token
137 )
138 end
139
140 @doc "PATCH /api/v1/accounts/update_credentials"
141 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
142 params =
143 params
144 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
145 |> Enum.into(%{})
146
147 # We use an empty string as a special value to reset
148 # avatars, banners, backgrounds
149 user_image_value = fn
150 "" -> {:ok, nil}
151 value -> {:ok, value}
152 end
153
154 user_params =
155 [
156 :no_rich_text,
157 :locked,
158 :hide_followers_count,
159 :hide_follows_count,
160 :hide_followers,
161 :hide_follows,
162 :hide_favorites,
163 :show_role,
164 :skip_thread_containment,
165 :allow_following_move,
166 :discoverable
167 ]
168 |> Enum.reduce(%{}, fn key, acc ->
169 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
170 end)
171 |> Maps.put_if_present(:name, params[:display_name])
172 |> Maps.put_if_present(:bio, params[:note])
173 |> Maps.put_if_present(:raw_bio, params[:note])
174 |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
175 |> Maps.put_if_present(:banner, params[:header], user_image_value)
176 |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
177 |> Maps.put_if_present(
178 :raw_fields,
179 params[:fields_attributes],
180 &{:ok, normalize_fields_attributes(&1)}
181 )
182 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
183 |> Maps.put_if_present(:default_scope, params[:default_scope])
184 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
185 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
186 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
187 end)
188 |> Maps.put_if_present(:actor_type, params[:actor_type])
189
190 # What happens here:
191 #
192 # We want to update the user through the pipeline, but the ActivityPub
193 # update information is not quite enough for this, because this also
194 # contains local settings that don't federate and don't even appear
195 # in the Update activity.
196 #
197 # So we first build the normal local changeset, then apply it to the
198 # user data, but don't persist it. With this, we generate the object
199 # data for our update activity. We feed this and the changeset as meta
200 # inforation into the pipeline, where they will be properly updated and
201 # federated.
202 with changeset <- User.update_changeset(user, user_params),
203 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
204 updated_object <-
205 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
206 |> Map.delete("@context"),
207 {:ok, update_data, []} <- Builder.update(user, updated_object),
208 {:ok, _update, _} <-
209 Pipeline.common_pipeline(update_data,
210 local: true,
211 user_update_changeset: changeset
212 ) do
213 render(conn, "show.json",
214 user: unpersisted_user,
215 for: unpersisted_user,
216 with_pleroma_settings: true
217 )
218 else
219 _e -> render_error(conn, :forbidden, "Invalid request")
220 end
221 end
222
223 defp normalize_fields_attributes(fields) do
224 if Enum.all?(fields, &is_tuple/1) do
225 Enum.map(fields, fn {_, v} -> v end)
226 else
227 Enum.map(fields, fn
228 %{} = field -> %{"name" => field.name, "value" => field.value}
229 field -> field
230 end)
231 end
232 end
233
234 @doc "GET /api/v1/accounts/relationships"
235 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
236 targets = User.get_all_by_ids(List.wrap(id))
237
238 render(conn, "relationships.json", user: user, targets: targets)
239 end
240
241 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
242 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
243
244 @doc "GET /api/v1/accounts/:id"
245 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
246 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
247 :visible <- User.visible_for(user, for_user) do
248 render(conn, "show.json", user: user, for: for_user)
249 else
250 error -> user_visibility_error(conn, error)
251 end
252 end
253
254 @doc "GET /api/v1/accounts/:id/statuses"
255 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
256 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
257 :visible <- User.visible_for(user, reading_user) do
258 params =
259 params
260 |> Map.delete(:tagged)
261 |> Map.put(:tag, params[:tagged])
262
263 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
264
265 conn
266 |> add_link_headers(activities)
267 |> put_view(StatusView)
268 |> render("index.json",
269 activities: activities,
270 for: reading_user,
271 as: :activity
272 )
273 else
274 error -> user_visibility_error(conn, error)
275 end
276 end
277
278 defp user_visibility_error(conn, error) do
279 case error do
280 :restrict_unauthenticated ->
281 render_error(conn, :unauthorized, "This API requires an authenticated user")
282
283 _ ->
284 render_error(conn, :not_found, "Can't find user")
285 end
286 end
287
288 @doc "GET /api/v1/accounts/:id/followers"
289 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
290 params =
291 params
292 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
293 |> Enum.into(%{})
294
295 followers =
296 cond do
297 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
298 user.hide_followers -> []
299 true -> MastodonAPI.get_followers(user, params)
300 end
301
302 conn
303 |> add_link_headers(followers)
304 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
305 |> render("index.json",
306 for: for_user,
307 users: followers,
308 as: :user,
309 embed_relationships: embed_relationships?(params)
310 )
311 end
312
313 @doc "GET /api/v1/accounts/:id/following"
314 def following(%{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_friends(user, params)
323 user.hide_follows -> []
324 true -> MastodonAPI.get_friends(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/lists"
339 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
340 lists = Pleroma.List.get_lists_account_belongs(user, account)
341
342 conn
343 |> put_view(ListView)
344 |> render("index.json", lists: lists)
345 end
346
347 @doc "POST /api/v1/accounts/:id/follow"
348 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
349 {:error, "Can not follow yourself"}
350 end
351
352 def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
353 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
354 render(conn, "relationship.json", user: follower, target: followed)
355 else
356 {:error, message} -> json_response(conn, :forbidden, %{error: message})
357 end
358 end
359
360 @doc "POST /api/v1/accounts/:id/unfollow"
361 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
362 {:error, "Can not unfollow yourself"}
363 end
364
365 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
366 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
367 render(conn, "relationship.json", user: follower, target: followed)
368 end
369 end
370
371 @doc "POST /api/v1/accounts/:id/mute"
372 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
373 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
374 render(conn, "relationship.json", user: muter, target: muted)
375 else
376 {:error, message} -> json_response(conn, :forbidden, %{error: message})
377 end
378 end
379
380 @doc "POST /api/v1/accounts/:id/unmute"
381 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
382 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
383 render(conn, "relationship.json", user: muter, target: muted)
384 else
385 {:error, message} -> json_response(conn, :forbidden, %{error: message})
386 end
387 end
388
389 @doc "POST /api/v1/accounts/:id/block"
390 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
391 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
392 render(conn, "relationship.json", user: blocker, target: blocked)
393 else
394 {:error, message} -> json_response(conn, :forbidden, %{error: message})
395 end
396 end
397
398 @doc "POST /api/v1/accounts/:id/unblock"
399 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
400 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
401 render(conn, "relationship.json", user: blocker, target: blocked)
402 else
403 {:error, message} -> json_response(conn, :forbidden, %{error: message})
404 end
405 end
406
407 @doc "POST /api/v1/follows"
408 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
409 case User.get_cached_by_nickname(uri) do
410 %User{} = user ->
411 conn
412 |> assign(:account, user)
413 |> follow(%{})
414
415 nil ->
416 {:error, :not_found}
417 end
418 end
419
420 @doc "GET /api/v1/mutes"
421 def mutes(%{assigns: %{user: user}} = conn, _) do
422 users = User.muted_users(user, _restrict_deactivated = true)
423 render(conn, "index.json", users: users, for: user, as: :user)
424 end
425
426 @doc "GET /api/v1/blocks"
427 def blocks(%{assigns: %{user: user}} = conn, _) do
428 users = User.blocked_users(user, _restrict_deactivated = true)
429 render(conn, "index.json", users: users, for: user, as: :user)
430 end
431
432 @doc "GET /api/v1/endorsements"
433 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
434
435 @doc "GET /api/v1/identity_proofs"
436 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
437 end