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