3fcaa6be6c9b4a0abfd325947b6abf5644ba2c15
[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: user}} = conn, params) do
144 user_params =
145 [
146 :no_rich_text,
147 :locked,
148 :hide_followers_count,
149 :hide_follows_count,
150 :hide_followers,
151 :hide_follows,
152 :hide_favorites,
153 :show_role,
154 :skip_thread_containment,
155 :allow_following_move,
156 :discoverable
157 ]
158 |> Enum.reduce(%{}, fn key, acc ->
159 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
160 end)
161 |> add_if_present(params, "display_name", :name)
162 |> add_if_present(params, "note", :bio)
163 |> add_if_present(params, "avatar", :avatar)
164 |> add_if_present(params, "header", :banner)
165 |> add_if_present(params, "pleroma_background_image", :background)
166 |> add_if_present(
167 params,
168 "fields_attributes",
169 :raw_fields,
170 &{:ok, normalize_fields_attributes(&1)}
171 )
172 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
173 |> add_if_present(params, "default_scope", :default_scope)
174 |> add_if_present(params, "actor_type", :actor_type)
175
176 changeset = User.update_changeset(user, user_params)
177
178 with {:ok, user} <- User.update_and_set_cache(changeset) do
179 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
180 else
181 _e -> render_error(conn, :forbidden, "Invalid request")
182 end
183 end
184
185 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
186 with true <- Map.has_key?(params, params_field),
187 {:ok, new_value} <- value_function.(params[params_field]) do
188 Map.put(map, map_field, new_value)
189 else
190 _ -> map
191 end
192 end
193
194 defp normalize_fields_attributes(fields) do
195 if Enum.all?(fields, &is_tuple/1) do
196 Enum.map(fields, fn {_, v} -> v end)
197 else
198 fields
199 end
200 end
201
202 @doc "GET /api/v1/accounts/relationships"
203 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
204 targets = User.get_all_by_ids(List.wrap(id))
205
206 render(conn, "relationships.json", user: user, targets: targets)
207 end
208
209 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
210 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
211
212 @doc "GET /api/v1/accounts/:id"
213 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
214 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
215 true <- User.visible_for?(user, for_user) do
216 render(conn, "show.json", user: user, for: for_user)
217 else
218 _e -> render_error(conn, :not_found, "Can't find user")
219 end
220 end
221
222 @doc "GET /api/v1/accounts/:id/statuses"
223 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
224 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
225 true <- User.visible_for?(user, reading_user) do
226 params =
227 params
228 |> Map.put("tag", params["tagged"])
229 |> Map.delete("godmode")
230
231 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
232
233 conn
234 |> add_link_headers(activities)
235 |> put_view(StatusView)
236 |> render("index.json", activities: activities, for: reading_user, as: :activity)
237 else
238 _e -> render_error(conn, :not_found, "Can't find user")
239 end
240 end
241
242 @doc "GET /api/v1/accounts/:id/followers"
243 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
244 followers =
245 cond do
246 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
247 user.hide_followers -> []
248 true -> MastodonAPI.get_followers(user, params)
249 end
250
251 conn
252 |> add_link_headers(followers)
253 |> render("index.json", for: for_user, users: followers, as: :user)
254 end
255
256 @doc "GET /api/v1/accounts/:id/following"
257 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
258 followers =
259 cond do
260 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
261 user.hide_follows -> []
262 true -> MastodonAPI.get_friends(user, params)
263 end
264
265 conn
266 |> add_link_headers(followers)
267 |> render("index.json", for: for_user, users: followers, as: :user)
268 end
269
270 @doc "GET /api/v1/accounts/:id/lists"
271 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
272 lists = Pleroma.List.get_lists_account_belongs(user, account)
273
274 conn
275 |> put_view(ListView)
276 |> render("index.json", lists: lists)
277 end
278
279 @doc "POST /api/v1/accounts/:id/follow"
280 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
281 {:error, :not_found}
282 end
283
284 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
285 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
286 render(conn, "relationship.json", user: follower, target: followed)
287 else
288 {:error, message} -> json_response(conn, :forbidden, %{error: message})
289 end
290 end
291
292 @doc "POST /api/v1/accounts/:id/unfollow"
293 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
294 {:error, :not_found}
295 end
296
297 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
298 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
299 render(conn, "relationship.json", user: follower, target: followed)
300 end
301 end
302
303 @doc "POST /api/v1/accounts/:id/mute"
304 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
305 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
306
307 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
308 render(conn, "relationship.json", user: muter, target: muted)
309 else
310 {:error, message} -> json_response(conn, :forbidden, %{error: message})
311 end
312 end
313
314 @doc "POST /api/v1/accounts/:id/unmute"
315 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
316 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
317 render(conn, "relationship.json", user: muter, target: muted)
318 else
319 {:error, message} -> json_response(conn, :forbidden, %{error: message})
320 end
321 end
322
323 @doc "POST /api/v1/accounts/:id/block"
324 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
325 with {:ok, _user_block} <- User.block(blocker, blocked),
326 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
327 render(conn, "relationship.json", user: blocker, target: blocked)
328 else
329 {:error, message} -> json_response(conn, :forbidden, %{error: message})
330 end
331 end
332
333 @doc "POST /api/v1/accounts/:id/unblock"
334 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
335 with {:ok, _user_block} <- User.unblock(blocker, blocked),
336 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
337 render(conn, "relationship.json", user: blocker, target: blocked)
338 else
339 {:error, message} -> json_response(conn, :forbidden, %{error: message})
340 end
341 end
342
343 @doc "POST /api/v1/follows"
344 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
345 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
346 {_, true} <- {:followed, follower.id != followed.id},
347 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
348 render(conn, "show.json", user: followed, for: follower)
349 else
350 {:followed, _} -> {:error, :not_found}
351 {:error, message} -> json_response(conn, :forbidden, %{error: message})
352 end
353 end
354
355 @doc "GET /api/v1/mutes"
356 def mutes(%{assigns: %{user: user}} = conn, _) do
357 users = User.muted_users(user, _restrict_deactivated = true)
358 render(conn, "index.json", users: users, for: user, as: :user)
359 end
360
361 @doc "GET /api/v1/blocks"
362 def blocks(%{assigns: %{user: user}} = conn, _) do
363 users = User.blocked_users(user, _restrict_deactivated = true)
364 render(conn, "index.json", users: users, for: user, as: :user)
365 end
366
367 @doc "GET /api/v1/endorsements"
368 def endorsements(conn, params),
369 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
370 end