Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms
[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: original_user}} = conn, params) do
150 user = original_user
151
152 user_params =
153 [
154 :no_rich_text,
155 :locked,
156 :hide_followers_count,
157 :hide_follows_count,
158 :hide_followers,
159 :hide_follows,
160 :hide_favorites,
161 :show_role,
162 :skip_thread_containment,
163 :allow_following_move,
164 :discoverable
165 ]
166 |> Enum.reduce(%{}, fn key, acc ->
167 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
168 end)
169 |> add_if_present(params, "display_name", :name)
170 |> add_if_present(params, "note", :bio)
171 |> add_if_present(params, "avatar", :avatar)
172 |> add_if_present(params, "header", :banner)
173 |> add_if_present(params, "pleroma_background_image", :background)
174 |> add_if_present(
175 params,
176 "fields_attributes",
177 :raw_fields,
178 &{:ok, normalize_fields_attributes(&1)}
179 )
180 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
181 |> add_if_present(params, "default_scope", :default_scope)
182 |> add_if_present(params, "actor_type", :actor_type)
183
184 changeset = User.update_changeset(user, user_params)
185
186 with {:ok, user} <- User.update_and_set_cache(changeset) do
187 if original_user != user, do: CommonAPI.update(user)
188
189 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
190 else
191 _e -> render_error(conn, :forbidden, "Invalid request")
192 end
193 end
194
195 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
196 with true <- Map.has_key?(params, params_field),
197 {:ok, new_value} <- value_function.(params[params_field]) do
198 Map.put(map, map_field, new_value)
199 else
200 _ -> map
201 end
202 end
203
204 defp normalize_fields_attributes(fields) do
205 if Enum.all?(fields, &is_tuple/1) do
206 Enum.map(fields, fn {_, v} -> v end)
207 else
208 fields
209 end
210 end
211
212 @doc "GET /api/v1/accounts/relationships"
213 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
214 targets = User.get_all_by_ids(List.wrap(id))
215
216 render(conn, "relationships.json", user: user, targets: targets)
217 end
218
219 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
220 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
221
222 @doc "GET /api/v1/accounts/:id"
223 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
224 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
225 true <- User.visible_for?(user, for_user) do
226 render(conn, "show.json", user: user, for: for_user)
227 else
228 _e -> render_error(conn, :not_found, "Can't find user")
229 end
230 end
231
232 @doc "GET /api/v1/accounts/:id/statuses"
233 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
234 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
235 true <- User.visible_for?(user, reading_user) do
236 params =
237 params
238 |> Map.put("tag", params["tagged"])
239 |> Map.delete("godmode")
240
241 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
242
243 conn
244 |> add_link_headers(activities)
245 |> put_view(StatusView)
246 |> render("index.json",
247 activities: activities,
248 for: reading_user,
249 as: :activity,
250 skip_relationships: skip_relationships?(params)
251 )
252 else
253 _e -> render_error(conn, :not_found, "Can't find user")
254 end
255 end
256
257 @doc "GET /api/v1/accounts/:id/followers"
258 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
259 followers =
260 cond do
261 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
262 user.hide_followers -> []
263 true -> MastodonAPI.get_followers(user, params)
264 end
265
266 conn
267 |> add_link_headers(followers)
268 |> render("index.json", for: for_user, users: followers, as: :user)
269 end
270
271 @doc "GET /api/v1/accounts/:id/following"
272 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
273 followers =
274 cond do
275 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
276 user.hide_follows -> []
277 true -> MastodonAPI.get_friends(user, params)
278 end
279
280 conn
281 |> add_link_headers(followers)
282 |> render("index.json", for: for_user, users: followers, as: :user)
283 end
284
285 @doc "GET /api/v1/accounts/:id/lists"
286 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
287 lists = Pleroma.List.get_lists_account_belongs(user, account)
288
289 conn
290 |> put_view(ListView)
291 |> render("index.json", lists: lists)
292 end
293
294 @doc "POST /api/v1/accounts/:id/follow"
295 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
296 {:error, :not_found}
297 end
298
299 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
300 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
301 render(conn, "relationship.json", user: follower, target: followed)
302 else
303 {:error, message} -> json_response(conn, :forbidden, %{error: message})
304 end
305 end
306
307 @doc "POST /api/v1/accounts/:id/unfollow"
308 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
309 {:error, :not_found}
310 end
311
312 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
313 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
314 render(conn, "relationship.json", user: follower, target: followed)
315 end
316 end
317
318 @doc "POST /api/v1/accounts/:id/mute"
319 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
320 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
321
322 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
323 render(conn, "relationship.json", user: muter, target: muted)
324 else
325 {:error, message} -> json_response(conn, :forbidden, %{error: message})
326 end
327 end
328
329 @doc "POST /api/v1/accounts/:id/unmute"
330 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
331 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
332 render(conn, "relationship.json", user: muter, target: muted)
333 else
334 {:error, message} -> json_response(conn, :forbidden, %{error: message})
335 end
336 end
337
338 @doc "POST /api/v1/accounts/:id/block"
339 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
340 with {:ok, _user_block} <- User.block(blocker, blocked),
341 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
342 render(conn, "relationship.json", user: blocker, target: blocked)
343 else
344 {:error, message} -> json_response(conn, :forbidden, %{error: message})
345 end
346 end
347
348 @doc "POST /api/v1/accounts/:id/unblock"
349 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
350 with {:ok, _user_block} <- User.unblock(blocker, blocked),
351 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
352 render(conn, "relationship.json", user: blocker, target: blocked)
353 else
354 {:error, message} -> json_response(conn, :forbidden, %{error: message})
355 end
356 end
357
358 @doc "POST /api/v1/follows"
359 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
360 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
361 {_, true} <- {:followed, follower.id != followed.id},
362 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
363 render(conn, "show.json", user: followed, for: follower)
364 else
365 {:followed, _} -> {:error, :not_found}
366 {:error, message} -> json_response(conn, :forbidden, %{error: message})
367 end
368 end
369
370 @doc "GET /api/v1/mutes"
371 def mutes(%{assigns: %{user: user}} = conn, _) do
372 users = User.muted_users(user, _restrict_deactivated = true)
373 render(conn, "index.json", users: users, for: user, as: :user)
374 end
375
376 @doc "GET /api/v1/blocks"
377 def blocks(%{assigns: %{user: user}} = conn, _) do
378 users = User.blocked_users(user, _restrict_deactivated = true)
379 render(conn, "index.json", users: users, for: user, as: :user)
380 end
381
382 @doc "GET /api/v1/endorsements"
383 def endorsements(conn, params),
384 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
385 end