56e6214c516461e961bfd85abfa20a3f40f02169
[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 != :create
63 )
64
65 @relations [:follow, :unfollow]
66 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
67
68 plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
69 plug(RateLimiter, [name: :relations_actions] when action in @relations)
70 plug(RateLimiter, [name: :app_account_creation] when action == :create)
71 plug(:assign_account_by_id when action in @needs_account)
72
73 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
74
75 @doc "POST /api/v1/accounts"
76 def create(
77 %{assigns: %{app: app}} = conn,
78 %{"username" => nickname, "password" => _, "agreement" => true} = params
79 ) do
80 params =
81 params
82 |> Map.take([
83 "email",
84 "captcha_solution",
85 "captcha_token",
86 "captcha_answer_data",
87 "token",
88 "password"
89 ])
90 |> Map.put("nickname", nickname)
91 |> Map.put("fullname", params["fullname"] || nickname)
92 |> Map.put("bio", params["bio"] || "")
93 |> Map.put("confirm", params["password"])
94
95 with :ok <- validate_email_param(params),
96 {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
97 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
98 json(conn, %{
99 token_type: "Bearer",
100 access_token: token.token,
101 scope: app.scopes,
102 created_at: Token.Utils.format_created_at(token)
103 })
104 else
105 {:error, errors} -> json_response(conn, :bad_request, errors)
106 end
107 end
108
109 def create(%{assigns: %{app: _app}} = conn, _) do
110 render_error(conn, :bad_request, "Missing parameters")
111 end
112
113 def create(conn, _) do
114 render_error(conn, :forbidden, "Invalid credentials")
115 end
116
117 defp validate_email_param(%{"email" => _}), do: :ok
118
119 defp validate_email_param(_) do
120 case Pleroma.Config.get([:instance, :account_activation_required]) do
121 true -> {:error, %{"error" => "Missing parameters"}}
122 _ -> :ok
123 end
124 end
125
126 @doc "GET /api/v1/accounts/verify_credentials"
127 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
128 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
129
130 render(conn, "show.json",
131 user: user,
132 for: user,
133 with_pleroma_settings: true,
134 with_chat_token: chat_token
135 )
136 end
137
138 @doc "PATCH /api/v1/accounts/update_credentials"
139 def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
140 user = original_user
141
142 user_params =
143 [
144 :no_rich_text,
145 :locked,
146 :hide_followers_count,
147 :hide_follows_count,
148 :hide_followers,
149 :hide_follows,
150 :hide_favorites,
151 :show_role,
152 :skip_thread_containment,
153 :allow_following_move,
154 :discoverable
155 ]
156 |> Enum.reduce(%{}, fn key, acc ->
157 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
158 end)
159 |> add_if_present(params, "display_name", :name)
160 |> add_if_present(params, "note", :bio)
161 |> add_if_present(params, "avatar", :avatar)
162 |> add_if_present(params, "header", :banner)
163 |> add_if_present(params, "pleroma_background_image", :background)
164 |> add_if_present(
165 params,
166 "fields_attributes",
167 :raw_fields,
168 &{:ok, normalize_fields_attributes(&1)}
169 )
170 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
171 |> add_if_present(params, "default_scope", :default_scope)
172 |> add_if_present(params, "actor_type", :actor_type)
173
174 changeset = User.update_changeset(user, user_params)
175
176 with {:ok, user} <- User.update_and_set_cache(changeset) do
177 if original_user != user, do: CommonAPI.update(user)
178
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) do
225 params =
226 params
227 |> Map.put("tag", params["tagged"])
228 |> Map.delete("godmode")
229
230 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
231
232 conn
233 |> add_link_headers(activities)
234 |> put_view(StatusView)
235 |> render("index.json", activities: activities, for: reading_user, as: :activity)
236 end
237 end
238
239 @doc "GET /api/v1/accounts/:id/followers"
240 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
241 followers =
242 cond do
243 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
244 user.hide_followers -> []
245 true -> MastodonAPI.get_followers(user, params)
246 end
247
248 conn
249 |> add_link_headers(followers)
250 |> render("index.json", for: for_user, users: followers, as: :user)
251 end
252
253 @doc "GET /api/v1/accounts/:id/following"
254 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
255 followers =
256 cond do
257 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
258 user.hide_follows -> []
259 true -> MastodonAPI.get_friends(user, params)
260 end
261
262 conn
263 |> add_link_headers(followers)
264 |> render("index.json", for: for_user, users: followers, as: :user)
265 end
266
267 @doc "GET /api/v1/accounts/:id/lists"
268 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
269 lists = Pleroma.List.get_lists_account_belongs(user, account)
270
271 conn
272 |> put_view(ListView)
273 |> render("index.json", lists: lists)
274 end
275
276 @doc "POST /api/v1/accounts/:id/follow"
277 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
278 {:error, :not_found}
279 end
280
281 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
282 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
283 render(conn, "relationship.json", user: follower, target: followed)
284 else
285 {:error, message} -> json_response(conn, :forbidden, %{error: message})
286 end
287 end
288
289 @doc "POST /api/v1/accounts/:id/unfollow"
290 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
291 {:error, :not_found}
292 end
293
294 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
295 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
296 render(conn, "relationship.json", user: follower, target: followed)
297 end
298 end
299
300 @doc "POST /api/v1/accounts/:id/mute"
301 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
302 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
303
304 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
305 render(conn, "relationship.json", user: muter, target: muted)
306 else
307 {:error, message} -> json_response(conn, :forbidden, %{error: message})
308 end
309 end
310
311 @doc "POST /api/v1/accounts/:id/unmute"
312 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
313 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
314 render(conn, "relationship.json", user: muter, target: muted)
315 else
316 {:error, message} -> json_response(conn, :forbidden, %{error: message})
317 end
318 end
319
320 @doc "POST /api/v1/accounts/:id/block"
321 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
322 with {:ok, _user_block} <- User.block(blocker, blocked),
323 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
324 render(conn, "relationship.json", user: blocker, target: blocked)
325 else
326 {:error, message} -> json_response(conn, :forbidden, %{error: message})
327 end
328 end
329
330 @doc "POST /api/v1/accounts/:id/unblock"
331 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
332 with {:ok, _user_block} <- User.unblock(blocker, blocked),
333 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
334 render(conn, "relationship.json", user: blocker, target: blocked)
335 else
336 {:error, message} -> json_response(conn, :forbidden, %{error: message})
337 end
338 end
339
340 @doc "POST /api/v1/follows"
341 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
342 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
343 {_, true} <- {:followed, follower.id != followed.id},
344 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
345 render(conn, "show.json", user: followed, for: follower)
346 else
347 {:followed, _} -> {:error, :not_found}
348 {:error, message} -> json_response(conn, :forbidden, %{error: message})
349 end
350 end
351
352 @doc "GET /api/v1/mutes"
353 def mutes(%{assigns: %{user: user}} = conn, _) do
354 users = User.muted_users(user, _restrict_deactivated = true)
355 render(conn, "index.json", users: users, for: user, as: :user)
356 end
357
358 @doc "GET /api/v1/blocks"
359 def blocks(%{assigns: %{user: user}} = conn, _) do
360 users = User.blocked_users(user, _restrict_deactivated = true)
361 render(conn, "index.json", users: users, for: user, as: :user)
362 end
363
364 @doc "GET /api/v1/endorsements"
365 def endorsements(conn, params),
366 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
367 end