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