diff --git a/HOST_IT.md b/HOST_IT.md new file mode 100644 index 0000000..0dbf11c --- /dev/null +++ b/HOST_IT.md @@ -0,0 +1,152 @@ +# How to Run the Discord Ticket Bot + +## Prerequisites +- **Java 21 or higher** installed +- A **Discord Bot Token** (from Discord Developer Portal) +- Your **Discord Server (Guild) ID** + +--- + +## Method 1: Run Locally with Gradle (Recommended for Development) + +### Step 1: Build the Project +Open PowerShell in the project directory and run: +```powershell +.\gradlew.bat shadowJar +``` + +This will compile the code and create a JAR file at: +`build\libs\discord-ticketbot.jar` + +### Step 2: Create Configuration Directory +```powershell +mkdir Tickets +``` + +### Step 3: Create Configuration File +Copy the example config and edit it: +```powershell +Copy-Item config.example.yml Tickets\config.yml +``` + +Now open `Tickets\config.yml` in a text editor and **at minimum** set: +- `token: "YOUR_BOT_TOKEN_HERE"` - Replace with your Discord bot token + +Optional settings you may want to configure: +- `staffId` - Role ID for staff members who can manage tickets +- `logChannel` - Channel ID for ticket transcripts +- `maxTicketsPerUser` - Maximum open tickets per user (default: 3) +- Other settings as needed + +### Step 4: Run the Bot +```powershell +java -jar build\libs\discord-ticketbot.jar +``` + +### Step 5: Set Up in Discord +1. Invite your bot to your Discord server (needs permissions: Manage Channels, Manage Threads, Send Messages, Embed Links, Attach Files, Manage Roles) +2. In Discord, run the command: `/ticket setup` +3. This will automatically configure your `serverId` and create necessary channels/categories + +--- + +## Method 2: Run with Docker + +### Using Docker Compose (Easiest) +```powershell +docker-compose up -d +``` + +Before running, make sure to: +1. Create the `Tickets` directory: `mkdir Tickets` +2. Copy and edit the config: `Copy-Item config.example.yml Tickets\config.yml` +3. Set your bot token in `Tickets\config.yml` + +### Using Docker Directly +```powershell +# Build the image +docker build -t discord-ticketbot . + +# Run the container +docker run -d --name ticketbot -v ${PWD}/Tickets:/app/Tickets discord-ticketbot +``` + +--- + +## Getting Your Bot Token + +1. Go to [Discord Developer Portal](https://discord.com/developers/applications) +2. Create a new application or select an existing one +3. Go to the "Bot" section +4. Click "Reset Token" to get your bot token (keep it secret!) +5. Enable these Privileged Gateway Intents: + - SERVER MEMBERS INTENT + - MESSAGE CONTENT INTENT +6. Go to OAuth2 > URL Generator: + - Select scope: `bot`, `applications.commands` + - Select permissions: Administrator (or specific permissions as needed) + - Copy the generated URL and use it to invite the bot to your server + +--- + +## Configuration Overview + +Key settings in `Tickets/config.yml`: + +| Setting | Required | Description | +|---------|----------|-------------| +| `token` | βœ“ | Your Discord bot token | +| `serverId` | Auto | Set automatically by `/ticket setup` | +| `staffId` | - | Staff role ID for ticket management | +| `logChannel` | - | Where transcripts are posted | +| `ratingStatsChannel` | - | Daily rating statistics | +| `pendingRatingCategory` | - | Category for tickets awaiting rating | +| `maxTicketsPerUser` | - | Max open tickets per user (default: 3) | +| `xpApiUrl` / `xpApiKey` | - | Optional XP system integration | + +--- + +## Troubleshooting + +**"Invalid Token" error**: +- Check that your bot token is correct in `Tickets/config.yml` +- Make sure there are no extra spaces or quotes + +**"Missing Permissions" error**: +- Ensure your bot has Administrator permission or at least: + - Manage Channels, Manage Threads, Send Messages, Embed Links, Attach Files, Manage Roles + +**"Database error"**: +- The bot creates a SQLite database automatically in the `Tickets` folder +- Make sure the bot has write permissions to this directory + +**Commands not showing**: +- Make sure you've enabled `applications.commands` scope when inviting the bot +- Wait a few minutes for Discord to sync commands +- Try running `/ticket setup` to register commands + +--- + +## Default Ticket Categories + +The bot supports these ticket types: +- General Support +- Bug Report +- Crash Report +- Payment Issue +- Report User +- Security Issue + +You can customize categories by modifying the category classes in `src/main/java/eu/greev/dcbot/ticketsystem/categories/` + +--- + +## Next Steps After Running + +1. Run `/ticket setup` in your Discord server +2. Configure staff roles with `staffId` in config +3. Set up log channels for transcripts +4. Test creating a ticket +5. Explore other commands like `/stats`, `/cleanup`, etc. + +Enjoy your new ticket system! 🎫 diff --git a/README.md b/README.md index c7b7d5f..2cb2927 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # Greev Ticket Bot The self coded ticket bot of Greev.eu with many features.\ -[Join the Greev.eu Discord](https://grv.sh/discord) +[Join the Greev.eu Discord](https://grv.sh/discord)\ +Fork by NoriskClient\ +[Join the NoriskClient Discord](https://discord.norisk.gg) ## How to use? -Copy the built Jar file from the Actions tab of this repository, run it the first time and paste afterwards your bot token into `./Tickets/config.yml`\ -Then run the bot again, use `/ticket setup` in Discord and enjoy your new ticket bot :) \ No newline at end of file +See the [self host guide](HOST_IT.md) for instructions on how to host the bot yourself. \ No newline at end of file diff --git a/config.example.yml b/config.example.yml index a8fa1a4..a53271c 100644 --- a/config.example.yml +++ b/config.example.yml @@ -55,6 +55,10 @@ addToTicketThread: ratingNotificationChannels: - 123456789012345678 +# Can see sensitive ticket info +privilegedSupporterRoles: + - 123456789012345678 + # Custom claim emojis per user (userId: emoji) claimEmojis: 123456789012345678: "🎫" @@ -72,7 +76,7 @@ categoryRoles: - 123456789012345678 # XP System Integration (optional - leave empty to disable) -# Backend API URL for awarding XP to helpers +# Backend API URL for awarding XP to helpers (will append /tickets/award-xp) xpApiUrl: "" # API key for authentication xpApiKey: "" diff --git a/src/main/java/eu/greev/dcbot/Main.java b/src/main/java/eu/greev/dcbot/Main.java index bccd7e7..1927531 100644 --- a/src/main/java/eu/greev/dcbot/Main.java +++ b/src/main/java/eu/greev/dcbot/Main.java @@ -28,6 +28,7 @@ import net.dv8tion.jda.api.exceptions.InvalidTokenException; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; import net.dv8tion.jda.api.requests.GatewayIntent; @@ -49,6 +50,7 @@ import java.util.List; import java.util.stream.Collectors; +@SuppressWarnings({"squid:S1192"}) // String literals should not be duplicated @Slf4j public class Main { public static final Map INTERACTIONS = new HashMap<>(); @@ -117,7 +119,8 @@ public static void main(String[] args) throws InterruptedException, IOException ticketService.loadOverflowCategories(); - jda.updateCommands().addCommands(Commands.slash("ticket", "Manage the ticket system") + SlashCommandData ticketCommand = Commands.slash("ticket", "Manage the ticket system"); + jda.updateCommands().addCommands(ticketCommand .addSubcommands(new SubcommandData("add", "Add a User to this ticket") .addOption(OptionType.USER, "member", "The user adding to the current ticket", true)) .addSubcommands(new SubcommandData("remove", "Remove a User from this ticket") @@ -153,7 +156,7 @@ public static void main(String[] args) throws InterruptedException, IOException .addOption(OptionType.STRING, "type", "Report type: daily, weekly, monthly", true)) .addSubcommands(new SubcommandData("set-privacy", "Toggle ob deine XP/Ratings ΓΆffentlich angezeigt werden") .addOption(OptionType.STRING, "mode", "visible oder hidden", true)) - ).queue(s -> s.get(0).getSubcommands().forEach(c -> { + ).queue(s -> s.getFirst().getSubcommands().forEach(c -> { if (c.getName().equals("get-tickets")) { getTicketCommandId = c.getId(); } else if (c.getName().equals("create")) { @@ -217,6 +220,11 @@ public static void main(String[] args) throws InterruptedException, IOException } private static void initDatasource() { + // Set SQLite temp directory to avoid permission issues with C:\Windows\TEMP + String sqliteTempDir = new File("./Tickets/temp").getAbsolutePath(); + new File(sqliteTempDir).mkdirs(); + System.setProperty("org.sqlite.tmpdir", sqliteTempDir); + SQLiteDataSource ds = new SQLiteDataSource(); ds.setUrl("jdbc:sqlite:./Tickets/tickets.db"); jdbi = Jdbi.create(ds); @@ -285,4 +293,5 @@ private static void registerCategory(ICategory category, Config config, TicketSe OVERFLOW_CHANNEL_CATEGORIES.put(category, new ArrayList<>()); CATEGORIES.add(category); } -} \ No newline at end of file +} + diff --git a/src/main/java/eu/greev/dcbot/scheduler/DailyScheduler.java b/src/main/java/eu/greev/dcbot/scheduler/DailyScheduler.java index b635728..02d2355 100644 --- a/src/main/java/eu/greev/dcbot/scheduler/DailyScheduler.java +++ b/src/main/java/eu/greev/dcbot/scheduler/DailyScheduler.java @@ -26,7 +26,7 @@ public class DailyScheduler { private TicketService ticketService; public void start() { - scheduler.scheduleAtFixedRate(this::run, getInitialDelay(), 24 * 60 * 60, TimeUnit.SECONDS); + scheduler.scheduleAtFixedRate(this::run, getInitialDelay(), 1, TimeUnit.DAYS); } private int getInitialDelay() { diff --git a/src/main/java/eu/greev/dcbot/scheduler/HourlyScheduler.java b/src/main/java/eu/greev/dcbot/scheduler/HourlyScheduler.java index c2f7afd..9b3e8fe 100644 --- a/src/main/java/eu/greev/dcbot/scheduler/HourlyScheduler.java +++ b/src/main/java/eu/greev/dcbot/scheduler/HourlyScheduler.java @@ -36,7 +36,7 @@ public class HourlyScheduler { private XpService xpService; public void start() { - scheduler.scheduleAtFixedRate(this::run, getInitialDelay(), 60 * 60, TimeUnit.SECONDS); + scheduler.scheduleAtFixedRate(this::run, getInitialDelay(), 1, TimeUnit.HOURS); } private int getInitialDelay() { diff --git a/src/main/java/eu/greev/dcbot/scheduler/RatingStatsScheduler.java b/src/main/java/eu/greev/dcbot/scheduler/RatingStatsScheduler.java index a1d4b34..9e692cf 100644 --- a/src/main/java/eu/greev/dcbot/scheduler/RatingStatsScheduler.java +++ b/src/main/java/eu/greev/dcbot/scheduler/RatingStatsScheduler.java @@ -1,6 +1,7 @@ package eu.greev.dcbot.scheduler; import eu.greev.dcbot.ticketsystem.service.RatingData; +import eu.greev.dcbot.ticketsystem.service.SupporterRatingStatsHelper; import eu.greev.dcbot.ticketsystem.service.SupporterSettingsData; import eu.greev.dcbot.ticketsystem.service.TicketData; import eu.greev.dcbot.utils.Config; @@ -42,11 +43,11 @@ public RatingStatsScheduler(Config config, RatingData ratingData, TicketData tic } public void start() { - scheduler.scheduleAtFixedRate(this::sendDailyReport, getInitialDelayForHour(9), 24 * 60 * 60, TimeUnit.SECONDS); + scheduler.scheduleAtFixedRate(this::sendDailyReport, getInitialDelayForHour(9), 1, TimeUnit.DAYS); - scheduler.scheduleAtFixedRate(this::sendWeeklyReport, getInitialDelayForWeekly(), 7 * 24 * 60 * 60, TimeUnit.SECONDS); + scheduler.scheduleAtFixedRate(this::sendWeeklyReport, getInitialDelayForWeekly(), 7, TimeUnit.DAYS); - scheduler.scheduleAtFixedRate(this::sendMonthlyReport, getInitialDelayForMonthly(), 30 * 24 * 60 * 60, TimeUnit.SECONDS); + scheduler.scheduleAtFixedRate(this::sendMonthlyReport, getInitialDelayForMonthly(), 30, TimeUnit.DAYS); log.info("RatingStatsScheduler started - Daily reports at 9:00, Weekly reports on Monday 9:00, Monthly reports on 1st at 9:00"); } @@ -298,7 +299,7 @@ public List buildMonthlyReport() { private String formatTicketStats(Map ticketsBySupporter, int limit) { StringBuilder sb = new StringBuilder(); int count = 0; - for (var entry : ticketsBySupporter.entrySet()) { + for (Map.Entry entry : ticketsBySupporter.entrySet()) { if (count >= limit) break; String userId = entry.getKey(); boolean hideStats = supporterSettingsData.isHideStats(userId); @@ -313,7 +314,7 @@ private String formatTicketStats(Map ticketsBySupporter, int li private String formatTicketStatsRanked(Map ticketsBySupporter, int limit) { StringBuilder sb = new StringBuilder(); int rank = 1; - for (var entry : ticketsBySupporter.entrySet()) { + for (Map.Entry entry : ticketsBySupporter.entrySet()) { if (rank > limit) break; String userId = entry.getKey(); boolean hideStats = supporterSettingsData.isHideStats(userId); @@ -327,42 +328,57 @@ private String formatTicketStatsRanked(Map ticketsBySupporter, } private String formatRatingStats(Map avgRatings, Map countRatings, int limit) { + List topSupporters + = SupporterRatingStatsHelper.topSupporters(avgRatings, countRatings, limit); StringBuilder sb = new StringBuilder(); - int count = 0; - for (var entry : avgRatings.entrySet()) { - if (count >= limit) break; - String userId = entry.getKey(); + for (SupporterRatingStatsHelper.SupporterRatingEntry entry : topSupporters) { + String userId = entry.supporterId(); boolean hideStats = supporterSettingsData.isHideStats(userId); String mention = hideStats ? "Anonym" : getUserMention(userId); if (hideStats) { sb.append(mention).append(": β˜…β˜…β˜…β˜…β˜… (Ø ???, ???x)\n"); } else { - double avg = entry.getValue(); - int ratings = countRatings.getOrDefault(userId, 0); - sb.append(mention).append(": ").append(getStarDisplay(avg)).append(" (Ø ").append(String.format("%.2f", avg)).append(", ").append(ratings).append("x)\n"); + double avg = entry.avgRating(); + int ratings = entry.ratingCount(); + sb.append(mention) + .append(": ") + .append(SupporterRatingStatsHelper.starDisplay(avg)) + .append(" (Ø ") + .append(String.format("%.2f", avg)) + .append(", ") + .append(ratings) + .append("x)\n"); } - count++; } return sb.toString().trim(); } private String formatRatingStatsRanked(Map avgRatings, Map countRatings, int limit) { StringBuilder sb = new StringBuilder(); - int rank = 1; - for (var entry : avgRatings.entrySet()) { - if (rank > limit) break; - String userId = entry.getKey(); + List topSupporters = SupporterRatingStatsHelper.topSupporters(avgRatings, countRatings, limit); + for (int i = 0; i < topSupporters.size(); i++) { + SupporterRatingStatsHelper.SupporterRatingEntry entry = topSupporters.get(i); + int rank = i + 1; + String userId = entry.supporterId(); boolean hideStats = supporterSettingsData.isHideStats(userId); String mention = hideStats ? "Anonym" : getUserMention(userId); String medal = rank == 1 ? "πŸ₯‡" : rank == 2 ? "πŸ₯ˆ" : rank == 3 ? "πŸ₯‰" : rank + "."; if (hideStats) { sb.append(medal).append(" ").append(mention).append(": β˜…β˜…β˜…β˜…β˜… (Ø ???, ???x)\n"); } else { - double avg = entry.getValue(); - int ratings = countRatings.getOrDefault(userId, 0); - sb.append(medal).append(" ").append(mention).append(": ").append(getStarDisplay(avg)).append(" (Ø ").append(String.format("%.2f", avg)).append(", ").append(ratings).append("x)\n"); + double avg = entry.avgRating(); + int ratings = entry.ratingCount(); + sb.append(medal) + .append(" ") + .append(mention) + .append(": ") + .append(SupporterRatingStatsHelper.starDisplay(avg)) + .append(" (Ø ") + .append(String.format("%.2f", avg)) + .append(", ") + .append(ratings) + .append("x)\n"); } - rank++; } return sb.toString().trim(); } @@ -378,8 +394,6 @@ private String getUserMention(String id) { } private String getStarDisplay(double avg) { - int fullStars = (int) Math.round(avg); - fullStars = Math.max(0, Math.min(5, fullStars)); - return "β˜…".repeat(fullStars) + "β˜†".repeat(5 - fullStars); + return SupporterRatingStatsHelper.starDisplay(avg); } } diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/CategorySelection.java b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/CategorySelection.java index 779e958..105dd2e 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/CategorySelection.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/CategorySelection.java @@ -5,6 +5,7 @@ import lombok.AllArgsConstructor; import net.dv8tion.jda.api.events.Event; import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; +import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu; @AllArgsConstructor @@ -25,7 +26,7 @@ public void execute(Event evt) { } event.getMessage().editMessageComponents( - net.dv8tion.jda.api.interactions.components.ActionRow.of(selectionBuilder.build()) + ActionRow.of(selectionBuilder.build()) ).queue(); } } \ No newline at end of file diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/TicketClose.java b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/TicketClose.java index 44ced10..5f073c0 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/TicketClose.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/TicketClose.java @@ -9,6 +9,7 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.channel.concrete.Category; import net.dv8tion.jda.api.events.Event; @@ -20,6 +21,7 @@ import java.awt.*; import java.time.Instant; +import java.util.List; /** * Handles ticket closing with mandatory rating flow. @@ -219,7 +221,7 @@ private void sendRatingRequest(Ticket ticket, InteractionHook hook) { Button.success("rating-5-" + ticket.getId(), "⭐⭐⭐⭐⭐") ) .addActionRow( - Button.danger("rating-skip-" + ticket.getId(), "Nein danke") + Button.danger("rating-skip-" + ticket.getId(), "No thanks") )) .queue( success -> { @@ -274,11 +276,11 @@ private void handleDMFailure(Ticket ticket, InteractionHook hook) { */ private boolean hasHelperReplied(Ticket ticket) { try { - var messages = ticket.getTextChannel().getIterableHistory() + List messages = ticket.getTextChannel().getIterableHistory() .takeAsync(100) .get(); - for (var message : messages) { + for (Message message : messages) { // Skip bots if (message.getAuthor().isBot()) continue; // Skip the ticket owner diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/buttons/GetTranscript.java b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/buttons/GetTranscript.java index 1469c45..f5f79eb 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/buttons/GetTranscript.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/buttons/GetTranscript.java @@ -26,7 +26,7 @@ public void execute(Event evt) { event.replyEmbeds(error.build()).setEphemeral(true).queue(); return; } - int ticketID = Integer.parseInt(event.getMessage().getEmbeds().get(0).getTitle().replace("Ticket #", "")); + int ticketID = Integer.parseInt(event.getMessage().getEmbeds().getFirst().getTitle().replace("Ticket #", "")); Ticket ticket = ticketService.getTicketByTicketId(ticketID); if (ticket == null) { @@ -37,13 +37,24 @@ public void execute(Event evt) { return; } + boolean isSensitive = ticket.getCategory().isSensitive(); + boolean isTicketOwner = event.getMember() != null && ticket.getOwner().getId().equals(event.getMember().getUser().getId()); // getMember can be null when user left the server + boolean isPrivilegedSupporter = ticketService.isUserPrivilegedSupporter(event.getMember()); + if (isSensitive && !(isTicketOwner || isPrivilegedSupporter)) { + EmbedBuilder error = new EmbedBuilder() + .setColor(Color.RED) + .setDescription("❌ **This ticket category is marked as sensitive, transcript cannot be viewed by you!**"); + event.replyEmbeds(error.build()).setEphemeral(true).queue(); + return; + } + event.getUser().openPrivateChannel() .flatMap(channel -> channel.sendFiles(FileUpload.fromData(ticket.getTranscript().toFile(ticketID)))) .queue(); EmbedBuilder builder = new EmbedBuilder() .setFooter(config.getServerName(), config.getServerLogo()) - .setAuthor(event.getMember().getEffectiveName(), null, event.getMember().getEffectiveAvatarUrl()) + .setAuthor(event.getMember() != null ? event.getMember().getEffectiveName() : "Unknown", null, event.getMember().getEffectiveAvatarUrl()) .setColor(Color.decode(config.getColor())) .setDescription("Sent transcript of Ticket #" + ticketID + " via DM"); event.replyEmbeds(builder.build()).setEphemeral(true).queue(); diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/buttons/RatingSkip.java b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/buttons/RatingSkip.java index fe1d1a6..09787cd 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/buttons/RatingSkip.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/buttons/RatingSkip.java @@ -6,13 +6,11 @@ import eu.greev.dcbot.ticketsystem.service.XpService; import eu.greev.dcbot.utils.Config; import lombok.extern.slf4j.Slf4j; -import me.ryzeon.transcripts.DiscordHtmlTranscripts; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.events.Event; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; -import net.dv8tion.jda.api.utils.FileUpload; import java.awt.*; @@ -107,23 +105,8 @@ public void execute(Event evt) { } private String sendSkipNotification(Ticket ticket) { - // Generate HTML transcript and upload to log channel to get URL - String transcriptUrl = null; - try { - if (ticket.getTextChannel() != null && config.getLogChannel() != 0) { - FileUpload transcriptUpload = DiscordHtmlTranscripts.getInstance() - .createTranscript(ticket.getTextChannel(), "transcript-" + ticket.getId() + ".html"); - - var logChannel = jda.getTextChannelById(config.getLogChannel()); - if (logChannel != null) { - var uploadMessage = logChannel.sendFiles(transcriptUpload).complete(); - // Use message jump URL instead of attachment URL for better Discord navigation - transcriptUrl = uploadMessage.getJumpUrl(); - } - } - } catch (Exception e) { - log.error("Failed to generate/upload HTML transcript for ticket #{}", ticket.getId(), e); - } + // Generate transcript (will return null for sensitive categories) + String transcriptUrl = ticketService.sendTranscript(ticket); if (config.getRatingNotificationChannels() == null || config.getRatingNotificationChannels().isEmpty()) { return transcriptUrl; @@ -144,17 +127,7 @@ private String sendSkipNotification(Ticket ticket) { notification.setThumbnail(thumbnailUrl); } - // Only add transcript link for non-sensitive categories - if (transcriptUrl != null && !ticket.getCategory().isSensitive()) { - notification.addField("πŸ“ Transcript", "[Hier klicken](" + transcriptUrl + ")", false); - } - - for (Long channelId : config.getRatingNotificationChannels()) { - var channel = jda.getTextChannelById(channelId); - if (channel != null) { - channel.sendMessageEmbeds(notification.build()).queue(); - } - } + ticketService.appendTranscriptLinkAndSendCloseEmbed(transcriptUrl, ticket, notification); return transcriptUrl; } diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/commands/RatingStats.java b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/commands/RatingStats.java index 7f1ce4d..de09414 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/commands/RatingStats.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/commands/RatingStats.java @@ -1,17 +1,18 @@ package eu.greev.dcbot.ticketsystem.interactions.commands; import eu.greev.dcbot.ticketsystem.service.RatingData; +import eu.greev.dcbot.ticketsystem.service.SupporterRatingStatsHelper; import eu.greev.dcbot.ticketsystem.service.TicketService; import eu.greev.dcbot.utils.Config; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.Event; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import java.awt.*; +import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; public class RatingStats extends AbstractCommand { private final RatingData ratingData; @@ -75,31 +76,21 @@ public void execute(Event evt) { } private String formatSupporterStats(Map avgRatings, Map countRatings) { - var sortedEntries = avgRatings.entrySet().stream() - .sorted((a, b) -> { - int cmp = Double.compare(b.getValue(), a.getValue()); - if (cmp == 0) { - return Integer.compare( - countRatings.getOrDefault(b.getKey(), 0), - countRatings.getOrDefault(a.getKey(), 0) - ); - } - return cmp; - }) - .limit(5) - .toList(); + List topSupporters = SupporterRatingStatsHelper.topSupporters(avgRatings, countRatings, 5); StringBuilder sb = new StringBuilder(); - for (int i = 0; i < sortedEntries.size(); i++) { - var e = sortedEntries.get(i); - String mention = Optional.ofNullable(jda.retrieveUserById(e.getKey()).complete()) - .map(u -> u.getAsMention()) - .orElse("<@" + e.getKey() + ">"); - double avg = e.getValue(); - int count = countRatings.getOrDefault(e.getKey(), 0); - String stars = getStarDisplay(avg); - sb.append(String.format("%d. %s %s (%.2f avg, %d ratings)", i + 1, mention, stars, avg, count)); - if (i < sortedEntries.size() - 1) { + for (int i = 0; i < topSupporters.size(); i++) { + SupporterRatingStatsHelper.SupporterRatingEntry entry = topSupporters.get(i); + String mention = getUserMention(entry.supporterId()); + double avg = entry.avgRating(); + int count = entry.ratingCount(); + sb.append(String.format("%d. %s %s (%.2f avg, %d ratings)", + i + 1, + mention, + SupporterRatingStatsHelper.starDisplay(avg), + avg, + count)); + if (i < topSupporters.size() - 1) { sb.append("\n"); } } @@ -107,8 +98,18 @@ private String formatSupporterStats(Map avgRatings, Map"; } } diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/commands/Stats.java b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/commands/Stats.java index 0c89764..9c79361 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/commands/Stats.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/commands/Stats.java @@ -1,5 +1,6 @@ package eu.greev.dcbot.ticketsystem.interactions.commands; +import eu.greev.dcbot.ticketsystem.service.TicketData; import eu.greev.dcbot.ticketsystem.service.TicketService; import eu.greev.dcbot.utils.Config; import net.dv8tion.jda.api.EmbedBuilder; @@ -27,7 +28,7 @@ public void execute(Event evt) { return; } - var data = ticketService.getTicketData(); + TicketData data = ticketService.getTicketData(); int total = data.countTotalTickets(); int open = data.countOpenTickets(); int waiting = data.countWaitingTickets(); diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/modals/RatingModal.java b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/modals/RatingModal.java index a5c97e7..c93f5d5 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/modals/RatingModal.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/modals/RatingModal.java @@ -8,14 +8,12 @@ import eu.greev.dcbot.ticketsystem.service.TicketService; import eu.greev.dcbot.ticketsystem.service.XpService; import eu.greev.dcbot.utils.Config; -import me.ryzeon.transcripts.DiscordHtmlTranscripts; import lombok.extern.slf4j.Slf4j; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.events.Event; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; -import net.dv8tion.jda.api.utils.FileUpload; import java.awt.*; import java.time.Instant; @@ -135,37 +133,22 @@ private String getStarDisplay(int stars) { return "\u2605".repeat(stars) + "\u2606".repeat(5 - stars); } + private Color getRatingColor(int stars) { + if (stars >= 4) { + return Color.GREEN; + } else if (stars == 3) { + return Color.YELLOW; + } else { + return Color.RED; + } + } + private String sendRatingNotification(Ticket ticket, int stars, String message) { String starDisplay = getStarDisplay(stars); - Color embedColor = stars >= 4 ? Color.GREEN : stars >= 3 ? Color.YELLOW : Color.RED; + Color embedColor = getRatingColor(stars); - // Generate HTML transcript and upload to log channel to get URL - String transcriptUrl = null; - try { - if (ticket.getTextChannel() != null && config.getLogChannel() != 0) { - // Fetch messages first to check if channel has content - var messages = ticket.getTextChannel().getIterableHistory() - .takeAsync(1000) - .get(); - - if (messages != null && !messages.isEmpty()) { - FileUpload transcriptUpload = DiscordHtmlTranscripts.getInstance() - .createTranscript(ticket.getTextChannel(), "transcript-" + ticket.getId() + ".html"); - - var logChannel = jda.getTextChannelById(config.getLogChannel()); - if (logChannel != null) { - var uploadMessage = logChannel.sendFiles(transcriptUpload).complete(); - // Use message jump URL instead of attachment URL for better Discord navigation - transcriptUrl = uploadMessage.getJumpUrl(); - } - } else { - log.warn("No messages found in ticket #{} channel, skipping transcript generation", ticket.getId()); - } - } - } catch (Exception e) { - log.error("Failed to generate/upload HTML transcript for ticket #{}: {}", ticket.getId(), e.getMessage()); - // Continue without transcript - don't let this block the rating notification - } + // Generate transcript (will return null for sensitive categories) + String transcriptUrl = ticketService.sendTranscript(ticket); if (config.getRatingNotificationChannels() == null || config.getRatingNotificationChannels().isEmpty()) { return transcriptUrl; @@ -177,7 +160,8 @@ private String sendRatingNotification(Ticket ticket, int stars, String message) String displayStars = hideStats ? "???" : stars + " Sterne"; String displayStarIcons = hideStats ? "β˜…β˜…β˜…β˜…β˜…" : starDisplay; String thumbnailUrl = hideStats ? null : ticket.getSupporter().getEffectiveAvatarUrl(); - String displayFeedback = hideStats ? "Versteckt" : ((message != null && !message.isBlank()) ? message : "Kein Feedback"); + String feedback = (message != null && !message.isBlank()) ? message : "Kein Feedback"; + String displayFeedback = hideStats ? "Versteckt" : feedback; EmbedBuilder notification = new EmbedBuilder() .setColor(embedColor) @@ -191,17 +175,7 @@ private String sendRatingNotification(Ticket ticket, int stars, String message) notification.addField("Feedback", displayFeedback, false); - // Only add transcript link for non-sensitive categories - if (transcriptUrl != null && !ticket.getCategory().isSensitive()) { - notification.addField("πŸ“ Transcript", "[Hier klicken](" + transcriptUrl + ")", false); - } - - for (Long channelId : config.getRatingNotificationChannels()) { - var channel = jda.getTextChannelById(channelId); - if (channel != null) { - channel.sendMessageEmbeds(notification.build()).queue(); - } - } + ticketService.appendTranscriptLinkAndSendCloseEmbed(transcriptUrl, ticket, notification); return transcriptUrl; } diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/modals/TicketModal.java b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/modals/TicketModal.java index f1c4268..6abfcb8 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/interactions/modals/TicketModal.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/interactions/modals/TicketModal.java @@ -50,11 +50,12 @@ public void execute(Event evt) { EmbedBuilder errorEmbed = new EmbedBuilder() .setColor(Color.RED) .setTitle("Invalid Link") - .setDescription("Please provide a valid **mclo.gs** link!\n\n" + - "**How to create one:**\n" + - "1. Go to [mclo.gs](https://mclo.gs)\n" + - "2. Upload your log file\n" + - "3. Copy the link") + .setDescription(""" + Please provide a valid **mclo.gs** link!\n\n + "**How to create one:**\n + "1. Go to [mclo.gs](https://mclo.gs)\n + "2. Upload your log file\n + "3. Copy the link""") .setFooter(config.getServerName(), config.getServerLogo()); event.getHook().sendMessageEmbeds(errorEmbed.build()).setEphemeral(true).queue(); return; @@ -63,16 +64,21 @@ public void execute(Event evt) { Optional error = ticketService.createNewTicket(info, category, event.getUser()); - if (error.isEmpty()) { - Ticket ticket = ticketService.getTicketByTicketId(ticketData.getLastTicketId()); - builder.setAuthor(event.getMember().getEffectiveName(), null, event.getMember().getEffectiveAvatarUrl()) - .setColor(Color.decode(config.getColor())) - .addField("βœ… **Ticket created**", "Successfully created a ticket for you " + ticket.getTextChannel().getAsMention(), false); - event.getHook().sendMessageEmbeds(builder.build()).setEphemeral(true).queue(); - } else { + if (error.isPresent()) { builder.addField("❌ **Creating ticket failed**", error.get(), false); - event.getHook().sendMessageEmbeds(builder.build()).setEphemeral(true).queue(); + return; + } + + Ticket ticket = ticketService.getTicketByTicketId(ticketData.getLastTicketId()); + builder.setAuthor(event.getMember().getEffectiveName(), null, event.getMember().getEffectiveAvatarUrl()) + .setColor(Color.decode(config.getColor())) + .addField("βœ… **Ticket created**", "Successfully created a ticket for you " + ticket.getTextChannel().getAsMention(), false); + event.getHook().sendMessageEmbeds(builder.build()).setEphemeral(true).queue(); + + ticket.getTranscript().addInfoMessage("Category", category.getLabel(), ticket.getId()); + for(Map.Entry entry : info.entrySet()) { + ticket.getTranscript().addInfoMessage(entry.getKey().replace(" ", "-"), entry.getValue(), ticket.getId()); } } diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/service/SupporterRatingStatsHelper.java b/src/main/java/eu/greev/dcbot/ticketsystem/service/SupporterRatingStatsHelper.java new file mode 100644 index 0000000..adafdea --- /dev/null +++ b/src/main/java/eu/greev/dcbot/ticketsystem/service/SupporterRatingStatsHelper.java @@ -0,0 +1,45 @@ +package eu.greev.dcbot.ticketsystem.service; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +/** + * Shared helper to keep supporter rating leaderboards consistent across commands and schedulers. + */ +public final class SupporterRatingStatsHelper { + private SupporterRatingStatsHelper() { + } + + public static List topSupporters(Map avgRatings, + Map countRatings, + int limit) { + Stream stream = avgRatings.entrySet().stream() + .map(entry -> new SupporterRatingEntry( + entry.getKey(), + entry.getValue(), + countRatings.getOrDefault(entry.getKey(), 0) + )) + .sorted(SUPPORTER_COMPARATOR); + + if (limit > 0) { + stream = stream.limit(limit); + } + + return stream.toList(); + } + + public static String starDisplay(double avg) { + int fullStars = Math.clamp((int) Math.round(avg), 0, 5); + return "β˜…".repeat(fullStars) + "β˜†".repeat(5 - fullStars); + } + + public record SupporterRatingEntry(String supporterId, double avgRating, int ratingCount) { + } + + private static final Comparator SUPPORTER_COMPARATOR = + Comparator.comparingDouble(SupporterRatingEntry::avgRating).reversed() + .thenComparing(Comparator.comparingInt(SupporterRatingEntry::ratingCount).reversed()) + .thenComparing(SupporterRatingEntry::supporterId); +} diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/service/TicketData.java b/src/main/java/eu/greev/dcbot/ticketsystem/service/TicketData.java index ad5bbee..0e91649 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/service/TicketData.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/service/TicketData.java @@ -9,6 +9,7 @@ import net.dv8tion.jda.api.JDA; import org.apache.logging.log4j.util.Strings; import org.jdbi.v3.core.Jdbi; +import org.jdbi.v3.core.statement.Update; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -147,7 +148,7 @@ public int saveTicket(Ticket ticket) { return jdbi.withHandle(handle -> { // If no ticketID (0), INSERT and return generated key; otherwise UPDATE and return existing id if (ticket.getId() == 0) { - var update = handle.createUpdate("INSERT INTO tickets (channelID, threadID, category, info, isWaiting, owner, supporter, involved, baseMessage, isOpen, waitingSince, remindersSent, closeMessage, closer, closedAt, pendingRatingSince, ratingRemindersSent, pendingCloser, lastSupporterMessageAt) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + Update update = handle.createUpdate("INSERT INTO tickets (channelID, threadID, category, info, isWaiting, owner, supporter, involved, baseMessage, isOpen, waitingSince, remindersSent, closeMessage, closer, closedAt, pendingRatingSince, ratingRemindersSent, pendingCloser, lastSupporterMessageAt) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); update .bind(0, ticket.getTextChannel() != null ? ticket.getTextChannel().getId() : "") .bind(1, ticket.getThreadChannel() != null ? ticket.getThreadChannel().getId() : "") diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/service/TicketService.java b/src/main/java/eu/greev/dcbot/ticketsystem/service/TicketService.java index 874b8fd..da25460 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/service/TicketService.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/service/TicketService.java @@ -1,5 +1,6 @@ package eu.greev.dcbot.ticketsystem.service; +import lombok.Getter; import me.ryzeon.transcripts.DiscordHtmlTranscripts; import eu.greev.dcbot.Main; import eu.greev.dcbot.ticketsystem.categories.ICategory; @@ -36,14 +37,11 @@ public class TicketService { private final JDA jda; private final Config config; private final Jdbi jdbi; + @Getter private final TicketData ticketData; private final Set allCurrentTickets = new HashSet<>(); public static final String WAITING_EMOTE = "\uD83D\uDD50"; - public TicketData getTicketData() { - return ticketData; - } - public TicketService(JDA jda, Config config, Jdbi jdbi, TicketData ticketData) { this.jda = jda; this.config = config; @@ -219,37 +217,23 @@ public void closeTicket(Ticket ticket, boolean wasAccident, Member closer, Strin transcript.addLogMessage("[%s] closed the ticket%s".formatted(closer.getUser().getName(), message == null ? "." : " with following message: " + message), Instant.now().getEpochSecond(), ticketId); - // Use existing transcript URL if provided, otherwise generate new one - String transcriptUrl = existingTranscriptUrl; - if (transcriptUrl == null) { - try { - if (ticket.getTextChannel() != null && config.getLogChannel() != 0) { - // Fetch messages first to avoid NPE in library when handling message references - var messages = ticket.getTextChannel().getIterableHistory() - .takeAsync(1000) - .get(); - - if (messages != null && !messages.isEmpty()) { - FileUpload htmlTranscriptUpload = DiscordHtmlTranscripts.getInstance() - .createTranscript(ticket.getTextChannel(), "transcript-" + ticketId + ".html"); - - var logChannel = jda.getGuildById(config.getServerId()).getTextChannelById(config.getLogChannel()); - if (logChannel != null) { - var uploadMessage = logChannel.sendFiles(htmlTranscriptUpload).complete(); - if (!uploadMessage.getAttachments().isEmpty()) { - transcriptUrl = uploadMessage.getAttachments().getFirst().getUrl(); - } - } - } else { - log.warn("No messages found in ticket #{} channel, skipping transcript generation", ticketId); - } - } - } catch (Exception e) { - log.error("Failed to generate/upload HTML transcript for ticket #{}: {}", ticketId, e.getMessage()); - // Continue without transcript - don't let this block ticket closure - } + boolean isSensitive = ticket.getCategory() != null && ticket.getCategory().isSensitive(); + + // For sensitive categories, transcripts are never generated and never linked. + String transcriptUrl; + if(isSensitive) { + transcriptUrl = null; + } else { + transcriptUrl = (existingTranscriptUrl == null) ? sendTranscript(ticket) : existingTranscriptUrl; } + log.debug("Closing ticket #{} (category={}, sensitive={}) existingTranscriptUrlPresent={} transcriptUrlPresent={} ", + ticketId, + ticket.getCategory() == null ? "null" : ticket.getCategory().getId(), + isSensitive, + existingTranscriptUrl != null, + transcriptUrl != null); + EmbedBuilder builder = new EmbedBuilder().setTitle("Ticket " + ticketId) .addField("Closed by", closer.getAsMention(), false); @@ -257,27 +241,36 @@ public void closeTicket(Ticket ticket, boolean wasAccident, Member closer, Strin builder.addField("Message", message, true); } - if (transcriptUrl != null) { + if (!isSensitive && transcriptUrl != null) { builder.addField("πŸ“ Transcript", "[Hier klicken](" + transcriptUrl + ")", false); } builder.setColor(Color.decode(config.getColor())) .setFooter(config.getServerName(), config.getServerLogo()); - if (ticket.getOwner().getMutualGuilds().contains(jda.getGuildById(config.getServerId()))) { + // DM the owner (best-effort) + Guild guild = jda.getGuildById(config.getServerId()); + if (guild != null && ticket.getOwner().getMutualGuilds().contains(guild)) { try { ticket.getOwner().openPrivateChannel() .flatMap(channel -> channel.sendMessageEmbeds(builder.build())) .complete(); } catch (ErrorResponseException e) { - log.warn("Couldn't send [{}] their transcript since an error occurred:\nMeaning:{} | Message:{} | Response:{}", ticket.getOwner().getName(), e.getMeaning(), e.getMessage(), e.getErrorResponse()); + log.warn("Couldn't DM [{}] the ticket close embed: Meaning:{} | Message:{} | Response:{}", ticket.getOwner().getName(), e.getMeaning(), e.getMessage(), e.getErrorResponse()); } } - if (config.getLogChannel() != 0 && transcriptUrl != null) { - jda.getGuildById(config.getServerId()).getTextChannelById(config.getLogChannel()) - .sendMessageEmbeds(builder.build()) - .queue(); + // Always send the close embed to the configured log channel (if configured) + if (config.getLogChannel() != 0) { + TextChannel logChannel = guild == null ? null : guild.getTextChannelById(config.getLogChannel()); + if (logChannel != null) { + logChannel.sendMessageEmbeds(builder.build()).queue( + success -> log.debug("Sent close embed for ticket #{} to log channel {} (sensitive={})", ticketId, config.getLogChannel(), isSensitive), + error -> log.error("Failed to send close embed for ticket #{} to log channel {}: {}", ticketId, config.getLogChannel(), error.getMessage()) + ); + } else { + log.warn("Log channel {} not found; cannot send close embed for ticket #{}", config.getLogChannel(), ticketId); + } } saveTranscriptChanges(ticket.getTranscript().getRecentChanges()); @@ -811,4 +804,82 @@ private void consolidatePendingRatingChannels(List categories, Categor category.delete().queue(); } } -} \ No newline at end of file + + public String sendTranscript(Ticket ticket) { + // Sensitive categories must not generate/upload transcripts. + if (ticket.getCategory() != null && ticket.getCategory().isSensitive()) return null; + String transcriptUrl = null; + int ticketId = ticket.getId(); + try { + if (ticket.getTextChannel() != null && config.getLogChannel() != 0) { + // Fetch messages first to avoid NPE in library when handling message references + List messages = ticket.getTextChannel().getIterableHistory() + .takeAsync(1000) + .get(); + + if (messages != null && !messages.isEmpty()) { + FileUpload htmlTranscriptUpload = DiscordHtmlTranscripts.getInstance() + .createTranscript(ticket.getTextChannel(), "transcript-" + ticketId + ".html"); + + TextChannel logChannel = jda.getGuildById(config.getServerId()).getTextChannelById(config.getLogChannel()); + if (logChannel != null) { + net.dv8tion.jda.api.entities.Message uploadMessage = logChannel.sendFiles(htmlTranscriptUpload).complete(); + transcriptUrl = uploadMessage.getJumpUrl(); + } + } else { + log.warn("No messages found in ticket #{} channel, skipping transcript generation", ticketId); + } + } + } catch (Exception e) { + log.error("Failed to generate/upload HTML transcript for ticket #{}: {}", ticketId, e.getMessage()); + // Continue without transcript - don't let this block ticket closure + } + return transcriptUrl; + } + + public void appendTranscriptLinkAndSendCloseEmbed(String transcriptUrl, Ticket ticket, EmbedBuilder notification) { + boolean isSensitive = ticket.getCategory() != null && ticket.getCategory().isSensitive(); + + // Only add transcript link for non-sensitive categories + if (transcriptUrl != null && !isSensitive) { + notification.addField("πŸ“ Transcript", "[Hier klicken](" + transcriptUrl + ")", false); + } + + List channels = config.getRatingNotificationChannels(); + if (channels == null || channels.isEmpty()) { + return; + } + + for (Long channelId : channels) { + TextChannel channel = jda.getTextChannelById(channelId); + if (channel != null) { + channel.sendMessageEmbeds(notification.build()).queue( + success -> log.info("Close embed send successfully to {}", channelId), + error -> log.error("Failed to send close embed {}: {}", channelId, error.getMessage()) + ); + } else { + log.warn("Rating notification channel not found: {}", channelId); + } + } + } + + public boolean isUserPrivilegedSupporter(Member member) { + Guild guild = jda.getGuildById(config.getServerId()); + if (guild == null) { + return false; + } + + // Check if the member has any of the privileged roles + List privilegedRoles = config.getPrivilegedSupporterRoles(); + if (privilegedRoles != null) { + for (Long roleId : privilegedRoles) { + Role role = guild.getRoleById(roleId); + if (role != null && member.getRoles().contains(role)) { + return true; + } + } + } + + return false; + } +} diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/service/Transcript.java b/src/main/java/eu/greev/dcbot/ticketsystem/service/Transcript.java index bac73ae..d87c0eb 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/service/Transcript.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/service/Transcript.java @@ -37,6 +37,12 @@ public void addLogMessage(String log, long timestamp, int ticketId) { recentChanges.add(message); } + public void addInfoMessage(String key, String info, int ticketId) { + Message message = new Message(0, info, "Info-"+key, Instant.now().getEpochSecond(), ticketId); + messages.add(message); + recentChanges.add(message); + } + public void editMessage(long messageId, String content, long timeEdited) { Edit edit = new Edit(content, timeEdited, messageId); diff --git a/src/main/java/eu/greev/dcbot/ticketsystem/service/XpService.java b/src/main/java/eu/greev/dcbot/ticketsystem/service/XpService.java index 814b34e..3087d5c 100644 --- a/src/main/java/eu/greev/dcbot/ticketsystem/service/XpService.java +++ b/src/main/java/eu/greev/dcbot/ticketsystem/service/XpService.java @@ -33,6 +33,7 @@ public XpService(Config config, SupporterSettingsData supporterSettingsData) { * Award XP to a helper for resolving a ticket. * This method collects ticket data and sends it to the API asynchronously. * No need to wait since we send the data directly (no channel fetch needed on backend). + * The backend API will send the XP notification in Discord. * * @param ticket The ticket being closed * @param rating The star rating given by the ticket owner (1-5), can be null if skipped @@ -229,7 +230,7 @@ private List> collectMessages(Ticket ticket) { * @deprecated Use {@link #awardTicketXp(Ticket, Integer)} instead. * This old method is kept for backwards compatibility but should not be used. */ - @Deprecated + @Deprecated(since = "2025-12-29") public boolean awardTicketXp(String channelId, String supporterId, Integer rating) { log.warn("[XP] Using deprecated awardTicketXp method - please update to use Ticket object"); // This old method can't work properly anymore since we need full ticket data diff --git a/src/main/java/eu/greev/dcbot/utils/Config.java b/src/main/java/eu/greev/dcbot/utils/Config.java index d044074..3d24308 100644 --- a/src/main/java/eu/greev/dcbot/utils/Config.java +++ b/src/main/java/eu/greev/dcbot/utils/Config.java @@ -35,6 +35,7 @@ public class Config { private int maxTicketsPerUser = 3; private List addToTicketThread; private List ratingNotificationChannels = new ArrayList<>(); + private List privilegedSupporterRoles = new ArrayList<>(); private Map claimEmojis = new HashMap<>(); private Map categories = new HashMap<>(); private Map> categoryRoles = new HashMap<>(); @@ -43,6 +44,7 @@ public class Config { private String xpApiUrl = ""; private String xpApiKey = ""; + public void dumpConfig(String path) { DumperOptions options = new DumperOptions(); options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);