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