diff --git a/config/config.exs b/config/config.exs index 05c40c6d73..b5f9d86e88 100644 --- a/config/config.exs +++ b/config/config.exs @@ -262,7 +262,8 @@ max_endorsed_users: 20, birthday_required: false, birthday_min_age: 0, - max_media_attachments: 1_000 + max_media_attachments: 1_000, + migration_cooldown_period: 30 config :pleroma, :welcome, direct_message: [ diff --git a/config/description.exs b/config/description.exs index bd87eee2c2..535950fc2d 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1015,6 +1015,13 @@ description: "Minimum required age (in days) for users to create account. Only used if birthday is required.", suggestions: [6570] + }, + %{ + key: :migration_cooldown_period, + type: :integer, + description: + "Number of days for which users won't be able to migrate account again after successful migration.", + suggestions: [30] } ] }, diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 1c635ee92c..03db604114 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -160,6 +160,7 @@ defmodule Pleroma.User do field(:show_birthday, :boolean, default: false) field(:location, :string) field(:language, :string) + field(:last_move_at, :naive_datetime) embeds_one( :notification_settings, @@ -2707,4 +2708,10 @@ def get_friends_birthdays_query(%User{} = user, day, month) do birthday_month: month }) end + + def update_last_move_at(%__MODULE__{local: true} = user) do + user + |> cast(%{last_move_at: NaiveDateTime.utc_now()}, [:last_move_at]) + |> update_and_set_cache() + end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index a12a32ebdd..e0f29b6666 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -450,6 +450,8 @@ def move(%User{} = origin, %User{} = target, local \\ true) do "target_id" => target.id }) + User.update_last_move_at(origin) + {:ok, activity} else false -> {:error, "Target account must have the origin in `alsoKnownAs`"} diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex index 29df03e34c..d9b47cdcde 100644 --- a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex +++ b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex @@ -229,7 +229,8 @@ def move_account_operation do }), 400 => Operation.response("Error", "application/json", ApiError), 403 => Operation.response("Error", "application/json", ApiError), - 404 => Operation.response("Error", "application/json", ApiError) + 404 => Operation.response("Error", "application/json", ApiError), + 429 => Operation.response("Error", "application/json", ApiError) } } end diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index cea23164c5..2215018fe2 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -51,7 +51,8 @@ def render("show.json", _) do post_formats: Config.get([:instance, :allowed_post_formats]), privileged_staff: Config.get([:instance, :privileged_staff]), birthday_required: Config.get([:instance, :birthday_required]), - birthday_min_age: Config.get([:instance, :birthday_min_age]) + birthday_min_age: Config.get([:instance, :birthday_min_age]), + migration_cooldown_period: Config.get([:instance, :migration_cooldown_period]) }, stats: %{mau: Pleroma.User.active_user_count()}, vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key), diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index d5a24ae6ca..c09d616a8c 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -259,6 +259,7 @@ def move_account(%{assigns: %{user: user}, body_params: body_params} = conn, %{} case CommonAPI.Utils.confirm_current_password(user, body_params.password) do {:ok, user} -> with {:ok, target_user} <- find_or_fetch_user_by_nickname(body_params.target_account), + {:period, false} <- {:period, within_cooldown?(user)}, {:ok, _user} <- ActivityPub.move(user, target_user) do json(conn, %{status: "success"}) else @@ -267,6 +268,11 @@ def move_account(%{assigns: %{user: user}, body_params: body_params} = conn, %{} |> put_status(404) |> json(%{error: "Target account not found."}) + {:period, true} -> + conn + |> put_status(429) + |> json(%{error: "You are within cooldown period."}) + {:error, error} -> json(conn, %{error: error}) end @@ -276,6 +282,19 @@ def move_account(%{assigns: %{user: user}, body_params: body_params} = conn, %{} end end + defp within_cooldown?(%{last_move_at: nil}), do: false + + defp within_cooldown?(user) do + cooldown_period = + Pleroma.Config.get([:instance, :migration_cooldown_period], 0) * 60 * 60 * 24 + + now = NaiveDateTime.utc_now() + + difference = NaiveDateTime.diff(now, user.last_move_at) + + difference < cooldown_period + end + def add_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do with {:ok, alias_user} <- find_user_by_nickname(body_params.alias), {:ok, _user} <- user |> User.add_alias(alias_user) do diff --git a/priv/repo/migrations/20220927220033_add_last_move_at_to_users.exs b/priv/repo/migrations/20220927220033_add_last_move_at_to_users.exs new file mode 100644 index 0000000000..a6f5cca9a4 --- /dev/null +++ b/priv/repo/migrations/20220927220033_add_last_move_at_to_users.exs @@ -0,0 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.AddLastMoveAtToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add(:last_move_at, :naive_datetime) + end + end +end diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs index a4da236359..ef54065578 100644 --- a/test/pleroma/web/twitter_api/util_controller_test.exs +++ b/test/pleroma/web/twitter_api/util_controller_test.exs @@ -6,9 +6,11 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do use Pleroma.Web.ConnCase use Oban.Testing, repo: Pleroma.Repo + alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User + import Ecto.Changeset import Pleroma.Factory import Mock @@ -838,6 +840,50 @@ test "prefix nickname by @ should work", %{ refute User.following?(follower, user) assert User.following?(follower, target_user) end + + test "do not allow to migrate account within cooldown period", %{conn: conn, user: user} do + user + |> cast( + %{last_move_at: NaiveDateTime.utc_now() |> NaiveDateTime.add(-1 * 24 * 60 * 60, :second)}, + [:last_move_at] + ) + |> Repo.update() + + target_user = insert(:user, also_known_as: [user.ap_id]) + + target_nick = target_user |> User.full_nickname() + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/move_account", %{password: "test", target_account: target_nick}) + + assert json_response_and_validate_schema(conn, 429) == %{ + "error" => "You are within cooldown period." + } + end + + test "allow to migrate account after cooldown period", %{conn: conn, user: user} do + user + |> cast( + %{ + last_move_at: NaiveDateTime.utc_now() |> NaiveDateTime.add(-31 * 24 * 60 * 60, :second) + }, + [:last_move_at] + ) + |> Repo.update() + + target_user = insert(:user, also_known_as: [user.ap_id]) + + target_nick = target_user |> User.full_nickname() + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/move_account", %{password: "test", target_account: target_nick}) + + assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"} + end end describe "GET /api/pleroma/aliases" do