Merge branch 'benchmarks/favourites_timeline' into 'develop'
[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.visible_for?(user, 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, muter} <- User.mute(muter, muted, notifications?) do
327 render(conn, "relationship.json", user: muter, target: muted)
328 else
329 {:error, message} -> json_response(conn, :forbidden, %{error: message})
330 end
331 end
332
333 @doc "POST /api/v1/accounts/:id/unmute"
334 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
335 with {:ok, muter} <- User.unmute(muter, muted) do
336 render(conn, "relationship.json", user: muter, target: muted)
337 else
338 {:error, message} -> json_response(conn, :forbidden, %{error: message})
339 end
340 end
341
342 @doc "POST /api/v1/accounts/:id/block"
343 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
344 with {:ok, blocker} <- User.block(blocker, blocked),
345 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
346 render(conn, "relationship.json", user: blocker, target: blocked)
347 else
348 {:error, message} -> json_response(conn, :forbidden, %{error: message})
349 end
350 end
351
352 @doc "POST /api/v1/accounts/:id/unblock"
353 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
354 with {:ok, blocker} <- User.unblock(blocker, blocked),
355 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
356 render(conn, "relationship.json", user: blocker, target: blocked)
357 else
358 {:error, message} -> json_response(conn, :forbidden, %{error: message})
359 end
360 end
361
362 @doc "POST /api/v1/follows"
363 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
364 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
365 {_, true} <- {:followed, follower.id != followed.id},
366 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
367 render(conn, "show.json", user: followed, for: follower)
368 else
369 {:followed, _} -> {:error, :not_found}
370 {:error, message} -> json_response(conn, :forbidden, %{error: message})
371 end
372 end
373
374 @doc "GET /api/v1/mutes"
375 def mutes(%{assigns: %{user: user}} = conn, _) do
376 render(conn, "index.json", users: User.muted_users(user), for: user, as: :user)
377 end
378
379 @doc "GET /api/v1/blocks"
380 def blocks(%{assigns: %{user: user}} = conn, _) do
381 render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user)
382 end
383
384 @doc "GET /api/v1/endorsements"
385 def endorsements(conn, params),
386 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
387 end