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