Merge branch 'exclude-visibilities-for-like-notifications' into 'develop'
[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
192 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
193
194 user_emojis =
195 user
196 |> Map.get(:emoji, [])
197 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
198 |> Enum.dedup()
199
200 user_params = Map.put(user_params, :emoji, user_emojis)
201 changeset = User.update_changeset(user, user_params)
202
203 with {:ok, user} <- User.update_and_set_cache(changeset) do
204 if original_user != user, do: CommonAPI.update(user)
205
206 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
207 else
208 _e -> render_error(conn, :forbidden, "Invalid request")
209 end
210 end
211
212 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
213 with true <- Map.has_key?(params, params_field),
214 {:ok, new_value} <- value_function.(params[params_field]) do
215 Map.put(map, map_field, new_value)
216 else
217 _ -> map
218 end
219 end
220
221 defp normalize_fields_attributes(fields) do
222 if Enum.all?(fields, &is_tuple/1) do
223 Enum.map(fields, fn {_, v} -> v end)
224 else
225 fields
226 end
227 end
228
229 @doc "GET /api/v1/accounts/relationships"
230 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
231 targets = User.get_all_by_ids(List.wrap(id))
232
233 render(conn, "relationships.json", user: user, targets: targets)
234 end
235
236 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
237 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
238
239 @doc "GET /api/v1/accounts/:id"
240 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
241 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
242 true <- User.visible_for?(user, for_user) do
243 render(conn, "show.json", user: user, for: for_user)
244 else
245 _e -> render_error(conn, :not_found, "Can't find user")
246 end
247 end
248
249 @doc "GET /api/v1/accounts/:id/statuses"
250 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
251 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
252 params =
253 params
254 |> Map.put("tag", params["tagged"])
255 |> Map.delete("godmode")
256
257 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
258
259 conn
260 |> add_link_headers(activities)
261 |> put_view(StatusView)
262 |> render("index.json", activities: activities, for: reading_user, as: :activity)
263 end
264 end
265
266 @doc "GET /api/v1/accounts/:id/followers"
267 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
268 followers =
269 cond do
270 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
271 user.hide_followers -> []
272 true -> MastodonAPI.get_followers(user, params)
273 end
274
275 conn
276 |> add_link_headers(followers)
277 |> render("index.json", for: for_user, users: followers, as: :user)
278 end
279
280 @doc "GET /api/v1/accounts/:id/following"
281 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
282 followers =
283 cond do
284 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
285 user.hide_follows -> []
286 true -> MastodonAPI.get_friends(user, params)
287 end
288
289 conn
290 |> add_link_headers(followers)
291 |> render("index.json", for: for_user, users: followers, as: :user)
292 end
293
294 @doc "GET /api/v1/accounts/:id/lists"
295 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
296 lists = Pleroma.List.get_lists_account_belongs(user, account)
297
298 conn
299 |> put_view(ListView)
300 |> render("index.json", lists: lists)
301 end
302
303 @doc "POST /api/v1/accounts/:id/follow"
304 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
305 {:error, :not_found}
306 end
307
308 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
309 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
310 render(conn, "relationship.json", user: follower, target: followed)
311 else
312 {:error, message} -> json_response(conn, :forbidden, %{error: message})
313 end
314 end
315
316 @doc "POST /api/v1/accounts/:id/unfollow"
317 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
318 {:error, :not_found}
319 end
320
321 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
322 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
323 render(conn, "relationship.json", user: follower, target: followed)
324 end
325 end
326
327 @doc "POST /api/v1/accounts/:id/mute"
328 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
329 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
330
331 with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
332 render(conn, "relationship.json", user: muter, target: muted)
333 else
334 {:error, message} -> json_response(conn, :forbidden, %{error: message})
335 end
336 end
337
338 @doc "POST /api/v1/accounts/:id/unmute"
339 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
340 with {:ok, _user_relationships} <- User.unmute(muter, muted) do
341 render(conn, "relationship.json", user: muter, target: muted)
342 else
343 {:error, message} -> json_response(conn, :forbidden, %{error: message})
344 end
345 end
346
347 @doc "POST /api/v1/accounts/:id/block"
348 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
349 with {:ok, _user_block} <- User.block(blocker, blocked),
350 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
351 render(conn, "relationship.json", user: blocker, target: blocked)
352 else
353 {:error, message} -> json_response(conn, :forbidden, %{error: message})
354 end
355 end
356
357 @doc "POST /api/v1/accounts/:id/unblock"
358 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
359 with {:ok, _user_block} <- User.unblock(blocker, blocked),
360 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
361 render(conn, "relationship.json", user: blocker, target: blocked)
362 else
363 {:error, message} -> json_response(conn, :forbidden, %{error: message})
364 end
365 end
366
367 @doc "POST /api/v1/follows"
368 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
369 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
370 {_, true} <- {:followed, follower.id != followed.id},
371 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
372 render(conn, "show.json", user: followed, for: follower)
373 else
374 {:followed, _} -> {:error, :not_found}
375 {:error, message} -> json_response(conn, :forbidden, %{error: message})
376 end
377 end
378
379 @doc "GET /api/v1/mutes"
380 def mutes(%{assigns: %{user: user}} = conn, _) do
381 users = User.muted_users(user, _restrict_deactivated = true)
382 render(conn, "index.json", users: users, for: user, as: :user)
383 end
384
385 @doc "GET /api/v1/blocks"
386 def blocks(%{assigns: %{user: user}} = conn, _) do
387 users = User.blocked_users(user, _restrict_deactivated = true)
388 render(conn, "index.json", users: users, for: user, as: :user)
389 end
390
391 @doc "GET /api/v1/endorsements"
392 def endorsements(conn, params),
393 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
394 end