Merge branch 'features/users-raw_bio' 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.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 |> Map.put(:tag, params[:tagged])
249
250 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
251
252 conn
253 |> add_link_headers(activities)
254 |> put_view(StatusView)
255 |> render("index.json",
256 activities: activities,
257 for: reading_user,
258 as: :activity
259 )
260 else
261 _e -> render_error(conn, :not_found, "Can't find user")
262 end
263 end
264
265 @doc "GET /api/v1/accounts/:id/followers"
266 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
267 params =
268 params
269 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
270 |> Enum.into(%{})
271
272 followers =
273 cond do
274 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
275 user.hide_followers -> []
276 true -> MastodonAPI.get_followers(user, params)
277 end
278
279 conn
280 |> add_link_headers(followers)
281 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
282 |> render("index.json",
283 for: for_user,
284 users: followers,
285 as: :user,
286 embed_relationships: embed_relationships?(params)
287 )
288 end
289
290 @doc "GET /api/v1/accounts/:id/following"
291 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
292 params =
293 params
294 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
295 |> Enum.into(%{})
296
297 followers =
298 cond do
299 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
300 user.hide_follows -> []
301 true -> MastodonAPI.get_friends(user, params)
302 end
303
304 conn
305 |> add_link_headers(followers)
306 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
307 |> render("index.json",
308 for: for_user,
309 users: followers,
310 as: :user,
311 embed_relationships: embed_relationships?(params)
312 )
313 end
314
315 @doc "GET /api/v1/accounts/:id/lists"
316 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
317 lists = Pleroma.List.get_lists_account_belongs(user, account)
318
319 conn
320 |> put_view(ListView)
321 |> render("index.json", lists: lists)
322 end
323
324 @doc "POST /api/v1/accounts/:id/follow"
325 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
326 {:error, "Can not follow yourself"}
327 end
328
329 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
330 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
331 render(conn, "relationship.json", user: follower, target: followed)
332 else
333 {:error, message} -> json_response(conn, :forbidden, %{error: message})
334 end
335 end
336
337 @doc "POST /api/v1/accounts/:id/unfollow"
338 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
339 {:error, "Can not unfollow yourself"}
340 end
341
342 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
343 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
344 render(conn, "relationship.json", user: follower, target: followed)
345 end
346 end
347
348 @doc "POST /api/v1/accounts/:id/mute"
349 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
350 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
351 render(conn, "relationship.json", user: muter, target: muted)
352 else
353 {:error, message} -> json_response(conn, :forbidden, %{error: message})
354 end
355 end
356
357 @doc "POST /api/v1/accounts/:id/unmute"
358 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
359 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
360 render(conn, "relationship.json", user: muter, target: muted)
361 else
362 {:error, message} -> json_response(conn, :forbidden, %{error: message})
363 end
364 end
365
366 @doc "POST /api/v1/accounts/:id/block"
367 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
368 with {:ok, _user_block} <- User.block(blocker, blocked),
369 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
370 render(conn, "relationship.json", user: blocker, target: blocked)
371 else
372 {:error, message} -> json_response(conn, :forbidden, %{error: message})
373 end
374 end
375
376 @doc "POST /api/v1/accounts/:id/unblock"
377 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
378 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
379 render(conn, "relationship.json", user: blocker, target: blocked)
380 else
381 {:error, message} -> json_response(conn, :forbidden, %{error: message})
382 end
383 end
384
385 @doc "POST /api/v1/follows"
386 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
387 case User.get_cached_by_nickname(uri) do
388 %User{} = user ->
389 conn
390 |> assign(:account, user)
391 |> follow(%{})
392
393 nil ->
394 {:error, :not_found}
395 end
396 end
397
398 @doc "GET /api/v1/mutes"
399 def mutes(%{assigns: %{user: user}} = conn, _) do
400 users = User.muted_users(user, _restrict_deactivated = true)
401 render(conn, "index.json", users: users, for: user, as: :user)
402 end
403
404 @doc "GET /api/v1/blocks"
405 def blocks(%{assigns: %{user: user}} = conn, _) do
406 users = User.blocked_users(user, _restrict_deactivated = true)
407 render(conn, "index.json", users: users, for: user, as: :user)
408 end
409
410 @doc "GET /api/v1/endorsements"
411 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
412
413 @doc "GET /api/v1/identity_proofs"
414 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
415 end