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