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