account_controller.ex 13 KB
Newer Older
minibikini's avatar
minibikini committed
1
# Pleroma: A lightweight social networking server
2
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
minibikini's avatar
minibikini committed
3
4
5
6
7
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.MastodonAPI.AccountController do
  use Pleroma.Web, :controller

minibikini's avatar
minibikini committed
8
  import Pleroma.Web.ControllerHelper,
9
10
11
12
13
14
15
    only: [
      add_link_headers: 2,
      truthy_param?: 1,
      assign_account_by_id: 2,
      json_response: 3,
      skip_relationships?: 1
    ]
minibikini's avatar
minibikini committed
16

17
  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
18
  alias Pleroma.Plugs.OAuthScopesPlug
19
  alias Pleroma.Plugs.RateLimiter
minibikini's avatar
minibikini committed
20
21
  alias Pleroma.User
  alias Pleroma.Web.ActivityPub.ActivityPub
22
  alias Pleroma.Web.CommonAPI
minibikini's avatar
minibikini committed
23
  alias Pleroma.Web.MastodonAPI.ListView
24
  alias Pleroma.Web.MastodonAPI.MastodonAPI
25
  alias Pleroma.Web.MastodonAPI.MastodonAPIController
26
27
28
  alias Pleroma.Web.MastodonAPI.StatusView
  alias Pleroma.Web.OAuth.Token
  alias Pleroma.Web.TwitterAPI.TwitterAPI
minibikini's avatar
minibikini committed
29

30
31
  plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)

32
  plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
33

34
  plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
35

36
37
38
  plug(
    OAuthScopesPlug,
    %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
39
    when action in [:show, :followers, :following]
40
41
42
43
44
45
  )

  plug(
    OAuthScopesPlug,
    %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
    when action == :statuses
46
47
48
49
50
  )

  plug(
    OAuthScopesPlug,
    %{scopes: ["read:accounts"]}
51
    when action in [:verify_credentials, :endorsements, :identity_proofs]
52
53
54
55
56
57
  )

  plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)

  plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)

58
59
60
61
62
  plug(
    OAuthScopesPlug,
    %{scopes: ["follow", "read:blocks"]} when action == :blocks
  )

63
64
65
66
67
68
69
70
71
  plug(
    OAuthScopesPlug,
    %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
  )

  plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)

  plug(
    OAuthScopesPlug,
72
    %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
73
74
  )

75
76
  plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)

77
78
  plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])

79
  @relationship_actions [:follow, :unfollow]
80
  @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
minibikini's avatar
minibikini committed
81

82
83
84
85
86
87
  plug(
    RateLimiter,
    [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
  )

  plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
Steven Fuchs's avatar
Steven Fuchs committed
88
  plug(RateLimiter, [name: :app_account_creation] when action == :create)
89
  plug(:assign_account_by_id when action in @needs_account)
minibikini's avatar
minibikini committed
90
91
92

  action_fallback(Pleroma.Web.MastodonAPI.FallbackController)

93
94
  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation

95
  @doc "POST /api/v1/accounts"
96
  def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
97
    with :ok <- validate_email_param(params),
98
         :ok <- TwitterAPI.validate_captcha(app, params),
99
         {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
100
101
102
103
104
105
106
107
         {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
      json(conn, %{
        token_type: "Bearer",
        access_token: token.token,
        scope: app.scopes,
        created_at: Token.Utils.format_created_at(token)
      })
    else
108
      {:error, error} -> json_response(conn, :bad_request, %{error: error})
109
110
111
112
113
114
115
116
117
118
119
    end
  end

  def create(%{assigns: %{app: _app}} = conn, _) do
    render_error(conn, :bad_request, "Missing parameters")
  end

  def create(conn, _) do
    render_error(conn, :forbidden, "Invalid credentials")
  end

120
  defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
121
122
123

  defp validate_email_param(_) do
    case Pleroma.Config.get([:instance, :account_activation_required]) do
124
      true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
125
126
127
128
      _ -> :ok
    end
  end

129
130
131
132
133
134
135
136
137
138
139
140
  @doc "GET /api/v1/accounts/verify_credentials"
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
    chat_token = Phoenix.Token.sign(conn, "user socket", user.id)

    render(conn, "show.json",
      user: user,
      for: user,
      with_pleroma_settings: true,
      with_chat_token: chat_token
    )
  end

141
  @doc "PATCH /api/v1/accounts/update_credentials"
142
  def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
143
144
    user = original_user

145
146
147
148
149
    params =
      params
      |> Enum.filter(fn {_, value} -> not is_nil(value) end)
      |> Enum.into(%{})

150
    user_params =
151
152
153
154
155
156
157
158
159
160
      [
        :no_rich_text,
        :locked,
        :hide_followers_count,
        :hide_follows_count,
        :hide_followers,
        :hide_follows,
        :hide_favorites,
        :show_role,
        :skip_thread_containment,
161
        :allow_following_move,
162
163
164
        :discoverable
      ]
      |> Enum.reduce(%{}, fn key, acc ->
165
        add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
166
      end)
167
168
169
170
171
      |> add_if_present(params, :display_name, :name)
      |> add_if_present(params, :note, :bio)
      |> add_if_present(params, :avatar, :avatar)
      |> add_if_present(params, :header, :banner)
      |> add_if_present(params, :pleroma_background_image, :background)
172
173
      |> add_if_present(
        params,
174
        :fields_attributes,
175
176
177
        :raw_fields,
        &{:ok, normalize_fields_attributes(&1)}
      )
178
179
180
      |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
      |> add_if_present(params, :default_scope, :default_scope)
      |> add_if_present(params, :actor_type, :actor_type)
181

182
    changeset = User.update_changeset(user, user_params)
183
184
185
186
187
188
189
190
191
192

    with {:ok, user} <- User.update_and_set_cache(changeset) do
      render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
    else
      _e -> render_error(conn, :forbidden, "Invalid request")
    end
  end

  defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
    with true <- Map.has_key?(params, params_field),
193
         {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
194
195
196
197
198
199
      Map.put(map, map_field, new_value)
    else
      _ -> map
    end
  end

200
201
202
203
  defp normalize_fields_attributes(fields) do
    if Enum.all?(fields, &is_tuple/1) do
      Enum.map(fields, fn {_, v} -> v end)
    else
204
      Enum.map(fields, fn
205
206
        %{} = field -> %{"name" => field.name, "value" => field.value}
        field -> field
207
      end)
208
    end
209
210
  end

211
  @doc "GET /api/v1/accounts/relationships"
212
  def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
213
214
215
216
217
218
219
220
    targets = User.get_all_by_ids(List.wrap(id))

    render(conn, "relationships.json", user: user, targets: targets)
  end

  # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
  def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])

minibikini's avatar
minibikini committed
221
  @doc "GET /api/v1/accounts/:id"
222
  def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
minibikini's avatar
minibikini committed
223
    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
224
         true <- User.visible_for?(user, for_user) do
minibikini's avatar
minibikini committed
225
226
227
228
229
230
231
232
      render(conn, "show.json", user: user, for: for_user)
    else
      _e -> render_error(conn, :not_found, "Can't find user")
    end
  end

  @doc "GET /api/v1/accounts/:id/statuses"
  def statuses(%{assigns: %{user: reading_user}} = conn, params) do
233
    with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
234
         true <- User.visible_for?(user, reading_user) do
235
236
      params =
        params
237
238
239
240
        |> Map.delete(:tagged)
        |> Enum.filter(&(not is_nil(&1)))
        |> Map.new(fn {key, value} -> {to_string(key), value} end)
        |> Map.put("tag", params[:tagged])
241

minibikini's avatar
minibikini committed
242
243
244
245
246
      activities = ActivityPub.fetch_user_activities(user, reading_user, params)

      conn
      |> add_link_headers(activities)
      |> put_view(StatusView)
247
248
249
250
251
252
      |> render("index.json",
        activities: activities,
        for: reading_user,
        as: :activity,
        skip_relationships: skip_relationships?(params)
      )
253
254
    else
      _e -> render_error(conn, :not_found, "Can't find user")
minibikini's avatar
minibikini committed
255
256
257
258
259
    end
  end

  @doc "GET /api/v1/accounts/:id/followers"
  def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
260
261
262
263
264
    params =
      params
      |> Enum.map(fn {key, value} -> {to_string(key), value} end)
      |> Enum.into(%{})

minibikini's avatar
minibikini committed
265
266
267
    followers =
      cond do
        for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
268
        user.hide_followers -> []
minibikini's avatar
minibikini committed
269
270
271
272
273
274
275
276
277
278
        true -> MastodonAPI.get_followers(user, params)
      end

    conn
    |> add_link_headers(followers)
    |> render("index.json", for: for_user, users: followers, as: :user)
  end

  @doc "GET /api/v1/accounts/:id/following"
  def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
279
280
281
282
283
    params =
      params
      |> Enum.map(fn {key, value} -> {to_string(key), value} end)
      |> Enum.into(%{})

minibikini's avatar
minibikini committed
284
285
286
    followers =
      cond do
        for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
287
        user.hide_follows -> []
minibikini's avatar
minibikini committed
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
        true -> MastodonAPI.get_friends(user, params)
      end

    conn
    |> add_link_headers(followers)
    |> render("index.json", for: for_user, users: followers, as: :user)
  end

  @doc "GET /api/v1/accounts/:id/lists"
  def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
    lists = Pleroma.List.get_lists_account_belongs(user, account)

    conn
    |> put_view(ListView)
    |> render("index.json", lists: lists)
  end

  @doc "POST /api/v1/accounts/:id/follow"
  def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
307
    {:error, "Can not follow yourself"}
minibikini's avatar
minibikini committed
308
309
  end

310
311
  def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
    with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
minibikini's avatar
minibikini committed
312
313
      render(conn, "relationship.json", user: follower, target: followed)
    else
minibikini's avatar
minibikini committed
314
      {:error, message} -> json_response(conn, :forbidden, %{error: message})
minibikini's avatar
minibikini committed
315
316
317
    end
  end

minibikini's avatar
minibikini committed
318
  @doc "POST /api/v1/accounts/:id/unfollow"
minibikini's avatar
minibikini committed
319
  def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
320
    {:error, "Can not unfollow yourself"}
minibikini's avatar
minibikini committed
321
322
323
324
325
326
327
328
329
  end

  def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
    with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
      render(conn, "relationship.json", user: follower, target: followed)
    end
  end

  @doc "POST /api/v1/accounts/:id/mute"
330
331
  def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
    with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
minibikini's avatar
minibikini committed
332
333
      render(conn, "relationship.json", user: muter, target: muted)
    else
minibikini's avatar
minibikini committed
334
      {:error, message} -> json_response(conn, :forbidden, %{error: message})
minibikini's avatar
minibikini committed
335
336
337
338
339
    end
  end

  @doc "POST /api/v1/accounts/:id/unmute"
  def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
340
    with {:ok, _user_relationships} <- User.unmute(muter, muted) do
minibikini's avatar
minibikini committed
341
342
      render(conn, "relationship.json", user: muter, target: muted)
    else
minibikini's avatar
minibikini committed
343
      {:error, message} -> json_response(conn, :forbidden, %{error: message})
minibikini's avatar
minibikini committed
344
345
346
347
348
    end
  end

  @doc "POST /api/v1/accounts/:id/block"
  def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
349
    with {:ok, _user_block} <- User.block(blocker, blocked),
minibikini's avatar
minibikini committed
350
351
352
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
      render(conn, "relationship.json", user: blocker, target: blocked)
    else
minibikini's avatar
minibikini committed
353
      {:error, message} -> json_response(conn, :forbidden, %{error: message})
minibikini's avatar
minibikini committed
354
355
356
357
358
    end
  end

  @doc "POST /api/v1/accounts/:id/unblock"
  def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
359
    with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
minibikini's avatar
minibikini committed
360
361
      render(conn, "relationship.json", user: blocker, target: blocked)
    else
minibikini's avatar
minibikini committed
362
      {:error, message} -> json_response(conn, :forbidden, %{error: message})
minibikini's avatar
minibikini committed
363
364
    end
  end
365

366
  @doc "POST /api/v1/follows"
367
  def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
368
369
370
371
372
373
374
375
    case User.get_cached_by_nickname(uri) do
      %User{} = user ->
        conn
        |> assign(:account, user)
        |> follow(%{})

      nil ->
        {:error, :not_found}
376
377
378
379
380
    end
  end

  @doc "GET /api/v1/mutes"
  def mutes(%{assigns: %{user: user}} = conn, _) do
381
382
    users = User.muted_users(user, _restrict_deactivated = true)
    render(conn, "index.json", users: users, for: user, as: :user)
383
384
385
386
  end

  @doc "GET /api/v1/blocks"
  def blocks(%{assigns: %{user: user}} = conn, _) do
387
388
    users = User.blocked_users(user, _restrict_deactivated = true)
    render(conn, "index.json", users: users, for: user, as: :user)
389
  end
390

391
  @doc "GET /api/v1/endorsements"
392
393
394
395
  def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)

  @doc "GET /api/v1/identity_proofs"
  def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
minibikini's avatar
minibikini committed
396
end