f72c91c51259f578af4ef5c0f1df26c4b5cd935c
[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.StatusView
25 alias Pleroma.Web.OAuth.Token
26 alias Pleroma.Web.TwitterAPI.TwitterAPI
27
28 plug(
29 OAuthScopesPlug,
30 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
31 when action == :show
32 )
33
34 plug(
35 OAuthScopesPlug,
36 %{scopes: ["read:accounts"]}
37 when action in [:endorsements, :verify_credentials, :followers, :following]
38 )
39
40 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
41
42 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
43
44 plug(
45 OAuthScopesPlug,
46 %{scopes: ["follow", "read:blocks"]} when action == :blocks
47 )
48
49 plug(
50 OAuthScopesPlug,
51 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
52 )
53
54 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
55
56 # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
57 plug(
58 OAuthScopesPlug,
59 %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
60 )
61
62 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
63
64 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
65
66 plug(
67 Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
68 when action not in [:create, :show, :statuses]
69 )
70
71 @relationship_actions [:follow, :unfollow]
72 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
73
74 plug(
75 RateLimiter,
76 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
77 )
78
79 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
80 plug(RateLimiter, [name: :app_account_creation] when action == :create)
81 plug(:assign_account_by_id when action in @needs_account)
82
83 plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
84
85 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
86
87 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
88
89 @doc "POST /api/v1/accounts"
90 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
91 params =
92 params
93 |> Map.take([
94 :email,
95 :bio,
96 :captcha_solution,
97 :captcha_token,
98 :captcha_answer_data,
99 :token,
100 :password,
101 :fullname
102 ])
103 |> Map.put(:nickname, params.username)
104 |> Map.put(:fullname, params.fullname || params.username)
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 => email}) when not is_nil(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}, body_params: params} = conn, _params) do
153 user = original_user
154
155 params =
156 params
157 |> Map.from_struct()
158 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
159 |> Enum.into(%{})
160
161 user_params =
162 [
163 :no_rich_text,
164 :locked,
165 :hide_followers_count,
166 :hide_follows_count,
167 :hide_followers,
168 :hide_follows,
169 :hide_favorites,
170 :show_role,
171 :skip_thread_containment,
172 :allow_following_move,
173 :discoverable
174 ]
175 |> Enum.reduce(%{}, fn key, acc ->
176 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
177 end)
178 |> add_if_present(params, :display_name, :name)
179 |> add_if_present(params, :note, :bio)
180 |> add_if_present(params, :avatar, :avatar)
181 |> add_if_present(params, :header, :banner)
182 |> add_if_present(params, :pleroma_background_image, :background)
183 |> add_if_present(
184 params,
185 :fields_attributes,
186 :raw_fields,
187 &{:ok, normalize_fields_attributes(&1)}
188 )
189 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
190 |> add_if_present(params, :default_scope, :default_scope)
191 |> add_if_present(params, :actor_type, :actor_type)
192
193 changeset = User.update_changeset(user, user_params)
194
195 with {:ok, user} <- User.update_and_set_cache(changeset) do
196 if original_user != user, do: CommonAPI.update(user)
197
198 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
199 else
200 _e -> render_error(conn, :forbidden, "Invalid request")
201 end
202 end
203
204 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
205 with true <- Map.has_key?(params, params_field),
206 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
207 Map.put(map, map_field, new_value)
208 else
209 _ -> map
210 end
211 end
212
213 defp normalize_fields_attributes(fields) do
214 if Enum.all?(fields, &is_tuple/1) do
215 Enum.map(fields, fn {_, v} -> v end)
216 else
217 Enum.map(fields, fn
218 %Pleroma.Web.ApiSpec.Schemas.AccountAttributeField{} = field ->
219 %{"name" => field.name, "value" => field.value}
220
221 field ->
222 field
223 end)
224 end
225 end
226
227 @doc "GET /api/v1/accounts/relationships"
228 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
229 targets = User.get_all_by_ids(List.wrap(id))
230
231 render(conn, "relationships.json", user: user, targets: targets)
232 end
233
234 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
235 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
236
237 @doc "GET /api/v1/accounts/:id"
238 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
239 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
240 true <- User.visible_for?(user, for_user) do
241 render(conn, "show.json", user: user, for: for_user)
242 else
243 _e -> render_error(conn, :not_found, "Can't find user")
244 end
245 end
246
247 @doc "GET /api/v1/accounts/:id/statuses"
248 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
249 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
250 true <- User.visible_for?(user, reading_user) do
251 params =
252 params
253 |> Map.delete(:tagged)
254 |> Enum.filter(&(not is_nil(&1)))
255 |> Map.new(fn {key, value} -> {to_string(key), value} end)
256 |> Map.put("tag", params[:tagged])
257
258 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
259
260 conn
261 |> add_link_headers(activities)
262 |> put_view(StatusView)
263 |> render("index.json",
264 activities: activities,
265 for: reading_user,
266 as: :activity,
267 skip_relationships: skip_relationships?(params)
268 )
269 else
270 _e -> render_error(conn, :not_found, "Can't find user")
271 end
272 end
273
274 @doc "GET /api/v1/accounts/:id/followers"
275 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
276 params =
277 params
278 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
279 |> Enum.into(%{})
280
281 followers =
282 cond do
283 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
284 user.hide_followers -> []
285 true -> MastodonAPI.get_followers(user, params)
286 end
287
288 conn
289 |> add_link_headers(followers)
290 |> render("index.json", for: for_user, users: followers, as: :user)
291 end
292
293 @doc "GET /api/v1/accounts/:id/following"
294 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
295 params =
296 params
297 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
298 |> Enum.into(%{})
299
300 followers =
301 cond do
302 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
303 user.hide_follows -> []
304 true -> MastodonAPI.get_friends(user, params)
305 end
306
307 conn
308 |> add_link_headers(followers)
309 |> render("index.json", for: for_user, users: followers, as: :user)
310 end
311
312 @doc "GET /api/v1/accounts/:id/lists"
313 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
314 lists = Pleroma.List.get_lists_account_belongs(user, account)
315
316 conn
317 |> put_view(ListView)
318 |> render("index.json", lists: lists)
319 end
320
321 @doc "POST /api/v1/accounts/:id/follow"
322 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
323 {:error, :not_found}
324 end
325
326 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
327 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
328 render(conn, "relationship.json", user: follower, target: followed)
329 else
330 {:error, message} -> json_response(conn, :forbidden, %{error: message})
331 end
332 end
333
334 @doc "POST /api/v1/accounts/:id/unfollow"
335 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
336 {:error, :not_found}
337 end
338
339 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
340 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
341 render(conn, "relationship.json", user: follower, target: followed)
342 end
343 end
344
345 @doc "POST /api/v1/accounts/:id/mute"
346 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
347 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
348 render(conn, "relationship.json", user: muter, target: muted)
349 else
350 {:error, message} -> json_response(conn, :forbidden, %{error: message})
351 end
352 end
353
354 @doc "POST /api/v1/accounts/:id/unmute"
355 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
356 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
357 render(conn, "relationship.json", user: muter, target: muted)
358 else
359 {:error, message} -> json_response(conn, :forbidden, %{error: message})
360 end
361 end
362
363 @doc "POST /api/v1/accounts/:id/block"
364 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
365 with {:ok, _user_block} <- User.block(blocker, blocked),
366 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
367 render(conn, "relationship.json", user: blocker, target: blocked)
368 else
369 {:error, message} -> json_response(conn, :forbidden, %{error: message})
370 end
371 end
372
373 @doc "POST /api/v1/accounts/:id/unblock"
374 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
375 with {:ok, _user_block} <- User.unblock(blocker, blocked),
376 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
377 render(conn, "relationship.json", user: blocker, target: blocked)
378 else
379 {:error, message} -> json_response(conn, :forbidden, %{error: message})
380 end
381 end
382
383 @doc "POST /api/v1/follows"
384 def follows(%{assigns: %{user: follower}, body_params: %{uri: uri}} = conn, _) do
385 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
386 {_, true} <- {:followed, follower.id != followed.id},
387 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
388 render(conn, "show.json", user: followed, for: follower)
389 else
390 {:followed, _} -> {:error, :not_found}
391 {:error, message} -> json_response(conn, :forbidden, %{error: message})
392 end
393 end
394
395 @doc "GET /api/v1/mutes"
396 def mutes(%{assigns: %{user: user}} = conn, _) do
397 users = User.muted_users(user, _restrict_deactivated = true)
398 render(conn, "index.json", users: users, for: user, as: :user)
399 end
400
401 @doc "GET /api/v1/blocks"
402 def blocks(%{assigns: %{user: user}} = conn, _) do
403 users = User.blocked_users(user, _restrict_deactivated = true)
404 render(conn, "index.json", users: users, for: user, as: :user)
405 end
406
407 @doc "GET /api/v1/endorsements"
408 def endorsements(conn, params),
409 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
410 end