Merge remote-tracking branch 'upstream/develop' into fix-prameter-name-of-accounts...
[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.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 @relations [:follow, :unfollow]
23 @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
24
25 plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
26 plug(RateLimiter, :relations_actions when action in @relations)
27 plug(RateLimiter, :app_account_creation when action == :create)
28 plug(:assign_account_by_id when action in @needs_account)
29
30 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
31
32 @doc "POST /api/v1/accounts"
33 def create(
34 %{assigns: %{app: app}} = conn,
35 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
36 ) do
37 params =
38 params
39 |> Map.take([
40 "email",
41 "captcha_solution",
42 "captcha_token",
43 "captcha_answer_data",
44 "token",
45 "password"
46 ])
47 |> Map.put("nickname", nickname)
48 |> Map.put("fullname", params["fullname"] || nickname)
49 |> Map.put("bio", params["bio"] || "")
50 |> Map.put("confirm", params["password"])
51
52 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
53 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
54 json(conn, %{
55 token_type: "Bearer",
56 access_token: token.token,
57 scope: app.scopes,
58 created_at: Token.Utils.format_created_at(token)
59 })
60 else
61 {:error, errors} -> json_response(conn, :bad_request, errors)
62 end
63 end
64
65 def create(%{assigns: %{app: _app}} = conn, _) do
66 render_error(conn, :bad_request, "Missing parameters")
67 end
68
69 def create(conn, _) do
70 render_error(conn, :forbidden, "Invalid credentials")
71 end
72
73 @doc "GET /api/v1/accounts/verify_credentials"
74 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
75 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
76
77 render(conn, "show.json",
78 user: user,
79 for: user,
80 with_pleroma_settings: true,
81 with_chat_token: chat_token
82 )
83 end
84
85 @doc "PATCH /api/v1/accounts/update_credentials"
86 def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
87 user = original_user
88
89 user_params =
90 %{}
91 |> add_if_present(params, "display_name", :name)
92 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
93 |> add_if_present(params, "avatar", :avatar, fn value ->
94 with %Plug.Upload{} <- value,
95 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
96 {:ok, object.data}
97 end
98 end)
99
100 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
101
102 user_info_emojis =
103 user.info
104 |> Map.get(:emoji, [])
105 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
106 |> Enum.dedup()
107
108 params =
109 if Map.has_key?(params, "fields_attributes") do
110 Map.update!(params, "fields_attributes", fn fields ->
111 fields
112 |> normalize_fields_attributes()
113 |> Enum.filter(fn %{"name" => n} -> n != "" end)
114 end)
115 else
116 params
117 end
118
119 info_params =
120 [
121 :no_rich_text,
122 :locked,
123 :hide_followers_count,
124 :hide_follows_count,
125 :hide_followers,
126 :hide_follows,
127 :hide_favorites,
128 :show_role,
129 :skip_thread_containment,
130 :discoverable
131 ]
132 |> Enum.reduce(%{}, fn key, acc ->
133 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
134 end)
135 |> add_if_present(params, "default_scope", :default_scope)
136 |> add_if_present(params, "fields_attributes", :fields, fn fields ->
137 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
138
139 {:ok, fields}
140 end)
141 |> add_if_present(params, "fields_attributes", :raw_fields)
142 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
143 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
144 end)
145 |> add_if_present(params, "header", :banner, fn value ->
146 with %Plug.Upload{} <- value,
147 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
148 {:ok, object.data}
149 end
150 end)
151 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
152 with %Plug.Upload{} <- value,
153 {:ok, object} <- ActivityPub.upload(value, type: :background) do
154 {:ok, object.data}
155 end
156 end)
157 |> Map.put(:emoji, user_info_emojis)
158
159 changeset =
160 user
161 |> User.update_changeset(user_params)
162 |> User.change_info(&User.Info.profile_update(&1, info_params))
163
164 with {:ok, user} <- User.update_and_set_cache(changeset) do
165 if original_user != user, do: CommonAPI.update(user)
166
167 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
168 else
169 _e -> render_error(conn, :forbidden, "Invalid request")
170 end
171 end
172
173 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
174 with true <- Map.has_key?(params, params_field),
175 {:ok, new_value} <- value_function.(params[params_field]) do
176 Map.put(map, map_field, new_value)
177 else
178 _ -> map
179 end
180 end
181
182 defp normalize_fields_attributes(fields) do
183 if Enum.all?(fields, &is_tuple/1) do
184 Enum.map(fields, fn {_, v} -> v end)
185 else
186 fields
187 end
188 end
189
190 @doc "GET /api/v1/accounts/relationships"
191 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
192 targets = User.get_all_by_ids(List.wrap(id))
193
194 render(conn, "relationships.json", user: user, targets: targets)
195 end
196
197 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
198 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
199
200 @doc "GET /api/v1/accounts/:id"
201 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
202 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
203 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
204 render(conn, "show.json", user: user, for: for_user)
205 else
206 _e -> render_error(conn, :not_found, "Can't find user")
207 end
208 end
209
210 @doc "GET /api/v1/accounts/:id/statuses"
211 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
212 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
213 params = Map.put(params, "tag", params["tagged"])
214 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
215
216 conn
217 |> add_link_headers(activities)
218 |> put_view(StatusView)
219 |> render("index.json", activities: activities, for: reading_user, as: :activity)
220 end
221 end
222
223 @doc "GET /api/v1/accounts/:id/followers"
224 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
225 followers =
226 cond do
227 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
228 user.info.hide_followers -> []
229 true -> MastodonAPI.get_followers(user, params)
230 end
231
232 conn
233 |> add_link_headers(followers)
234 |> render("index.json", for: for_user, users: followers, as: :user)
235 end
236
237 @doc "GET /api/v1/accounts/:id/following"
238 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
239 followers =
240 cond do
241 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
242 user.info.hide_follows -> []
243 true -> MastodonAPI.get_friends(user, params)
244 end
245
246 conn
247 |> add_link_headers(followers)
248 |> render("index.json", for: for_user, users: followers, as: :user)
249 end
250
251 @doc "GET /api/v1/accounts/:id/lists"
252 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
253 lists = Pleroma.List.get_lists_account_belongs(user, account)
254
255 conn
256 |> put_view(ListView)
257 |> render("index.json", lists: lists)
258 end
259
260 @doc "POST /api/v1/accounts/:id/follow"
261 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
262 {:error, :not_found}
263 end
264
265 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
266 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
267 render(conn, "relationship.json", user: follower, target: followed)
268 else
269 {:error, message} -> json_response(conn, :forbidden, %{error: message})
270 end
271 end
272
273 @doc "POST /api/v1/accounts/:id/unfollow"
274 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
275 {:error, :not_found}
276 end
277
278 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
279 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
280 render(conn, "relationship.json", user: follower, target: followed)
281 end
282 end
283
284 @doc "POST /api/v1/accounts/:id/mute"
285 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
286 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
287
288 with {:ok, muter} <- User.mute(muter, muted, notifications?) do
289 render(conn, "relationship.json", user: muter, target: muted)
290 else
291 {:error, message} -> json_response(conn, :forbidden, %{error: message})
292 end
293 end
294
295 @doc "POST /api/v1/accounts/:id/unmute"
296 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
297 with {:ok, muter} <- User.unmute(muter, muted) do
298 render(conn, "relationship.json", user: muter, target: muted)
299 else
300 {:error, message} -> json_response(conn, :forbidden, %{error: message})
301 end
302 end
303
304 @doc "POST /api/v1/accounts/:id/block"
305 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
306 with {:ok, blocker} <- User.block(blocker, blocked),
307 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
308 render(conn, "relationship.json", user: blocker, target: blocked)
309 else
310 {:error, message} -> json_response(conn, :forbidden, %{error: message})
311 end
312 end
313
314 @doc "POST /api/v1/accounts/:id/unblock"
315 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
316 with {:ok, blocker} <- User.unblock(blocker, blocked),
317 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
318 render(conn, "relationship.json", user: blocker, target: blocked)
319 else
320 {:error, message} -> json_response(conn, :forbidden, %{error: message})
321 end
322 end
323 end