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