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