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