208df569884b4e6d9997b7de5b26ca6a88356212
[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 ]
94 )
95
96 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
97
98 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
99
100 @doc "POST /api/v1/accounts"
101 def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
102 params =
103 params
104 |> Map.take([
105 :email,
106 :bio,
107 :captcha_solution,
108 :captcha_token,
109 :captcha_answer_data,
110 :token,
111 :password,
112 :fullname
113 ])
114 |> Map.put(:nickname, params.username)
115 |> Map.put(:fullname, params.fullname || params.username)
116 |> Map.put(:bio, params.bio || "")
117 |> Map.put(:confirm, params.password)
118
119 with :ok <- validate_email_param(params),
120 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
121 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
122 json(conn, %{
123 token_type: "Bearer",
124 access_token: token.token,
125 scope: app.scopes,
126 created_at: Token.Utils.format_created_at(token)
127 })
128 else
129 {:error, errors} -> json_response(conn, :bad_request, errors)
130 end
131 end
132
133 def create(%{assigns: %{app: _app}} = conn, _) do
134 render_error(conn, :bad_request, "Missing parameters")
135 end
136
137 def create(conn, _) do
138 render_error(conn, :forbidden, "Invalid credentials")
139 end
140
141 defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
142
143 defp validate_email_param(_) do
144 case Pleroma.Config.get([:instance, :account_activation_required]) do
145 true -> {:error, %{"error" => "Missing parameters"}}
146 _ -> :ok
147 end
148 end
149
150 @doc "GET /api/v1/accounts/verify_credentials"
151 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
152 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
153
154 render(conn, "show.json",
155 user: user,
156 for: user,
157 with_pleroma_settings: true,
158 with_chat_token: chat_token
159 )
160 end
161
162 @doc "PATCH /api/v1/accounts/update_credentials"
163 def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
164 user = original_user
165
166 params =
167 params
168 |> Map.from_struct()
169 |> Enum.filter(fn {_, value} -> not is_nil(value) end)
170 |> Enum.into(%{})
171
172 user_params =
173 [
174 :no_rich_text,
175 :locked,
176 :hide_followers_count,
177 :hide_follows_count,
178 :hide_followers,
179 :hide_follows,
180 :hide_favorites,
181 :show_role,
182 :skip_thread_containment,
183 :allow_following_move,
184 :discoverable
185 ]
186 |> Enum.reduce(%{}, fn key, acc ->
187 add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
188 end)
189 |> add_if_present(params, :display_name, :name)
190 |> add_if_present(params, :note, :bio)
191 |> add_if_present(params, :avatar, :avatar)
192 |> add_if_present(params, :header, :banner)
193 |> add_if_present(params, :pleroma_background_image, :background)
194 |> add_if_present(
195 params,
196 :fields_attributes,
197 :raw_fields,
198 &{:ok, normalize_fields_attributes(&1)}
199 )
200 |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
201 |> add_if_present(params, :default_scope, :default_scope)
202 |> add_if_present(params, :actor_type, :actor_type)
203
204 changeset = User.update_changeset(user, user_params)
205
206 with {:ok, user} <- User.update_and_set_cache(changeset) do
207 if original_user != user, do: CommonAPI.update(user)
208
209 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
210 else
211 _e -> render_error(conn, :forbidden, "Invalid request")
212 end
213 end
214
215 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
216 with true <- Map.has_key?(params, params_field),
217 {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
218 Map.put(map, map_field, new_value)
219 else
220 _ -> map
221 end
222 end
223
224 defp normalize_fields_attributes(fields) do
225 if Enum.all?(fields, &is_tuple/1) do
226 Enum.map(fields, fn {_, v} -> v end)
227 else
228 Enum.map(fields, fn
229 %Pleroma.Web.ApiSpec.Schemas.AccountAttributeField{} = field ->
230 %{"name" => field.name, "value" => field.value}
231
232 field ->
233 field
234 end)
235 end
236 end
237
238 @doc "GET /api/v1/accounts/relationships"
239 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
240 targets = User.get_all_by_ids(List.wrap(id))
241
242 render(conn, "relationships.json", user: user, targets: targets)
243 end
244
245 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
246 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
247
248 @doc "GET /api/v1/accounts/:id"
249 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
250 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
251 true <- User.visible_for?(user, for_user) do
252 render(conn, "show.json", user: user, for: for_user)
253 else
254 _e -> render_error(conn, :not_found, "Can't find user")
255 end
256 end
257
258 @doc "GET /api/v1/accounts/:id/statuses"
259 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
260 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
261 true <- User.visible_for?(user, reading_user) do
262 params =
263 params
264 |> Map.delete(:tagged)
265 |> Enum.filter(&(not is_nil(&1)))
266 |> Map.new(fn {key, value} -> {to_string(key), value} end)
267 |> Map.put("tag", params[:tagged])
268
269 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
270
271 conn
272 |> add_link_headers(activities)
273 |> put_view(StatusView)
274 |> render("index.json",
275 activities: activities,
276 for: reading_user,
277 as: :activity,
278 skip_relationships: skip_relationships?(params)
279 )
280 else
281 _e -> render_error(conn, :not_found, "Can't find user")
282 end
283 end
284
285 @doc "GET /api/v1/accounts/:id/followers"
286 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
287 followers =
288 cond do
289 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
290 user.hide_followers -> []
291 true -> MastodonAPI.get_followers(user, params)
292 end
293
294 conn
295 |> add_link_headers(followers)
296 |> render("index.json", for: for_user, users: followers, as: :user)
297 end
298
299 @doc "GET /api/v1/accounts/:id/following"
300 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
301 followers =
302 cond do
303 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
304 user.hide_follows -> []
305 true -> MastodonAPI.get_friends(user, params)
306 end
307
308 conn
309 |> add_link_headers(followers)
310 |> render("index.json", for: for_user, users: followers, as: :user)
311 end
312
313 @doc "GET /api/v1/accounts/:id/lists"
314 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
315 lists = Pleroma.List.get_lists_account_belongs(user, account)
316
317 conn
318 |> put_view(ListView)
319 |> render("index.json", lists: lists)
320 end
321
322 @doc "POST /api/v1/accounts/:id/follow"
323 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
324 {:error, :not_found}
325 end
326
327 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
328 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
329 render(conn, "relationship.json", user: follower, target: followed)
330 else
331 {:error, message} -> json_response(conn, :forbidden, %{error: message})
332 end
333 end
334
335 @doc "POST /api/v1/accounts/:id/unfollow"
336 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
337 {:error, :not_found}
338 end
339
340 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
341 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
342 render(conn, "relationship.json", user: follower, target: followed)
343 end
344 end
345
346 @doc "POST /api/v1/accounts/:id/mute"
347 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
348 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
349
350 with {:ok, _user_relationships} <- User.mute(muter, muted, 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, _user_block} <- User.unblock(blocker, blocked),
379 {:ok, _activity} <- ActivityPub.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 follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
388 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
389 {_, true} <- {:followed, follower.id != followed.id},
390 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
391 render(conn, "show.json", user: followed, for: follower)
392 else
393 {:followed, _} -> {:error, :not_found}
394 {:error, message} -> json_response(conn, :forbidden, %{error: message})
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),
412 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
413 end