edecbf41886660b35964e700a3fc676c219042d9
[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 |> Map.put(:tag, params[:tagged])
258
259 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
260
261 conn
262 |> add_link_headers(activities)
263 |> put_view(StatusView)
264 |> render("index.json",
265 activities: activities,
266 for: reading_user,
267 as: :activity
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 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
291 |> render("index.json",
292 for: for_user,
293 users: followers,
294 as: :user,
295 embed_relationships: embed_relationships?(params)
296 )
297 end
298
299 @doc "GET /api/v1/accounts/:id/following"
300 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
301 params =
302 params
303 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
304 |> Enum.into(%{})
305
306 followers =
307 cond do
308 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
309 user.hide_follows -> []
310 true -> MastodonAPI.get_friends(user, params)
311 end
312
313 conn
314 |> add_link_headers(followers)
315 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
316 |> render("index.json",
317 for: for_user,
318 users: followers,
319 as: :user,
320 embed_relationships: embed_relationships?(params)
321 )
322 end
323
324 @doc "GET /api/v1/accounts/:id/lists"
325 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
326 lists = Pleroma.List.get_lists_account_belongs(user, account)
327
328 conn
329 |> put_view(ListView)
330 |> render("index.json", lists: lists)
331 end
332
333 @doc "POST /api/v1/accounts/:id/follow"
334 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
335 {:error, "Can not follow yourself"}
336 end
337
338 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
339 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
340 render(conn, "relationship.json", user: follower, target: followed)
341 else
342 {:error, message} -> json_response(conn, :forbidden, %{error: message})
343 end
344 end
345
346 @doc "POST /api/v1/accounts/:id/unfollow"
347 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
348 {:error, "Can not unfollow yourself"}
349 end
350
351 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
352 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
353 render(conn, "relationship.json", user: follower, target: followed)
354 end
355 end
356
357 @doc "POST /api/v1/accounts/:id/mute"
358 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
359 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
360 render(conn, "relationship.json", user: muter, target: muted)
361 else
362 {:error, message} -> json_response(conn, :forbidden, %{error: message})
363 end
364 end
365
366 @doc "POST /api/v1/accounts/:id/unmute"
367 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
368 with {:ok, _user_relationships} <- User.unmute(muter, muted) 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/block"
376 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
377 with {:ok, _user_block} <- User.block(blocker, blocked),
378 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
379 render(conn, "relationship.json", user: blocker, target: blocked)
380 else
381 {:error, message} -> json_response(conn, :forbidden, %{error: message})
382 end
383 end
384
385 @doc "POST /api/v1/accounts/:id/unblock"
386 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
387 with {:ok, _activity} <- CommonAPI.unblock(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/follows"
395 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
396 case User.get_cached_by_nickname(uri) do
397 %User{} = user ->
398 conn
399 |> assign(:account, user)
400 |> follow(%{})
401
402 nil ->
403 {:error, :not_found}
404 end
405 end
406
407 @doc "GET /api/v1/mutes"
408 def mutes(%{assigns: %{user: user}} = conn, _) do
409 users = User.muted_users(user, _restrict_deactivated = true)
410 render(conn, "index.json", users: users, for: user, as: :user)
411 end
412
413 @doc "GET /api/v1/blocks"
414 def blocks(%{assigns: %{user: user}} = conn, _) do
415 users = User.blocked_users(user, _restrict_deactivated = true)
416 render(conn, "index.json", users: users, for: user, as: :user)
417 end
418
419 @doc "GET /api/v1/endorsements"
420 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
421
422 @doc "GET /api/v1/identity_proofs"
423 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
424 end