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