Merge branch 'split-masto-api/accounts' 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.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 info_params =
109 [
110 :no_rich_text,
111 :locked,
112 :hide_followers_count,
113 :hide_follows_count,
114 :hide_followers,
115 :hide_follows,
116 :hide_favorites,
117 :show_role,
118 :skip_thread_containment,
119 :discoverable
120 ]
121 |> Enum.reduce(%{}, fn key, acc ->
122 add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
123 end)
124 |> add_if_present(params, "default_scope", :default_scope)
125 |> add_if_present(params, "fields", :fields, fn fields ->
126 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
127
128 {:ok, fields}
129 end)
130 |> add_if_present(params, "fields", :raw_fields)
131 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
132 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
133 end)
134 |> add_if_present(params, "header", :banner, fn value ->
135 with %Plug.Upload{} <- value,
136 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
137 {:ok, object.data}
138 end
139 end)
140 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
141 with %Plug.Upload{} <- value,
142 {:ok, object} <- ActivityPub.upload(value, type: :background) do
143 {:ok, object.data}
144 end
145 end)
146 |> Map.put(:emoji, user_info_emojis)
147
148 changeset =
149 user
150 |> User.update_changeset(user_params)
151 |> User.change_info(&User.Info.profile_update(&1, info_params))
152
153 with {:ok, user} <- User.update_and_set_cache(changeset) do
154 if original_user != user, do: CommonAPI.update(user)
155
156 render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
157 else
158 _e -> render_error(conn, :forbidden, "Invalid request")
159 end
160 end
161
162 defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
163 with true <- Map.has_key?(params, params_field),
164 {:ok, new_value} <- value_function.(params[params_field]) do
165 Map.put(map, map_field, new_value)
166 else
167 _ -> map
168 end
169 end
170
171 @doc "GET /api/v1/accounts/relationships"
172 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
173 targets = User.get_all_by_ids(List.wrap(id))
174
175 render(conn, "relationships.json", user: user, targets: targets)
176 end
177
178 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
179 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
180
181 @doc "GET /api/v1/accounts/:id"
182 def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
183 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
184 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
185 render(conn, "show.json", user: user, for: for_user)
186 else
187 _e -> render_error(conn, :not_found, "Can't find user")
188 end
189 end
190
191 @doc "GET /api/v1/accounts/:id/statuses"
192 def statuses(%{assigns: %{user: reading_user}} = conn, params) do
193 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
194 params = Map.put(params, "tag", params["tagged"])
195 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
196
197 conn
198 |> add_link_headers(activities)
199 |> put_view(StatusView)
200 |> render("index.json", activities: activities, for: reading_user, as: :activity)
201 end
202 end
203
204 @doc "GET /api/v1/accounts/:id/followers"
205 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
206 followers =
207 cond do
208 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
209 user.info.hide_followers -> []
210 true -> MastodonAPI.get_followers(user, params)
211 end
212
213 conn
214 |> add_link_headers(followers)
215 |> render("index.json", for: for_user, users: followers, as: :user)
216 end
217
218 @doc "GET /api/v1/accounts/:id/following"
219 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
220 followers =
221 cond do
222 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
223 user.info.hide_follows -> []
224 true -> MastodonAPI.get_friends(user, params)
225 end
226
227 conn
228 |> add_link_headers(followers)
229 |> render("index.json", for: for_user, users: followers, as: :user)
230 end
231
232 @doc "GET /api/v1/accounts/:id/lists"
233 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
234 lists = Pleroma.List.get_lists_account_belongs(user, account)
235
236 conn
237 |> put_view(ListView)
238 |> render("index.json", lists: lists)
239 end
240
241 @doc "POST /api/v1/accounts/:id/follow"
242 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
243 {:error, :not_found}
244 end
245
246 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
247 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
248 render(conn, "relationship.json", user: follower, target: followed)
249 else
250 {:error, message} -> json_response(conn, :forbidden, %{error: message})
251 end
252 end
253
254 @doc "POST /api/v1/accounts/:id/unfollow"
255 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
256 {:error, :not_found}
257 end
258
259 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
260 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
261 render(conn, "relationship.json", user: follower, target: followed)
262 end
263 end
264
265 @doc "POST /api/v1/accounts/:id/mute"
266 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
267 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
268
269 with {:ok, muter} <- User.mute(muter, muted, notifications?) do
270 render(conn, "relationship.json", user: muter, target: muted)
271 else
272 {:error, message} -> json_response(conn, :forbidden, %{error: message})
273 end
274 end
275
276 @doc "POST /api/v1/accounts/:id/unmute"
277 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
278 with {:ok, muter} <- User.unmute(muter, muted) do
279 render(conn, "relationship.json", user: muter, target: muted)
280 else
281 {:error, message} -> json_response(conn, :forbidden, %{error: message})
282 end
283 end
284
285 @doc "POST /api/v1/accounts/:id/block"
286 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
287 with {:ok, blocker} <- User.block(blocker, blocked),
288 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
289 render(conn, "relationship.json", user: blocker, target: blocked)
290 else
291 {:error, message} -> json_response(conn, :forbidden, %{error: message})
292 end
293 end
294
295 @doc "POST /api/v1/accounts/:id/unblock"
296 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
297 with {:ok, blocker} <- User.unblock(blocker, blocked),
298 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
299 render(conn, "relationship.json", user: blocker, target: blocked)
300 else
301 {:error, message} -> json_response(conn, :forbidden, %{error: message})
302 end
303 end
304 end