Transmogrifier: Extract user update handling tests.
[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.CommonAPI
24 alias Pleroma.Web.MastodonAPI.ListView
25 alias Pleroma.Web.MastodonAPI.MastodonAPI
26 alias Pleroma.Web.MastodonAPI.MastodonAPIController
27 alias Pleroma.Web.MastodonAPI.StatusView
28 alias Pleroma.Web.OAuth.Token
29 alias Pleroma.Web.TwitterAPI.TwitterAPI
30
31 plug(Pleroma.Web.ApiSpec.CastAndValidate)
32
33 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
34
35 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
36
37 plug(
38 OAuthScopesPlug,
39 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
40 when action in [:show, :followers, :following]
41 )
42
43 plug(
44 OAuthScopesPlug,
45 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
46 when action == :statuses
47 )
48
49 plug(
50 OAuthScopesPlug,
51 %{scopes: ["read:accounts"]}
52 when action in [:verify_credentials, :endorsements, :identity_proofs]
53 )
54
55 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
56
57 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
58
59 plug(
60 OAuthScopesPlug,
61 %{scopes: ["follow", "read:blocks"]} when action == :blocks
62 )
63
64 plug(
65 OAuthScopesPlug,
66 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
67 )
68
69 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
70
71 plug(
72 OAuthScopesPlug,
73 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
74 )
75
76 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
77
78 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
79
80 @relationship_actions [:follow, :unfollow]
81 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
82
83 plug(
84 RateLimiter,
85 [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
86 )
87
88 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
89 plug(RateLimiter, [name: :app_account_creation] when action == :create)
90 plug(:assign_account_by_id when action in @needs_account)
91
92 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
93
94 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
95
96 @doc "POST /api/v1/accounts"
97 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
98 with :ok <- validate_email_param(params),
99 :ok <- TwitterAPI.validate_captcha(app, params),
100 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
101 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
102 json(conn, %{
103 token_type: "Bearer",
104 access_token: token.token,
105 scope: app.scopes,
106 created_at: Token.Utils.format_created_at(token)
107 })
108 else
109 {:error, error} -> json_response(conn, :bad_request, %{error: error})
110 end
111 end
112
113 def create(%{assigns: %{app: _app}} = conn, _) do
114 render_error(conn, :bad_request, "Missing parameters")
115 end
116
117 def create(conn, _) do
118 render_error(conn, :forbidden, "Invalid credentials")
119 end
120
121 defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
122
123 defp validate_email_param(_) do
124 case Pleroma.Config.get([:instance, :account_activation_required]) do
125 true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
126 _ -> :ok
127 end
128 end
129
130 @doc "GET /api/v1/accounts/verify_credentials"
131 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
132 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
133
134 render(conn, "show.json",
135 user: user,
136 for: user,
137 with_pleroma_settings: true,
138 with_chat_token: chat_token
139 )
140 end
141
142 @doc "PATCH /api/v1/accounts/update_credentials"
143 def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
144 params =
145 params
146 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
147 |> Enum.into(%{})
148
149 user_params =
150 [
151 :no_rich_text,
152 :locked,
153 :hide_followers_count,
154 :hide_follows_count,
155 :hide_followers,
156 :hide_follows,
157 :hide_favorites,
158 :show_role,
159 :skip_thread_containment,
160 :allow_following_move,
161 :discoverable
162 ]
163 |> Enum.reduce(%{}, fn key, acc ->
164 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
165 end)
166 |> Maps.put_if_present(:name, params[:display_name])
167 |> Maps.put_if_present(:bio, params[:note])
168 |> Maps.put_if_present(:avatar, params[:avatar])
169 |> Maps.put_if_present(:banner, params[:header])
170 |> Maps.put_if_present(:background, params[:pleroma_background_image])
171 |> Maps.put_if_present(
172 :raw_fields,
173 params[:fields_attributes],
174 &{:ok, normalize_fields_attributes(&1)}
175 )
176 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
177 |> Maps.put_if_present(:default_scope, params[:default_scope])
178 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
179 |> Maps.put_if_present(:actor_type, params[:actor_type])
180
181 changeset = User.update_changeset(user, user_params)
182
183 with {:ok, user} <- User.update_and_set_cache(changeset) do
184 user
185 |> build_update_activity_params()
186 |> ActivityPub.update()
187
188 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
189 else
190 _e -> render_error(conn, :forbidden, "Invalid request")
191 end
192 end
193
194 # Hotfix, handling will be redone with the pipeline
195 defp build_update_activity_params(user) do
196 object =
197 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
198 |> Map.delete("@context")
199
200 %{
201 local: true,
202 to: [user.follower_address],
203 cc: [],
204 object: object,
205 actor: user.ap_id
206 }
207 end
208
209 defp normalize_fields_attributes(fields) do
210 if Enum.all?(fields, &is_tuple/1) do
211 Enum.map(fields, fn {_, v} -> v end)
212 else
213 Enum.map(fields, fn
214 %{} = field -> %{"name" => field.name, "value" => field.value}
215 field -> field
216 end)
217 end
218 end
219
220 @doc "GET /api/v1/accounts/relationships"
221 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
222 targets = User.get_all_by_ids(List.wrap(id))
223
224 render(conn, "relationships.json", user: user, targets: targets)
225 end
226
227 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
228 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
229
230 @doc "GET /api/v1/accounts/:id"
231 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
232 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
233 true <- User.visible_for?(user, for_user) do
234 render(conn, "show.json", user: user, for: for_user)
235 else
236 _e -> render_error(conn, :not_found, "Can't find user")
237 end
238 end
239
240 @doc "GET /api/v1/accounts/:id/statuses"
241 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
242 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
243 true <- User.visible_for?(user, reading_user) do
244 params =
245 params
246 |> Map.delete(:tagged)
247 |> Map.put(:tag, params[:tagged])
248
249 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
250
251 conn
252 |> add_link_headers(activities)
253 |> put_view(StatusView)
254 |> render("index.json",
255 activities: activities,
256 for: reading_user,
257 as: :activity
258 )
259 else
260 _e -> render_error(conn, :not_found, "Can't find user")
261 end
262 end
263
264 @doc "GET /api/v1/accounts/:id/followers"
265 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
266 params =
267 params
268 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
269 |> Enum.into(%{})
270
271 followers =
272 cond do
273 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
274 user.hide_followers -> []
275 true -> MastodonAPI.get_followers(user, params)
276 end
277
278 conn
279 |> add_link_headers(followers)
280 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
281 |> render("index.json",
282 for: for_user,
283 users: followers,
284 as: :user,
285 embed_relationships: embed_relationships?(params)
286 )
287 end
288
289 @doc "GET /api/v1/accounts/:id/following"
290 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
291 params =
292 params
293 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
294 |> Enum.into(%{})
295
296 followers =
297 cond do
298 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
299 user.hide_follows -> []
300 true -> MastodonAPI.get_friends(user, params)
301 end
302
303 conn
304 |> add_link_headers(followers)
305 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
306 |> render("index.json",
307 for: for_user,
308 users: followers,
309 as: :user,
310 embed_relationships: embed_relationships?(params)
311 )
312 end
313
314 @doc "GET /api/v1/accounts/:id/lists"
315 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
316 lists = Pleroma.List.get_lists_account_belongs(user, account)
317
318 conn
319 |> put_view(ListView)
320 |> render("index.json", lists: lists)
321 end
322
323 @doc "POST /api/v1/accounts/:id/follow"
324 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
325 {:error, "Can not follow yourself"}
326 end
327
328 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
329 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
330 render(conn, "relationship.json", user: follower, target: followed)
331 else
332 {:error, message} -> json_response(conn, :forbidden, %{error: message})
333 end
334 end
335
336 @doc "POST /api/v1/accounts/:id/unfollow"
337 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
338 {:error, "Can not unfollow yourself"}
339 end
340
341 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
342 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
343 render(conn, "relationship.json", user: follower, target: followed)
344 end
345 end
346
347 @doc "POST /api/v1/accounts/:id/mute"
348 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
349 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
350 render(conn, "relationship.json", user: muter, target: muted)
351 else
352 {:error, message} -> json_response(conn, :forbidden, %{error: message})
353 end
354 end
355
356 @doc "POST /api/v1/accounts/:id/unmute"
357 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
358 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
359 render(conn, "relationship.json", user: muter, target: muted)
360 else
361 {:error, message} -> json_response(conn, :forbidden, %{error: message})
362 end
363 end
364
365 @doc "POST /api/v1/accounts/:id/block"
366 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
367 with {:ok, _user_block} <- User.block(blocker, blocked),
368 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
369 render(conn, "relationship.json", user: blocker, target: blocked)
370 else
371 {:error, message} -> json_response(conn, :forbidden, %{error: message})
372 end
373 end
374
375 @doc "POST /api/v1/accounts/:id/unblock"
376 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
377 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
378 render(conn, "relationship.json", user: blocker, target: blocked)
379 else
380 {:error, message} -> json_response(conn, :forbidden, %{error: message})
381 end
382 end
383
384 @doc "POST /api/v1/follows"
385 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
386 case User.get_cached_by_nickname(uri) do
387 %User{} = user ->
388 conn
389 |> assign(:account, user)
390 |> follow(%{})
391
392 nil ->
393 {:error, :not_found}
394 end
395 end
396
397 @doc "GET /api/v1/mutes"
398 def mutes(%{assigns: %{user: user}} = conn, _) do
399 users = User.muted_users(user, _restrict_deactivated = true)
400 render(conn, "index.json", users: users, for: user, as: :user)
401 end
402
403 @doc "GET /api/v1/blocks"
404 def blocks(%{assigns: %{user: user}} = conn, _) do
405 users = User.blocked_users(user, _restrict_deactivated = true)
406 render(conn, "index.json", users: users, for: user, as: :user)
407 end
408
409 @doc "GET /api/v1/endorsements"
410 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
411
412 @doc "GET /api/v1/identity_proofs"
413 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
414 end