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