Merge branch 'block-handling' into 'develop'
[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.Builder
24 alias Pleroma.Web.ActivityPub.Pipeline
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[:bot], fn bot ->
183 if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
184 end)
185 |> Maps.put_if_present(:actor_type, params[:actor_type])
186
187 # What happens here:
188 #
189 # We want to update the user through the pipeline, but the ActivityPub
190 # update information is not quite enough for this, because this also
191 # contains local settings that don't federate and don't even appear
192 # in the Update activity.
193 #
194 # So we first build the normal local changeset, then apply it to the
195 # user data, but don't persist it. With this, we generate the object
196 # data for our update activity. We feed this and the changeset as meta
197 # inforation into the pipeline, where they will be properly updated and
198 # federated.
199 with changeset <- User.update_changeset(user, user_params),
200 {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
201 updated_object <-
202 Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
203 |> Map.delete("@context"),
204 {:ok, update_data, []} <- Builder.update(user, updated_object),
205 {:ok, _update, _} <-
206 Pipeline.common_pipeline(update_data,
207 local: true,
208 user_update_changeset: changeset
209 ) do
210 render(conn, "show.json",
211 user: unpersisted_user,
212 for: unpersisted_user,
213 with_pleroma_settings: true
214 )
215 else
216 _e -> render_error(conn, :forbidden, "Invalid request")
217 end
218 end
219
220 defp normalize_fields_attributes(fields) do
221 if Enum.all?(fields, &is_tuple/1) do
222 Enum.map(fields, fn {_, v} -> v end)
223 else
224 Enum.map(fields, fn
225 %{} = field -> %{"name" => field.name, "value" => field.value}
226 field -> field
227 end)
228 end
229 end
230
231 @doc "GET /api/v1/accounts/relationships"
232 def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
233 targets = User.get_all_by_ids(List.wrap(id))
234
235 render(conn, "relationships.json", user: user, targets: targets)
236 end
237
238 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
239 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
240
241 @doc "GET /api/v1/accounts/:id"
242 def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
243 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
244 :visible <- User.visible_for(user, for_user) do
245 render(conn, "show.json", user: user, for: for_user)
246 else
247 error -> user_visibility_error(conn, error)
248 end
249 end
250
251 @doc "GET /api/v1/accounts/:id/statuses"
252 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
253 with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
254 :visible <- User.visible_for(user, reading_user) do
255 params =
256 params
257 |> Map.delete(:tagged)
258 |> Map.put(:tag, params[:tagged])
259
260 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
261
262 conn
263 |> add_link_headers(activities)
264 |> put_view(StatusView)
265 |> render("index.json",
266 activities: activities,
267 for: reading_user,
268 as: :activity
269 )
270 else
271 error -> user_visibility_error(conn, error)
272 end
273 end
274
275 defp user_visibility_error(conn, error) do
276 case error do
277 :restrict_unauthenticated ->
278 render_error(conn, :unauthorized, "This API requires an authenticated user")
279
280 _ ->
281 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 params =
288 params
289 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
290 |> Enum.into(%{})
291
292 followers =
293 cond do
294 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
295 user.hide_followers -> []
296 true -> MastodonAPI.get_followers(user, params)
297 end
298
299 conn
300 |> add_link_headers(followers)
301 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
302 |> render("index.json",
303 for: for_user,
304 users: followers,
305 as: :user,
306 embed_relationships: embed_relationships?(params)
307 )
308 end
309
310 @doc "GET /api/v1/accounts/:id/following"
311 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
312 params =
313 params
314 |> Enum.map(fn {key, value} -> {to_string(key), value} end)
315 |> Enum.into(%{})
316
317 followers =
318 cond do
319 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
320 user.hide_follows -> []
321 true -> MastodonAPI.get_friends(user, params)
322 end
323
324 conn
325 |> add_link_headers(followers)
326 # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
327 |> render("index.json",
328 for: for_user,
329 users: followers,
330 as: :user,
331 embed_relationships: embed_relationships?(params)
332 )
333 end
334
335 @doc "GET /api/v1/accounts/:id/lists"
336 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
337 lists = Pleroma.List.get_lists_account_belongs(user, account)
338
339 conn
340 |> put_view(ListView)
341 |> render("index.json", lists: lists)
342 end
343
344 @doc "POST /api/v1/accounts/:id/follow"
345 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
346 {:error, "Can not follow yourself"}
347 end
348
349 def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
350 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
351 render(conn, "relationship.json", user: follower, target: followed)
352 else
353 {:error, message} -> json_response(conn, :forbidden, %{error: message})
354 end
355 end
356
357 @doc "POST /api/v1/accounts/:id/unfollow"
358 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
359 {:error, "Can not unfollow yourself"}
360 end
361
362 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
363 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
364 render(conn, "relationship.json", user: follower, target: followed)
365 end
366 end
367
368 @doc "POST /api/v1/accounts/:id/mute"
369 def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
370 with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
371 render(conn, "relationship.json", user: muter, target: muted)
372 else
373 {:error, message} -> json_response(conn, :forbidden, %{error: message})
374 end
375 end
376
377 @doc "POST /api/v1/accounts/:id/unmute"
378 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
379 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
380 render(conn, "relationship.json", user: muter, target: muted)
381 else
382 {:error, message} -> json_response(conn, :forbidden, %{error: message})
383 end
384 end
385
386 @doc "POST /api/v1/accounts/:id/block"
387 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
388 with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
389 render(conn, "relationship.json", user: blocker, target: blocked)
390 else
391 {:error, message} -> json_response(conn, :forbidden, %{error: message})
392 end
393 end
394
395 @doc "POST /api/v1/accounts/:id/unblock"
396 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
397 with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
398 render(conn, "relationship.json", user: blocker, target: blocked)
399 else
400 {:error, message} -> json_response(conn, :forbidden, %{error: message})
401 end
402 end
403
404 @doc "POST /api/v1/follows"
405 def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
406 case User.get_cached_by_nickname(uri) do
407 %User{} = user ->
408 conn
409 |> assign(:account, user)
410 |> follow(%{})
411
412 nil ->
413 {:error, :not_found}
414 end
415 end
416
417 @doc "GET /api/v1/mutes"
418 def mutes(%{assigns: %{user: user}} = conn, _) do
419 users = User.muted_users(user, _restrict_deactivated = true)
420 render(conn, "index.json", users: users, for: user, as: :user)
421 end
422
423 @doc "GET /api/v1/blocks"
424 def blocks(%{assigns: %{user: user}} = conn, _) do
425 users = User.blocked_users(user, _restrict_deactivated = true)
426 render(conn, "index.json", users: users, for: user, as: :user)
427 end
428
429 @doc "GET /api/v1/endorsements"
430 def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
431
432 @doc "GET /api/v1/identity_proofs"
433 def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
434 end