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