ffa82731f2d8e90adb6d74c3c7b976973667f22d
[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 error -> user_visibility_error(conn, error)
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 error -> user_visibility_error(conn, error)
254 end
255 end
256
257 defp user_visibility_error(conn, error) do
258 case error do
259 :deactivated ->
260 render_error(conn, :gone, "")
261
262 :restrict_unauthenticated ->
263 render_error(conn, :unauthorized, "This API requires an authenticated user")
264
265 _ ->
266 render_error(conn, :not_found, "Can't find user")
267 end
268 end
269
270 @doc "GET /api/v1/accounts/:id/followers"
271 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
272 params =
273 params
274 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
275 |> Enum.into(%{})
276
277 followers =
278 cond do
279 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
280 user.hide_followers -> []
281 true -> MastodonAPI.get_followers(user, params)
282 end
283
284 conn
285 |> add_link_headers(followers)
286 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
287 |> render("index.json",
288 for: for_user,
289 users: followers,
290 as: :user,
291 embed_relationships: embed_relationships?(params)
292 )
293 end
294
295 @doc "GET /api/v1/accounts/:id/following"
296 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
297 params =
298 params
299 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
300 |> Enum.into(%{})
301
302 followers =
303 cond do
304 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
305 user.hide_follows -> []
306 true -> MastodonAPI.get_friends(user, params)
307 end
308
309 conn
310 |> add_link_headers(followers)
311 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
312 |> render("index.json",
313 for: for_user,
314 users: followers,
315 as: :user,
316 embed_relationships: embed_relationships?(params)
317 )
318 end
319
320 @doc "GET /api/v1/accounts/:id/lists"
321 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
322 lists = Pleroma.List.get_lists_account_belongs(user, account)
323
324 conn
325 |> put_view(ListView)
326 |> render("index.json", lists: lists)
327 end
328
329 @doc "POST /api/v1/accounts/:id/follow"
330 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
331 {:error, "Can not follow yourself"}
332 end
333
334 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
335 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
336 render(conn, "relationship.json", user: follower, target: followed)
337 else
338 {:error, message} -> json_response(conn, :forbidden, %{error: message})
339 end
340 end
341
342 @doc "POST /api/v1/accounts/:id/unfollow"
343 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
344 {:error, "Can not unfollow yourself"}
345 end
346
347 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
348 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
349 render(conn, "relationship.json", user: follower, target: followed)
350 end
351 end
352
353 @doc "POST /api/v1/accounts/:id/mute"
354 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
355 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
356 render(conn, "relationship.json", user: muter, target: muted)
357 else
358 {:error, message} -> json_response(conn, :forbidden, %{error: message})
359 end
360 end
361
362 @doc "POST /api/v1/accounts/:id/unmute"
363 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
364 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
365 render(conn, "relationship.json", user: muter, target: muted)
366 else
367 {:error, message} -> json_response(conn, :forbidden, %{error: message})
368 end
369 end
370
371 @doc "POST /api/v1/accounts/:id/block"
372 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
373 with {:ok, _user_block} <- User.block(blocker, blocked),
374 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
375 render(conn, "relationship.json", user: blocker, target: blocked)
376 else
377 {:error, message} -> json_response(conn, :forbidden, %{error: message})
378 end
379 end
380
381 @doc "POST /api/v1/accounts/:id/unblock"
382 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
383 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
384 render(conn, "relationship.json", user: blocker, target: blocked)
385 else
386 {:error, message} -> json_response(conn, :forbidden, %{error: message})
387 end
388 end
389
390 @doc "POST /api/v1/follows"
391 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
392 case User.get_cached_by_nickname(uri) do
393 %User{} = user ->
394 conn
395 |> assign(:account, user)
396 |> follow(%{})
397
398 nil ->
399 {:error, :not_found}
400 end
401 end
402
403 @doc "GET /api/v1/mutes"
404 def mutes(%{assigns: %{user: user}} = conn, _) do
405 users = User.muted_users(user, _restrict_deactivated = true)
406 render(conn, "index.json", users: users, for: user, as: :user)
407 end
408
409 @doc "GET /api/v1/blocks"
410 def blocks(%{assigns: %{user: user}} = conn, _) do
411 users = User.blocked_users(user, _restrict_deactivated = true)
412 render(conn, "index.json", users: users, for: user, as: :user)
413 end
414
415 @doc "GET /api/v1/endorsements"
416 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
417
418 @doc "GET /api/v1/identity_proofs"
419 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
420 end