Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
28a8f58
fix bad formatting examples
zefir-git Dec 30, 2023
7c57033
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Jan 2, 2024
a3b4ef6
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Jan 5, 2024
6046378
db tables for owner change requests
zefir-git Jan 5, 2024
53e0418
change account owner config
zefir-git Jan 5, 2024
b9907a4
account ownership change permissions
zefir-git Jan 5, 2024
6f9db50
ChangeOwnerRequest class
zefir-git Jan 5, 2024
492475a
tasks that run every minute
zefir-git Jan 5, 2024
d5818f2
remove unused import
zefir-git Jan 5, 2024
64f93ee
use Runnable instead of Supplier for minute interval sched
zefir-git Jan 5, 2024
8207d3d
move interest timer to minuteLoopTasks
zefir-git Jan 5, 2024
b2bcc62
add err msg for when account is already owned by "new owner" player
zefir-git Jan 5, 2024
977f552
change owner command
zefir-git Jan 5, 2024
5dc8dee
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Jan 14, 2024
96be807
register change owner sub-command
zefir-git Jan 14, 2024
2be20d4
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Jan 15, 2024
8b2dec9
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Jan 15, 2024
46e4ba1
accept ownership change sub-command
zefir-git Jan 15, 2024
ca654e5
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Jan 21, 2024
eb1fec7
use new config
zefir-git Jan 21, 2024
ddb1fb3
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Feb 6, 2024
c68b82f
allow sending message to any audience
zefir-git Feb 6, 2024
87e52dd
send account owner change request message
zefir-git Feb 6, 2024
fca0b46
de-register minute loop tasks
zefir-git Feb 8, 2024
8679196
fix config path for change owner req msg
zefir-git Feb 8, 2024
6485ede
message for account ownership request sent
zefir-git Feb 8, 2024
52a6a7e
successfully accepted account ownership message
zefir-git Feb 8, 2024
cbaa08a
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Feb 28, 2024
da3ea69
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git May 3, 2024
9cec648
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git May 29, 2024
bff92a1
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Sep 16, 2024
641eb68
Revert "move interest timer to minuteLoopTasks"
zefir-git Sep 16, 2024
0031341
remove unused import
zefir-git Sep 16, 2024
d77ec31
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Sep 17, 2024
568c4c6
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Nov 5, 2024
223df5c
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Feb 24, 2025
5580d06
allow Message command result to use null audience
zefir-git Feb 25, 2025
4bd8660
fix transfer ownership send & accept
zefir-git Feb 25, 2025
d73d9b7
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Feb 25, 2025
f83c8a9
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Mar 3, 2025
37cecdb
fix: check for request expiration in accept command
zefir-git Feb 25, 2025
f841adb
Merge branch 'master' into 65-add-the-ability-to-transfer-ownership-o…
zefir-git Oct 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 202 additions & 1 deletion src/main/java/pro/cloudnode/smp/bankaccounts/Account.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
import java.util.stream.Collectors;

/**
* Bank account
Expand All @@ -40,7 +42,7 @@ public class Account {
/**
* Account owner
*/
public final @NotNull OfflinePlayer owner;
public @NotNull OfflinePlayer owner;
/**
* Account type
*/
Expand Down Expand Up @@ -525,4 +527,203 @@ public enum Type {
USERNAME
}
}

/**
* A request to change the owner of a bank account (sent to the new owner)
*/
public final static class ChangeOwnerRequest {
/**
* Account id
*/
private final @NotNull String account;

/**
* New owner UUID
*/
public final @NotNull OfflinePlayer newOwner;

/**
* Request creation timestamp
*/
public final @NotNull Date created;

/**
* Create a new account ownership transfer request instance
*
* @param account Account to transfer ownership of
* @param newOwner The new account owner
*/
public ChangeOwnerRequest(final @NotNull Account account, final @NotNull OfflinePlayer newOwner) {
this.account = account.id;
this.newOwner = newOwner;
this.created = new Date();
}

private ChangeOwnerRequest(final @NotNull ResultSet rs) throws SQLException {
this.account = rs.getString("id");
this.newOwner = BankAccounts.getInstance().getServer().getOfflinePlayer(UUID.fromString(rs.getString("new_owner")));
this.created = new Date(rs.getDate("created").getTime());
}

/**
* Get account
*/
public @NotNull Optional<@NotNull Account> account() {
return Account.get(account);
}

/**
* Check if request has expired
*/
public boolean expired() {
return System.currentTimeMillis() - created.getTime() > BankAccounts.getInstance().config().changeOwnerTimeout() * 6e4;
}

/**
* Confirm/accept the request
*
* @return Whether the change was successful
*/
public boolean confirm() {
final @NotNull Optional<@NotNull Account> account = this.account();
if (account.isEmpty()) return false;
if (account.get().frozen) return false;
account.get().owner = newOwner;
account.get().update();
this.delete();
return true;
}

/**
* Insert into database
*/
public void insert() {
try (final @NotNull Connection conn = BankAccounts.getInstance().getDb().getConnection();
final @NotNull PreparedStatement stmt = conn.prepareStatement("INSERT INTO `change_owner_requests` (`account`, `new_owner`, `created`) VALUES (?, ?, ?)")) {
stmt.setString(1, account);
stmt.setString(2, newOwner.toString());
stmt.setDate(3, new java.sql.Date(created.getTime()));

stmt.executeUpdate();
}
catch (final @NotNull Exception e) {
BankAccounts.getInstance().getLogger().log(Level.SEVERE, "Could not save account ownership change request. account: " + account + ", newOwner: " + newOwner, e);
}
}

/**
* Delete from database
*/
public void delete() {
try (final @NotNull Connection conn = BankAccounts.getInstance().getDb().getConnection();
final @NotNull PreparedStatement stmt = conn.prepareStatement("DELETE FROM `change_owner_requests` WHERE `account` = ? AND `new_owner` = ?")) {
stmt.setString(1, account);
stmt.setString(2, newOwner.toString());
stmt.executeUpdate();
}
catch (final @NotNull Exception e) {
BankAccounts.getInstance().getLogger().log(Level.SEVERE, "Could not delete account ownership change request. account: " + account + ", newOwner: " + newOwner, e);
}
}

/**
* Delete all request to transfer a certain account
*
* @param account Account ID
*/
public static void delete(final @NotNull UUID account) {
try (final @NotNull Connection conn = BankAccounts.getInstance().getDb().getConnection();
final @NotNull PreparedStatement stmt = conn.prepareStatement("DELETE FROM `change_owner_requests` WHERE `account` = ?")) {
stmt.setString(1, account.toString());
stmt.executeUpdate();
}
catch (final @NotNull Exception e) {
BankAccounts.getInstance().getLogger().log(Level.SEVERE, "Could not delete account ownership change request. account: " + account, e);
}
}

/**
* Delete expired requests
*/
private static void deleteExpired() {
try (final @NotNull Connection conn = BankAccounts.getInstance().getDb().getConnection();
final @NotNull PreparedStatement stmt = conn.prepareStatement("DELETE FROM `change_owner_requests` WHERE `created` = ?")) {
stmt.setDate(1, new java.sql.Date(System.currentTimeMillis() - BankAccounts.getInstance().config().changeOwnerTimeout() * 60_000L));
stmt.executeUpdate();
}
catch (final @NotNull Exception e) {
BankAccounts.getInstance().getLogger().log(Level.SEVERE, "Could not delete account ownership change request. account: ", e);
}
}

/**
* Asynchronously delete expired requests
*/
public final static @NotNull Runnable deleteExpiredLater = () -> BankAccounts.getInstance().getServer().getScheduler().runTaskAsynchronously(BankAccounts.getInstance(), ChangeOwnerRequest::deleteExpired);

/**
* Get account ownership change request
*
* @param account Account ID
* @param newOwner New owner
*/
public static @NotNull Optional<@NotNull ChangeOwnerRequest> get(final @NotNull String account, @NotNull OfflinePlayer newOwner) {
try (final @NotNull Connection conn = BankAccounts.getInstance().getDb().getConnection();
final @NotNull PreparedStatement stmt = conn.prepareStatement("SELECT * FROM `change_owner_requests` WHERE `account` = ? AND `new_owner` = ? LIMIT 1")) {
stmt.setString(1, account);
stmt.setString(2, newOwner.getUniqueId().toString());
final @NotNull ResultSet rs = stmt.executeQuery();
return rs.next() ? Optional.of(new ChangeOwnerRequest(rs)) : Optional.empty();
}
catch (final @NotNull Exception e) {
BankAccounts.getInstance().getLogger().log(Level.SEVERE, "Could not get account ownership change request. account: " + account + ", newOwner: " + newOwner.getUniqueId(), e);
return Optional.empty();
}
}

/**
* List a player's incoming (received) account ownership change requests.
*
* @param player The player whose requests to list
*/
public static @NotNull Account @NotNull [] incoming(final @NotNull OfflinePlayer player) {
try (final @NotNull Connection conn = BankAccounts.getInstance().getDb().getConnection();
final @NotNull PreparedStatement stmt = conn.prepareStatement("SELECT * FROM `change_owner_requests` WHERE `new_owner` = ?")) {
stmt.setString(1, player.getUniqueId().toString());
final @NotNull ResultSet rs = stmt.executeQuery();

final @NotNull List<@NotNull Account> accounts = new ArrayList<>();
while (rs.next()) accounts.add(new Account(rs));
return accounts.toArray(new Account[0]);
}
catch (final @NotNull Exception e) {
BankAccounts.getInstance().getLogger().log(Level.SEVERE, "Could not get incoming account ownership change requests. player: " + player.getUniqueId(), e);
return new Account[0];
}
}

/**
* List a player’s outgoing (sent) account ownership change requests.
*
* @param player The player whose requests to list
*/
public static @NotNull Account @NotNull [] outgoing(final @NotNull OfflinePlayer player) {
final @NotNull String @NotNull [] ids = Arrays.stream(Account.get(player)).map(a -> a.id).toArray(String[]::new);
final @NotNull String placeholders = Arrays.stream(ids).map(id -> "?").collect(Collectors.joining(", "));
try (final @NotNull Connection conn = BankAccounts.getInstance().getDb().getConnection();
final @NotNull PreparedStatement stmt = conn.prepareStatement("SELECT * FROM `change_owner_requests` WHERE `account` in (" + placeholders + ")")) {
for (int i = ids.length; i > 0;)
stmt.setString(i, ids[--i]);
final @NotNull ResultSet rs = stmt.executeQuery();

final @NotNull List<@NotNull Account> accounts = new ArrayList<>();
while (rs.next()) accounts.add(new Account(rs));
return accounts.toArray(new Account[0]);
}
catch (final @NotNull Exception e) {
BankAccounts.getInstance().getLogger().log(Level.SEVERE, "Could not get outgoing account ownership change requests. player: " + player.getUniqueId(), e);
return new Account[0];
}
}
}
}
97 changes: 97 additions & 0 deletions src/main/java/pro/cloudnode/smp/bankaccounts/BankConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,21 @@ public boolean instrumentsGlintEnabled() {
return Objects.requireNonNull(Registry.ENCHANTMENT.get(NamespacedKey.minecraft(Objects.requireNonNull(config.getString("instruments.glint.enchantment")))));
}

// change-owner.confirm
public boolean changeOwnerConfirm() {
return config.getBoolean("change-owner.confirm");
}

// change-owner.timeout
public int changeOwnerTimeout() {
return config.getInt("change-owner.timeout");
}

// change-owner.limit-send
public int changeOwnerLimitSend() {
return config.getInt("change-owner.limit-send");
}

// pos.allow-personal
public boolean posAllowPersonal() {
return config.getBoolean("pos.allow-personal");
Expand Down Expand Up @@ -766,6 +781,32 @@ public int invoiceNotifyInterval() {
return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("messages.errors.player-never-joined")));
}

// messages.errors.change-owner-same
public @NotNull Component messagesErrorsAlreadyOwnsAccount(final @NotNull Account account) {
return MiniMessage.miniMessage().deserialize(
Objects.requireNonNull(config.getString("messages.errors.change-owner-same")),
Placeholder.parsed("player", account.ownerNameUnparsed())
);
}

// messages.errors.change-owner-limit-send
public @NotNull Component messagesErrorsChangeOwnerLimitSend() {
return MiniMessage.miniMessage().deserialize(
Objects.requireNonNull(config.getString("messages.errors.change-owner-limit-send")),
Placeholder.unparsed("limit", String.valueOf(this.changeOwnerLimitSend()))
);
}

// messages.errors.change-owner-not-found
public @NotNull Component messagesErrorsChangeOwnerNotFound() {
return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("messages.errors.change-owner-not-found")));
}

// messages.errors.change-owner-accept-failed
public @NotNull Component messagesErrorsChangeOwnerAcceptFailed() {
return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("messages.errors.change-owner-accept-failed")));
}

// messages.errors.async-failed
public @NotNull Component messagesErrorsAsyncFailed() {
return MiniMessage.miniMessage().deserialize(Objects.requireNonNull(config.getString("messages.errors.async-failed")));
Expand Down Expand Up @@ -1449,6 +1490,61 @@ public int invoiceNotifyInterval() {
Formatter.choice("unpaid-choice", unpaid)
));
}

// messages.change-owner.request
public @NotNull Component messagesChangeOwnerRequest(final @NotNull Account.ChangeOwnerRequest request, final @NotNull String acceptCommand) {
final @NotNull Account account = request.account().orElse(new Account.ClosedAccount());
return MiniMessage.miniMessage().deserialize(
Objects.requireNonNull(config.getString("messages.change-owner.request"))
.replace("<new-owner-uuid>", request.newOwner.getUniqueId().toString())
.replace("<new-owner>", request.newOwner.getName() == null ? "<i>unknown player</i>" : request.newOwner.getName())
.replace("<accept-command>", acceptCommand)
.replace("<account>", account.name())
.replace("<account-id>", account.id)
.replace("<account-type>", account.type.getName())
.replace("<account-owner>", account.ownerNameUnparsed())
.replace("<balance>", account.balance == null ? "∞" : account.balance.toPlainString())
.replace("<balance-formatted>", BankAccounts.formatCurrency(account.balance))
.replace("<balance-short>", BankAccounts.formatCurrencyShort(account.balance)),
Formatter.date("date", request.created.toInstant().atZone(ZoneOffset.UTC).toLocalDateTime())
);
}

// messages.change-owner.sent
public @NotNull Component messagesChangeOwnerSent(final @NotNull Account.ChangeOwnerRequest request) {
final @NotNull Account account = request.account().orElse(new Account.ClosedAccount());
return MiniMessage.miniMessage().deserialize(
Objects.requireNonNull(config.getString("messages.change-owner.sent"))
.replace("<new-owner-uuid>", request.newOwner.getUniqueId().toString())
.replace("<new-owner>", request.newOwner.getName() == null ? "<i>unknown player</i>" : request.newOwner.getName())
.replace("<account>", account.name())
.replace("<account-id>", account.id)
.replace("<account-type>", account.type.getName())
.replace("<account-owner>", account.ownerNameUnparsed())
.replace("<balance>", account.balance == null ? "∞" : account.balance.toPlainString())
.replace("<balance-formatted>", BankAccounts.formatCurrency(account.balance))
.replace("<balance-short>", BankAccounts.formatCurrencyShort(account.balance)),
Formatter.date("date", request.created.toInstant().atZone(ZoneOffset.UTC).toLocalDateTime())
);
}

// messages.change-owner.accepted
public @NotNull Component messagesChangeOwnerAccepted(final @NotNull Account.ChangeOwnerRequest request) {
final @NotNull Account account = request.account().orElse(new Account.ClosedAccount());
return MiniMessage.miniMessage().deserialize(
Objects.requireNonNull(config.getString("messages.change-owner.accepted"))
.replace("<new-owner-uuid>", request.newOwner.getUniqueId().toString())
.replace("<new-owner>", request.newOwner.getName() == null ? "<i>unknown player</i>" : request.newOwner.getName())
.replace("<account>", account.name())
.replace("<account-id>", account.id)
.replace("<account-type>", account.type.getName())
.replace("<account-owner>", account.ownerNameUnparsed())
.replace("<balance>", account.balance == null ? "∞" : account.balance.toPlainString())
.replace("<balance-formatted>", BankAccounts.formatCurrency(account.balance))
.replace("<balance-short>", BankAccounts.formatCurrencyShort(account.balance)),
Formatter.date("date", request.created.toInstant().atZone(ZoneOffset.UTC).toLocalDateTime())
);
}

// messages.update-available
public @NotNull Component messagesUpdateAvailable(final @NotNull ModrinthUpdate update) {
Expand All @@ -1471,6 +1567,7 @@ public enum HelpCommandsBank {
FREEZE("freeze"),
UNFREEZE("unfreeze"),
DELETE("delete"),
CHANGE_OWNER("change-owner"),
INSTRUMENT("instrument"),
WHOIS("whois"),
BALTOP("baltop"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import pro.cloudnode.smp.bankaccounts.commands.result.CommandResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public final class Permissions {
public static final @NotNull String SET_NAME = "bank.set.name";
public static final @NotNull String FREEZE = "bank.freeze";
public static final @NotNull String DELETE = "bank.delete";
public static final @NotNull String CHANGE_OWNER = "bank.change.owner";
public static final @NotNull String CHANGE_OWNER_OTHER = "bank.change.owner.other";
public static final @NotNull String CHANGE_OWNER_BYPASS_CONFIRM = "bank.change.owner.bypass.confirm";
public static final @NotNull String CHANGE_OWNER_BYPASS_LIMIT = "bank.change.owner.bypass.limit";
public static final @NotNull String CHANGE_OWNER_ACCEPT = "bank.change.owner.accept";
public static final @NotNull String BALTOP = "bank.baltop";
public static final @NotNull String POS_CREATE = "bank.pos.create";
public static final @NotNull String POS_USE = "bank.pos.use";
Expand Down
Loading
Loading