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.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 user_params =
152 [
153 :no_rich_text,
154 :locked,
155 :hide_followers_count,
156 :hide_follows_count,
157 :hide_followers,
158 :hide_follows,
159 :hide_favorites,
160 :show_role,
161 :skip_thread_containment,
162 :allow_following_move,
163 :discoverable,
164 :accepts_chat_messages
165 ]
166 |> Enum.reduce(%{}, fn key, acc ->
167 Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
168 end)
169 |> Maps.put_if_present(:name, params[:display_name])
170 |> Maps.put_if_present(:bio, params[:note])
171 |> Maps.put_if_present(:raw_bio, params[:note])
172 |> Maps.put_if_present(:avatar, params[:avatar])
173 |> Maps.put_if_present(:banner, params[:header])
174 |> Maps.put_if_present(:background, params[:pleroma_background_image])
175 |> Maps.put_if_present(
176 :raw_fields,
177 params[:fields_attributes],
178 &{:ok, normalize_fields_attributes(&1)}
179 )
180 |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
181 |> Maps.put_if_present(:default_scope, params[:default_scope])
182 |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
183 |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
184 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
185 end)
186 |> Maps.put_if_present(:actor_type, params[:actor_type])
187
188 # What happens here:
189 #
190 # We want to update the user through the pipeline, but the ActivityPub
191 # update information is not quite enough for this, because this also
192 # contains local settings that don't federate and don't even appear
193 # in the Update activity.
194 #
195 # So we first build the normal local changeset, then apply it to the
196 # user data, but don't persist it. With this, we generate the object
197 # data for our update activity. We feed this and the changeset as meta
198 # inforation into the pipeline, where they will be properly updated and
199 # federated.
200 with changeset <- User.update_changeset(user, user_params),
201 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
202 updated_object <-
203 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
204 |> Map.delete("@context"),
205 {:ok, update_data, []} <- Builder.update(user, updated_object),
206 {:ok, _update, _} <-
207 Pipeline.common_pipeline(update_data,
208 local: true,
209 user_update_changeset: changeset
210 ) do
211 render(conn, "show.json",
212 user: unpersisted_user,
213 for: unpersisted_user,
214 with_pleroma_settings: true
215 )
216 else
217 _e -> render_error(conn, :forbidden, "Invalid request")
218 end
219 end
220
221 defp normalize_fields_attributes(fields) do
222 if Enum.all?(fields, &is_tuple/1) do
223 Enum.map(fields, fn {_, v} -> v end)
224 else
225 Enum.map(fields, fn
226 %{} = field -> %{"name" => field.name, "value" => field.value}
227 field -> field
228 end)
229 end
230 end
231
232 @doc "GET /api/v1/accounts/relationships"
233 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
234 targets = User.get_all_by_ids(List.wrap(id))
235
236 render(conn, "relationships.json", user: user, targets: targets)
237 end
238
239 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
240 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
241
242 @doc "GET /api/v1/accounts/:id"
243 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
244 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
245 :visible <- User.visible_for(user, for_user) do
246 render(conn, "show.json", user: user, for: for_user)
247 else
248 error -> user_visibility_error(conn, error)
249 end
250 end
251
252 @doc "GET /api/v1/accounts/:id/statuses"
253 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
254 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
255 :visible <- User.visible_for(user, reading_user) do
256 params =
257 params
258 |> Map.delete(:tagged)
259 |> Map.put(:tag, params[:tagged])
260
261 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
262
263 conn
264 |> add_link_headers(activities)
265 |> put_view(StatusView)
266 |> render("index.json",
267 activities: activities,
268 for: reading_user,
269 as: :activity
270 )
271 else
272 error -> user_visibility_error(conn, error)
273 end
274 end
275
276 defp user_visibility_error(conn, error) do
277 case error do
278 :restrict_unauthenticated ->
279 render_error(conn, :unauthorized, "This API requires an authenticated user")
280
281 _ ->
282 render_error(conn, :not_found, "Can't find user")
283 end
284 end
285
286 @doc "GET /api/v1/accounts/:id/followers"
287 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
288 params =
289 params
290 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
291 |> Enum.into(%{})
292
293 followers =
294 cond do
295 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
296 user.hide_followers -> []
297 true -> MastodonAPI.get_followers(user, params)
298 end
299
300 conn
301 |> add_link_headers(followers)
302 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
303 |> render("index.json",
304 for: for_user,
305 users: followers,
306 as: :user,
307 embed_relationships: embed_relationships?(params)
308 )
309 end
310
311 @doc "GET /api/v1/accounts/:id/following"
312 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
313 params =
314 params
315 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
316 |> Enum.into(%{})
317
318 followers =
319 cond do
320 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
321 user.hide_follows -> []
322 true -> MastodonAPI.get_friends(user, params)
323 end
324
325 conn
326 |> add_link_headers(followers)
327 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
328 |> render("index.json",
329 for: for_user,
330 users: followers,
331 as: :user,
332 embed_relationships: embed_relationships?(params)
333 )
334 end
335
336 @doc "GET /api/v1/accounts/:id/lists"
337 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
338 lists = Pleroma.List.get_lists_account_belongs(user, account)
339
340 conn
341 |> put_view(ListView)
342 |> render("index.json", lists: lists)
343 end
344
345 @doc "POST /api/v1/accounts/:id/follow"
346 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
347 {:error, "Can not follow yourself"}
348 end
349
350 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
351 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
352 render(conn, "relationship.json", user: follower, target: followed)
353 else
354 {:error, message} -> json_response(conn, :forbidden, %{error: message})
355 end
356 end
357
358 @doc "POST /api/v1/accounts/:id/unfollow"
359 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
360 {:error, "Can not unfollow yourself"}
361 end
362
363 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
364 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
365 render(conn, "relationship.json", user: follower, target: followed)
366 end
367 end
368
369 @doc "POST /api/v1/accounts/:id/mute"
370 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
371 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
372 render(conn, "relationship.json", user: muter, target: muted)
373 else
374 {:error, message} -> json_response(conn, :forbidden, %{error: message})
375 end
376 end
377
378 @doc "POST /api/v1/accounts/:id/unmute"
379 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
380 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
381 render(conn, "relationship.json", user: muter, target: muted)
382 else
383 {:error, message} -> json_response(conn, :forbidden, %{error: message})
384 end
385 end
386
387 @doc "POST /api/v1/accounts/:id/block"
388 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
389 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
390 render(conn, "relationship.json", user: blocker, target: blocked)
391 else
392 {:error, message} -> json_response(conn, :forbidden, %{error: message})
393 end
394 end
395
396 @doc "POST /api/v1/accounts/:id/unblock"
397 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
398 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
399 render(conn, "relationship.json", user: blocker, target: blocked)
400 else
401 {:error, message} -> json_response(conn, :forbidden, %{error: message})
402 end
403 end
404
405 @doc "POST /api/v1/follows"
406 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
407 case User.get_cached_by_nickname(uri) do
408 %User{} = user ->
409 conn
410 |> assign(:account, user)
411 |> follow(%{})
412
413 nil ->
414 {:error, :not_found}
415 end
416 end
417
418 @doc "GET /api/v1/mutes"
419 def mutes(%{assigns: %{user: user}} = conn, _) do
420 users = User.muted_users(user, _restrict_deactivated = true)
421 render(conn, "index.json", users: users, for: user, as: :user)
422 end
423
424 @doc "GET /api/v1/blocks"
425 def blocks(%{assigns: %{user: user}} = conn, _) do
426 users = User.blocked_users(user, _restrict_deactivated = true)
427 render(conn, "index.json", users: users, for: user, as: :user)
428 end
429
430 @doc "GET /api/v1/endorsements"
431 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
432
433 @doc "GET /api/v1/identity_proofs"
434 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
435 end