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