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