Merge branch 'develop' into issue/1276-2
[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 json_response: 3,
14 skip_relationships?: 1
15 ]
16
17 alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
18 alias Pleroma.Plugs.OAuthScopesPlug
19 alias Pleroma.Plugs.RateLimiter
20 alias Pleroma.User
21 alias Pleroma.Web.ActivityPub.ActivityPub
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonAPI
25 alias Pleroma.Web.MastodonAPI.MastodonAPIController
26 alias Pleroma.Web.MastodonAPI.StatusView
27 alias Pleroma.Web.OAuth.Token
28 alias Pleroma.Web.TwitterAPI.TwitterAPI
29
30 plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
31
32 plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
33
34 plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
35
36 plug(
37 OAuthScopesPlug,
38 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
39 when action in [:show, :followers, :following]
40 )
41
42 plug(
43 OAuthScopesPlug,
44 %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
45 when action == :statuses
46 )
47
48 plug(
49 OAuthScopesPlug,
50 %{scopes: ["read:accounts"]}
51 when action in [:verify_credentials, :endorsements, :identity_proofs]
52 )
53
54 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
55
56 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
57
58 plug(
59 OAuthScopesPlug,
60 %{scopes: ["follow", "read:blocks"]} when action == :blocks
61 )
62
63 plug(
64 OAuthScopesPlug,
65 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
66 )
67
68 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
69
70 plug(
71 OAuthScopesPlug,
72 %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
73 )
74
75 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
76
77 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
78
79 @relationship_actions [:follow, :unfollow]
80 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
81
82 plug(
83 RateLimiter,
84 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
85 )
86
87 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
88 plug(RateLimiter, [name: :app_account_creation] when action == :create)
89 plug(:assign_account_by_id when action in @needs_account)
90
91 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
92
93 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
94
95 @doc "POST /api/v1/accounts"
96 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
97 params =
98 params
99 |> Map.take([
100 :email,
101 :bio,
102 :captcha_solution,
103 :captcha_token,
104 :captcha_answer_data,
105 :token,
106 :password,
107 :fullname
108 ])
109 |> Map.put(:nickname, params.username)
110 |> Map.put(:fullname, Map.get(params, :fullname, params.username))
111 |> Map.put(:confirm, params.password)
112 |> Map.put(:trusted_app, app.trusted)
113
114 with :ok <- validate_email_param(params),
115 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
116 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
117 json(conn, %{
118 token_type: "Bearer",
119 access_token: token.token,
120 scope: app.scopes,
121 created_at: Token.Utils.format_created_at(token)
122 })
123 else
124 {:error, errors} -> json_response(conn, :bad_request, errors)
125 end
126 end
127
128 def create(%{assigns: %{app: _app}} = conn, _) do
129 render_error(conn, :bad_request, "Missing parameters")
130 end
131
132 def create(conn, _) do
133 render_error(conn, :forbidden, "Invalid credentials")
134 end
135
136 defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
137
138 defp validate_email_param(_) do
139 case Pleroma.Config.get([:instance, :account_activation_required]) do
140 true -> {:error, %{"error" => "Missing parameters"}}
141 _ -> :ok
142 end
143 end
144
145 @doc "GET /api/v1/accounts/verify_credentials"
146 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
147 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
148
149 render(conn, "show.json",
150 user: user,
151 for: user,
152 with_pleroma_settings: true,
153 with_chat_token: chat_token
154 )
155 end
156
157 @doc "PATCH /api/v1/accounts/update_credentials"
158 def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
159 user = original_user
160
161 params =
162 params
163 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
164 |> Enum.into(%{})
165
166 user_params =
167 [
168 :no_rich_text,
169 :locked,
170 :hide_followers_count,
171 :hide_follows_count,
172 :hide_followers,
173 :hide_follows,
174 :hide_favorites,
175 :show_role,
176 :skip_thread_containment,
177 :allow_following_move,
178 :discoverable
179 ]
180 |> Enum.reduce(%{}, fn key, acc ->
181 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
182 end)
183 |> add_if_present(params, :display_name, :name)
184 |> add_if_present(params, :note, :bio)
185 |> add_if_present(params, :avatar, :avatar)
186 |> add_if_present(params, :header, :banner)
187 |> add_if_present(params, :pleroma_background_image, :background)
188 |> add_if_present(
189 params,
190 :fields_attributes,
191 :raw_fields,
192 &{:ok, normalize_fields_attributes(&1)}
193 )
194 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
195 |> add_if_present(params, :default_scope, :default_scope)
196 |> add_if_present(params, :actor_type, :actor_type)
197
198 changeset = User.update_changeset(user, user_params)
199
200 with {:ok, user} <- User.update_and_set_cache(changeset) do
201 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
202 else
203 _e -> render_error(conn, :forbidden, "Invalid request")
204 end
205 end
206
207 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
208 with true <- Map.has_key?(params, params_field),
209 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
210 Map.put(map, map_field, new_value)
211 else
212 _ -> map
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 true <- User.visible_for?(user, for_user) do
241 render(conn, "show.json", user: user, for: for_user)
242 else
243 _e -> render_error(conn, :not_found, "Can't find user")
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 true <- User.visible_for?(user, reading_user) do
251 params =
252 params
253 |> Map.delete(:tagged)
254 |> Enum.filter(&(not is_nil(&1)))
255 |> Map.new(fn {key, value} -> {to_string(key), value} end)
256 |> Map.put("tag", params[:tagged])
257
258 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
259
260 conn
261 |> add_link_headers(activities)
262 |> put_view(StatusView)
263 |> render("index.json",
264 activities: activities,
265 for: reading_user,
266 as: :activity,
267 skip_relationships: skip_relationships?(params)
268 )
269 else
270 _e -> render_error(conn, :not_found, "Can't find user")
271 end
272 end
273
274 @doc "GET /api/v1/accounts/:id/followers"
275 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
276 params =
277 params
278 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
279 |> Enum.into(%{})
280
281 followers =
282 cond do
283 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
284 user.hide_followers -> []
285 true -> MastodonAPI.get_followers(user, params)
286 end
287
288 conn
289 |> add_link_headers(followers)
290 |> render("index.json", for: for_user, users: followers, as: :user)
291 end
292
293 @doc "GET /api/v1/accounts/:id/following"
294 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
295 params =
296 params
297 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
298 |> Enum.into(%{})
299
300 followers =
301 cond do
302 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
303 user.hide_follows -> []
304 true -> MastodonAPI.get_friends(user, params)
305 end
306
307 conn
308 |> add_link_headers(followers)
309 |> render("index.json", for: for_user, users: followers, as: :user)
310 end
311
312 @doc "GET /api/v1/accounts/:id/lists"
313 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
314 lists = Pleroma.List.get_lists_account_belongs(user, account)
315
316 conn
317 |> put_view(ListView)
318 |> render("index.json", lists: lists)
319 end
320
321 @doc "POST /api/v1/accounts/:id/follow"
322 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
323 {:error, "Can not follow yourself"}
324 end
325
326 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
327 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
328 render(conn, "relationship.json", user: follower, target: followed)
329 else
330 {:error, message} -> json_response(conn, :forbidden, %{error: message})
331 end
332 end
333
334 @doc "POST /api/v1/accounts/:id/unfollow"
335 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
336 {:error, "Can not unfollow yourself"}
337 end
338
339 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
340 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
341 render(conn, "relationship.json", user: follower, target: followed)
342 end
343 end
344
345 @doc "POST /api/v1/accounts/:id/mute"
346 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
347 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
348 render(conn, "relationship.json", user: muter, target: muted)
349 else
350 {:error, message} -> json_response(conn, :forbidden, %{error: message})
351 end
352 end
353
354 @doc "POST /api/v1/accounts/:id/unmute"
355 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
356 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
357 render(conn, "relationship.json", user: muter, target: muted)
358 else
359 {:error, message} -> json_response(conn, :forbidden, %{error: message})
360 end
361 end
362
363 @doc "POST /api/v1/accounts/:id/block"
364 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
365 with {:ok, _user_block} <- User.block(blocker, blocked),
366 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
367 render(conn, "relationship.json", user: blocker, target: blocked)
368 else
369 {:error, message} -> json_response(conn, :forbidden, %{error: message})
370 end
371 end
372
373 @doc "POST /api/v1/accounts/:id/unblock"
374 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
375 with {:ok, _user_block} <- User.unblock(blocker, blocked),
376 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
377 render(conn, "relationship.json", user: blocker, target: blocked)
378 else
379 {:error, message} -> json_response(conn, :forbidden, %{error: message})
380 end
381 end
382
383 @doc "POST /api/v1/follows"
384 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
385 case User.get_cached_by_nickname(uri) do
386 %User{} = user ->
387 conn
388 |> assign(:account, user)
389 |> follow(%{})
390
391 nil ->
392 {:error, :not_found}
393 end
394 end
395
396 @doc "GET /api/v1/mutes"
397 def mutes(%{assigns: %{user: user}} = conn, _) do
398 users = User.muted_users(user, _restrict_deactivated = true)
399 render(conn, "index.json", users: users, for: user, as: :user)
400 end
401
402 @doc "GET /api/v1/blocks"
403 def blocks(%{assigns: %{user: user}} = conn, _) do
404 users = User.blocked_users(user, _restrict_deactivated = true)
405 render(conn, "index.json", users: users, for: user, as: :user)
406 end
407
408 @doc "GET /api/v1/endorsements"
409 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
410
411 @doc "GET /api/v1/identity_proofs"
412 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
413 end