Merge branch 'develop' into global-status-expiration
[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.Plugs.OAuthScopesPlug
12 alias Pleroma.Plugs.RateLimiter
13 alias Pleroma.User
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.CommonAPI
16 alias Pleroma.Web.MastodonAPI.ListView
17 alias Pleroma.Web.MastodonAPI.MastodonAPI
18 alias Pleroma.Web.MastodonAPI.StatusView
19 alias Pleroma.Web.OAuth.Token
20 alias Pleroma.Web.TwitterAPI.TwitterAPI
21
22 plug(
23 OAuthScopesPlug,
24 %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
25 when action == :show
26 )
27
28 plug(
29 OAuthScopesPlug,
30 %{scopes: ["read:accounts"]}
31 when action in [:endorsements, :verify_credentials, :followers, :following]
32 )
33
34 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
35
36 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
37
38 plug(
39 OAuthScopesPlug,
40 %{scopes: ["follow", "read:blocks"]} when action == :blocks
41 )
42
43 plug(
44 OAuthScopesPlug,
45 %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
46 )
47
48 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
49
50 # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
51 plug(
52 OAuthScopesPlug,
53 %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
54 )
55
56 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
57
58 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
59
60 plug(
61 Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
62 when action not in [:create, :show, :statuses]
63 )
64
65 @relationship_actions [:follow, :unfollow]
66 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
67
68 plug(
69 RateLimiter,
70 [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
71 )
72
73 plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
74 plug(RateLimiter, [name: :app_account_creation] when action == :create)
75 plug(:assign_account_by_id when action in @needs_account)
76
77 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
78
79 @doc "POST /api/v1/accounts"
80 def create(
81 %{assigns: %{app: app}} = conn,
82 %{"username" => nickname, "password" => _, "agreement" => true} = params
83 ) do
84 params =
85 params
86 |> Map.take([
87 "email",
88 "captcha_solution",
89 "captcha_token",
90 "captcha_answer_data",
91 "token",
92 "password"
93 ])
94 |> Map.put("nickname", nickname)
95 |> Map.put("fullname", params["fullname"] || nickname)
96 |> Map.put("bio", params["bio"] || "")
97 |> Map.put("confirm", params["password"])
98
99 with :ok <- validate_email_param(params),
100 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
101 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
102 json(conn, %{
103 token_type: "Bearer",
104 access_token: token.token,
105 scope: app.scopes,
106 created_at: Token.Utils.format_created_at(token)
107 })
108 else
109 {:error, errors} -> json_response(conn, :bad_request, errors)
110 end
111 end
112
113 def create(%{assigns: %{app: _app}} = conn, _) do
114 render_error(conn, :bad_request, "Missing parameters")
115 end
116
117 def create(conn, _) do
118 render_error(conn, :forbidden, "Invalid credentials")
119 end
120
121 defp validate_email_param(%{"email" => _}), do: :ok
122
123 defp validate_email_param(_) do
124 case Pleroma.Config.get([:instance, :account_activation_required]) do
125 true -> {:error, %{"error" => "Missing parameters"}}
126 _ -> :ok
127 end
128 end
129
130 @doc "GET /api/v1/accounts/verify_credentials"
131 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
132 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
133
134 render(conn, "show.json",
135 user: user,
136 for: user,
137 with_pleroma_settings: true,
138 with_chat_token: chat_token
139 )
140 end
141
142 @doc "PATCH /api/v1/accounts/update_credentials"
143 def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
144 user = original_user
145
146 user_params =
147 [
148 :no_rich_text,
149 :locked,
150 :hide_followers_count,
151 :hide_follows_count,
152 :hide_followers,
153 :hide_follows,
154 :hide_favorites,
155 :show_role,
156 :skip_thread_containment,
157 :allow_following_move,
158 :discoverable
159 ]
160 |> Enum.reduce(%{}, fn key, acc ->
161 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
162 end)
163 |> add_if_present(params, "display_name", :name)
164 |> add_if_present(params, "note", :bio)
165 |> add_if_present(params, "avatar", :avatar)
166 |> add_if_present(params, "header", :banner)
167 |> add_if_present(params, "pleroma_background_image", :background)
168 |> add_if_present(
169 params,
170 "fields_attributes",
171 :raw_fields,
172 &{:ok, normalize_fields_attributes(&1)}
173 )
174 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
175 |> add_if_present(params, "default_scope", :default_scope)
176 |> add_if_present(params, "actor_type", :actor_type)
177
178 changeset = User.update_changeset(user, user_params)
179
180 with {:ok, user} <- User.update_and_set_cache(changeset) do
181 if original_user != user, do: CommonAPI.update(user)
182
183 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
184 else
185 _e -> render_error(conn, :forbidden, "Invalid request")
186 end
187 end
188
189 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
190 with true <- Map.has_key?(params, params_field),
191 {:ok, new_value} <- value_function.(params[params_field]) do
192 Map.put(map, map_field, new_value)
193 else
194 _ -> map
195 end
196 end
197
198 defp normalize_fields_attributes(fields) do
199 if Enum.all?(fields, &is_tuple/1) do
200 Enum.map(fields, fn {_, v} -> v end)
201 else
202 fields
203 end
204 end
205
206 @doc "GET /api/v1/accounts/relationships"
207 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
208 targets = User.get_all_by_ids(List.wrap(id))
209
210 render(conn, "relationships.json", user: user, targets: targets)
211 end
212
213 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
214 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
215
216 @doc "GET /api/v1/accounts/:id"
217 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
218 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
219 true <- User.visible_for?(user, for_user) do
220 render(conn, "show.json", user: user, for: for_user)
221 else
222 _e -> render_error(conn, :not_found, "Can't find user")
223 end
224 end
225
226 @doc "GET /api/v1/accounts/:id/statuses"
227 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
228 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
229 true <- User.visible_for?(user, reading_user) do
230 params =
231 params
232 |> Map.put("tag", params["tagged"])
233 |> Map.delete("godmode")
234
235 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
236
237 conn
238 |> add_link_headers(activities)
239 |> put_view(StatusView)
240 |> render("index.json", activities: activities, for: reading_user, as: :activity)
241 else
242 _e -> render_error(conn, :not_found, "Can't find user")
243 end
244 end
245
246 @doc "GET /api/v1/accounts/:id/followers"
247 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
248 followers =
249 cond do
250 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
251 user.hide_followers -> []
252 true -> MastodonAPI.get_followers(user, params)
253 end
254
255 conn
256 |> add_link_headers(followers)
257 |> render("index.json", for: for_user, users: followers, as: :user)
258 end
259
260 @doc "GET /api/v1/accounts/:id/following"
261 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
262 followers =
263 cond do
264 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
265 user.hide_follows -> []
266 true -> MastodonAPI.get_friends(user, params)
267 end
268
269 conn
270 |> add_link_headers(followers)
271 |> render("index.json", for: for_user, users: followers, as: :user)
272 end
273
274 @doc "GET /api/v1/accounts/:id/lists"
275 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
276 lists = Pleroma.List.get_lists_account_belongs(user, account)
277
278 conn
279 |> put_view(ListView)
280 |> render("index.json", lists: lists)
281 end
282
283 @doc "POST /api/v1/accounts/:id/follow"
284 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
285 {:error, :not_found}
286 end
287
288 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
289 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
290 render(conn, "relationship.json", user: follower, target: followed)
291 else
292 {:error, message} -> json_response(conn, :forbidden, %{error: message})
293 end
294 end
295
296 @doc "POST /api/v1/accounts/:id/unfollow"
297 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
298 {:error, :not_found}
299 end
300
301 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
302 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
303 render(conn, "relationship.json", user: follower, target: followed)
304 end
305 end
306
307 @doc "POST /api/v1/accounts/:id/mute"
308 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
309 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
310
311 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
312 render(conn, "relationship.json", user: muter, target: muted)
313 else
314 {:error, message} -> json_response(conn, :forbidden, %{error: message})
315 end
316 end
317
318 @doc "POST /api/v1/accounts/:id/unmute"
319 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
320 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
321 render(conn, "relationship.json", user: muter, target: muted)
322 else
323 {:error, message} -> json_response(conn, :forbidden, %{error: message})
324 end
325 end
326
327 @doc "POST /api/v1/accounts/:id/block"
328 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
329 with {:ok, _user_block} <- User.block(blocker, blocked),
330 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
331 render(conn, "relationship.json", user: blocker, target: blocked)
332 else
333 {:error, message} -> json_response(conn, :forbidden, %{error: message})
334 end
335 end
336
337 @doc "POST /api/v1/accounts/:id/unblock"
338 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
339 with {:ok, _user_block} <- User.unblock(blocker, blocked),
340 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
341 render(conn, "relationship.json", user: blocker, target: blocked)
342 else
343 {:error, message} -> json_response(conn, :forbidden, %{error: message})
344 end
345 end
346
347 @doc "POST /api/v1/follows"
348 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
349 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
350 {_, true} <- {:followed, follower.id != followed.id},
351 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
352 render(conn, "show.json", user: followed, for: follower)
353 else
354 {:followed, _} -> {:error, :not_found}
355 {:error, message} -> json_response(conn, :forbidden, %{error: message})
356 end
357 end
358
359 @doc "GET /api/v1/mutes"
360 def mutes(%{assigns: %{user: user}} = conn, _) do
361 users = User.muted_users(user, _restrict_deactivated = true)
362 render(conn, "index.json", users: users, for: user, as: :user)
363 end
364
365 @doc "GET /api/v1/blocks"
366 def blocks(%{assigns: %{user: user}} = conn, _) do
367 users = User.blocked_users(user, _restrict_deactivated = true)
368 render(conn, "index.json", users: users, for: user, as: :user)
369 end
370
371 @doc "GET /api/v1/endorsements"
372 def endorsements(conn, params),
373 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
374 end