Merge branch 'fix-feed-pagination' into 'develop'
[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: [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 not in [:create, :show, :statuses]
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, "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 <- validate_email_param(params),
97 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
98 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
99 json(conn, %{
100 token_type: "Bearer",
101 access_token: token.token,
102 scope: app.scopes,
103 created_at: Token.Utils.format_created_at(token)
104 })
105 else
106 {:error, errors} -> json_response(conn, :bad_request, errors)
107 end
108 end
109
110 def create(%{assigns: %{app: _app}} = conn, _) do
111 render_error(conn, :bad_request, "Missing parameters")
112 end
113
114 def create(conn, _) do
115 render_error(conn, :forbidden, "Invalid credentials")
116 end
117
118 defp validate_email_param(%{"email" => _}), do: :ok
119
120 defp validate_email_param(_) do
121 case Pleroma.Config.get([:instance, :account_activation_required]) do
122 true -> {:error, %{"error" => "Missing parameters"}}
123 _ -> :ok
124 end
125 end
126
127 @doc "GET /api/v1/accounts/verify_credentials"
128 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
129 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
130
131 render(conn, "show.json",
132 user: user,
133 for: user,
134 with_pleroma_settings: true,
135 with_chat_token: chat_token
136 )
137 end
138
139 @doc "PATCH /api/v1/accounts/update_credentials"
140 def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
141 user = original_user
142
143 params =
144 if Map.has_key?(params, "fields_attributes") do
145 Map.update!(params, "fields_attributes", fn fields ->
146 fields
147 |> normalize_fields_attributes()
148 |> Enum.filter(fn %{"name" => n} -> n != "" end)
149 end)
150 else
151 params
152 end
153
154 user_params =
155 [
156 :no_rich_text,
157 :locked,
158 :hide_followers_count,
159 :hide_follows_count,
160 :hide_followers,
161 :hide_follows,
162 :hide_favorites,
163 :show_role,
164 :skip_thread_containment,
165 :allow_following_move,
166 :discoverable
167 ]
168 |> Enum.reduce(%{}, fn key, acc ->
169 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
170 end)
171 |> add_if_present(params, "display_name", :name)
172 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
173 |> add_if_present(params, "avatar", :avatar, fn value ->
174 with %Plug.Upload{} <- value,
175 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
176 {:ok, object.data}
177 end
178 end)
179 |> add_if_present(params, "header", :banner, fn value ->
180 with %Plug.Upload{} <- value,
181 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
182 {:ok, object.data}
183 end
184 end)
185 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
186 with %Plug.Upload{} <- value,
187 {:ok, object} <- ActivityPub.upload(value, type: :background) do
188 {:ok, object.data}
189 end
190 end)
191 |> add_if_present(params, "fields_attributes", :fields, fn fields ->
192 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
193
194 {:ok, fields}
195 end)
196 |> add_if_present(params, "fields_attributes", :raw_fields)
197 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
198 {:ok, Map.merge(user.pleroma_settings_store, value)}
199 end)
200 |> add_if_present(params, "default_scope", :default_scope)
201 |> add_if_present(params, "actor_type", :actor_type)
202
203 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
204
205 user_emojis =
206 user
207 |> Map.get(:emoji, [])
208 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
209 |> Enum.dedup()
210
211 user_params = Map.put(user_params, :emoji, user_emojis)
212 changeset = User.update_changeset(user, user_params)
213
214 with {:ok, user} <- User.update_and_set_cache(changeset) do
215 if original_user != user, do: CommonAPI.update(user)
216
217 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
218 else
219 _e -> render_error(conn, :forbidden, "Invalid request")
220 end
221 end
222
223 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
224 with true <- Map.has_key?(params, params_field),
225 {:ok, new_value} <- value_function.(params[params_field]) do
226 Map.put(map, map_field, new_value)
227 else
228 _ -> map
229 end
230 end
231
232 defp normalize_fields_attributes(fields) do
233 if Enum.all?(fields, &is_tuple/1) do
234 Enum.map(fields, fn {_, v} -> v end)
235 else
236 fields
237 end
238 end
239
240 @doc "GET /api/v1/accounts/relationships"
241 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
242 targets = User.get_all_by_ids(List.wrap(id))
243
244 render(conn, "relationships.json", user: user, targets: targets)
245 end
246
247 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
248 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
249
250 @doc "GET /api/v1/accounts/:id"
251 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
252 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
253 true <- User.visible_for?(user, for_user) do
254 render(conn, "show.json", user: user, for: for_user)
255 else
256 _e -> render_error(conn, :not_found, "Can't find user")
257 end
258 end
259
260 @doc "GET /api/v1/accounts/:id/statuses"
261 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
262 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
263 true <- User.visible_for?(user, reading_user) do
264 params =
265 params
266 |> Map.put("tag", params["tagged"])
267 |> Map.delete("godmode")
268
269 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
270
271 conn
272 |> add_link_headers(activities)
273 |> put_view(StatusView)
274 |> render("index.json", activities: activities, for: reading_user, as: :activity)
275 else
276 _e -> render_error(conn, :not_found, "Can't find user")
277 end
278 end
279
280 @doc "GET /api/v1/accounts/:id/followers"
281 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
282 followers =
283 cond do
284 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
285 user.hide_followers -> []
286 true -> MastodonAPI.get_followers(user, params)
287 end
288
289 conn
290 |> add_link_headers(followers)
291 |> render("index.json", for: for_user, users: followers, as: :user)
292 end
293
294 @doc "GET /api/v1/accounts/:id/following"
295 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
296 followers =
297 cond do
298 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
299 user.hide_follows -> []
300 true -> MastodonAPI.get_friends(user, params)
301 end
302
303 conn
304 |> add_link_headers(followers)
305 |> render("index.json", for: for_user, users: followers, as: :user)
306 end
307
308 @doc "GET /api/v1/accounts/:id/lists"
309 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
310 lists = Pleroma.List.get_lists_account_belongs(user, account)
311
312 conn
313 |> put_view(ListView)
314 |> render("index.json", lists: lists)
315 end
316
317 @doc "POST /api/v1/accounts/:id/follow"
318 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
319 {:error, :not_found}
320 end
321
322 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
323 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
324 render(conn, "relationship.json", user: follower, target: followed)
325 else
326 {:error, message} -> json_response(conn, :forbidden, %{error: message})
327 end
328 end
329
330 @doc "POST /api/v1/accounts/:id/unfollow"
331 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
332 {:error, :not_found}
333 end
334
335 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
336 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
337 render(conn, "relationship.json", user: follower, target: followed)
338 end
339 end
340
341 @doc "POST /api/v1/accounts/:id/mute"
342 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
343 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
344
345 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
346 render(conn, "relationship.json", user: muter, target: muted)
347 else
348 {:error, message} -> json_response(conn, :forbidden, %{error: message})
349 end
350 end
351
352 @doc "POST /api/v1/accounts/:id/unmute"
353 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
354 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
355 render(conn, "relationship.json", user: muter, target: muted)
356 else
357 {:error, message} -> json_response(conn, :forbidden, %{error: message})
358 end
359 end
360
361 @doc "POST /api/v1/accounts/:id/block"
362 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
363 with {:ok, _user_block} <- User.block(blocker, blocked),
364 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
365 render(conn, "relationship.json", user: blocker, target: blocked)
366 else
367 {:error, message} -> json_response(conn, :forbidden, %{error: message})
368 end
369 end
370
371 @doc "POST /api/v1/accounts/:id/unblock"
372 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
373 with {:ok, _user_block} <- User.unblock(blocker, blocked),
374 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
375 render(conn, "relationship.json", user: blocker, target: blocked)
376 else
377 {:error, message} -> json_response(conn, :forbidden, %{error: message})
378 end
379 end
380
381 @doc "POST /api/v1/follows"
382 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
383 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
384 {_, true} <- {:followed, follower.id != followed.id},
385 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
386 render(conn, "show.json", user: followed, for: follower)
387 else
388 {:followed, _} -> {:error, :not_found}
389 {:error, message} -> json_response(conn, :forbidden, %{error: message})
390 end
391 end
392
393 @doc "GET /api/v1/mutes"
394 def mutes(%{assigns: %{user: user}} = conn, _) do
395 users = User.muted_users(user, _restrict_deactivated = true)
396 render(conn, "index.json", users: users, for: user, as: :user)
397 end
398
399 @doc "GET /api/v1/blocks"
400 def blocks(%{assigns: %{user: user}} = conn, _) do
401 users = User.blocked_users(user, _restrict_deactivated = true)
402 render(conn, "index.json", users: users, for: user, as: :user)
403 end
404
405 @doc "GET /api/v1/endorsements"
406 def endorsements(conn, params),
407 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
408 end