75512442de1c352cda4aeb60fc075566aa8cfb6c
[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: original_user}, body_params: params} = conn, _params) do
143 user = original_user
144
145 params =
146 params
147 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
148 |> Enum.into(%{})
149
150 user_params =
151 [
152 :no_rich_text,
153 :locked,
154 :hide_followers_count,
155 :hide_follows_count,
156 :hide_followers,
157 :hide_follows,
158 :hide_favorites,
159 :show_role,
160 :skip_thread_containment,
161 :allow_following_move,
162 :discoverable
163 ]
164 |> Enum.reduce(%{}, fn key, acc ->
165 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
166 end)
167 |> add_if_present(params, :display_name, :name)
168 |> add_if_present(params, :note, :bio)
169 |> add_if_present(params, :avatar, :avatar)
170 |> add_if_present(params, :header, :banner)
171 |> add_if_present(params, :pleroma_background_image, :background)
172 |> add_if_present(
173 params,
174 :fields_attributes,
175 :raw_fields,
176 &{:ok, normalize_fields_attributes(&1)}
177 )
178 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
179 |> add_if_present(params, :default_scope, :default_scope)
180 |> add_if_present(params["source"], "privacy", :default_scope)
181 |> add_if_present(params, :actor_type, :actor_type)
182
183 changeset = User.update_changeset(user, user_params)
184
185 with {:ok, user} <- User.update_and_set_cache(changeset) do
186 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
187 else
188 _e -> render_error(conn, :forbidden, "Invalid request")
189 end
190 end
191
192 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
193 with true <- is_map(params),
194 true <- Map.has_key?(params, params_field),
195 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
196 Map.put(map, map_field, new_value)
197 else
198 _ -> map
199 end
200 end
201
202 defp normalize_fields_attributes(fields) do
203 if Enum.all?(fields, &is_tuple/1) do
204 Enum.map(fields, fn {_, v} -> v end)
205 else
206 Enum.map(fields, fn
207 %{} = field -> %{"name" => field.name, "value" => field.value}
208 field -> field
209 end)
210 end
211 end
212
213 @doc "GET /api/v1/accounts/relationships"
214 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
215 targets = User.get_all_by_ids(List.wrap(id))
216
217 render(conn, "relationships.json", user: user, targets: targets)
218 end
219
220 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
221 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
222
223 @doc "GET /api/v1/accounts/:id"
224 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
225 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
226 true <- User.visible_for?(user, for_user) do
227 render(conn, "show.json", user: user, for: for_user)
228 else
229 _e -> render_error(conn, :not_found, "Can't find user")
230 end
231 end
232
233 @doc "GET /api/v1/accounts/:id/statuses"
234 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
235 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
236 true <- User.visible_for?(user, reading_user) do
237 params =
238 params
239 |> Map.delete(:tagged)
240 |> Enum.filter(&(not is_nil(&1)))
241 |> Map.new(fn {key, value} -> {to_string(key), value} end)
242 |> Map.put("tag", params[:tagged])
243
244 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
245
246 conn
247 |> add_link_headers(activities)
248 |> put_view(StatusView)
249 |> render("index.json",
250 activities: activities,
251 for: reading_user,
252 as: :activity
253 )
254 else
255 _e -> render_error(conn, :not_found, "Can't find user")
256 end
257 end
258
259 @doc "GET /api/v1/accounts/:id/followers"
260 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
261 params =
262 params
263 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
264 |> Enum.into(%{})
265
266 followers =
267 cond do
268 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
269 user.hide_followers -> []
270 true -> MastodonAPI.get_followers(user, params)
271 end
272
273 conn
274 |> add_link_headers(followers)
275 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
276 |> render("index.json",
277 for: for_user,
278 users: followers,
279 as: :user,
280 embed_relationships: embed_relationships?(params)
281 )
282 end
283
284 @doc "GET /api/v1/accounts/:id/following"
285 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
286 params =
287 params
288 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
289 |> Enum.into(%{})
290
291 followers =
292 cond do
293 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
294 user.hide_follows -> []
295 true -> MastodonAPI.get_friends(user, params)
296 end
297
298 conn
299 |> add_link_headers(followers)
300 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
301 |> render("index.json",
302 for: for_user,
303 users: followers,
304 as: :user,
305 embed_relationships: embed_relationships?(params)
306 )
307 end
308
309 @doc "GET /api/v1/accounts/:id/lists"
310 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
311 lists = Pleroma.List.get_lists_account_belongs(user, account)
312
313 conn
314 |> put_view(ListView)
315 |> render("index.json", lists: lists)
316 end
317
318 @doc "POST /api/v1/accounts/:id/follow"
319 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
320 {:error, "Can not follow yourself"}
321 end
322
323 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
324 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
325 render(conn, "relationship.json", user: follower, target: followed)
326 else
327 {:error, message} -> json_response(conn, :forbidden, %{error: message})
328 end
329 end
330
331 @doc "POST /api/v1/accounts/:id/unfollow"
332 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
333 {:error, "Can not unfollow yourself"}
334 end
335
336 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
337 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
338 render(conn, "relationship.json", user: follower, target: followed)
339 end
340 end
341
342 @doc "POST /api/v1/accounts/:id/mute"
343 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
344 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
345 render(conn, "relationship.json", user: muter, target: muted)
346 else
347 {:error, message} -> json_response(conn, :forbidden, %{error: message})
348 end
349 end
350
351 @doc "POST /api/v1/accounts/:id/unmute"
352 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
353 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
354 render(conn, "relationship.json", user: muter, target: muted)
355 else
356 {:error, message} -> json_response(conn, :forbidden, %{error: message})
357 end
358 end
359
360 @doc "POST /api/v1/accounts/:id/block"
361 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
362 with {:ok, _user_block} <- User.block(blocker, blocked),
363 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
364 render(conn, "relationship.json", user: blocker, target: blocked)
365 else
366 {:error, message} -> json_response(conn, :forbidden, %{error: message})
367 end
368 end
369
370 @doc "POST /api/v1/accounts/:id/unblock"
371 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
372 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
373 render(conn, "relationship.json", user: blocker, target: blocked)
374 else
375 {:error, message} -> json_response(conn, :forbidden, %{error: message})
376 end
377 end
378
379 @doc "POST /api/v1/follows"
380 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
381 case User.get_cached_by_nickname(uri) do
382 %User{} = user ->
383 conn
384 |> assign(:account, user)
385 |> follow(%{})
386
387 nil ->
388 {:error, :not_found}
389 end
390 end
391
392 @doc "GET /api/v1/mutes"
393 def mutes(%{assigns: %{user: user}} = conn, _) do
394 users = User.muted_users(user, _restrict_deactivated = true)
395 render(conn, "index.json", users: users, for: user, as: :user)
396 end
397
398 @doc "GET /api/v1/blocks"
399 def blocks(%{assigns: %{user: user}} = conn, _) do
400 users = User.blocked_users(user, _restrict_deactivated = true)
401 render(conn, "index.json", users: users, for: user, as: :user)
402 end
403
404 @doc "GET /api/v1/endorsements"
405 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
406
407 @doc "GET /api/v1/identity_proofs"
408 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
409 end