Merge branch 'feature/move-activity' 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 = Map.put(params, "tag", params["tagged"])
253 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
254
255 conn
256 |> add_link_headers(activities)
257 |> put_view(StatusView)
258 |> render("index.json", activities: activities, for: reading_user, as: :activity)
259 end
260 end
261
262 @doc "GET /api/v1/accounts/:id/followers"
263 def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
264 followers =
265 cond do
266 for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
267 user.hide_followers -> []
268 true -> MastodonAPI.get_followers(user, params)
269 end
270
271 conn
272 |> add_link_headers(followers)
273 |> render("index.json", for: for_user, users: followers, as: :user)
274 end
275
276 @doc "GET /api/v1/accounts/:id/following"
277 def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
278 followers =
279 cond do
280 for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
281 user.hide_follows -> []
282 true -> MastodonAPI.get_friends(user, params)
283 end
284
285 conn
286 |> add_link_headers(followers)
287 |> render("index.json", for: for_user, users: followers, as: :user)
288 end
289
290 @doc "GET /api/v1/accounts/:id/lists"
291 def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
292 lists = Pleroma.List.get_lists_account_belongs(user, account)
293
294 conn
295 |> put_view(ListView)
296 |> render("index.json", lists: lists)
297 end
298
299 @doc "POST /api/v1/accounts/:id/follow"
300 def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
301 {:error, :not_found}
302 end
303
304 def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
305 with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
306 render(conn, "relationship.json", user: follower, target: followed)
307 else
308 {:error, message} -> json_response(conn, :forbidden, %{error: message})
309 end
310 end
311
312 @doc "POST /api/v1/accounts/:id/unfollow"
313 def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
314 {:error, :not_found}
315 end
316
317 def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
318 with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
319 render(conn, "relationship.json", user: follower, target: followed)
320 end
321 end
322
323 @doc "POST /api/v1/accounts/:id/mute"
324 def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
325 notifications? = params |> Map.get("notifications", true) |> truthy_param?()
326
327 with {:ok, muter} <- User.mute(muter, muted, notifications?) do
328 render(conn, "relationship.json", user: muter, target: muted)
329 else
330 {:error, message} -> json_response(conn, :forbidden, %{error: message})
331 end
332 end
333
334 @doc "POST /api/v1/accounts/:id/unmute"
335 def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
336 with {:ok, muter} <- User.unmute(muter, muted) do
337 render(conn, "relationship.json", user: muter, target: muted)
338 else
339 {:error, message} -> json_response(conn, :forbidden, %{error: message})
340 end
341 end
342
343 @doc "POST /api/v1/accounts/:id/block"
344 def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
345 with {:ok, blocker} <- User.block(blocker, blocked),
346 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
347 render(conn, "relationship.json", user: blocker, target: blocked)
348 else
349 {:error, message} -> json_response(conn, :forbidden, %{error: message})
350 end
351 end
352
353 @doc "POST /api/v1/accounts/:id/unblock"
354 def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
355 with {:ok, blocker} <- User.unblock(blocker, blocked),
356 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
357 render(conn, "relationship.json", user: blocker, target: blocked)
358 else
359 {:error, message} -> json_response(conn, :forbidden, %{error: message})
360 end
361 end
362
363 @doc "POST /api/v1/follows"
364 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
365 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
366 {_, true} <- {:followed, follower.id != followed.id},
367 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
368 render(conn, "show.json", user: followed, for: follower)
369 else
370 {:followed, _} -> {:error, :not_found}
371 {:error, message} -> json_response(conn, :forbidden, %{error: message})
372 end
373 end
374
375 @doc "GET /api/v1/mutes"
376 def mutes(%{assigns: %{user: user}} = conn, _) do
377 render(conn, "index.json", users: User.muted_users(user), for: user, as: :user)
378 end
379
380 @doc "GET /api/v1/blocks"
381 def blocks(%{assigns: %{user: user}} = conn, _) do
382 render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user)
383 end
384
385 @doc "GET /api/v1/endorsements"
386 def endorsements(conn, params),
387 do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
388 end