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