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