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