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