Merge branch 'docs/direct_conversation_id' 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 != :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, "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) do
263 params =
264 params
265 |> Map.put("tag", params["tagged"])
266 |> Map.delete("godmode")
267
268 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
269
270 conn
271 |> add_link_headers(activities)
272 |> put_view(StatusView)
273 |> render("index.json", activities: activities, for: reading_user, as: :activity)
274 end
275 end
276
277 @doc "GET /api/v1/accounts/:id/followers"
278 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
279 followers =
280 cond do
281 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
282 user.hide_followers -> []
283 true -> MastodonAPI.get_followers(user, params)
284 end
285
286 conn
287 |> add_link_headers(followers)
288 |> render("index.json", for: for_user, users: followers, as: :user)
289 end
290
291 @doc "GET /api/v1/accounts/:id/following"
292 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
293 followers =
294 cond do
295 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
296 user.hide_follows -> []
297 true -> MastodonAPI.get_friends(user, params)
298 end
299
300 conn
301 |> add_link_headers(followers)
302 |> render("index.json", for: for_user, users: followers, as: :user)
303 end
304
305 @doc "GET /api/v1/accounts/:id/lists"
306 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
307 lists = Pleroma.List.get_lists_account_belongs(user, account)
308
309 conn
310 |> put_view(ListView)
311 |> render("index.json", lists: lists)
312 end
313
314 @doc "POST /api/v1/accounts/:id/follow"
315 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
316 {:error, :not_found}
317 end
318
319 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
320 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
321 render(conn, "relationship.json", user: follower, target: followed)
322 else
323 {:error, message} -> json_response(conn, :forbidden, %{error: message})
324 end
325 end
326
327 @doc "POST /api/v1/accounts/:id/unfollow"
328 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
329 {:error, :not_found}
330 end
331
332 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
333 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
334 render(conn, "relationship.json", user: follower, target: followed)
335 end
336 end
337
338 @doc "POST /api/v1/accounts/:id/mute"
339 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
340 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
341
342 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
343 render(conn, "relationship.json", user: muter, target: muted)
344 else
345 {:error, message} -> json_response(conn, :forbidden, %{error: message})
346 end
347 end
348
349 @doc "POST /api/v1/accounts/:id/unmute"
350 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
351 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
352 render(conn, "relationship.json", user: muter, target: muted)
353 else
354 {:error, message} -> json_response(conn, :forbidden, %{error: message})
355 end
356 end
357
358 @doc "POST /api/v1/accounts/:id/block"
359 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
360 with {:ok, _user_block} <- User.block(blocker, blocked),
361 {:ok, _activity} <- ActivityPub.block(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/accounts/:id/unblock"
369 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
370 with {:ok, _user_block} <- User.unblock(blocker, blocked),
371 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
372 render(conn, "relationship.json", user: blocker, target: blocked)
373 else
374 {:error, message} -> json_response(conn, :forbidden, %{error: message})
375 end
376 end
377
378 @doc "POST /api/v1/follows"
379 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
380 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
381 {_, true} <- {:followed, follower.id != followed.id},
382 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
383 render(conn, "show.json", user: followed, for: follower)
384 else
385 {:followed, _} -> {:error, :not_found}
386 {:error, message} -> json_response(conn, :forbidden, %{error: message})
387 end
388 end
389
390 @doc "GET /api/v1/mutes"
391 def mutes(%{assigns: %{user: user}} = conn, _) do
392 users = User.muted_users(user, _restrict_deactivated = true)
393 render(conn, "index.json", users: users, for: user, as: :user)
394 end
395
396 @doc "GET /api/v1/blocks"
397 def blocks(%{assigns: %{user: user}} = conn, _) do
398 users = User.blocked_users(user, _restrict_deactivated = true)
399 render(conn, "index.json", users: users, for: user, as: :user)
400 end
401
402 @doc "GET /api/v1/endorsements"
403 def endorsements(conn, params),
404 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
405 end