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