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