diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/AdminModule.java b/src/main/java/com/froobworld/nabsuite/modules/admin/AdminModule.java index 3eb1897..a7039f4 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/AdminModule.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/AdminModule.java @@ -132,6 +132,7 @@ public void onDisable() { @Override public void postModulesEnable() { ticketManager.postStartup(); + discordStaffLog.postStartup(); } public AdminConfig getAdminConfig() { diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/chat/ProfanityFilter.java b/src/main/java/com/froobworld/nabsuite/modules/admin/chat/ProfanityFilter.java index ac32643..38f1f05 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/chat/ProfanityFilter.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/chat/ProfanityFilter.java @@ -38,6 +38,11 @@ public ProfanityFilter(AdminModule adminModule) { highlyOffensiveWords.add(Pattern.compile(expandPattern(offensiveWord), Pattern.CASE_INSENSITIVE)); } Bukkit.getPluginManager().registerEvents(this, adminModule.getPlugin()); + adminModule.getTicketManager().registerTicketType("profanity", (ticket, subject) -> Component + .text("Player ") + .append(subject.displayName()) + .append(Component.text(" - Highly offensive language")) + ); } public Component filter(Component component) { @@ -63,6 +68,8 @@ private void onPlayerChat(AsyncChatEvent event) { ); adminModule.getTicketManager().createSystemTicket( event.getPlayer().getLocation(), + event.getPlayer().getUniqueId(), + "profanity", String.format("Player %s was automatically muted for highly offensive language. Message was \"%s\".", event.getPlayer().getName(), plainTextMessage) ); } diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketAddNoteCommand.java b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketAddNoteCommand.java index ea4f4fa..3b7863b 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketAddNoteCommand.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketAddNoteCommand.java @@ -4,6 +4,7 @@ import cloud.commandframework.arguments.standard.StringArgument; import cloud.commandframework.context.CommandContext; import com.froobworld.nabsuite.command.NabCommand; +import com.froobworld.nabsuite.command.argument.predicate.ArgumentPredicate; import com.froobworld.nabsuite.modules.admin.AdminModule; import com.froobworld.nabsuite.modules.admin.command.argument.TicketArgument; import com.froobworld.nabsuite.modules.admin.ticket.Ticket; @@ -30,6 +31,7 @@ public void execute(CommandContext context) { Ticket ticket = context.get("ticket"); String message = context.get("message"); ticket.addNote(ConsoleUtils.getSenderUUID(context.getSender()), message); + adminModule.getDiscordStaffLog().updateTicketNotification(ticket); context.getSender().sendMessage(Component.text("Note added.", NamedTextColor.YELLOW)); } @@ -39,7 +41,12 @@ public Command.Builder populateBuilder(Command.Builder( true, "ticket", - adminModule.getTicketManager() + adminModule.getTicketManager(), + new ArgumentPredicate<>( + true, + (context, ticket) -> context.getSender().hasPermission(ticket.getPermission()), + "You don't have permission for that ticket" + ) )) .argument(StringArgument.newBuilder("message").greedy()); } diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketCloseCommand.java b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketCloseCommand.java index 59bae23..dacaead 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketCloseCommand.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketCloseCommand.java @@ -46,6 +46,11 @@ public Command.Builder populateBuilder(Command.Builder( + true, + (context, ticket) -> context.getSender().hasPermission(ticket.getPermission()), + "You don't have permission for that ticket" + ), new ArgumentPredicate<>( true, (context, ticket) -> ticket.isOpen(), diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketCommand.java b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketCommand.java index b73e7e5..73886d7 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketCommand.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketCommand.java @@ -19,7 +19,9 @@ public TicketCommand(AdminModule adminModule) { new TicketReadCommand(adminModule), new TicketTeleportCommand(adminModule), new TicketAddNoteCommand(adminModule), - new TicketCloseCommand(adminModule) + new TicketCloseCommand(adminModule), + new TicketDelegateCommand(adminModule), + new TicketEscalateCommand(adminModule) )); } diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketDelegateCommand.java b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketDelegateCommand.java new file mode 100644 index 0000000..1d64dab --- /dev/null +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketDelegateCommand.java @@ -0,0 +1,84 @@ +package com.froobworld.nabsuite.modules.admin.command; + +import cloud.commandframework.Command; +import cloud.commandframework.arguments.standard.StringArgument; +import cloud.commandframework.context.CommandContext; +import com.froobworld.nabsuite.command.NabCommand; +import com.froobworld.nabsuite.command.argument.predicate.ArgumentPredicate; +import com.froobworld.nabsuite.modules.admin.AdminModule; +import com.froobworld.nabsuite.modules.admin.command.argument.TicketArgument; +import com.froobworld.nabsuite.modules.admin.ticket.Ticket; +import com.froobworld.nabsuite.util.ConsoleUtils; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; + +import java.util.Optional; + +public class TicketDelegateCommand extends NabCommand { + private final AdminModule adminModule; + + public TicketDelegateCommand(AdminModule adminModule) { + super( + "delegate", + "Delegate a ticket with an optional note.", + "nabsuite.command.ticket.delegate", + CommandSender.class + ); + this.adminModule = adminModule; + } + + @Override + public void execute(CommandContext context) { + Ticket ticket = context.get("ticket"); + Optional note = context.getOptional("note"); + + String currentLevel = ticket.getLevel(); + int currentIndex = adminModule.getAdminConfig().ticketLevels.get().indexOf(currentLevel); + if (currentIndex > 0) { + ticket.setLevel(adminModule.getAdminConfig().ticketLevels.get().get(currentIndex - 1)); + ticket.addNote(ConsoleUtils.getSenderUUID(context.getSender()), "(Delegated ticket" + note.map(s -> " with note: '" + s + "'").orElse("") + ")"); + context.getSender().sendMessage(Component.text("Ticket delegated.", NamedTextColor.YELLOW)); + adminModule.getDiscordStaffLog().updateTicketNotification(ticket); + adminModule.getStaffTaskManager().notifyNewTask( + ticket.getPermission(), + // Only notify players that haven't seen the ticket before + player -> !player.hasPermission("nabsuite.ticket." + currentLevel) + ); + } else { + context.getSender().sendMessage(Component.text("Unable to determine level to delegate to.", NamedTextColor.RED)); + } + } + + @Override + public Command.Builder populateBuilder(Command.Builder builder) { + return builder + .argument(new TicketArgument<>( + true, + "ticket", + adminModule.getTicketManager(), + new ArgumentPredicate<>( + true, + (context, ticket) -> context.getSender().hasPermission(ticket.getPermission()), + "You don't have permission for that ticket" + ), + new ArgumentPredicate<>( + true, + (context, ticket) -> ticket.canDelegate(), + "Ticket cannot be delegated" + ), + new ArgumentPredicate<>( + true, + (context, ticket) -> ticket.isOpen(), + "That ticket is closed" + ) + )) + .argument(StringArgument.newBuilder("note").greedy().asOptional()); + } + + @Override + public String getUsage() { + return "/ticket delegate [note]"; + } + +} diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketEscalateCommand.java b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketEscalateCommand.java new file mode 100644 index 0000000..c35e792 --- /dev/null +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketEscalateCommand.java @@ -0,0 +1,80 @@ +package com.froobworld.nabsuite.modules.admin.command; + +import cloud.commandframework.Command; +import cloud.commandframework.arguments.standard.StringArgument; +import cloud.commandframework.context.CommandContext; +import com.froobworld.nabsuite.command.NabCommand; +import com.froobworld.nabsuite.command.argument.predicate.ArgumentPredicate; +import com.froobworld.nabsuite.modules.admin.AdminModule; +import com.froobworld.nabsuite.modules.admin.command.argument.TicketArgument; +import com.froobworld.nabsuite.modules.admin.ticket.Ticket; +import com.froobworld.nabsuite.util.ConsoleUtils; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; + +import java.util.Optional; + +public class TicketEscalateCommand extends NabCommand { + private final AdminModule adminModule; + + public TicketEscalateCommand(AdminModule adminModule) { + super( + "escalate", + "Escalate a ticket with an optional note.", + "nabsuite.command.ticket.escalate", + CommandSender.class + ); + this.adminModule = adminModule; + } + + @Override + public void execute(CommandContext context) { + Ticket ticket = context.get("ticket"); + Optional note = context.getOptional("note"); + + String currentLevel = ticket.getLevel(); + int currentIndex = adminModule.getAdminConfig().ticketLevels.get().indexOf(currentLevel); + if (currentIndex >= 0 && currentIndex < adminModule.getAdminConfig().ticketLevels.get().size() - 1) { + ticket.setLevel(adminModule.getAdminConfig().ticketLevels.get().get(currentIndex + 1)); + ticket.addNote(ConsoleUtils.getSenderUUID(context.getSender()), "(Escalated ticket" + note.map(s -> " with note: '" + s + "'").orElse("") + ")"); + context.getSender().sendMessage(Component.text("Ticket escalated.", NamedTextColor.YELLOW)); + adminModule.getDiscordStaffLog().updateTicketNotification(ticket); + adminModule.getStaffTaskManager().notifyNewTask(ticket.getPermission()); + } else { + context.getSender().sendMessage(Component.text("Unable to determine level to escalate to.", NamedTextColor.RED)); + } + } + + @Override + public Command.Builder populateBuilder(Command.Builder builder) { + return builder + .argument(new TicketArgument<>( + true, + "ticket", + adminModule.getTicketManager(), + new ArgumentPredicate<>( + true, + (context, ticket) -> context.getSender().hasPermission(ticket.getPermission()), + "You don't have permission for that ticket" + ), + new ArgumentPredicate<>( + true, + (context, ticket) -> ticket.canEscalate(), + "Ticket cannot be escalated" + ), + new ArgumentPredicate<>( + true, + (context, ticket) -> ticket.isOpen(), + "That ticket is closed" + ) + )) + .argument(StringArgument.newBuilder("note").greedy().asOptional()); + } + + @Override + public String getUsage() { + return "/ticket escalate [note]"; + } + +} diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketReadCommand.java b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketReadCommand.java index f97c024..938c7fe 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketReadCommand.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketReadCommand.java @@ -3,6 +3,7 @@ import cloud.commandframework.Command; import cloud.commandframework.context.CommandContext; import com.froobworld.nabsuite.command.NabCommand; +import com.froobworld.nabsuite.command.argument.predicate.ArgumentPredicate; import com.froobworld.nabsuite.modules.admin.AdminModule; import com.froobworld.nabsuite.modules.admin.command.argument.TicketArgument; import com.froobworld.nabsuite.modules.admin.ticket.Ticket; @@ -73,7 +74,12 @@ public Command.Builder populateBuilder(Command.Builder( true, "ticket", - adminModule.getTicketManager() + adminModule.getTicketManager(), + new ArgumentPredicate<>( + true, + (context, ticket) -> context.getSender().hasPermission(ticket.getPermission()), + "You don't have permission for that ticket" + ) )); } diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketTeleportCommand.java b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketTeleportCommand.java index 30b4c42..71e5698 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketTeleportCommand.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/command/TicketTeleportCommand.java @@ -8,13 +8,20 @@ import com.froobworld.nabsuite.modules.admin.command.argument.TicketArgument; import com.froobworld.nabsuite.modules.admin.ticket.Ticket; import com.froobworld.nabsuite.modules.basics.BasicsModule; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.time.Duration; +import java.util.UUID; + public class TicketTeleportCommand extends NabCommand { private final AdminModule adminModule; + private final Cache recentTeleports = CacheBuilder.newBuilder().expireAfterAccess(Duration.ofMinutes(5)).build(); public TicketTeleportCommand(AdminModule adminModule) { super( @@ -30,8 +37,19 @@ public TicketTeleportCommand(AdminModule adminModule) { public void execute(CommandContext context) { Player player = (Player) context.getSender(); Ticket ticket = context.get("ticket"); + UUID recentTeleport = recentTeleports.getIfPresent(ticket.getId()); + boolean coordinateWarning = ticket.isOpen() && recentTeleport != null && recentTeleport != player.getUniqueId(); + if (recentTeleport == null) { + recentTeleports.put(ticket.getId(), player.getUniqueId()); + } + adminModule.getPlugin().getModule(BasicsModule.class).getPlayerTeleporter().teleportAsync(player, ticket.getLocation()).thenRun(() -> { player.sendMessage(Component.text("Whoosh!", NamedTextColor.YELLOW)); + + if (coordinateWarning && Bukkit.getPlayer(recentTeleport) instanceof Player other) { + player.sendMessage(other.displayName() + .append(Component.text(" recently teleported to this ticket, please coordinate review.").color(NamedTextColor.YELLOW))); + } }); } @@ -42,6 +60,11 @@ public Command.Builder populateBuilder(Command.Builder( + true, + (context, ticket) -> context.getSender().hasPermission(ticket.getPermission()), + "You don't have permission for that ticket" + ), new ArgumentPredicate<>( true, (sender, ticket) -> ticket.getLocation() != null, diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/config/AdminConfig.java b/src/main/java/com/froobworld/nabsuite/modules/admin/config/AdminConfig.java index 2d17bb7..1bf6ad3 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/config/AdminConfig.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/config/AdminConfig.java @@ -16,7 +16,7 @@ import java.util.List; public class AdminConfig extends NabConfiguration { - private static final int CONFIG_VERSION = 4; + private static final int CONFIG_VERSION = 5; public AdminConfig(AdminModule adminModule) { super( @@ -76,5 +76,19 @@ public static class DeputySettings extends ConfigSection { public final ConfigEntry expiryNotificationTime = new ConfigEntry<>(duration -> DurationParser.fromString(duration.toString())); } + @Entry(key = "ticket-levels") + public final ConfigEntry> ticketLevels = ConfigEntries.stringListEntry(); + + @SectionMap(key = "ticket-types", defaultKey = "default") + public ConfigSectionMap ticketTypes = new ConfigSectionMap<>(s -> s, TicketType.class, true); + + public static class TicketType extends ConfigSection { + + @Entry(key = "level") + public final ConfigEntry level = new ConfigEntry<>(); + + @Entry(key = "allow-delegate") + public final ConfigEntry allowDelegate = new ConfigEntry<>(); + } } diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/deputy/DeputyManager.java b/src/main/java/com/froobworld/nabsuite/modules/admin/deputy/DeputyManager.java index fa8b4af..cdb99f3 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/deputy/DeputyManager.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/deputy/DeputyManager.java @@ -5,6 +5,7 @@ import com.froobworld.nabsuite.modules.admin.AdminModule; import com.froobworld.nabsuite.modules.basics.BasicsModule; import com.froobworld.nabsuite.util.DurationDisplayer; +import net.kyori.adventure.text.Component; import net.luckperms.api.LuckPerms; import net.luckperms.api.cacheddata.Result; import net.luckperms.api.event.node.NodeMutateEvent; @@ -46,6 +47,13 @@ public DeputyManager(AdminModule adminModule) { updateAllDeputies(); Bukkit.getScheduler().scheduleSyncRepeatingTask(adminModule.getPlugin(), this::runExpiryCheckTask, 200, 600); } + levels.forEach(level -> adminModule.getTicketManager().registerTicketType( + "deputy-expiry-"+level.getName(), + (ticket,subject) -> Component + .text("Player ") + .append(subject.displayName()) + .append(Component.text(" - "+level.getName()+" deputy expiry")) + )); } private void sendDeputyAddNotification(CommandSender sender, DeputyPlayer previous, DeputyPlayer current, long duration) { @@ -76,6 +84,8 @@ private void sendDeputyExpiryWarning(DeputyPlayer deputyPlayer) { long expiryTime = Math.ceilDiv(deputyPlayer.getExpiry() - System.currentTimeMillis(), 3600000) * 3600000; String duration = DurationDisplayer.asDurationString(expiryTime); adminModule.getTicketManager().createSystemTicket( + deputyPlayer.getUuid(), + "deputy-expiry-"+deputyPlayer.getDeputyLevel().getName(), "Appointment of " + deputyPlayer.getPlayerIdentity().getLastName() + " as " + deputyPlayer.getDeputyLevel().getName() + " deputy expires in less than " + duration + ". Please determine if it should be renewed, if another deputy should be appointed, or if no action is needed." ); basicsModule.getMailCentre().sendSystemMail(deputyPlayer.getUuid(), "Your appointment as " + deputyPlayer.getDeputyLevel().getName() + " deputy will expire in less than " + duration + "."); diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/notification/DiscordStaffLog.java b/src/main/java/com/froobworld/nabsuite/modules/admin/notification/DiscordStaffLog.java index 231ee1f..28e928d 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/notification/DiscordStaffLog.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/notification/DiscordStaffLog.java @@ -1,12 +1,12 @@ package com.froobworld.nabsuite.modules.admin.notification; -import com.froobworld.nabsuite.data.identity.PlayerIdentity; import com.froobworld.nabsuite.modules.admin.AdminModule; import com.froobworld.nabsuite.modules.admin.deputy.DeputyPlayer; import com.froobworld.nabsuite.modules.admin.note.PlayerNote; import com.froobworld.nabsuite.modules.admin.punishment.PunishmentLogItem; import com.froobworld.nabsuite.modules.admin.ticket.Ticket; import com.froobworld.nabsuite.modules.discord.DiscordModule; +import com.froobworld.nabsuite.modules.discord.bot.command.DiscordCommandSender; import com.froobworld.nabsuite.modules.discord.utils.DiscordUtils; import com.froobworld.nabsuite.modules.protect.area.Area; import com.froobworld.nabsuite.util.ConsoleUtils; @@ -14,11 +14,28 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.ItemComponent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.text.TextInput; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; +import net.dv8tion.jda.api.interactions.modals.Modal; +import net.dv8tion.jda.api.interactions.modals.ModalMapping; +import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; +import net.dv8tion.jda.api.utils.messages.MessageEditBuilder; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.OfflinePlayer; -import java.awt.*; +import java.awt.Color; +import java.util.LinkedList; +import java.util.List; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; public class DiscordStaffLog { private final AdminModule adminModule; @@ -29,6 +46,13 @@ public DiscordStaffLog(AdminModule adminModule) { this.discordModule = adminModule.getPlugin().getModule(DiscordModule.class); } + public void postStartup() { + if (this.discordModule != null) { + this.discordModule.getDiscordBot().addButtonListener("stafflog-ticket", this::showTicketModalWithMessage); + this.discordModule.getDiscordBot().addModalListener("stafflog-ticket", this::handleTicketModal); + } + } + public void sendPunishmentLogItemNotification(PunishmentLogItem punishmentLogItem) { if (discordModule == null) { return; @@ -69,26 +93,68 @@ public void sendTicketCreationNotification(Ticket ticket) { if (discordModule == null) { return; } + TextChannel channel = discordModule.getDiscordBot().getStaffLogChannel(); + if (channel != null) { + channel.sendMessage(buildTicketNotification(ticket)) + .onSuccess(result -> Bukkit.getScheduler().runTask(adminModule.getPlugin(), () -> ticket.setStaffLogId(result.getIdLong()))) + .queue(); + } + } + public void updateTicketNotification(Ticket ticket) { + if (discordModule == null || ticket.getStaffLogId() == null) { + return; + } TextChannel channel = discordModule.getDiscordBot().getStaffLogChannel(); if (channel != null) { - String creator = ConsoleUtils.CONSOLE_UUID.equals(ticket.getCreator()) ? "System generated" : adminModule.getPlugin().getPlayerIdentityManager().getPlayerIdentity(ticket.getCreator()).getLastName(); + channel.editMessageById(ticket.getStaffLogId(), MessageEditBuilder.fromCreateData(buildTicketNotification(ticket)).build()).queue(); + } + } - EmbedBuilder embedBuilder = new EmbedBuilder().setTitle("Ticket opened") - .setColor(Color.CYAN) - .setThumbnail(getSkinUrl(ticket.getCreator())) - .addField("Creator", DiscordUtils.escapeMarkdown(creator), true) - .addField("Id", ticket.getId() + "", true) - .addField("Message", DiscordUtils.escapeMarkdown(ticket.getMessage()), true); + private MessageCreateData buildTicketNotification(Ticket ticket) { + String creator = ConsoleUtils.CONSOLE_UUID.equals(ticket.getCreator()) ? "System generated" : adminModule.getPlugin().getPlayerIdentityManager().getPlayerIdentity(ticket.getCreator()).getLastName(); + + String notes = ticket.getNotes().stream().map( + note -> "**" + + DiscordUtils.escapeMarkdown(ConsoleUtils.CONSOLE_UUID.equals(note.getCreator()) ? "Console" : adminModule.getPlugin().getPlayerIdentityManager().getPlayerIdentity(note.getCreator()).getLastName()) + + "**: " + + DiscordUtils.escapeMarkdown(note.getMessage()) + ).collect(Collectors.joining("\n")); + EmbedBuilder embedBuilder = new EmbedBuilder().setTitle("Ticket opened") + .setColor(Color.CYAN) + .setThumbnail(getSkinUrl(ticket.getCreator())) + .addField("Creator", DiscordUtils.escapeMarkdown(creator), true) + .addField("Id", ticket.getId() + "", true) + .addField("Message", DiscordUtils.escapeMarkdown(ticket.getMessage()), true); + if (!notes.isBlank()) { + embedBuilder.addField("Notes", notes, false); + } - channel.sendMessageEmbeds(embedBuilder.build()).queue(); + MessageCreateBuilder message = new MessageCreateBuilder(); + message.addEmbeds(embedBuilder.build()); + + if (ticket.isOpen()) { + List buttons = new LinkedList<>(); + if (ticket.canDelegate()) { + buttons.add(Button.primary("stafflog-ticket:delegate:" + ticket.getId(), "Delegate")); + } + /* Might get busy/confusing with too many actions? Escalate will probably not be used a lot + if (ticket.canEscalate()) { + buttons.add(Button.primary("stafflog-ticket:escalate:" + ticket.getId(), "Escalate")); + }*/ + buttons.add(Button.secondary("stafflog-ticket:addnote:" + ticket.getId(), "Add Note")); + buttons.add(Button.danger("stafflog-ticket:close:" + ticket.getId(), "Close")); + message.addComponents(ActionRow.of(buttons)); } + + return message.build(); } public void sendTicketClosureNotification(Ticket ticket, CommandSender resolver, String closureMessage) { if (discordModule == null) { return; } + updateTicketNotification(ticket); TextChannel channel = discordModule.getDiscordBot().getStaffLogChannel(); if (channel != null) { @@ -229,4 +295,74 @@ private static String getSkinUrl(UUID uuid) { return DiscordUtils.getAvatarUrl(uuid, 64); } + private void handleTicketModal(DiscordCommandSender sender, ModalInteractionEvent event) { + try { + String[] parts = event.getModalId().split(":"); + String action = parts.length > 1 ? parts[1].replaceAll("[^a-zA-Z0-9]", "") : ""; + int id = parts.length > 2 ? Integer.parseInt(parts[2]) : -1; + if (id <= 0) { + throw new NumberFormatException(); + } + ModalMapping messageMapping = event.getValue("message"); + String message = messageMapping != null ? + messageMapping.getAsString().replaceAll("[\r\n]", " ") : + ""; + event.deferReply(true).queue(hook -> { + sender.setHook(hook); + Bukkit.getScheduler().runTask(adminModule.getPlugin(), () -> { + try { + Ticket ticket = adminModule.getTicketManager().getTicket(id); + if (ticket == null || !ticket.isOpen()) { + sender.replyError("Ticket not found or already closed"); + return; + } + + String command = "ticket "+action+" " + ticket.getId() + " " + message; + adminModule.getPlugin().getSLF4JLogger().info( + "DiscordStaffLog: {} issued server command: /{}", + sender.getName(), + command + ); + Bukkit.dispatchCommand(sender, command); + } catch (Throwable e) { + sender.replyError(e.getMessage()); + } + }); + }); + + } catch (NumberFormatException e) { + event.reply("Invalid ticket id") + .setEphemeral(true) + .queue(hook -> hook.deleteOriginal().queueAfter(10, TimeUnit.SECONDS)); + } + + } + + private void showTicketModalWithMessage(DiscordCommandSender sender, ButtonInteractionEvent event) { + try { + String[] parts = event.getComponentId().split(":"); + String action = parts.length > 1 ? parts[1] : ""; + event.replyModal(Modal.create(event.getComponentId(), switch(action) { + case "addnote" -> "Add Note to Ticket"; + case "close" -> "Close Ticket"; + case "delegate" -> "Delegate Ticket"; + case "escalate" -> "Escalate Ticket"; + default -> throw new IllegalArgumentException(); + }) + .addComponents(ActionRow.of( + TextInput.create("message", "Message", TextInputStyle.PARAGRAPH) + .setRequired(action.equals("addnote") || action.equals("close")) + .setMaxLength(255) + .build() + )) + .build() + ).queue(); + + } catch (Exception e) { + event.reply("Invalid action or ticket id") + .setEphemeral(true) + .queue(hook -> hook.deleteOriginal().queueAfter(10, TimeUnit.SECONDS)); + } + } + } diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/suspicious/SuspiciousActivityMonitor.java b/src/main/java/com/froobworld/nabsuite/modules/admin/suspicious/SuspiciousActivityMonitor.java index 015f5a7..6525133 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/suspicious/SuspiciousActivityMonitor.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/suspicious/SuspiciousActivityMonitor.java @@ -6,6 +6,7 @@ import com.froobworld.nabsuite.modules.admin.suspicious.monitors.GriefMonitor; import com.froobworld.nabsuite.modules.admin.suspicious.monitors.LavaCastMonitor; import com.froobworld.nabsuite.modules.admin.suspicious.monitors.TheftMonitor; +import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.NamespacedKey; import org.bukkit.Statistic; @@ -30,6 +31,11 @@ public SuspiciousActivityMonitor(AdminModule adminModule) { new GriefMonitor(adminModule) ); this.adminModule = adminModule; + adminModule.getTicketManager().registerTicketType("suspicion", (ticket, subject) -> Component + .text("Player ") + .append(subject.displayName()) + .append(Component.text(" - Suspicious activity")) + ); Bukkit.getScheduler().scheduleSyncRepeatingTask(adminModule.getPlugin(), this::checkAllPlayers, 1200, 1200); } @@ -43,6 +49,8 @@ private void checkAllPlayers() { } adminModule.getTicketManager().createSystemTicket( player.getLocation(), + player.getUniqueId(), + "suspicion", "Player " + player.getName() + " has suspicious activity that could indicate they are breaking the rules. Please investigate.\n\n" + getSuspicionSummary(player) ); diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/tasks/StaffTaskManager.java b/src/main/java/com/froobworld/nabsuite/modules/admin/tasks/StaffTaskManager.java index 1e0ec5d..ef369f9 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/tasks/StaffTaskManager.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/tasks/StaffTaskManager.java @@ -13,7 +13,9 @@ import org.bukkit.event.player.PlayerJoinEvent; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -55,13 +57,16 @@ public List getStaffTasks(CommandSender sender) { .collect(Collectors.toList()); } - public void notifyNewTask(String permission) { + @SafeVarargs + public final void notifyNewTask(String permission, Predicate... playerPredicates) { for (Player player : Bukkit.getOnlinePlayers()) { if (player.hasPermission(permission)) { - player.sendMessage( - Component.text("There is a new staff task requiring action (/stafftasks).", NamedTextColor.YELLOW) - .clickEvent(ClickEvent.runCommand("/stafftasks")) - ); + if (playerPredicates.length == 0 || Arrays.stream(playerPredicates).allMatch(predicate -> predicate.test(player))) { + player.sendMessage( + Component.text("There is a new staff task requiring action (/stafftasks).", NamedTextColor.YELLOW) + .clickEvent(ClickEvent.runCommand("/stafftasks")) + ); + } } } } diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/ticket/Ticket.java b/src/main/java/com/froobworld/nabsuite/modules/admin/ticket/Ticket.java index 918ea12..e39a3d0 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/ticket/Ticket.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/ticket/Ticket.java @@ -2,7 +2,9 @@ import com.froobworld.nabsuite.data.SchemaEntries; import com.froobworld.nabsuite.data.SimpleDataSchema; +import com.froobworld.nabsuite.modules.admin.config.AdminConfig; import com.google.gson.stream.JsonReader; +import net.kyori.adventure.text.Component; import org.bukkit.Location; import java.io.IOException; @@ -24,6 +26,22 @@ public class Ticket { ticket -> ticket.creator, (ticket, creator) -> ticket.creator = creator )) + .addField("subject", SchemaEntries.uuidEntry( + ticket -> ticket.subject, + (ticket, subject) -> ticket.subject = subject + )) + .addField("type", SchemaEntries.stringEntry( + ticket -> ticket.type, + (ticket, type) -> ticket.type = type + )) + .addField("level", SchemaEntries.stringEntry( + ticket -> ticket.level, + (ticket, level) -> ticket.level = level + )) + .addField("staffLogId", SchemaEntries.longEntry( + ticket -> ticket.staffLogId, + (ticket, staffLogId) -> ticket.staffLogId = staffLogId + )) .addField("location", SchemaEntries.locationEntry( ticket -> ticket.location, (ticket, location) -> ticket.location = location @@ -47,6 +65,10 @@ public class Ticket { private int id; private long timestamp; private UUID creator; + private UUID subject; + private String type; + private String level; + private Long staffLogId; private Location location; private String message; private boolean open; @@ -67,6 +89,20 @@ private Ticket(TicketManager ticketManager) { this.notes = new ArrayList<>(); } + Ticket(TicketManager ticketManager, int id, UUID creator, UUID subject, Location location, String type, String message) { + this.ticketManager = ticketManager; + this.id = id; + this.timestamp = System.currentTimeMillis(); + this.creator = creator; + this.subject = subject; + this.location = location; + this.type = type; + this.level = getTypeSettings().level.get(); + this.message = message; + this.open = true; + this.notes = new ArrayList<>(); + } + public int getId() { return id; } @@ -79,10 +115,36 @@ public UUID getCreator() { return creator; } + public UUID getSubject() { + return subject; + } + public Location getLocation() { return location; } + public String getType() { + return type; + } + + public String getLevel() { + return level == null ? "default" : level; + } + + public void setLevel(String level) { + this.level = level; + ticketManager.ticketSaver.scheduleSave(this); + } + + public Long getStaffLogId() { + return staffLogId; + } + + public void setStaffLogId(Long staffLogId) { + this.staffLogId = staffLogId; + ticketManager.ticketSaver.scheduleSave(this); + } + public String getMessage() { return message; } @@ -108,6 +170,33 @@ public void close(UUID closer, String message) { } } + public Boolean canDelegate() { + return isOpen() && + getTypeSettings().allowDelegate.get() && + ticketManager.getAdminModule().getAdminConfig().ticketLevels.get().indexOf(getLevel()) > 0 ; + } + + public Boolean canEscalate() { + if (!isOpen()) { + return false; + } + int index = ticketManager.getAdminModule().getAdminConfig().ticketLevels.get().indexOf(getLevel()); + int size = ticketManager.getAdminModule().getAdminConfig().ticketLevels.get().size(); + return index >= 0 && index < (size - 1); + } + + public Component getSummary() { + return ticketManager.getTicketSummary(this); + } + + public String getPermission() { + return "nabsuite.ticket." + getLevel(); + } + + private AdminConfig.TicketType getTypeSettings() { + return ticketManager.getAdminModule().getAdminConfig().ticketTypes.of(type == null ? "default" : type); + } + public static Ticket fromJsonString(TicketManager ticketManager, String jsonString) { Ticket ticket = new Ticket(ticketManager); try { diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/ticket/TicketManager.java b/src/main/java/com/froobworld/nabsuite/modules/admin/ticket/TicketManager.java index bbb42a0..8dd6d48 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/ticket/TicketManager.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/ticket/TicketManager.java @@ -2,25 +2,29 @@ import com.froobworld.nabsuite.data.DataLoader; import com.froobworld.nabsuite.data.DataSaver; +import com.froobworld.nabsuite.data.identity.PlayerIdentity; import com.froobworld.nabsuite.modules.admin.AdminModule; import com.froobworld.nabsuite.modules.admin.tasks.StaffTask; import com.froobworld.nabsuite.util.ConsoleUtils; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.event.ClickEvent; import org.bukkit.Location; import org.bukkit.entity.Player; import java.io.File; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.regex.Pattern; -import java.util.stream.Collectors; public class TicketManager { private static final Pattern fileNamePattern = Pattern.compile("^[0-9]+\\.json$"); @@ -30,6 +34,8 @@ public class TicketManager { private final File directory; private final AtomicInteger idSupplier; + private final Map> typeSummaryProvider = new HashMap<>(); + public TicketManager(AdminModule adminModule) { this.adminModule = adminModule; directory = new File(adminModule.getDataFolder(), "tickets/"); @@ -52,19 +58,26 @@ public TicketManager(AdminModule adminModule) { idSupplier = new AtomicInteger(largestExistingId + 1); } + public void registerTicketType(String type, BiFunction typeSummaryProvider) { + this.typeSummaryProvider.put(type, typeSummaryProvider); + } + public void postStartup() { Supplier> openTicketTaskSupplier = () -> { List tasks = new ArrayList<>(); - List openTicketIds = ticketMap.entrySet().stream() - .filter(entry -> entry.getValue().isOpen()) - .map(Map.Entry::getKey) - .sorted() + List openTickets = ticketMap.values().stream() + .filter(Ticket::isOpen) + // Sort by highest level and lowest id + .sorted((a,b) -> !a.getLevel().equals(b.getLevel()) ? + adminModule.getAdminConfig().ticketLevels.get().indexOf(b.getLevel()) - adminModule.getAdminConfig().ticketLevels.get().indexOf(a.getLevel()) : + a.getId() - b.getId() + ) .toList(); - for (int id : openTicketIds) { + for (Ticket ticket : openTickets) { StaffTask task = new StaffTask( - "nabsuite.command.ticket", - Component.text("Ticket with id '" + id + "' needs resolving (/ticket).") - .clickEvent(ClickEvent.suggestCommand("/ticket read " + id)) + ticket.getPermission(), + ticket.getSummary() + .clickEvent(ClickEvent.suggestCommand("/ticket read " + ticket.getId())) ); tasks.add(task); } @@ -78,25 +91,25 @@ public void shutdown() { } public Ticket createTicket(Player player, String message) { - Ticket ticket = new Ticket(this, idSupplier.getAndIncrement(), player.getUniqueId(), player.getLocation(), message); + Ticket ticket = new Ticket(this, idSupplier.getAndIncrement(), player.getUniqueId(), null, player.getLocation(), "modreq", message); ticketMap.put(ticket.getId(), ticket); ticketSaver.scheduleSave(ticket); - adminModule.getStaffTaskManager().notifyNewTask("nabsuite.command.ticket"); + adminModule.getStaffTaskManager().notifyNewTask(ticket.getPermission()); adminModule.getDiscordStaffLog().sendTicketCreationNotification(ticket); return ticket; } - public Ticket createSystemTicket(Location location, String message) { - Ticket ticket = new Ticket(this, idSupplier.getAndIncrement(), ConsoleUtils.CONSOLE_UUID, location, message); + public Ticket createSystemTicket(Location location, UUID subject, String type, String message) { + Ticket ticket = new Ticket(this, idSupplier.getAndIncrement(), ConsoleUtils.CONSOLE_UUID, subject, location, type, message); ticketMap.put(ticket.getId(), ticket); ticketSaver.scheduleSave(ticket); - adminModule.getStaffTaskManager().notifyNewTask("nabsuite.command.ticket"); + adminModule.getStaffTaskManager().notifyNewTask(ticket.getPermission()); adminModule.getDiscordStaffLog().sendTicketCreationNotification(ticket); return ticket; } - public Ticket createSystemTicket(String message) { - return createSystemTicket(null, message); + public Ticket createSystemTicket(UUID subject, String type, String message) { + return createSystemTicket(null, subject, type, message); } public Ticket getTicket(int id) { @@ -107,4 +120,37 @@ public Set getTickets() { return ticketMap.values(); } + protected AdminModule getAdminModule() { + return adminModule; + } + + protected Component getTicketSummary(Ticket ticket) { + + TextComponent.Builder summary = Component.text(); + summary.append(Component.text("Ticket #"+ticket.getId()+" ")); + + if (!ticket.getLevel().equals("default")) { + summary.append(Component.text("[" + ticket.getLevel() + "] ")); + } + + if (ticket.getType() != null && typeSummaryProvider.containsKey(ticket.getType())) { + PlayerIdentity subject = ticket.getSubject() != null ? adminModule.getPlugin().getPlayerIdentityManager().getPlayerIdentity(ticket.getSubject()) : null; + summary.append(typeSummaryProvider.get(ticket.getType()).apply(ticket, subject)); + } else { + if (ticket.getCreator() != null && !ticket.getCreator().equals(ConsoleUtils.CONSOLE_UUID)) { + PlayerIdentity creator = adminModule.getPlugin().getPlayerIdentityManager().getPlayerIdentity(ticket.getCreator()); + summary.append(Component.text("by ")) + .append(creator.displayName()); + } + summary.append(Component.text(" - \"")); + if (ticket.getMessage().length() > 18) { + summary.append(Component.text(ticket.getMessage().substring(0, 15) + "...\"")); + } else { + summary.append(Component.text(ticket.getMessage()+"\"")); + } + } + + return summary.build(); + } + } diff --git a/src/main/java/com/froobworld/nabsuite/modules/admin/xray/XrayMonitor.java b/src/main/java/com/froobworld/nabsuite/modules/admin/xray/XrayMonitor.java index c8f56f2..ce4a8e9 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/admin/xray/XrayMonitor.java +++ b/src/main/java/com/froobworld/nabsuite/modules/admin/xray/XrayMonitor.java @@ -38,6 +38,11 @@ public class XrayMonitor implements Listener { public XrayMonitor(AdminModule adminModule) { this.adminModule = adminModule; this.diamondVeinTracker = new VeinTracker(); + adminModule.getTicketManager().registerTicketType("xray", (ticket, subject) -> Component + .text("Player ") + .append(subject.displayName()) + .append(Component.text(" - Investigate x-ray")) + ); Bukkit.getPluginManager().registerEvents(this, adminModule.getPlugin()); } @@ -58,6 +63,8 @@ private void onBlockBreak(BlockBreakEvent event) { if (marked) { adminModule.getTicketManager().createSystemTicket( event.getPlayer().getLocation(), + event.getPlayer().getUniqueId(), + "xray", "Player " + event.getPlayer().getName() + " has suspicious mining activity. Please investigate if they have been x-raying." + " They may have just been cave mining or gotten lucky." ); diff --git a/src/main/java/com/froobworld/nabsuite/modules/discord/bot/DiscordBot.java b/src/main/java/com/froobworld/nabsuite/modules/discord/bot/DiscordBot.java index 6944e4a..a3b8fa0 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/discord/bot/DiscordBot.java +++ b/src/main/java/com/froobworld/nabsuite/modules/discord/bot/DiscordBot.java @@ -3,6 +3,7 @@ import com.froobworld.nabsuite.modules.discord.DiscordModule; import com.froobworld.nabsuite.modules.discord.bot.command.DiscordCommandBridge; import com.froobworld.nabsuite.modules.discord.bot.chat.ChatBridge; +import com.froobworld.nabsuite.modules.discord.bot.command.DiscordCommandSender; import com.froobworld.nabsuite.modules.discord.bot.linking.AccountLinkManager; import com.froobworld.nabsuite.modules.discord.bot.syncer.DiscordSyncer; import net.dv8tion.jda.api.JDA; @@ -12,11 +13,17 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.Webhook; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.requests.GatewayIntent; import org.bukkit.Bukkit; import javax.security.auth.login.LoginException; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; public class DiscordBot { private final DiscordModule discordModule; @@ -26,6 +33,8 @@ public class DiscordBot { private DiscordCommandBridge commandBridge; private JDA jda; private Webhook chatWebhook; + private final Map> buttonHandlers = new HashMap<>(); + private final Map> modalHandlers = new HashMap<>(); public DiscordBot(DiscordModule discordModule) throws LoginException { this.discordModule = discordModule; @@ -132,4 +141,40 @@ public TextChannel getStaffLogChannel() { return jda.getTextChannelById(discordModule.getDiscordConfig().channels.staffLog.get()); } + public void addButtonListener(String action, BiConsumer listener) { + this.buttonHandlers.put(action, listener); + } + public void addButtonListener(String action, String permission, BiConsumer listener) { + addButtonListener(action, (sender, event) -> { + if (permission == null || permission.isEmpty() || sender.hasPermission(permission)) { + listener.accept(sender, event); + } else { + event.reply("You lack permission to execute this action") + .setEphemeral(true) + .queue(msg -> msg.deleteOriginal().queueAfter(10, TimeUnit.SECONDS)); + } + }); + this.buttonHandlers.put(action, listener); + } + public void addModalListener(String action, BiConsumer listener) { + this.modalHandlers.put(action, listener); + } + public void addModalListener(String action, String permission, BiConsumer listener) { + addModalListener(action, (sender, event) -> { + if (permission == null || permission.isEmpty() || sender.hasPermission(permission)) { + listener.accept(sender, event); + } else { + event.reply("You lack permission to execute this action") + .setEphemeral(true) + .queue(msg -> msg.deleteOriginal().queueAfter(10, TimeUnit.SECONDS)); + } + }); + } + public BiConsumer getButtonHandler(String action) { + return this.buttonHandlers.get(action); + } + public BiConsumer getModalHandler(String action) { + return this.modalHandlers.get(action); + } + } diff --git a/src/main/java/com/froobworld/nabsuite/modules/discord/bot/command/DiscordCommandBridge.java b/src/main/java/com/froobworld/nabsuite/modules/discord/bot/command/DiscordCommandBridge.java index b43c70b..c0d68b9 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/discord/bot/command/DiscordCommandBridge.java +++ b/src/main/java/com/froobworld/nabsuite/modules/discord/bot/command/DiscordCommandBridge.java @@ -5,12 +5,16 @@ import com.froobworld.nabsuite.data.identity.PlayerIdentity; import com.froobworld.nabsuite.modules.admin.AdminModule; import com.froobworld.nabsuite.modules.discord.DiscordModule; +import com.froobworld.nabsuite.modules.discord.bot.DiscordBot; import com.froobworld.nabsuite.modules.discord.bot.linking.AccountLinkManager; import com.froobworld.nabsuite.modules.discord.config.DiscordConfig; import net.dv8tion.jda.api.entities.ApplicationInfo; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import net.dv8tion.jda.api.interactions.commands.Command; import org.bukkit.Bukkit; import org.bukkit.command.CommandMap; @@ -19,6 +23,8 @@ import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.stream.Collectors; public class DiscordCommandBridge extends ListenerAdapter { @@ -198,4 +204,43 @@ public void onCommandAutoCompleteInteraction(@NotNull CommandAutoCompleteInterac Bukkit.getScheduler().runTask(discordModule.getPlugin(), () -> command.autocomplete(sender, event)); } + private void callInteractionHandler(String id, Function> supplier, T event) { + String action = id; + if (id.contains(":")) { + String[] parts = id.split(":", 2); + action = parts[0]; + } + BiConsumer consumer = supplier.apply(action); + if (consumer == null) { + event.reply("Unknown action: " + action) + .setEphemeral(true) + .queue(msg -> msg.deleteOriginal().queueAfter(10, TimeUnit.SECONDS)); + } else { + PlayerIdentity player = accountLinkManager.getLinkedMinecraftAccount(event.getUser()); + if (player == null) { + event.reply("User is not linked") + .setEphemeral(true) + .queue(msg -> msg.deleteOriginal().queueAfter(10, TimeUnit.SECONDS)); + } else if (adminModule != null && adminModule.getPunishmentManager().getBanEnforcer().testBan(player.getUuid()) != null) { + event.reply("You cannot execute commands while banned") + .setEphemeral(true) + .queue(msg -> msg.deleteOriginal().queueAfter(10, TimeUnit.SECONDS)); + } else { + DiscordCommandSender sender = new DiscordCommandSender(discordModule.getPlugin(), player); + consumer.accept(sender, event); + } + } + } + + @Override + public void onButtonInteraction(@NotNull ButtonInteractionEvent event) { + DiscordBot bot = discordModule.getDiscordBot(); + callInteractionHandler(event.getComponentId(), bot::getButtonHandler, event); + } + + @Override + public void onModalInteraction(@NotNull ModalInteractionEvent event) { + DiscordBot bot = discordModule.getDiscordBot(); + callInteractionHandler(event.getModalId(), bot::getModalHandler, event); + } } diff --git a/src/main/java/com/froobworld/nabsuite/modules/discord/bot/command/DiscordCommandSender.java b/src/main/java/com/froobworld/nabsuite/modules/discord/bot/command/DiscordCommandSender.java index 94066c6..f0d7940 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/discord/bot/command/DiscordCommandSender.java +++ b/src/main/java/com/froobworld/nabsuite/modules/discord/bot/command/DiscordCommandSender.java @@ -24,15 +24,21 @@ public class DiscordCommandSender extends OfflineCommandSender { public DiscordCommandSender(NabSuite plugin, PlayerIdentity player, InteractionHook event) { this(plugin, player); - this.hook = event; - this.replyBuffer = new StringBuilder(); - replyBuffer.append("```ansi\n"); + setHook(event); } public DiscordCommandSender(NabSuite plugin, PlayerIdentity player) { super(plugin, player); } + public void setHook(InteractionHook hook) { + this.hook = hook; + if (this.replyBuffer == null) { + this.replyBuffer = new StringBuilder(); + replyBuffer.append("```ansi\n"); + } + } + @Override public void sendMessage(@NotNull String message) { sendMessage(LegacyComponentSerializer.legacySection().deserialize(message)); diff --git a/src/main/java/com/froobworld/nabsuite/modules/protect/command/AreaTeleportCommand.java b/src/main/java/com/froobworld/nabsuite/modules/protect/command/AreaTeleportCommand.java index 88d0413..2cb5f8d 100644 --- a/src/main/java/com/froobworld/nabsuite/modules/protect/command/AreaTeleportCommand.java +++ b/src/main/java/com/froobworld/nabsuite/modules/protect/command/AreaTeleportCommand.java @@ -3,22 +3,27 @@ import cloud.commandframework.Command; import cloud.commandframework.context.CommandContext; import com.froobworld.nabsuite.command.NabCommand; -import com.froobworld.nabsuite.command.argument.predicate.ArgumentPredicate; import com.froobworld.nabsuite.modules.basics.BasicsModule; import com.froobworld.nabsuite.modules.protect.ProtectModule; import com.froobworld.nabsuite.modules.protect.area.Area; import com.froobworld.nabsuite.modules.protect.command.argument.AreaArgument; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.time.Duration; +import java.util.UUID; import java.util.function.Consumer; public class AreaTeleportCommand extends NabCommand { private final ProtectModule protectModule; + private final Cache recentTeleports = CacheBuilder.newBuilder().expireAfterAccess(Duration.ofMinutes(5)).build(); public AreaTeleportCommand(ProtectModule protectModule) { super( @@ -39,11 +44,22 @@ public void execute(CommandContext context) { int zCentre = (area.getCorner1().getBlockZ() + area.getCorner2().getBlockZ()) / 2; Location location = new Location(area.getWorld(), xCentre + 0.5, 0, zCentre + 0.5); + UUID recentTeleport = recentTeleports.getIfPresent(area.getName()); + boolean coordinateWarning = !area.isApproved() && recentTeleport != null && recentTeleport != player.getUniqueId(); + if (recentTeleport == null) { + recentTeleports.put(area.getName(), player.getUniqueId()); + } + area.getWorld().getChunkAtAsync(location, (Consumer) chunk -> { Location newLocation = chunk.getWorld().getHighestBlockAt(location).getLocation(); newLocation.setY(newLocation.getY() + 1); protectModule.getPlugin().getModule(BasicsModule.class).getPlayerTeleporter().teleport(player, newLocation); player.sendMessage(Component.text("Teleported to area '" + area.getName() + "'.", NamedTextColor.YELLOW)); + + if (coordinateWarning && Bukkit.getPlayer(recentTeleport) instanceof Player other) { + player.sendMessage(other.displayName() + .append(Component.text(" recently teleported to this area, please coordinate review.").color(NamedTextColor.YELLOW))); + } }); } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 3d5ebfa..5b61f58 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -237,6 +237,14 @@ permissions: description: "Access to the /ticket close command." children: - nabsuite.command.ticket + nabsuite.command.ticket.delegate: + description: "Access to the /ticket delegate command." + children: + - nabsuite.command.ticket + nabsuite.command.ticket.escalate: + description: "Access to the /ticket escalate command." + children: + - nabsuite.command.ticket nabsuite.command.nostealing: description: "Confirm you understand the rules on stealing." nabsuite.command.seeinv: @@ -309,6 +317,12 @@ permissions: description: "Allows players to receive ore alerts." nabsuite.joinfull: description: "Allows players to join a full server." + nabsuite.ticket.deputy: + description: "Allow reading tickets for deputy level" + nabsuite.ticket.default: + description: "Allow reading tickets for default level" + nabsuite.ticket.admin: + description: "Allow reading tickets for admin level" # Mechs module commands nabsuite.command.pvp: diff --git a/src/main/resources/resources/admin/config-patches/4.patch b/src/main/resources/resources/admin/config-patches/4.patch new file mode 100644 index 0000000..61aff9e --- /dev/null +++ b/src/main/resources/resources/admin/config-patches/4.patch @@ -0,0 +1,65 @@ +[add-field] +key=ticket-levels +value=\n - "deputy"\n - "default"\n - "admin" +comment=# Available ticket levels, ordered by lowest to highest + +[add-section] +key=ticket-types +comment=# Default values for different ticket types + +[add-section] +key=ticket-types.default + +[add-field] +key=ticket-types.default.level +value="default" +comment=# Default ticket level + +[add-field] +key=ticket-types.default.allow-delegate +value=true +comment=# Allow ticket delegation by default + +[add-section] +key=ticket-types.deputy-expiry-staff +comment=# Staff deputy expiry ticket + +[add-field] +key=ticket-types.deputy-expiry-staff.allow-delegate +value=false + +[add-section] +key=ticket-types.deputy-expiry-admin +comment=# Admin deputy expiry ticket + +[add-field] +key=ticket-types.deputy-expiry-admin.level +value="admin" + +[add-field] +key=ticket-types.deputy-expiry-admin.allow-delegate +value=false + +[add-section] +key=ticket-types.suspicion +comment=# Grief/theft/lava cast suspicion + +[add-field] +key=ticket-types.suspicion.level +value="deputy" + +[add-section] +key=ticket-types.modreq +comment=# Player created tickets + +[add-section] +key=ticket-types.profanity +comment=# Highly offensive language + +[add-section] +key=ticket-types.xray +comment=# Suspicious mining activity + +[add-field] +key=ticket-types.xray.level +value="deputy" diff --git a/src/main/resources/resources/admin/config.yml b/src/main/resources/resources/admin/config.yml index 7d6f169..bd9c9ff 100644 --- a/src/main/resources/resources/admin/config.yml +++ b/src/main/resources/resources/admin/config.yml @@ -57,4 +57,43 @@ deputy-settings: deputy-group: admin-deputy candidate-groups: - - staff \ No newline at end of file + - staff + +# Available ticket levels, ordered by lowest to highest +ticket-levels: + - "deputy" + - "default" + - "admin" + +# Default values for different ticket types +ticket-types: + default: + # Default ticket level + level: "default" + + # Allow ticket delegation by default + allow-delegate: true + + # Staff deputy expiry ticket + deputy-expiry-staff: + allow-delegate: false + + # Admin deputy expiry ticket + deputy-expiry-admin: + level: "admin" + + allow-delegate: false + + # Grief/theft/lava cast suspicion + suspicion: + level: "deputy" + + # Player created tickets + modreq: + + # Highly offensive language + profanity: + + # Suspicious mining activity + xray: + level: "deputy" \ No newline at end of file