Merge branch '1364-notifications-sending-control' 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 @relationship_actions [:follow, :unfollow]
67 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
68
69 plug(
70 RateLimiter,
71 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
72 )
73
74 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
75 plug(RateLimiter, [name: :app_account_creation] when action == :create)
76 plug(:assign_account_by_id when action in @needs_account)
77
78 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
79
80 @doc "POST /api/v1/accounts"
81 def create(
82 %{assigns: %{app: app}} = conn,
83 %{"username" => nickname, "password" => _, "agreement" => true} = params
84 ) do
85 params =
86 params
87 |> Map.take([
88 "email",
89 "captcha_solution",
90 "captcha_token",
91 "captcha_answer_data",
92 "token",
93 "password"
94 ])
95 |> Map.put("nickname", nickname)
96 |> Map.put("fullname", params["fullname"] || nickname)
97 |> Map.put("bio", params["bio"] || "")
98 |> Map.put("confirm", params["password"])
99
100 with :ok <- validate_email_param(params),
101 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
102 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
103 json(conn, %{
104 token_type: "Bearer",
105 access_token: token.token,
106 scope: app.scopes,
107 created_at: Token.Utils.format_created_at(token)
108 })
109 else
110 {:error, errors} -> json_response(conn, :bad_request, errors)
111 end
112 end
113
114 def create(%{assigns: %{app: _app}} = conn, _) do
115 render_error(conn, :bad_request, "Missing parameters")
116 end
117
118 def create(conn, _) do
119 render_error(conn, :forbidden, "Invalid credentials")
120 end
121
122 defp validate_email_param(%{"email" => _}), do: :ok
123
124 defp validate_email_param(_) do
125 case Pleroma.Config.get([:instance, :account_activation_required]) do
126 true -> {:error, %{"error" => "Missing parameters"}}
127 _ -> :ok
128 end
129 end
130
131 @doc "GET /api/v1/accounts/verify_credentials"
132 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
133 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
134
135 render(conn, "show.json",
136 user: user,
137 for: user,
138 with_pleroma_settings: true,
139 with_chat_token: chat_token
140 )
141 end
142
143 @doc "PATCH /api/v1/accounts/update_credentials"
144 def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
145 user = original_user
146
147 params =
148 if Map.has_key?(params, "fields_attributes") do
149 Map.update!(params, "fields_attributes", fn fields ->
150 fields
151 |> normalize_fields_attributes()
152 |> Enum.filter(fn %{"name" => n} -> n != "" end)
153 end)
154 else
155 params
156 end
157
158 user_params =
159 [
160 :no_rich_text,
161 :locked,
162 :hide_followers_count,
163 :hide_follows_count,
164 :hide_followers,
165 :hide_follows,
166 :hide_favorites,
167 :show_role,
168 :skip_thread_containment,
169 :allow_following_move,
170 :discoverable
171 ]
172 |> Enum.reduce(%{}, fn key, acc ->
173 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
174 end)
175 |> add_if_present(params, "display_name", :name)
176 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
177 |> add_if_present(params, "avatar", :avatar, fn value ->
178 with %Plug.Upload{} <- value,
179 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
180 {:ok, object.data}
181 end
182 end)
183 |> add_if_present(params, "header", :banner, fn value ->
184 with %Plug.Upload{} <- value,
185 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
186 {:ok, object.data}
187 end
188 end)
189 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
190 with %Plug.Upload{} <- value,
191 {:ok, object} <- ActivityPub.upload(value, type: :background) do
192 {:ok, object.data}
193 end
194 end)
195 |> add_if_present(params, "fields_attributes", :fields, fn fields ->
196 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
197
198 {:ok, fields}
199 end)
200 |> add_if_present(params, "fields_attributes", :raw_fields)
201 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
202 {:ok, Map.merge(user.pleroma_settings_store, value)}
203 end)
204 |> add_if_present(params, "default_scope", :default_scope)
205 |> add_if_present(params, "actor_type", :actor_type)
206
207 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
208
209 user_emojis =
210 user
211 |> Map.get(:emoji, [])
212 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
213 |> Enum.dedup()
214
215 user_params = Map.put(user_params, :emoji, user_emojis)
216 changeset = User.update_changeset(user, user_params)
217
218 with {:ok, user} <- User.update_and_set_cache(changeset) do
219 if original_user != user, do: CommonAPI.update(user)
220
221 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
222 else
223 _e -> render_error(conn, :forbidden, "Invalid request")
224 end
225 end
226
227 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
228 with true <- Map.has_key?(params, params_field),
229 {:ok, new_value} <- value_function.(params[params_field]) do
230 Map.put(map, map_field, new_value)
231 else
232 _ -> map
233 end
234 end
235
236 defp normalize_fields_attributes(fields) do
237 if Enum.all?(fields, &is_tuple/1) do
238 Enum.map(fields, fn {_, v} -> v end)
239 else
240 fields
241 end
242 end
243
244 @doc "GET /api/v1/accounts/relationships"
245 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
246 targets = User.get_all_by_ids(List.wrap(id))
247
248 render(conn, "relationships.json", user: user, targets: targets)
249 end
250
251 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
252 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
253
254 @doc "GET /api/v1/accounts/:id"
255 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
256 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
257 true <- User.visible_for?(user, for_user) do
258 render(conn, "show.json", user: user, for: for_user)
259 else
260 _e -> render_error(conn, :not_found, "Can't find user")
261 end
262 end
263
264 @doc "GET /api/v1/accounts/:id/statuses"
265 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
266 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
267 params =
268 params
269 |> Map.put("tag", params["tagged"])
270 |> Map.delete("godmode")
271
272 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
273
274 conn
275 |> add_link_headers(activities)
276 |> put_view(StatusView)
277 |> render("index.json", activities: activities, for: reading_user, as: :activity)
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