Merge branch 'feature/1584-client-captcha-options' 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.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 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
87
88 @doc "POST /api/v1/accounts"
89 def create(
90 %{assigns: %{app: app}} = conn,
91 %{"username" => nickname, "password" => _, "agreement" => true} = params
92 ) do
93 params =
94 params
95 |> Map.take([
96 "email",
97 "captcha_solution",
98 "captcha_token",
99 "captcha_answer_data",
100 "token",
101 "password"
102 ])
103 |> Map.put("nickname", nickname)
104 |> Map.put("fullname", params["fullname"] || nickname)
105 |> Map.put("bio", params["bio"] || "")
106 |> Map.put("confirm", params["password"])
107 |> Map.put("trusted_app", app.trusted)
108
109 with :ok <- validate_email_param(params),
110 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
111 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
112 json(conn, %{
113 token_type: "Bearer",
114 access_token: token.token,
115 scope: app.scopes,
116 created_at: Token.Utils.format_created_at(token)
117 })
118 else
119 {:error, errors} -> json_response(conn, :bad_request, errors)
120 end
121 end
122
123 def create(%{assigns: %{app: _app}} = conn, _) do
124 render_error(conn, :bad_request, "Missing parameters")
125 end
126
127 def create(conn, _) do
128 render_error(conn, :forbidden, "Invalid credentials")
129 end
130
131 defp validate_email_param(%{"email" => _}), do: :ok
132
133 defp validate_email_param(_) do
134 case Pleroma.Config.get([:instance, :account_activation_required]) do
135 true -> {:error, %{"error" => "Missing parameters"}}
136 _ -> :ok
137 end
138 end
139
140 @doc "GET /api/v1/accounts/verify_credentials"
141 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
142 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
143
144 render(conn, "show.json",
145 user: user,
146 for: user,
147 with_pleroma_settings: true,
148 with_chat_token: chat_token
149 )
150 end
151
152 @doc "PATCH /api/v1/accounts/update_credentials"
153 def update_credentials(%{assigns: %{user: user}} = conn, params) do
154 user_params =
155 [
156 :no_rich_text,
157 :locked,
158 :hide_followers_count,
159 :hide_follows_count,
160 :hide_followers,
161 :hide_follows,
162 :hide_favorites,
163 :show_role,
164 :skip_thread_containment,
165 :allow_following_move,
166 :discoverable
167 ]
168 |> Enum.reduce(%{}, fn key, acc ->
169 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
170 end)
171 |> add_if_present(params, "display_name", :name)
172 |> add_if_present(params, "note", :bio)
173 |> add_if_present(params, "avatar", :avatar)
174 |> add_if_present(params, "header", :banner)
175 |> add_if_present(params, "pleroma_background_image", :background)
176 |> add_if_present(
177 params,
178 "fields_attributes",
179 :raw_fields,
180 &{:ok, normalize_fields_attributes(&1)}
181 )
182 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
183 |> add_if_present(params, "default_scope", :default_scope)
184 |> add_if_present(params, "actor_type", :actor_type)
185
186 changeset = User.update_changeset(user, user_params)
187
188 with {:ok, user} <- User.update_and_set_cache(changeset) do
189 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
190 else
191 _e -> render_error(conn, :forbidden, "Invalid request")
192 end
193 end
194
195 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
196 with true <- Map.has_key?(params, params_field),
197 {:ok, new_value} <- value_function.(params[params_field]) do
198 Map.put(map, map_field, new_value)
199 else
200 _ -> map
201 end
202 end
203
204 defp normalize_fields_attributes(fields) do
205 if Enum.all?(fields, &is_tuple/1) do
206 Enum.map(fields, fn {_, v} -> v end)
207 else
208 fields
209 end
210 end
211
212 @doc "GET /api/v1/accounts/relationships"
213 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
214 targets = User.get_all_by_ids(List.wrap(id))
215
216 render(conn, "relationships.json", user: user, targets: targets)
217 end
218
219 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
220 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
221
222 @doc "GET /api/v1/accounts/:id"
223 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
224 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
225 true <- User.visible_for?(user, for_user) do
226 render(conn, "show.json", user: user, for: for_user)
227 else
228 _e -> render_error(conn, :not_found, "Can't find user")
229 end
230 end
231
232 @doc "GET /api/v1/accounts/:id/statuses"
233 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
234 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
235 true <- User.visible_for?(user, reading_user) do
236 params =
237 params
238 |> Map.put("tag", params["tagged"])
239 |> Map.delete("godmode")
240
241 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
242
243 conn
244 |> add_link_headers(activities)
245 |> put_view(StatusView)
246 |> render("index.json",
247 activities: activities,
248 for: reading_user,
249 as: :activity,
250 skip_relationships: skip_relationships?(params)
251 )
252 else
253 _e -> render_error(conn, :not_found, "Can't find user")
254 end
255 end
256
257 @doc "GET /api/v1/accounts/:id/followers"
258 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
259 followers =
260 cond do
261 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
262 user.hide_followers -> []
263 true -> MastodonAPI.get_followers(user, params)
264 end
265
266 conn
267 |> add_link_headers(followers)
268 |> render("index.json", for: for_user, users: followers, as: :user)
269 end
270
271 @doc "GET /api/v1/accounts/:id/following"
272 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
273 followers =
274 cond do
275 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
276 user.hide_follows -> []
277 true -> MastodonAPI.get_friends(user, params)
278 end
279
280 conn
281 |> add_link_headers(followers)
282 |> render("index.json", for: for_user, users: followers, as: :user)
283 end
284
285 @doc "GET /api/v1/accounts/:id/lists"
286 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
287 lists = Pleroma.List.get_lists_account_belongs(user, account)
288
289 conn
290 |> put_view(ListView)
291 |> render("index.json", lists: lists)
292 end
293
294 @doc "POST /api/v1/accounts/:id/follow"
295 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
296 {:error, :not_found}
297 end
298
299 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
300 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
301 render(conn, "relationship.json", user: follower, target: followed)
302 else
303 {:error, message} -> json_response(conn, :forbidden, %{error: message})
304 end
305 end
306
307 @doc "POST /api/v1/accounts/:id/unfollow"
308 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
309 {:error, :not_found}
310 end
311
312 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
313 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
314 render(conn, "relationship.json", user: follower, target: followed)
315 end
316 end
317
318 @doc "POST /api/v1/accounts/:id/mute"
319 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
320 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
321
322 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
323 render(conn, "relationship.json", user: muter, target: muted)
324 else
325 {:error, message} -> json_response(conn, :forbidden, %{error: message})
326 end
327 end
328
329 @doc "POST /api/v1/accounts/:id/unmute"
330 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
331 with {:ok, _user_relationships} <- User.unmute(muter, muted) 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/block"
339 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
340 with {:ok, _user_block} <- User.block(blocker, blocked),
341 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
342 render(conn, "relationship.json", user: blocker, target: blocked)
343 else
344 {:error, message} -> json_response(conn, :forbidden, %{error: message})
345 end
346 end
347
348 @doc "POST /api/v1/accounts/:id/unblock"
349 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
350 with {:ok, _user_block} <- User.unblock(blocker, blocked),
351 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
352 render(conn, "relationship.json", user: blocker, target: blocked)
353 else
354 {:error, message} -> json_response(conn, :forbidden, %{error: message})
355 end
356 end
357
358 @doc "POST /api/v1/follows"
359 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
360 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
361 {_, true} <- {:followed, follower.id != followed.id},
362 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
363 render(conn, "show.json", user: followed, for: follower)
364 else
365 {:followed, _} -> {:error, :not_found}
366 {:error, message} -> json_response(conn, :forbidden, %{error: message})
367 end
368 end
369
370 @doc "GET /api/v1/mutes"
371 def mutes(%{assigns: %{user: user}} = conn, _) do
372 users = User.muted_users(user, _restrict_deactivated = true)
373 render(conn, "index.json", users: users, for: user, as: :user)
374 end
375
376 @doc "GET /api/v1/blocks"
377 def blocks(%{assigns: %{user: user}} = conn, _) do
378 users = User.blocked_users(user, _restrict_deactivated = true)
379 render(conn, "index.json", users: users, for: user, as: :user)
380 end
381
382 @doc "GET /api/v1/endorsements"
383 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
384
385 @doc "GET /api/v1/identity_proofs"
386 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
387 end