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