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