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