e195f56c452c903c748ad43f047cad52a1be39c6
[akkoma] / lib / pleroma / web / mastodon_api / controllers / account_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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", "write:blocks"]} when action in [:block, :unblock]
42 )
43
44 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
45
46 plug(
47 OAuthScopesPlug,
48 %{scopes: ["follow", "write:follows"]} when action in [:follow, :unfollow]
49 )
50
51 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
52
53 plug(
54 Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
55 when action != :create
56 )
57
58 @relations [:follow, :unfollow]
59 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
60
61 plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
62 plug(RateLimiter, :relations_actions when action in @relations)
63 plug(RateLimiter, :app_account_creation when action == :create)
64 plug(:assign_account_by_id when action in @needs_account)
65
66 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
67
68 @doc "POST /api/v1/accounts"
69 def create(
70 %{assigns: %{app: app}} = conn,
71 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
72 ) do
73 params =
74 params
75 |> Map.take([
76 "email",
77 "captcha_solution",
78 "captcha_token",
79 "captcha_answer_data",
80 "token",
81 "password"
82 ])
83 |> Map.put("nickname", nickname)
84 |> Map.put("fullname", params["fullname"] || nickname)
85 |> Map.put("bio", params["bio"] || "")
86 |> Map.put("confirm", params["password"])
87
88 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
89 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
90 json(conn, %{
91 token_type: "Bearer",
92 access_token: token.token,
93 scope: app.scopes,
94 created_at: Token.Utils.format_created_at(token)
95 })
96 else
97 {:error, errors} -> json_response(conn, :bad_request, errors)
98 end
99 end
100
101 def create(%{assigns: %{app: _app}} = conn, _) do
102 render_error(conn, :bad_request, "Missing parameters")
103 end
104
105 def create(conn, _) do
106 render_error(conn, :forbidden, "Invalid credentials")
107 end
108
109 @doc "GET /api/v1/accounts/verify_credentials"
110 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
111 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
112
113 render(conn, "show.json",
114 user: user,
115 for: user,
116 with_pleroma_settings: true,
117 with_chat_token: chat_token
118 )
119 end
120
121 @doc "PATCH /api/v1/accounts/update_credentials"
122 def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
123 user = original_user
124
125 user_params =
126 %{}
127 |> add_if_present(params, "display_name", :name)
128 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
129 |> add_if_present(params, "avatar", :avatar, fn value ->
130 with %Plug.Upload{} <- value,
131 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
132 {:ok, object.data}
133 end
134 end)
135
136 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
137
138 user_info_emojis =
139 user.info
140 |> Map.get(:emoji, [])
141 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
142 |> Enum.dedup()
143
144 params =
145 if Map.has_key?(params, "fields_attributes") do
146 Map.update!(params, "fields_attributes", fn fields ->
147 fields
148 |> normalize_fields_attributes()
149 |> Enum.filter(fn %{"name" => n} -> n != "" end)
150 end)
151 else
152 params
153 end
154
155 info_params =
156 [
157 :no_rich_text,
158 :locked,
159 :hide_followers_count,
160 :hide_follows_count,
161 :hide_followers,
162 :hide_follows,
163 :hide_favorites,
164 :show_role,
165 :skip_thread_containment,
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, "default_scope", :default_scope)
172 |> add_if_present(params, "fields_attributes", :fields, fn fields ->
173 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
174
175 {:ok, fields}
176 end)
177 |> add_if_present(params, "fields_attributes", :raw_fields)
178 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
179 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
180 end)
181 |> add_if_present(params, "header", :banner, fn value ->
182 with %Plug.Upload{} <- value,
183 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
184 {:ok, object.data}
185 end
186 end)
187 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
188 with %Plug.Upload{} <- value,
189 {:ok, object} <- ActivityPub.upload(value, type: :background) do
190 {:ok, object.data}
191 end
192 end)
193 |> Map.put(:emoji, user_info_emojis)
194
195 changeset =
196 user
197 |> User.update_changeset(user_params)
198 |> User.change_info(&User.Info.profile_update(&1, info_params))
199
200 with {:ok, user} <- User.update_and_set_cache(changeset) do
201 if original_user != user, do: CommonAPI.update(user)
202
203 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
204 else
205 _e -> render_error(conn, :forbidden, "Invalid request")
206 end
207 end
208
209 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
210 with true <- Map.has_key?(params, params_field),
211 {:ok, new_value} <- value_function.(params[params_field]) do
212 Map.put(map, map_field, new_value)
213 else
214 _ -> map
215 end
216 end
217
218 defp normalize_fields_attributes(fields) do
219 if Enum.all?(fields, &is_tuple/1) do
220 Enum.map(fields, fn {_, v} -> v end)
221 else
222 fields
223 end
224 end
225
226 @doc "GET /api/v1/accounts/relationships"
227 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
228 targets = User.get_all_by_ids(List.wrap(id))
229
230 render(conn, "relationships.json", user: user, targets: targets)
231 end
232
233 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
234 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
235
236 @doc "GET /api/v1/accounts/:id"
237 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
238 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
239 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
240 render(conn, "show.json", user: user, for: for_user)
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/statuses"
247 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
248 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
249 params = Map.put(params, "tag", params["tagged"])
250 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
251
252 conn
253 |> add_link_headers(activities)
254 |> put_view(StatusView)
255 |> render("index.json", activities: activities, for: reading_user, as: :activity)
256 end
257 end
258
259 @doc "GET /api/v1/accounts/:id/followers"
260 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
261 followers =
262 cond do
263 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
264 user.info.hide_followers -> []
265 true -> MastodonAPI.get_followers(user, params)
266 end
267
268 conn
269 |> add_link_headers(followers)
270 |> render("index.json", for: for_user, users: followers, as: :user)
271 end
272
273 @doc "GET /api/v1/accounts/:id/following"
274 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
275 followers =
276 cond do
277 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
278 user.info.hide_follows -> []
279 true -> MastodonAPI.get_friends(user, params)
280 end
281
282 conn
283 |> add_link_headers(followers)
284 |> render("index.json", for: for_user, users: followers, as: :user)
285 end
286
287 @doc "GET /api/v1/accounts/:id/lists"
288 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
289 lists = Pleroma.List.get_lists_account_belongs(user, account)
290
291 conn
292 |> put_view(ListView)
293 |> render("index.json", lists: lists)
294 end
295
296 @doc "POST /api/v1/accounts/:id/follow"
297 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
298 {:error, :not_found}
299 end
300
301 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
302 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
303 render(conn, "relationship.json", user: follower, target: followed)
304 else
305 {:error, message} -> json_response(conn, :forbidden, %{error: message})
306 end
307 end
308
309 @doc "POST /api/v1/accounts/:id/unfollow"
310 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
311 {:error, :not_found}
312 end
313
314 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
315 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
316 render(conn, "relationship.json", user: follower, target: followed)
317 end
318 end
319
320 @doc "POST /api/v1/accounts/:id/mute"
321 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
322 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
323
324 with {:ok, muter} <- User.mute(muter, muted, notifications?) do
325 render(conn, "relationship.json", user: muter, target: muted)
326 else
327 {:error, message} -> json_response(conn, :forbidden, %{error: message})
328 end
329 end
330
331 @doc "POST /api/v1/accounts/:id/unmute"
332 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
333 with {:ok, muter} <- User.unmute(muter, muted) do
334 render(conn, "relationship.json", user: muter, target: muted)
335 else
336 {:error, message} -> json_response(conn, :forbidden, %{error: message})
337 end
338 end
339
340 @doc "POST /api/v1/accounts/:id/block"
341 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
342 with {:ok, blocker} <- User.block(blocker, blocked),
343 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
344 render(conn, "relationship.json", user: blocker, target: blocked)
345 else
346 {:error, message} -> json_response(conn, :forbidden, %{error: message})
347 end
348 end
349
350 @doc "POST /api/v1/accounts/:id/unblock"
351 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
352 with {:ok, blocker} <- User.unblock(blocker, blocked),
353 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
354 render(conn, "relationship.json", user: blocker, target: blocked)
355 else
356 {:error, message} -> json_response(conn, :forbidden, %{error: message})
357 end
358 end
359
360 @doc "POST /api/v1/follows"
361 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
362 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
363 {_, true} <- {:followed, follower.id != followed.id},
364 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
365 render(conn, "show.json", user: followed, for: follower)
366 else
367 {:followed, _} -> {:error, :not_found}
368 {:error, message} -> json_response(conn, :forbidden, %{error: message})
369 end
370 end
371
372 @doc "GET /api/v1/mutes"
373 def mutes(%{assigns: %{user: user}} = conn, _) do
374 render(conn, "index.json", users: User.muted_users(user), for: user, as: :user)
375 end
376
377 @doc "GET /api/v1/blocks"
378 def blocks(%{assigns: %{user: user}} = conn, _) do
379 render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user)
380 end
381
382 @doc "GET /api/v1/endorsements"
383 def endorsements(conn, params),
384 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
385 end