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