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