ebfa533dd58b547f47a42c60f5449baef446c680
[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[:actor_type])
181
182 changeset = User.update_changeset(user, user_params)
183
184 with {:ok, user} <- User.update_and_set_cache(changeset) do
185 user
186 |> build_update_activity_params()
187 |> ActivityPub.update()
188
189 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
190 else
191 _e -> render_error(conn, :forbidden, "Invalid request")
192 end
193 end
194
195 # Hotfix, handling will be redone with the pipeline
196 defp build_update_activity_params(user) do
197 object =
198 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
199 |> Map.delete("@context")
200
201 %{
202 local: true,
203 to: [user.follower_address],
204 cc: [],
205 object: object,
206 actor: user.ap_id
207 }
208 end
209
210 defp normalize_fields_attributes(fields) do
211 if Enum.all?(fields, &is_tuple/1) do
212 Enum.map(fields, fn {_, v} -> v end)
213 else
214 Enum.map(fields, fn
215 %{} = field -> %{"name" => field.name, "value" => field.value}
216 field -> field
217 end)
218 end
219 end
220
221 @doc "GET /api/v1/accounts/relationships"
222 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
223 targets = User.get_all_by_ids(List.wrap(id))
224
225 render(conn, "relationships.json", user: user, targets: targets)
226 end
227
228 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
229 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
230
231 @doc "GET /api/v1/accounts/:id"
232 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
233 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
234 true <- User.visible_for?(user, for_user) do
235 render(conn, "show.json", user: user, for: for_user)
236 else
237 _e -> render_error(conn, :not_found, "Can't find user")
238 end
239 end
240
241 @doc "GET /api/v1/accounts/:id/statuses"
242 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
243 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
244 true <- User.visible_for?(user, reading_user) do
245 params =
246 params
247 |> Map.delete(:tagged)
248 |> Enum.filter(&(not is_nil(&1)))
249 |> Map.new(fn {key, value} -> {to_string(key), value} end)
250 |> Map.put("tag", params[:tagged])
251
252 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
253
254 conn
255 |> add_link_headers(activities)
256 |> put_view(StatusView)
257 |> render("index.json",
258 activities: activities,
259 for: reading_user,
260 as: :activity
261 )
262 else
263 _e -> render_error(conn, :not_found, "Can't find user")
264 end
265 end
266
267 @doc "GET /api/v1/accounts/:id/followers"
268 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
269 params =
270 params
271 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
272 |> Enum.into(%{})
273
274 followers =
275 cond do
276 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
277 user.hide_followers -> []
278 true -> MastodonAPI.get_followers(user, params)
279 end
280
281 conn
282 |> add_link_headers(followers)
283 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
284 |> render("index.json",
285 for: for_user,
286 users: followers,
287 as: :user,
288 embed_relationships: embed_relationships?(params)
289 )
290 end
291
292 @doc "GET /api/v1/accounts/:id/following"
293 def following(%{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_friends(user, params)
302 user.hide_follows -> []
303 true -> MastodonAPI.get_friends(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/lists"
318 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
319 lists = Pleroma.List.get_lists_account_belongs(user, account)
320
321 conn
322 |> put_view(ListView)
323 |> render("index.json", lists: lists)
324 end
325
326 @doc "POST /api/v1/accounts/:id/follow"
327 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
328 {:error, "Can not follow yourself"}
329 end
330
331 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
332 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
333 render(conn, "relationship.json", user: follower, target: followed)
334 else
335 {:error, message} -> json_response(conn, :forbidden, %{error: message})
336 end
337 end
338
339 @doc "POST /api/v1/accounts/:id/unfollow"
340 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
341 {:error, "Can not unfollow yourself"}
342 end
343
344 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
345 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
346 render(conn, "relationship.json", user: follower, target: followed)
347 end
348 end
349
350 @doc "POST /api/v1/accounts/:id/mute"
351 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
352 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
353 render(conn, "relationship.json", user: muter, target: muted)
354 else
355 {:error, message} -> json_response(conn, :forbidden, %{error: message})
356 end
357 end
358
359 @doc "POST /api/v1/accounts/:id/unmute"
360 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
361 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
362 render(conn, "relationship.json", user: muter, target: muted)
363 else
364 {:error, message} -> json_response(conn, :forbidden, %{error: message})
365 end
366 end
367
368 @doc "POST /api/v1/accounts/:id/block"
369 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
370 with {:ok, _user_block} <- User.block(blocker, blocked),
371 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
372 render(conn, "relationship.json", user: blocker, target: blocked)
373 else
374 {:error, message} -> json_response(conn, :forbidden, %{error: message})
375 end
376 end
377
378 @doc "POST /api/v1/accounts/:id/unblock"
379 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
380 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
381 render(conn, "relationship.json", user: blocker, target: blocked)
382 else
383 {:error, message} -> json_response(conn, :forbidden, %{error: message})
384 end
385 end
386
387 @doc "POST /api/v1/follows"
388 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
389 case User.get_cached_by_nickname(uri) do
390 %User{} = user ->
391 conn
392 |> assign(:account, user)
393 |> follow(%{})
394
395 nil ->
396 {:error, :not_found}
397 end
398 end
399
400 @doc "GET /api/v1/mutes"
401 def mutes(%{assigns: %{user: user}} = conn, _) do
402 users = User.muted_users(user, _restrict_deactivated = true)
403 render(conn, "index.json", users: users, for: user, as: :user)
404 end
405
406 @doc "GET /api/v1/blocks"
407 def blocks(%{assigns: %{user: user}} = conn, _) do
408 users = User.blocked_users(user, _restrict_deactivated = true)
409 render(conn, "index.json", users: users, for: user, as: :user)
410 end
411
412 @doc "GET /api/v1/endorsements"
413 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
414
415 @doc "GET /api/v1/identity_proofs"
416 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
417 end