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