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