Add specs for AccountController.mute and AccountController.unmute
[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 ]
101 )
102
103 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
104
105 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
106
107 @doc "POST /api/v1/accounts"
108 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
109 params =
110 params
111 |> Map.take([
112 :email,
113 :bio,
114 :captcha_solution,
115 :captcha_token,
116 :captcha_answer_data,
117 :token,
118 :password,
119 :fullname
120 ])
121 |> Map.put(:nickname, params.username)
122 |> Map.put(:fullname, params.fullname || params.username)
123 |> Map.put(:bio, params.bio || "")
124 |> Map.put(:confirm, params.password)
125
126 with :ok <- validate_email_param(params),
127 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
128 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
129 json(conn, %{
130 token_type: "Bearer",
131 access_token: token.token,
132 scope: app.scopes,
133 created_at: Token.Utils.format_created_at(token)
134 })
135 else
136 {:error, errors} -> json_response(conn, :bad_request, errors)
137 end
138 end
139
140 def create(%{assigns: %{app: _app}} = conn, _) do
141 render_error(conn, :bad_request, "Missing parameters")
142 end
143
144 def create(conn, _) do
145 render_error(conn, :forbidden, "Invalid credentials")
146 end
147
148 defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
149
150 defp validate_email_param(_) do
151 case Pleroma.Config.get([:instance, :account_activation_required]) do
152 true -> {:error, %{"error" => "Missing parameters"}}
153 _ -> :ok
154 end
155 end
156
157 @doc "GET /api/v1/accounts/verify_credentials"
158 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
159 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
160
161 render(conn, "show.json",
162 user: user,
163 for: user,
164 with_pleroma_settings: true,
165 with_chat_token: chat_token
166 )
167 end
168
169 @doc "PATCH /api/v1/accounts/update_credentials"
170 def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
171 user = original_user
172
173 params =
174 params
175 |> Map.from_struct()
176 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
177 |> Enum.into(%{})
178
179 user_params =
180 [
181 :no_rich_text,
182 :locked,
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 :discoverable
192 ]
193 |> Enum.reduce(%{}, fn key, acc ->
194 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
195 end)
196 |> add_if_present(params, :display_name, :name)
197 |> add_if_present(params, :note, :bio)
198 |> add_if_present(params, :avatar, :avatar)
199 |> add_if_present(params, :header, :banner)
200 |> add_if_present(params, :pleroma_background_image, :background)
201 |> add_if_present(
202 params,
203 :fields_attributes,
204 :raw_fields,
205 &{:ok, normalize_fields_attributes(&1)}
206 )
207 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
208 |> add_if_present(params, :default_scope, :default_scope)
209 |> add_if_present(params, :actor_type, :actor_type)
210
211 changeset = User.update_changeset(user, user_params)
212
213 with {:ok, user} <- User.update_and_set_cache(changeset) do
214 if original_user != user, do: CommonAPI.update(user)
215
216 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
217 else
218 _e -> render_error(conn, :forbidden, "Invalid request")
219 end
220 end
221
222 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
223 with true <- Map.has_key?(params, params_field),
224 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
225 Map.put(map, map_field, new_value)
226 else
227 _ -> map
228 end
229 end
230
231 defp normalize_fields_attributes(fields) do
232 if Enum.all?(fields, &is_tuple/1) do
233 Enum.map(fields, fn {_, v} -> v end)
234 else
235 Enum.map(fields, fn
236 %Pleroma.Web.ApiSpec.Schemas.AccountAttributeField{} = field ->
237 %{"name" => field.name, "value" => field.value}
238
239 field ->
240 field
241 end)
242 end
243 end
244
245 @doc "GET /api/v1/accounts/relationships"
246 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
247 targets = User.get_all_by_ids(List.wrap(id))
248
249 render(conn, "relationships.json", user: user, targets: targets)
250 end
251
252 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
253 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
254
255 @doc "GET /api/v1/accounts/:id"
256 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
257 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
258 true <- User.visible_for?(user, for_user) do
259 render(conn, "show.json", user: user, for: for_user)
260 else
261 _e -> render_error(conn, :not_found, "Can't find user")
262 end
263 end
264
265 @doc "GET /api/v1/accounts/:id/statuses"
266 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
267 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
268 true <- User.visible_for?(user, reading_user) do
269 params =
270 params
271 |> Map.delete(:tagged)
272 |> Enum.filter(&(not is_nil(&1)))
273 |> Map.new(fn {key, value} -> {to_string(key), value} end)
274 |> Map.put("tag", params[:tagged])
275
276 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
277
278 conn
279 |> add_link_headers(activities)
280 |> put_view(StatusView)
281 |> render("index.json",
282 activities: activities,
283 for: reading_user,
284 as: :activity,
285 skip_relationships: skip_relationships?(params)
286 )
287 else
288 _e -> render_error(conn, :not_found, "Can't find user")
289 end
290 end
291
292 @doc "GET /api/v1/accounts/:id/followers"
293 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
294 params =
295 params
296 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
297 |> Enum.into(%{})
298
299 followers =
300 cond do
301 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
302 user.hide_followers -> []
303 true -> MastodonAPI.get_followers(user, params)
304 end
305
306 conn
307 |> add_link_headers(followers)
308 |> render("index.json", for: for_user, users: followers, as: :user)
309 end
310
311 @doc "GET /api/v1/accounts/:id/following"
312 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
313 params =
314 params
315 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
316 |> Enum.into(%{})
317
318 followers =
319 cond do
320 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
321 user.hide_follows -> []
322 true -> MastodonAPI.get_friends(user, params)
323 end
324
325 conn
326 |> add_link_headers(followers)
327 |> render("index.json", for: for_user, users: followers, as: :user)
328 end
329
330 @doc "GET /api/v1/accounts/:id/lists"
331 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
332 lists = Pleroma.List.get_lists_account_belongs(user, account)
333
334 conn
335 |> put_view(ListView)
336 |> render("index.json", lists: lists)
337 end
338
339 @doc "POST /api/v1/accounts/:id/follow"
340 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
341 {:error, :not_found}
342 end
343
344 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
345 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
346 render(conn, "relationship.json", user: follower, target: followed)
347 else
348 {:error, message} -> json_response(conn, :forbidden, %{error: message})
349 end
350 end
351
352 @doc "POST /api/v1/accounts/:id/unfollow"
353 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
354 {:error, :not_found}
355 end
356
357 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
358 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
359 render(conn, "relationship.json", user: follower, target: followed)
360 end
361 end
362
363 @doc "POST /api/v1/accounts/:id/mute"
364 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
365 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
366 render(conn, "relationship.json", user: muter, target: muted)
367 else
368 {:error, message} -> json_response(conn, :forbidden, %{error: message})
369 end
370 end
371
372 @doc "POST /api/v1/accounts/:id/unmute"
373 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
374 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
375 render(conn, "relationship.json", user: muter, target: muted)
376 else
377 {:error, message} -> json_response(conn, :forbidden, %{error: message})
378 end
379 end
380
381 @doc "POST /api/v1/accounts/:id/block"
382 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
383 with {:ok, _user_block} <- User.block(blocker, blocked),
384 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
385 render(conn, "relationship.json", user: blocker, target: blocked)
386 else
387 {:error, message} -> json_response(conn, :forbidden, %{error: message})
388 end
389 end
390
391 @doc "POST /api/v1/accounts/:id/unblock"
392 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
393 with {:ok, _user_block} <- User.unblock(blocker, blocked),
394 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
395 render(conn, "relationship.json", user: blocker, target: blocked)
396 else
397 {:error, message} -> json_response(conn, :forbidden, %{error: message})
398 end
399 end
400
401 @doc "POST /api/v1/follows"
402 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
403 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
404 {_, true} <- {:followed, follower.id != followed.id},
405 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
406 render(conn, "show.json", user: followed, for: follower)
407 else
408 {:followed, _} -> {:error, :not_found}
409 {:error, message} -> json_response(conn, :forbidden, %{error: message})
410 end
411 end
412
413 @doc "GET /api/v1/mutes"
414 def mutes(%{assigns: %{user: user}} = conn, _) do
415 users = User.muted_users(user, _restrict_deactivated = true)
416 render(conn, "index.json", users: users, for: user, as: :user)
417 end
418
419 @doc "GET /api/v1/blocks"
420 def blocks(%{assigns: %{user: user}} = conn, _) do
421 users = User.blocked_users(user, _restrict_deactivated = true)
422 render(conn, "index.json", users: users, for: user, as: :user)
423 end
424
425 @doc "GET /api/v1/endorsements"
426 def endorsements(conn, params),
427 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
428 end