diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/Account.java b/src/main/java/pro/cloudnode/smp/bankaccounts/Account.java index 4fe7f8e..c0dd4ea 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/Account.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/Account.java @@ -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 @@ -40,7 +42,7 @@ public class Account { /** * Account owner */ - public final @NotNull OfflinePlayer owner; + public @NotNull OfflinePlayer owner; /** * Account type */ @@ -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]; + } + } + } } diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/BankConfig.java b/src/main/java/pro/cloudnode/smp/bankaccounts/BankConfig.java index 66fff30..73c0d85 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/BankConfig.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/BankConfig.java @@ -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"); @@ -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"))); @@ -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("", request.newOwner.getUniqueId().toString()) + .replace("", request.newOwner.getName() == null ? "unknown player" : request.newOwner.getName()) + .replace("", acceptCommand) + .replace("", account.name()) + .replace("", account.id) + .replace("", account.type.getName()) + .replace("", account.ownerNameUnparsed()) + .replace("", account.balance == null ? "∞" : account.balance.toPlainString()) + .replace("", BankAccounts.formatCurrency(account.balance)) + .replace("", 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("", request.newOwner.getUniqueId().toString()) + .replace("", request.newOwner.getName() == null ? "unknown player" : request.newOwner.getName()) + .replace("", account.name()) + .replace("", account.id) + .replace("", account.type.getName()) + .replace("", account.ownerNameUnparsed()) + .replace("", account.balance == null ? "∞" : account.balance.toPlainString()) + .replace("", BankAccounts.formatCurrency(account.balance)) + .replace("", 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("", request.newOwner.getUniqueId().toString()) + .replace("", request.newOwner.getName() == null ? "unknown player" : request.newOwner.getName()) + .replace("", account.name()) + .replace("", account.id) + .replace("", account.type.getName()) + .replace("", account.ownerNameUnparsed()) + .replace("", account.balance == null ? "∞" : account.balance.toPlainString()) + .replace("", BankAccounts.formatCurrency(account.balance)) + .replace("", 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) { @@ -1471,6 +1567,7 @@ public enum HelpCommandsBank { FREEZE("freeze"), UNFREEZE("unfreeze"), DELETE("delete"), + CHANGE_OWNER("change-owner"), INSTRUMENT("instrument"), WHOIS("whois"), BALTOP("baltop"), diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/Command.java b/src/main/java/pro/cloudnode/smp/bankaccounts/Command.java index 0011e22..91ab1c4 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/Command.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/Command.java @@ -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; diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/Permissions.java b/src/main/java/pro/cloudnode/smp/bankaccounts/Permissions.java index a6dab29..021fdc5 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/Permissions.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/Permissions.java @@ -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"; diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/commands/BankCommand.java b/src/main/java/pro/cloudnode/smp/bankaccounts/commands/BankCommand.java index 9e943d7..2338663 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/commands/BankCommand.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/commands/BankCommand.java @@ -48,6 +48,7 @@ public class BankCommand extends Command { if (sender.hasPermission(Permissions.SET_NAME)) suggestions.addAll(Arrays.asList("setname", "rename")); if (sender.hasPermission(Permissions.FREEZE)) suggestions.addAll(Arrays.asList("freeze", "disable", "block", "unfreeze", "enable", "unblock")); if (sender.hasPermission(Permissions.DELETE)) suggestions.add("delete"); + if (sender.hasPermission(Permissions.CHANGE_OWNER)) suggestions.addAll(Arrays.asList("changeowner", "newowner", "newholder", "changeholder")); if (sender.hasPermission(Permissions.TRANSFER_SELF) || sender.hasPermission(Permissions.TRANSFER_OTHER)) suggestions.addAll(Arrays.asList("transfer", "send", "pay")); if (sender.hasPermission(Permissions.HISTORY)) suggestions.addAll(Arrays.asList("transactions", "history")); @@ -135,6 +136,12 @@ else if (args.length == 4 && args[2].equals("--player") && sender.hasPermission( suggestions.addAll(Arrays.stream(Account.get(player)).map(account -> account.id).collect(Collectors.toSet())); } } + case "changeowner", "newowner", "newholder", "changeholder" -> { + if (!sender.hasPermission(Permissions.CHANGE_OWNER)) return suggestions; + if (args.length == 2) suggestions.addAll(Arrays + .stream(sender.hasPermission(Permissions.CHANGE_OWNER_OTHER) ? Account.get() : Account.get(BankAccounts.getOfflinePlayer(sender))) + .map(account -> account.id).collect(Collectors.toSet())); + } case "transfer", "send", "pay" -> { if (!sender.hasPermission(Permissions.TRANSFER_SELF) && !sender.hasPermission(Permissions.TRANSFER_OTHER)) return suggestions; @@ -220,6 +227,8 @@ else if (args.length == 3 && sender.hasPermission(Permissions.INSTRUMENT_CREATE_ case "freeze", "disable", "block" -> freeze(sender, argsSubset, label); case "unfreeze", "enable", "unblock" -> unfreeze(sender, argsSubset, label); case "delete" -> delete(sender, argsSubset, label); + case "changeowner", "newowner", "newholder", "changeholder" -> changeOwner(sender, argsSubset, label); + case "acceptchangeowner" -> acceptChangeOwner(sender, argsSubset, label); case "transfer", "send", "pay" -> transfer(sender, argsSubset, label); case "transactions", "history" -> transactions(sender, argsSubset, label); case "instrument", "card" -> instrument(sender, argsSubset, label); @@ -262,6 +271,8 @@ else if (args.length == 3 && sender.hasPermission(Permissions.INSTRUMENT_CREATE_ } if (sender.hasPermission(Permissions.DELETE)) BankAccounts.getInstance().config().messagesHelpBankCommands(BankConfig.HelpCommandsBank.DELETE, label + " delete", "").ifPresent(sender::sendMessage); + if (sender.hasPermission(Permissions.CHANGE_OWNER)) + BankAccounts.getInstance().config().messagesHelpBankCommands(BankConfig.HelpCommandsBank.CHANGE_OWNER, label + " changeowner", " ").ifPresent(sender::sendMessage); if (sender.hasPermission(Permissions.INSTRUMENT_CREATE)) BankAccounts.getInstance().config().messagesHelpBankCommands(BankConfig.HelpCommandsBank.INSTRUMENT, label + " instrument", "" + (sender.hasPermission(Permissions.INSTRUMENT_CREATE_OTHER) ? " [player]" : "")).ifPresent(sender::sendMessage); if (sender.hasPermission(Permissions.WHOIS)) @@ -493,6 +504,69 @@ else if (accounts.length == 1) return new Message(sender, BankAccounts.getInstance().config().messagesAccountDeleted(account.get())); } + /** + * Change ownership of account + */ + public static @NotNull CommandResult changeOwner(final @NotNull CommandSender sender, final @NotNull String @NotNull [] args, final @NotNull String label) { + if (!sender.hasPermission(Permissions.CHANGE_OWNER)) return new Message(sender, BankAccounts.getInstance().config().messagesErrorsNoPermission()); + if (args.length < 2) return sendUsage(sender, label, "changeowner " + (args.length > 0 ? args[0] : "") + " "); + final @NotNull Optional<@NotNull Account> account = Account.get(Account.Tag.from(args[0])); + if (account.isEmpty()) return new Message(sender, BankAccounts.getInstance().config().messagesErrorsAccountNotFound()); + if (!sender.hasPermission(Permissions.CHANGE_OWNER_OTHER) && !account.get().owner.getUniqueId() + .equals(BankAccounts.getOfflinePlayer(sender).getUniqueId())) + return new Message(sender, BankAccounts.getInstance().config().messagesErrorsNotAccountOwner()); + final @NotNull OfflinePlayer recipient = BankAccounts.getInstance().getServer().getOfflinePlayer(args[1]); + if (!recipient.hasPlayedBefore()) + return new Message(sender, BankAccounts.getInstance().config().messagesErrorsPlayerNeverJoined()); + if (recipient.getUniqueId().equals(account.get().owner.getUniqueId())) + return new Message(sender, BankAccounts.getInstance().config().messagesErrorsAlreadyOwnsAccount(account.get())); + final int limit = BankAccounts.getInstance().config().changeOwnerLimitSend(); + if (limit >= 0 && !sender.hasPermission(Permissions.CHANGE_OWNER_BYPASS_LIMIT) && sender instanceof final @NotNull Player player) { + final @NotNull Account @NotNull [] accounts = Account.ChangeOwnerRequest.outgoing(player); + if (accounts.length >= limit) + return new Message(sender, BankAccounts.getInstance().config().messagesErrorsChangeOwnerLimitSend()); + } + final @NotNull Account.ChangeOwnerRequest request = new Account.ChangeOwnerRequest(account.get(), recipient); + if (BankAccounts.getInstance().config().changeOwnerConfirm() && !sender.hasPermission(Permissions.CHANGE_OWNER_BYPASS_CONFIRM)) { + request.insert(); + final @NotNull String acceptCommand = "/" + label + " acceptchangeowner " + account.get().id; + new Message( + recipient.getPlayer(), + BankAccounts.getInstance().config().messagesChangeOwnerRequest(request, acceptCommand) + ).send(); + return new Message(sender, BankAccounts.getInstance().config().messagesChangeOwnerSent(request)); + } + request.confirm(); + return new Message(sender, BankAccounts.getInstance().config().messagesChangeOwnerSent(request)); + } + + /** + * Accept ownership change request + */ + public static @NotNull CommandResult acceptChangeOwner(final @NotNull CommandSender sender, final @NotNull String @NotNull [] args, final @NotNull String label) { + if (!sender.hasPermission(Permissions.CHANGE_OWNER_ACCEPT)) + return new Message(sender, BankAccounts.getInstance().config().messagesErrorsNoPermission()); + if (args.length < 1) return sendUsage(sender, label, "acceptchangeowner "); + final @NotNull Optional request = Account.ChangeOwnerRequest.get(args[0], BankAccounts.getOfflinePlayer(sender)); + if (request.isEmpty() || request.get().expired()) + return new Message(sender, BankAccounts.getInstance().config().messagesErrorsChangeOwnerNotFound()); + final @NotNull Optional<@NotNull Account> account = request.get().account(); + if (account.isEmpty()) { + request.get().delete(); + return new Message(sender, BankAccounts.getInstance().config().messagesErrorsAccountNotFound()); + } + + if (!sender.hasPermission(Permissions.ACCOUNT_CREATE_BYPASS)) { + final @NotNull Account @NotNull [] accounts = Account.get(BankAccounts.getOfflinePlayer(sender), account.get().type); + int limit = BankAccounts.getInstance().config().accountLimits(account.get().type); + if (limit != -1 && accounts.length >= limit) + return new Message(sender, BankAccounts.getInstance().config().messagesErrorsMaxAccounts(account.get().type, limit)); + } + + if (request.get().confirm()) return new Message(sender, BankAccounts.getInstance().config().messagesChangeOwnerAccepted(request.get())); + return new Message(sender, BankAccounts.getInstance().config().messagesErrorsChangeOwnerAcceptFailed()); + } + /** * Make a transfer to another account *

diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/commands/result/Message.java b/src/main/java/pro/cloudnode/smp/bankaccounts/commands/result/Message.java index 5c6fde2..0374968 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/commands/result/Message.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/commands/result/Message.java @@ -5,22 +5,24 @@ import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public final class Message extends CommandResult { - public final @NotNull Audience audience; + public final @Nullable Audience audience; public final @NotNull Component message; - public Message(@NotNull Audience audience, @NotNull Component message) { + public Message(@Nullable Audience audience, @NotNull Component message) { super(); this.audience = audience; this.message = message; } - public Message(@NotNull Audience audience, @NotNull String message, final @NotNull TagResolver @NotNull ... placeholders) { + public Message(@Nullable Audience audience, @NotNull String message, final @NotNull TagResolver @NotNull ... placeholders) { this(audience, MiniMessage.miniMessage().deserialize(message, placeholders)); } public void send() { - audience.sendMessage(message); + if (audience != null) + audience.sendMessage(message); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 18f07da..371c3e6 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -182,6 +182,20 @@ instruments: # BankAccounts will always use level 1 of the enchantment. enchantment: unbreaking +# Change account owner +change-owner: + # Require confirmation/approval by the new owner. + # To bypass approval and forcefully transfer ownership, use the ‘bank.change.owner.bypass.confirm’ permission. + confirm: true + + # In minutes, when should an ownership change request expire. + timeout: 15 + + # The maximum number of pending sent ownership change requests that a player can have (to prevent spam). + # Set to ‘0’ to disable and allow unlimited ownership change requests. + # To bypass this limit, use the ‘bank.change.owner.bypass.limit’ permission. + limit-send: 5 + # Point of Sale. # This is a chest that can have any items. # Players can buy all the items in the POS for the specified price. @@ -361,6 +375,7 @@ messages: freeze: >/ - Freeze/disable an account. unfreeze: >/ - Unfreeze/unblock an account. delete: >/ - Close a bank account. + change-owner: >/ - Change account owner. instrument: >/ - Create a payment instrument (bank card). whois: >/ - Get information about an account. baltop: >/ - Top balances leaderboard. @@ -664,6 +679,30 @@ messages: # → Choice placeholder for unpaid invoices. # See: https://docs.advntr.dev/minimessage/dynamic-replacements.html#insert-a-choice notify: (!) You have unpaid invoice. Click to view. + + # Account owner change requests + change-owner: + # You have received a request to become the new account owner + # Placeholders: + # - new owner UUID + # - new owner username. + # - command to accept the request + # - Account name. Defaults to owner username or ID if not set. + # - Account ID + # - Account type (Personal or Business) + # - Account owner username + # - Account balance without formatting, example: 123456.78 + # - Account balance with formatting, example: $123,456.78 + # - Account balance with formatting, example: $123k + request: (!) You have received a request to become the new owner of : Owned by '>> > ACCEPT + + # You have sent an account owner change request + # Same placeholders as details (except ) + sent: (!) You have sent a request to to become the new owner of account : Owned by '>. + + # You have accepted an account owner change request + # Same placeholders as details (except ) + accepted: >(!) You are now the new owner of : Owned by '>. Click to view account. # A new version of the plugin is available. # Placeholders: @@ -751,3 +790,16 @@ messages: async-failed: (!) The request failed. See the console for details. delete-vault-account: (!) You cannot delete this account. transfer-to-server-vault: (!) You cannot transfer funds to this account. This account is for internal use only. + + # Placeholders: + # → The maximum number of pending sent ownership change requests. + change-owner-limit-send: (!) You’ve reached the limit for pending ownership change requests. You can send up to at a time. + + # Placeholders: + # → The name of the current account owner. + change-owner-same: (!) The account is already owned by . + + # Change owner request not found + change-owner-not-found: (!) The account ownership change request was not found. + # Cannot accept change owner request + change-owner-accept-failed: (!) Failed to accept ownership of account. diff --git a/src/main/resources/db-init/mysql.sql b/src/main/resources/db-init/mysql.sql index 72058c0..bc2cfe5 100644 --- a/src/main/resources/db-init/mysql.sql +++ b/src/main/resources/db-init/mysql.sql @@ -66,3 +66,11 @@ ALTER TABLE `pos` ALTER TABLE `bank_invoices` CHANGE COLUMN `description` `description` VARCHAR(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL; + +CREATE TABLE IF NOT EXISTS `change_owner_requests` +( + `account` CHAR(16) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL, + `new_owner` CHAR(36) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL, + `created` DATETIME NOT NULL DEFAULT UTC_TIMESTAMP(), + KEY `id` (`account`, `new_owner`) +); diff --git a/src/main/resources/db-init/sql.sql b/src/main/resources/db-init/sql.sql index 6a36d7e..dff6fb8 100644 --- a/src/main/resources/db-init/sql.sql +++ b/src/main/resources/db-init/sql.sql @@ -92,3 +92,11 @@ DROP TABLE `pos`; ALTER TABLE `new_pos` RENAME TO `pos`; -- END OF `pos` MODIFICATION + +CREATE TABLE IF NOT EXISTS `change_owner_requests` +( + `account` CHAR(16) NOT NULL COLLATE BINARY, + `new_owner` CHAR(36) NOT NULL COLLATE BINARY, + `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`account`, `new_owner`) +);