Merge remote-tracking branch 'origin/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-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", "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 @relations [:follow, :unfollow]
67 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
68
69 plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
70 plug(RateLimiter, [name: :relations_actions] when action in @relations)
71 plug(RateLimiter, [name: :app_account_creation] when action == :create)
72 plug(:assign_account_by_id when action in @needs_account)
73
74 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
75
76 @doc "POST /api/v1/accounts"
77 def create(
78 %{assigns: %{app: app}} = conn,
79 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
80 ) do
81 params =
82 params
83 |> Map.take([
84 "email",
85 "captcha_solution",
86 "captcha_token",
87 "captcha_answer_data",
88 "token",
89 "password"
90 ])
91 |> Map.put("nickname", nickname)
92 |> Map.put("fullname", params["fullname"] || nickname)
93 |> Map.put("bio", params["bio"] || "")
94 |> Map.put("confirm", params["password"])
95
96 with {: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 @doc "GET /api/v1/accounts/verify_credentials"
118 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
119 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
120
121 render(conn, "show.json",
122 user: user,
123 for: user,
124 with_pleroma_settings: true,
125 with_chat_token: chat_token
126 )
127 end
128
129 @doc "PATCH /api/v1/accounts/update_credentials"
130 def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
131 user = original_user
132
133 params =
134 if Map.has_key?(params, "fields_attributes") do
135 Map.update!(params, "fields_attributes", fn fields ->
136 fields
137 |> normalize_fields_attributes()
138 |> Enum.filter(fn %{"name" => n} -> n != "" end)
139 end)
140 else
141 params
142 end
143
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, fn value -> {:ok, User.parse_bio(value, user)} end)
163 |> add_if_present(params, "avatar", :avatar, fn value ->
164 with %Plug.Upload{} <- value,
165 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
166 {:ok, object.data}
167 end
168 end)
169 |> add_if_present(params, "header", :banner, fn value ->
170 with %Plug.Upload{} <- value,
171 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
172 {:ok, object.data}
173 end
174 end)
175 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
176 with %Plug.Upload{} <- value,
177 {:ok, object} <- ActivityPub.upload(value, type: :background) do
178 {:ok, object.data}
179 end
180 end)
181 |> add_if_present(params, "fields_attributes", :fields, fn fields ->
182 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
183
184 {:ok, fields}
185 end)
186 |> add_if_present(params, "fields_attributes", :raw_fields)
187 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
188 {:ok, Map.merge(user.pleroma_settings_store, value)}
189 end)
190 |> add_if_present(params, "default_scope", :default_scope)
191 |> add_if_present(params, "actor_type", :actor_type)
192
193 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
194
195 user_emojis =
196 user
197 |> Map.get(:emoji, [])
198 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
199 |> Enum.dedup()
200
201 user_params = Map.put(user_params, :emoji, user_emojis)
202 changeset = User.update_changeset(user, user_params)
203
204 with {:ok, user} <- User.update_and_set_cache(changeset) do
205 if original_user != user, do: CommonAPI.update(user)
206
207 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
208 else
209 _e -> render_error(conn, :forbidden, "Invalid request")
210 end
211 end
212
213 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
214 with true <- Map.has_key?(params, params_field),
215 {:ok, new_value} <- value_function.(params[params_field]) do
216 Map.put(map, map_field, new_value)
217 else
218 _ -> map
219 end
220 end
221
222 defp normalize_fields_attributes(fields) do
223 if Enum.all?(fields, &is_tuple/1) do
224 Enum.map(fields, fn {_, v} -> v end)
225 else
226 fields
227 end
228 end
229
230 @doc "GET /api/v1/accounts/relationships"
231 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
232 targets = User.get_all_by_ids(List.wrap(id))
233
234 render(conn, "relationships.json", user: user, targets: targets)
235 end
236
237 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
238 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
239
240 @doc "GET /api/v1/accounts/:id"
241 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
242 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
243 true <- User.visible_for?(user, for_user) do
244 render(conn, "show.json", user: user, for: for_user)
245 else
246 _e -> render_error(conn, :not_found, "Can't find user")
247 end
248 end
249
250 @doc "GET /api/v1/accounts/:id/statuses"
251 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
252 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
253 params =
254 params
255 |> Map.put("tag", params["tagged"])
256 |> Map.delete("godmode")
257
258 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
259
260 conn
261 |> add_link_headers(activities)
262 |> put_view(StatusView)
263 |> render("index.json", activities: activities, for: reading_user, as: :activity)
264 end
265 end
266
267 @doc "GET /api/v1/accounts/:id/followers"
268 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
269 followers =
270 cond do
271 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
272 user.hide_followers -> []
273 true -> MastodonAPI.get_followers(user, params)
274 end
275
276 conn
277 |> add_link_headers(followers)
278 |> render("index.json", for: for_user, users: followers, as: :user)
279 end
280
281 @doc "GET /api/v1/accounts/:id/following"
282 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
283 followers =
284 cond do
285 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
286 user.hide_follows -> []
287 true -> MastodonAPI.get_friends(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/lists"
296 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
297 lists = Pleroma.List.get_lists_account_belongs(user, account)
298
299 conn
300 |> put_view(ListView)
301 |> render("index.json", lists: lists)
302 end
303
304 @doc "POST /api/v1/accounts/:id/follow"
305 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
306 {:error, :not_found}
307 end
308
309 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
310 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
311 render(conn, "relationship.json", user: follower, target: followed)
312 else
313 {:error, message} -> json_response(conn, :forbidden, %{error: message})
314 end
315 end
316
317 @doc "POST /api/v1/accounts/:id/unfollow"
318 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
319 {:error, :not_found}
320 end
321
322 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
323 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
324 render(conn, "relationship.json", user: follower, target: followed)
325 end
326 end
327
328 @doc "POST /api/v1/accounts/:id/mute"
329 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
330 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
331
332 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
333 render(conn, "relationship.json", user: muter, target: muted)
334 else
335 {:error, message} -> json_response(conn, :forbidden, %{error: message})
336 end
337 end
338
339 @doc "POST /api/v1/accounts/:id/unmute"
340 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
341 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
342 render(conn, "relationship.json", user: muter, target: muted)
343 else
344 {:error, message} -> json_response(conn, :forbidden, %{error: message})
345 end
346 end
347
348 @doc "POST /api/v1/accounts/:id/block"
349 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
350 with {:ok, _user_block} <- User.block(blocker, blocked),
351 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
352 render(conn, "relationship.json", user: blocker, target: blocked)
353 else
354 {:error, message} -> json_response(conn, :forbidden, %{error: message})
355 end
356 end
357
358 @doc "POST /api/v1/accounts/:id/unblock"
359 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
360 with {:ok, _user_block} <- User.unblock(blocker, blocked),
361 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
362 render(conn, "relationship.json", user: blocker, target: blocked)
363 else
364 {:error, message} -> json_response(conn, :forbidden, %{error: message})
365 end
366 end
367
368 @doc "POST /api/v1/follows"
369 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
370 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
371 {_, true} <- {:followed, follower.id != followed.id},
372 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
373 render(conn, "show.json", user: followed, for: follower)
374 else
375 {:followed, _} -> {:error, :not_found}
376 {:error, message} -> json_response(conn, :forbidden, %{error: message})
377 end
378 end
379
380 @doc "GET /api/v1/mutes"
381 def mutes(%{assigns: %{user: user}} = conn, _) do
382 users = User.muted_users(user, _restrict_deactivated = true)
383 render(conn, "index.json", users: users, for: user, as: :user)
384 end
385
386 @doc "GET /api/v1/blocks"
387 def blocks(%{assigns: %{user: user}} = conn, _) do
388 users = User.blocked_users(user, _restrict_deactivated = true)
389 render(conn, "index.json", users: users, for: user, as: :user)
390 end
391
392 @doc "GET /api/v1/endorsements"
393 def endorsements(conn, params),
394 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
395 end