Merge branch 'authenticated-api-oauth-check-enforcement' 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: [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 != :create
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) do
232 params =
233 params
234 |> Map.put("tag", params["tagged"])
235 |> Map.delete("godmode")
236
237 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
238
239 conn
240 |> add_link_headers(activities)
241 |> put_view(StatusView)
242 |> render("index.json", activities: activities, for: reading_user, as: :activity)
243 end
244 end
245
246 @doc "GET /api/v1/accounts/:id/followers"
247 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
248 followers =
249 cond do
250 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
251 user.hide_followers -> []
252 true -> MastodonAPI.get_followers(user, params)
253 end
254
255 conn
256 |> add_link_headers(followers)
257 |> render("index.json", for: for_user, users: followers, as: :user)
258 end
259
260 @doc "GET /api/v1/accounts/:id/following"
261 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
262 followers =
263 cond do
264 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
265 user.hide_follows -> []
266 true -> MastodonAPI.get_friends(user, params)
267 end
268
269 conn
270 |> add_link_headers(followers)
271 |> render("index.json", for: for_user, users: followers, as: :user)
272 end
273
274 @doc "GET /api/v1/accounts/:id/lists"
275 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
276 lists = Pleroma.List.get_lists_account_belongs(user, account)
277
278 conn
279 |> put_view(ListView)
280 |> render("index.json", lists: lists)
281 end
282
283 @doc "POST /api/v1/accounts/:id/follow"
284 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
285 {:error, :not_found}
286 end
287
288 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
289 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
290 render(conn, "relationship.json", user: follower, target: followed)
291 else
292 {:error, message} -> json_response(conn, :forbidden, %{error: message})
293 end
294 end
295
296 @doc "POST /api/v1/accounts/:id/unfollow"
297 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
298 {:error, :not_found}
299 end
300
301 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
302 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
303 render(conn, "relationship.json", user: follower, target: followed)
304 end
305 end
306
307 @doc "POST /api/v1/accounts/:id/mute"
308 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
309 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
310
311 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
312 render(conn, "relationship.json", user: muter, target: muted)
313 else
314 {:error, message} -> json_response(conn, :forbidden, %{error: message})
315 end
316 end
317
318 @doc "POST /api/v1/accounts/:id/unmute"
319 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
320 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
321 render(conn, "relationship.json", user: muter, target: muted)
322 else
323 {:error, message} -> json_response(conn, :forbidden, %{error: message})
324 end
325 end
326
327 @doc "POST /api/v1/accounts/:id/block"
328 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
329 with {:ok, _user_block} <- User.block(blocker, blocked),
330 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
331 render(conn, "relationship.json", user: blocker, target: blocked)
332 else
333 {:error, message} -> json_response(conn, :forbidden, %{error: message})
334 end
335 end
336
337 @doc "POST /api/v1/accounts/:id/unblock"
338 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
339 with {:ok, _user_block} <- User.unblock(blocker, blocked),
340 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
341 render(conn, "relationship.json", user: blocker, target: blocked)
342 else
343 {:error, message} -> json_response(conn, :forbidden, %{error: message})
344 end
345 end
346
347 @doc "POST /api/v1/follows"
348 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
349 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
350 {_, true} <- {:followed, follower.id != followed.id},
351 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
352 render(conn, "show.json", user: followed, for: follower)
353 else
354 {:followed, _} -> {:error, :not_found}
355 {:error, message} -> json_response(conn, :forbidden, %{error: message})
356 end
357 end
358
359 @doc "GET /api/v1/mutes"
360 def mutes(%{assigns: %{user: user}} = conn, _) do
361 users = User.muted_users(user, _restrict_deactivated = true)
362 render(conn, "index.json", users: users, for: user, as: :user)
363 end
364
365 @doc "GET /api/v1/blocks"
366 def blocks(%{assigns: %{user: user}} = conn, _) do
367 users = User.blocked_users(user, _restrict_deactivated = true)
368 render(conn, "index.json", users: users, for: user, as: :user)
369 end
370
371 @doc "GET /api/v1/endorsements"
372 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
373
374 @doc "GET /api/v1/identity_proofs"
375 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
376 end