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