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