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