[#1234] Merge remote-tracking branch 'remotes/upstream/develop' into 1234-mastodon...
[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 info_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 :discoverable
156 ]
157 |> Enum.reduce(%{}, fn key, acc ->
158 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
159 end)
160 |> add_if_present(params, "default_scope", :default_scope)
161 |> add_if_present(params, "fields", :fields, fn fields ->
162 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
163
164 {:ok, fields}
165 end)
166 |> add_if_present(params, "fields", :raw_fields)
167 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
168 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
169 end)
170 |> add_if_present(params, "header", :banner, fn value ->
171 with %Plug.Upload{} <- value,
172 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
173 {:ok, object.data}
174 end
175 end)
176 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
177 with %Plug.Upload{} <- value,
178 {:ok, object} <- ActivityPub.upload(value, type: :background) do
179 {:ok, object.data}
180 end
181 end)
182 |> Map.put(:emoji, user_info_emojis)
183
184 changeset =
185 user
186 |> User.update_changeset(user_params)
187 |> User.change_info(&User.Info.profile_update(&1, info_params))
188
189 with {:ok, user} <- User.update_and_set_cache(changeset) do
190 if original_user != user, do: CommonAPI.update(user)
191
192 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
193 else
194 _e -> render_error(conn, :forbidden, "Invalid request")
195 end
196 end
197
198 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
199 with true <- Map.has_key?(params, params_field),
200 {:ok, new_value} <- value_function.(params[params_field]) do
201 Map.put(map, map_field, new_value)
202 else
203 _ -> map
204 end
205 end
206
207 @doc "GET /api/v1/accounts/relationships"
208 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
209 targets = User.get_all_by_ids(List.wrap(id))
210
211 render(conn, "relationships.json", user: user, targets: targets)
212 end
213
214 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
215 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
216
217 @doc "GET /api/v1/accounts/:id"
218 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
219 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
220 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
221 render(conn, "show.json", user: user, for: for_user)
222 else
223 _e -> render_error(conn, :not_found, "Can't find user")
224 end
225 end
226
227 @doc "GET /api/v1/accounts/:id/statuses"
228 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
229 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
230 params = Map.put(params, "tag", params["tagged"])
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 end
238 end
239
240 @doc "GET /api/v1/accounts/:id/followers"
241 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
242 followers =
243 cond do
244 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
245 user.info.hide_followers -> []
246 true -> MastodonAPI.get_followers(user, params)
247 end
248
249 conn
250 |> add_link_headers(followers)
251 |> render("index.json", for: for_user, users: followers, as: :user)
252 end
253
254 @doc "GET /api/v1/accounts/:id/following"
255 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
256 followers =
257 cond do
258 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
259 user.info.hide_follows -> []
260 true -> MastodonAPI.get_friends(user, params)
261 end
262
263 conn
264 |> add_link_headers(followers)
265 |> render("index.json", for: for_user, users: followers, as: :user)
266 end
267
268 @doc "GET /api/v1/accounts/:id/lists"
269 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
270 lists = Pleroma.List.get_lists_account_belongs(user, account)
271
272 conn
273 |> put_view(ListView)
274 |> render("index.json", lists: lists)
275 end
276
277 @doc "POST /api/v1/accounts/:id/follow"
278 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
279 {:error, :not_found}
280 end
281
282 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
283 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
284 render(conn, "relationship.json", user: follower, target: followed)
285 else
286 {:error, message} -> json_response(conn, :forbidden, %{error: message})
287 end
288 end
289
290 @doc "POST /api/v1/accounts/:id/unfollow"
291 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
292 {:error, :not_found}
293 end
294
295 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
296 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
297 render(conn, "relationship.json", user: follower, target: followed)
298 end
299 end
300
301 @doc "POST /api/v1/accounts/:id/mute"
302 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
303 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
304
305 with {:ok, muter} <- User.mute(muter, muted, notifications?) do
306 render(conn, "relationship.json", user: muter, target: muted)
307 else
308 {:error, message} -> json_response(conn, :forbidden, %{error: message})
309 end
310 end
311
312 @doc "POST /api/v1/accounts/:id/unmute"
313 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
314 with {:ok, muter} <- User.unmute(muter, muted) do
315 render(conn, "relationship.json", user: muter, target: muted)
316 else
317 {:error, message} -> json_response(conn, :forbidden, %{error: message})
318 end
319 end
320
321 @doc "POST /api/v1/accounts/:id/block"
322 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
323 with {:ok, blocker} <- User.block(blocker, blocked),
324 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
325 render(conn, "relationship.json", user: blocker, target: blocked)
326 else
327 {:error, message} -> json_response(conn, :forbidden, %{error: message})
328 end
329 end
330
331 @doc "POST /api/v1/accounts/:id/unblock"
332 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
333 with {:ok, blocker} <- User.unblock(blocker, blocked),
334 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
335 render(conn, "relationship.json", user: blocker, target: blocked)
336 else
337 {:error, message} -> json_response(conn, :forbidden, %{error: message})
338 end
339 end
340
341 @doc "GET /api/v1/endorsements"
342 def endorsements(conn, params),
343 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
344 end