From 8e1ae7d24352ce331c9d46ec09497e540c4050dd Mon Sep 17 00:00:00 2001 From: vishv843 <201901453@daiict.ac.in> Date: Thu, 29 Aug 2024 12:14:46 -0400 Subject: [PATCH 1/5] added help stats command --- .../togetherjava/tjbot/features/Features.java | 12 +- .../features/help/HelpThreadStatsCommand.java | 156 ++++++++++++++++++ 2 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java diff --git a/application/src/main/java/org/togetherjava/tjbot/features/Features.java b/application/src/main/java/org/togetherjava/tjbot/features/Features.java index 893adbc00f..657dd1f5b7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -23,16 +23,7 @@ import org.togetherjava.tjbot.features.filesharing.FileSharingMessageListener; import org.togetherjava.tjbot.features.github.GitHubCommand; import org.togetherjava.tjbot.features.github.GitHubReference; -import org.togetherjava.tjbot.features.help.GuildLeaveCloseThreadListener; -import org.togetherjava.tjbot.features.help.HelpSystemHelper; -import org.togetherjava.tjbot.features.help.HelpThreadActivityUpdater; -import org.togetherjava.tjbot.features.help.HelpThreadAutoArchiver; -import org.togetherjava.tjbot.features.help.HelpThreadCommand; -import org.togetherjava.tjbot.features.help.HelpThreadCreatedListener; -import org.togetherjava.tjbot.features.help.HelpThreadLifecycleListener; -import org.togetherjava.tjbot.features.help.HelpThreadMetadataPurger; -import org.togetherjava.tjbot.features.help.MarkHelpThreadCloseInDBRoutine; -import org.togetherjava.tjbot.features.help.PinnedNotificationRemover; +import org.togetherjava.tjbot.features.help.*; import org.togetherjava.tjbot.features.javamail.RSSHandlerRoutine; import org.togetherjava.tjbot.features.jshell.JShellCommand; import org.togetherjava.tjbot.features.jshell.JShellEval; @@ -192,6 +183,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new BookmarksCommand(bookmarksSystem)); features.add(new ChatGptCommand(chatGptService, helpSystemHelper)); features.add(new JShellCommand(jshellEval)); + features.add(new HelpThreadStatsCommand()); FeatureBlacklist> blacklist = blacklistConfig.normal(); return blacklist.filterStream(features.stream(), Object::getClass).toList(); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java new file mode 100644 index 0000000000..c61ea74f8a --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java @@ -0,0 +1,156 @@ +package org.togetherjava.tjbot.features.help; + +import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.forums.ForumTag; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; +import net.dv8tion.jda.api.requests.restaction.pagination.ThreadChannelPaginationAction; + +import org.togetherjava.tjbot.features.CommandVisibility; +import org.togetherjava.tjbot.features.SlashCommandAdapter; + +import java.time.OffsetDateTime; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.averagingDouble; +import static java.util.stream.Collectors.toMap; + +public class HelpThreadStatsCommand extends SlashCommandAdapter { + + public static final String COMMAND_NAME = "help-thread-stats"; + public static final String DURATION_OPTION = "duration-option"; + public static final String DURATION_SUBCOMMAND = "duration"; + public static final String OPTIONAL_SUBCOMMAND_GROUP = "optional"; + private final Map nameToSubcommand; + + public HelpThreadStatsCommand() { + super(COMMAND_NAME, "Display Help Thread Statistics", CommandVisibility.GUILD); + OptionData durationOption = + new OptionData(OptionType.STRING, DURATION_OPTION, "optional duration", false) + .setMinLength(1); + SubcommandData duration = Subcommand.DURATION.toSubcommandData().addOptions(durationOption); + SubcommandGroupData optionalCommands = + new SubcommandGroupData(OPTIONAL_SUBCOMMAND_GROUP, "optional commands") + .addSubcommands(duration); + getData().addSubcommandGroups(optionalCommands); + nameToSubcommand = streamSubcommands() + .collect(Collectors.toMap(Subcommand::getCommandName, Function.identity())); + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event) { + List forumChannels = + Objects.requireNonNull(event.getGuild()).getForumChannels(); + Subcommand invokedSubcommand = nameToSubcommand.get(event.getSubcommandName()); + OffsetDateTime startDate = OffsetDateTime.MIN; + if (Objects.nonNull(invokedSubcommand) && invokedSubcommand.equals(Subcommand.DURATION) + && Objects.nonNull(event.getOption(DURATION_OPTION))) { + startDate = + OffsetDateTime.now().minusDays(event.getOption(DURATION_OPTION).getAsLong()); + } + ForumTag mostPopularTag = getMostPopularForumTag(forumChannels, startDate); + Double averageNumberOfParticipants = + getAverageNumberOfParticipantsPerThread(forumChannels, startDate); + Integer totalNumberOfThreads = + getThreadChannelsStream(forumChannels, startDate).toList().size(); + Long emptyThreads = getThreadsWithNoParticipants(forumChannels, startDate); + Integer totalMessages = getTotalNumberOfMessages(forumChannels, startDate); + Double averageNumberOfMessages = Double.valueOf(totalMessages) / totalNumberOfThreads; + Double averageThreadLifecycle = getAverageThreadLifecycle(forumChannels, startDate); + String statistics = + "Most Popular Tag: %s%nAverage Number Of Participants: %.2f%nEmpty Threads: %s%nAverage Number Of Messages: %.2f%nAverage Thread Lifecycle: %.2f" + .formatted(mostPopularTag.getName(), averageNumberOfParticipants, emptyThreads, + averageNumberOfMessages, averageThreadLifecycle); + event.reply(statistics).delay(2, TimeUnit.SECONDS).queue(); + } + + private ForumTag getMostPopularForumTag(List forumChannels, + OffsetDateTime startDate) { + Map tagCount = getThreadChannelsStream(forumChannels, startDate) + .flatMap((threadChannel -> threadChannel.getAppliedTags().stream())) + .collect(toMap(Function.identity(), tag -> 1, Integer::sum)); + return Collections.max(tagCount.entrySet(), Map.Entry.comparingByValue()).getKey(); + } + + private Double getAverageNumberOfParticipantsPerThread(List forumChannels, + OffsetDateTime startDate) { + return getThreadChannelsStream(forumChannels, startDate) + .collect(averagingDouble((ThreadChannel::getMemberCount))); + } + + private Long getThreadsWithNoParticipants(List forumChannels, + OffsetDateTime startDate) { + return getThreadChannelsStream(forumChannels, startDate) + .filter(threadChannel -> threadChannel.getMemberCount() > 1) + .count(); + } + + private Integer getTotalNumberOfMessages(List forumChannels, + OffsetDateTime startDate) { + return getThreadChannelsStream(forumChannels, startDate) + .mapToInt(ThreadChannel::getMessageCount) + .sum(); + } + + private Double getAverageThreadLifecycle(List forumChannels, + OffsetDateTime startDate) { + return getThreadChannelsStream(forumChannels, startDate).filter(ThreadChannel::isArchived) + .mapToDouble(threadChannel -> calculateDurationInDays( + threadChannel.getTimeArchiveInfoLastModified(), threadChannel.getTimeCreated())) + .average() + .orElse(0); + } + + private Double calculateDurationInDays(OffsetDateTime t1, OffsetDateTime t2) { + long time1 = t1.toEpochSecond(); + long time2 = t2.toEpochSecond(); + return (time1 - time2) / 86400.0; + } + + private Stream getThreadChannelsStream(List forumChannels, + OffsetDateTime startDate) { + return forumChannels.stream() + .flatMap(forumChannel -> getAllThreadChannels(forumChannel).stream()) + .filter(threadChannel -> threadChannel.getTimeCreated().isAfter(startDate)); + } + + private Set getAllThreadChannels(ForumChannel forumChannel) { + Set threadChannels = new HashSet<>(forumChannel.getThreadChannels()); + Optional publicThreadChannels = + Optional.of(forumChannel.retrieveArchivedPublicThreadChannels()); + publicThreadChannels.ifPresent(threads -> threads.forEach(threadChannels::add)); + return threadChannels; + } + + private static Stream streamSubcommands() { + return Arrays.stream(Subcommand.values()); + } + + enum Subcommand { + DURATION(DURATION_SUBCOMMAND, "Set the duration"); + + private final String commandName; + private final String description; + + Subcommand(String commandName, String description) { + this.commandName = commandName; + this.description = description; + } + + String getCommandName() { + return commandName; + } + + SubcommandData toSubcommandData() { + return new SubcommandData(commandName, description); + } + } +} From a1e9ab5acfb901d55ce781d0584612b079b55e31 Mon Sep 17 00:00:00 2001 From: Ankit Yadav Date: Sun, 21 Dec 2025 16:20:08 +0530 Subject: [PATCH 2/5] refactor: help-stats slash command replaces fetching metrics directly from database instead of discord, uses embed for showcases stats and making duration as optional choice in terms of days --- .../togetherjava/tjbot/features/Features.java | 2 +- .../features/help/HelpThreadStatsCommand.java | 270 ++++++++++-------- 2 files changed, 144 insertions(+), 128 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/Features.java b/application/src/main/java/org/togetherjava/tjbot/features/Features.java index aa95930c07..b9d339be95 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -204,7 +204,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new BookmarksCommand(bookmarksSystem)); features.add(new ChatGptCommand(chatGptService, helpSystemHelper)); features.add(new JShellCommand(jshellEval)); - features.add(new HelpThreadStatsCommand()); + features.add(new HelpThreadStatsCommand(database)); FeatureBlacklist> blacklist = blacklistConfig.normal(); return blacklist.filterStream(features.stream(), Object::getClass).toList(); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java index c61ea74f8a..efab34a5c8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java @@ -1,156 +1,172 @@ package org.togetherjava.tjbot.features.help; -import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel; -import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; -import net.dv8tion.jda.api.entities.channel.forums.ForumTag; +import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; -import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; -import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; -import net.dv8tion.jda.api.requests.restaction.pagination.ThreadChannelPaginationAction; - +import org.jooq.DSLContext; +import org.jooq.OrderField; +import org.jooq.Record1; +import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.features.CommandVisibility; import org.togetherjava.tjbot.features.SlashCommandAdapter; -import java.time.OffsetDateTime; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.util.stream.Collectors.averagingDouble; -import static java.util.stream.Collectors.toMap; - +import java.awt.Color; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Objects; + +import static org.jooq.impl.DSL.*; +import static org.togetherjava.tjbot.db.generated.Tables.HELP_THREADS; + +/** + * Implements the '/help-thread-stats' command which provides analytical insights into the + * help forum's activity over a specific duration. + *

+ * Example usage: + *

+ * {@code
+ * /help-thread-stats duration-option: 7 Days
+ * }
+ * 
+ *

+ * The command aggregates data such as response rates, engagement metrics (messages/helpers), + * tag popularity, and resolution speeds. + */ public class HelpThreadStatsCommand extends SlashCommandAdapter { - public static final String COMMAND_NAME = "help-thread-stats"; public static final String DURATION_OPTION = "duration-option"; - public static final String DURATION_SUBCOMMAND = "duration"; - public static final String OPTIONAL_SUBCOMMAND_GROUP = "optional"; - private final Map nameToSubcommand; - - public HelpThreadStatsCommand() { - super(COMMAND_NAME, "Display Help Thread Statistics", CommandVisibility.GUILD); - OptionData durationOption = - new OptionData(OptionType.STRING, DURATION_OPTION, "optional duration", false) - .setMinLength(1); - SubcommandData duration = Subcommand.DURATION.toSubcommandData().addOptions(durationOption); - SubcommandGroupData optionalCommands = - new SubcommandGroupData(OPTIONAL_SUBCOMMAND_GROUP, "optional commands") - .addSubcommands(duration); - getData().addSubcommandGroups(optionalCommands); - nameToSubcommand = streamSubcommands() - .collect(Collectors.toMap(Subcommand::getCommandName, Function.identity())); - } - @Override - public void onSlashCommand(SlashCommandInteractionEvent event) { - List forumChannels = - Objects.requireNonNull(event.getGuild()).getForumChannels(); - Subcommand invokedSubcommand = nameToSubcommand.get(event.getSubcommandName()); - OffsetDateTime startDate = OffsetDateTime.MIN; - if (Objects.nonNull(invokedSubcommand) && invokedSubcommand.equals(Subcommand.DURATION) - && Objects.nonNull(event.getOption(DURATION_OPTION))) { - startDate = - OffsetDateTime.now().minusDays(event.getOption(DURATION_OPTION).getAsLong()); - } - ForumTag mostPopularTag = getMostPopularForumTag(forumChannels, startDate); - Double averageNumberOfParticipants = - getAverageNumberOfParticipantsPerThread(forumChannels, startDate); - Integer totalNumberOfThreads = - getThreadChannelsStream(forumChannels, startDate).toList().size(); - Long emptyThreads = getThreadsWithNoParticipants(forumChannels, startDate); - Integer totalMessages = getTotalNumberOfMessages(forumChannels, startDate); - Double averageNumberOfMessages = Double.valueOf(totalMessages) / totalNumberOfThreads; - Double averageThreadLifecycle = getAverageThreadLifecycle(forumChannels, startDate); - String statistics = - "Most Popular Tag: %s%nAverage Number Of Participants: %.2f%nEmpty Threads: %s%nAverage Number Of Messages: %.2f%nAverage Thread Lifecycle: %.2f" - .formatted(mostPopularTag.getName(), averageNumberOfParticipants, emptyThreads, - averageNumberOfMessages, averageThreadLifecycle); - event.reply(statistics).delay(2, TimeUnit.SECONDS).queue(); - } + private final Database database; - private ForumTag getMostPopularForumTag(List forumChannels, - OffsetDateTime startDate) { - Map tagCount = getThreadChannelsStream(forumChannels, startDate) - .flatMap((threadChannel -> threadChannel.getAppliedTags().stream())) - .collect(toMap(Function.identity(), tag -> 1, Integer::sum)); - return Collections.max(tagCount.entrySet(), Map.Entry.comparingByValue()).getKey(); - } + /** + * Creates an instance of the command. + * + * @param database the database to fetch help thread metrics from + */ + public HelpThreadStatsCommand(Database database) { + super(COMMAND_NAME, "Display Help Thread Statistics", CommandVisibility.GUILD); - private Double getAverageNumberOfParticipantsPerThread(List forumChannels, - OffsetDateTime startDate) { - return getThreadChannelsStream(forumChannels, startDate) - .collect(averagingDouble((ThreadChannel::getMemberCount))); - } + OptionData durationOption = new OptionData(OptionType.INTEGER, DURATION_OPTION, "The time range for statistics", false) + .addChoice("1 Day", 1) + .addChoice("7 Days", 7) + .addChoice("30 Days", 30) + .addChoice("90 Days", 90) + .addChoice("180 Days", 180); - private Long getThreadsWithNoParticipants(List forumChannels, - OffsetDateTime startDate) { - return getThreadChannelsStream(forumChannels, startDate) - .filter(threadChannel -> threadChannel.getMemberCount() > 1) - .count(); + getData().addOptions(durationOption); + this.database = database; } - private Integer getTotalNumberOfMessages(List forumChannels, - OffsetDateTime startDate) { - return getThreadChannelsStream(forumChannels, startDate) - .mapToInt(ThreadChannel::getMessageCount) - .sum(); + @Override + public void onSlashCommand(SlashCommandInteractionEvent event) { + long days = event.getOption(DURATION_OPTION) != null + ? Objects.requireNonNull(event.getOption(DURATION_OPTION)).getAsLong() + : 1; + Instant startDate = Instant.now().minus(days, ChronoUnit.DAYS); + + event.deferReply().queue(); + + database.read(context -> { + var statsRecord = context.select( + count().as("total_created"), + count().filterWhere(HELP_THREADS.TICKET_STATUS.eq(HelpSystemHelper.TicketStatus.ACTIVE.val)).as("open_now"), + count().filterWhere(HELP_THREADS.PARTICIPANTS.eq(1)).as("ghost_count"), + avg(HELP_THREADS.PARTICIPANTS).as("avg_parts"), + avg(HELP_THREADS.MESSAGE_COUNT).as("avg_msgs"), + avg(field("unixepoch({0}) - unixepoch({1})", Double.class, HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)).as("avg_sec"), + min(field("unixepoch({0}) - unixepoch({1})", Double.class, HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)).as("min_sec"), + max(field("unixepoch({0}) - unixepoch({1})", Double.class, HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)).as("max_sec") + ) + .from(HELP_THREADS) + .where(HELP_THREADS.CREATED_AT.ge(startDate)) + .fetchOne(); + + if (statsRecord == null || statsRecord.get("total_created", Integer.class) == 0) { + event.getHook().editOriginal("No stats available for the last " + days + " days.").queue(); + return null; + } + + int totalCreated = statsRecord.get("total_created", Integer.class); + int openThreads = statsRecord.get("open_now", Integer.class); + long ghostThreads = statsRecord.get("ghost_count", Number.class).longValue(); + + double rawResRate = totalCreated > 0 ? ((double) (totalCreated - ghostThreads) / totalCreated) * 100 : 0; + + String highVolumeTag = getTopTag(context, startDate, count().desc()); + String highActivityTag = getTopTag(context, startDate, avg(HELP_THREADS.MESSAGE_COUNT).desc()); + String lowActivityTag = getTopTag(context, startDate, avg(HELP_THREADS.MESSAGE_COUNT).asc()); + + String peakHourRange = getPeakHour(context, startDate); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("📊 Help Thread Stats (Last " + days + " Days)") + .setColor(getStatusColor(totalCreated, ghostThreads)) + .setTimestamp(Instant.now()) + .setDescription("\u200B") + .setFooter("Together Java Community Stats", Objects.requireNonNull(event.getGuild()).getIconUrl()); + + embed.addField("📝 THREAD ACTIVITY", + "Created: `%d`\nCurrently Open: `%d`\nResponse Rate: %.1f%%\nPeak Hours: `%s`" + .formatted(totalCreated, openThreads, rawResRate, peakHourRange), false); + + embed.addField("💬 ENGAGEMENT", + "Avg Messages: `%s`\nAvg Helpers: `%s`\nUnanswered (Ghost): `%d`".formatted( + formatDouble(Objects.requireNonNull(statsRecord.get("avg_msgs"))), + formatDouble(Objects.requireNonNull(statsRecord.get("avg_parts"))), + ghostThreads), false); + + embed.addField("🏷️ TAG ACTIVITY", + "Most Used: `%s`\nMost Active: `%s`\nNeeds Love: `%s`".formatted( + highVolumeTag, highActivityTag, lowActivityTag), false); + + embed.addField("⚡ RESOLUTION SPEED", + "Average: `%s`\nFastest: `%s`\nSlowest: `%s`".formatted( + smartFormat(statsRecord.get("avg_sec", Double.class)), + smartFormat(statsRecord.get("min_sec", Double.class)), + smartFormat(statsRecord.get("max_sec", Double.class))), false); + + event.getHook().editOriginalEmbeds(embed.build()).queue(); + return null; + }); } - private Double getAverageThreadLifecycle(List forumChannels, - OffsetDateTime startDate) { - return getThreadChannelsStream(forumChannels, startDate).filter(ThreadChannel::isArchived) - .mapToDouble(threadChannel -> calculateDurationInDays( - threadChannel.getTimeArchiveInfoLastModified(), threadChannel.getTimeCreated())) - .average() - .orElse(0); - } + private static Color getStatusColor(int totalCreated, long ghostThreads) { + double rawResRate = totalCreated > 0 ? ((double) (totalCreated - ghostThreads) / totalCreated) * 100 : -1; - private Double calculateDurationInDays(OffsetDateTime t1, OffsetDateTime t2) { - long time1 = t1.toEpochSecond(); - long time2 = t2.toEpochSecond(); - return (time1 - time2) / 86400.0; + if (rawResRate >= 70) return Color.GREEN; + if (rawResRate >= 30) return Color.YELLOW; + if (rawResRate >= 0) return Color.RED; + return Color.GRAY; } - private Stream getThreadChannelsStream(List forumChannels, - OffsetDateTime startDate) { - return forumChannels.stream() - .flatMap(forumChannel -> getAllThreadChannels(forumChannel).stream()) - .filter(threadChannel -> threadChannel.getTimeCreated().isAfter(startDate)); + private String getTopTag(DSLContext context, Instant start, OrderField order) { + return context.select(HELP_THREADS.TAGS).from(HELP_THREADS) + .where(HELP_THREADS.CREATED_AT.ge(start)).and(HELP_THREADS.TAGS.ne("none")) + .groupBy(HELP_THREADS.TAGS).orderBy(order).limit(1) + .fetchOptional(HELP_THREADS.TAGS).orElse("N/A"); } - private Set getAllThreadChannels(ForumChannel forumChannel) { - Set threadChannels = new HashSet<>(forumChannel.getThreadChannels()); - Optional publicThreadChannels = - Optional.of(forumChannel.retrieveArchivedPublicThreadChannels()); - publicThreadChannels.ifPresent(threads -> threads.forEach(threadChannels::add)); - return threadChannels; + private String getPeakHour(DSLContext context, Instant start) { + return context.select(field("strftime('%H', {0})", String.class, HELP_THREADS.CREATED_AT)) + .from(HELP_THREADS).where(HELP_THREADS.CREATED_AT.ge(start)) + .groupBy(field("strftime('%H', {0})", String.class, HELP_THREADS.CREATED_AT)) + .orderBy(count().desc()).limit(1).fetchOptional(Record1::value1) + .map(hour -> { + int h = Integer.parseInt(hour); + return "%02d:00 - %02d:00 UTC".formatted(h, (h + 1) % 24); + }).orElse("N/A"); } - private static Stream streamSubcommands() { - return Arrays.stream(Subcommand.values()); + private String smartFormat(Double seconds) { + if (seconds < 0) return "N/A"; + if (seconds < 60) return "%.0f secs".formatted(seconds); + if (seconds < 3600) return "%.1f mins".formatted(seconds / 60.0); + if (seconds < 86400) return "%.1f hrs".formatted(seconds / 3600.0); + return "%.1f days".formatted(seconds / 86400.0); } - enum Subcommand { - DURATION(DURATION_SUBCOMMAND, "Set the duration"); - - private final String commandName; - private final String description; - - Subcommand(String commandName, String description) { - this.commandName = commandName; - this.description = description; - } - - String getCommandName() { - return commandName; - } - - SubcommandData toSubcommandData() { - return new SubcommandData(commandName, description); - } + private String formatDouble(Object val) { + return val instanceof Number num ? "%.2f".formatted(num.doubleValue()) : "0.00"; } -} +} \ No newline at end of file From 70fafefa6b0446d8eaaafa222e1827c6fcbdb79f Mon Sep 17 00:00:00 2001 From: Ankit Yadav Date: Sun, 21 Dec 2025 16:38:20 +0530 Subject: [PATCH 3/5] chore: spotless fix --- .../features/help/HelpThreadStatsCommand.java | 157 +++++++++++------- 1 file changed, 98 insertions(+), 59 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java index efab34a5c8..9b3bd08788 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java @@ -7,6 +7,7 @@ import org.jooq.DSLContext; import org.jooq.OrderField; import org.jooq.Record1; + import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.features.CommandVisibility; import org.togetherjava.tjbot.features.SlashCommandAdapter; @@ -20,18 +21,19 @@ import static org.togetherjava.tjbot.db.generated.Tables.HELP_THREADS; /** - * Implements the '/help-thread-stats' command which provides analytical insights into the - * help forum's activity over a specific duration. + * Implements the '/help-thread-stats' command which provides analytical insights into the help + * forum's activity over a specific duration. *

* Example usage: + * *

  * {@code
  * /help-thread-stats duration-option: 7 Days
  * }
  * 
*

- * The command aggregates data such as response rates, engagement metrics (messages/helpers), - * tag popularity, and resolution speeds. + * The command aggregates data such as response rates, engagement metrics (messages/helpers), tag + * popularity, and resolution speeds. */ public class HelpThreadStatsCommand extends SlashCommandAdapter { public static final String COMMAND_NAME = "help-thread-stats"; @@ -47,12 +49,13 @@ public class HelpThreadStatsCommand extends SlashCommandAdapter { public HelpThreadStatsCommand(Database database) { super(COMMAND_NAME, "Display Help Thread Statistics", CommandVisibility.GUILD); - OptionData durationOption = new OptionData(OptionType.INTEGER, DURATION_OPTION, "The time range for statistics", false) - .addChoice("1 Day", 1) - .addChoice("7 Days", 7) - .addChoice("30 Days", 30) - .addChoice("90 Days", 90) - .addChoice("180 Days", 180); + OptionData durationOption = new OptionData(OptionType.INTEGER, DURATION_OPTION, + "The time range for statistics", false) + .addChoice("1 Day", 1) + .addChoice("7 Days", 7) + .addChoice("30 Days", 30) + .addChoice("90 Days", 90) + .addChoice("180 Days", 180); getData().addOptions(durationOption); this.database = database; @@ -68,22 +71,31 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { event.deferReply().queue(); database.read(context -> { - var statsRecord = context.select( - count().as("total_created"), - count().filterWhere(HELP_THREADS.TICKET_STATUS.eq(HelpSystemHelper.TicketStatus.ACTIVE.val)).as("open_now"), - count().filterWhere(HELP_THREADS.PARTICIPANTS.eq(1)).as("ghost_count"), - avg(HELP_THREADS.PARTICIPANTS).as("avg_parts"), - avg(HELP_THREADS.MESSAGE_COUNT).as("avg_msgs"), - avg(field("unixepoch({0}) - unixepoch({1})", Double.class, HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)).as("avg_sec"), - min(field("unixepoch({0}) - unixepoch({1})", Double.class, HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)).as("min_sec"), - max(field("unixepoch({0}) - unixepoch({1})", Double.class, HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)).as("max_sec") - ) - .from(HELP_THREADS) - .where(HELP_THREADS.CREATED_AT.ge(startDate)) - .fetchOne(); + var statsRecord = context + .select(count().as("total_created"), count() + .filterWhere( + HELP_THREADS.TICKET_STATUS.eq(HelpSystemHelper.TicketStatus.ACTIVE.val)) + .as("open_now"), + count().filterWhere(HELP_THREADS.PARTICIPANTS.eq(1)).as("ghost_count"), + avg(HELP_THREADS.PARTICIPANTS).as("avg_parts"), + avg(HELP_THREADS.MESSAGE_COUNT).as("avg_msgs"), + avg(field("unixepoch({0}) - unixepoch({1})", Double.class, + HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)) + .as("avg_sec"), + min(field("unixepoch({0}) - unixepoch({1})", Double.class, + HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)) + .as("min_sec"), + max(field("unixepoch({0}) - unixepoch({1})", Double.class, + HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)) + .as("max_sec")) + .from(HELP_THREADS) + .where(HELP_THREADS.CREATED_AT.ge(startDate)) + .fetchOne(); if (statsRecord == null || statsRecord.get("total_created", Integer.class) == 0) { - event.getHook().editOriginal("No stats available for the last " + days + " days.").queue(); + event.getHook() + .editOriginal("No stats available for the last " + days + " days.") + .queue(); return null; } @@ -91,40 +103,49 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { int openThreads = statsRecord.get("open_now", Integer.class); long ghostThreads = statsRecord.get("ghost_count", Number.class).longValue(); - double rawResRate = totalCreated > 0 ? ((double) (totalCreated - ghostThreads) / totalCreated) * 100 : 0; + double rawResRate = + totalCreated > 0 ? ((double) (totalCreated - ghostThreads) / totalCreated) * 100 + : 0; String highVolumeTag = getTopTag(context, startDate, count().desc()); - String highActivityTag = getTopTag(context, startDate, avg(HELP_THREADS.MESSAGE_COUNT).desc()); - String lowActivityTag = getTopTag(context, startDate, avg(HELP_THREADS.MESSAGE_COUNT).asc()); + String highActivityTag = + getTopTag(context, startDate, avg(HELP_THREADS.MESSAGE_COUNT).desc()); + String lowActivityTag = + getTopTag(context, startDate, avg(HELP_THREADS.MESSAGE_COUNT).asc()); String peakHourRange = getPeakHour(context, startDate); - EmbedBuilder embed = new EmbedBuilder() - .setTitle("📊 Help Thread Stats (Last " + days + " Days)") - .setColor(getStatusColor(totalCreated, ghostThreads)) - .setTimestamp(Instant.now()) - .setDescription("\u200B") - .setFooter("Together Java Community Stats", Objects.requireNonNull(event.getGuild()).getIconUrl()); + EmbedBuilder embed = + new EmbedBuilder().setTitle("📊 Help Thread Stats (Last " + days + " Days)") + .setColor(getStatusColor(totalCreated, ghostThreads)) + .setTimestamp(Instant.now()) + .setDescription("\u200B") + .setFooter("Together Java Community Stats", + Objects.requireNonNull(event.getGuild()).getIconUrl()); embed.addField("📝 THREAD ACTIVITY", "Created: `%d`\nCurrently Open: `%d`\nResponse Rate: %.1f%%\nPeak Hours: `%s`" - .formatted(totalCreated, openThreads, rawResRate, peakHourRange), false); + .formatted(totalCreated, openThreads, rawResRate, peakHourRange), + false); embed.addField("💬 ENGAGEMENT", "Avg Messages: `%s`\nAvg Helpers: `%s`\nUnanswered (Ghost): `%d`".formatted( formatDouble(Objects.requireNonNull(statsRecord.get("avg_msgs"))), formatDouble(Objects.requireNonNull(statsRecord.get("avg_parts"))), - ghostThreads), false); + ghostThreads), + false); embed.addField("🏷️ TAG ACTIVITY", - "Most Used: `%s`\nMost Active: `%s`\nNeeds Love: `%s`".formatted( - highVolumeTag, highActivityTag, lowActivityTag), false); + "Most Used: `%s`\nMost Active: `%s`\nNeeds Love: `%s`".formatted(highVolumeTag, + highActivityTag, lowActivityTag), + false); embed.addField("⚡ RESOLUTION SPEED", "Average: `%s`\nFastest: `%s`\nSlowest: `%s`".formatted( smartFormat(statsRecord.get("avg_sec", Double.class)), smartFormat(statsRecord.get("min_sec", Double.class)), - smartFormat(statsRecord.get("max_sec", Double.class))), false); + smartFormat(statsRecord.get("max_sec", Double.class))), + false); event.getHook().editOriginalEmbeds(embed.build()).queue(); return null; @@ -132,41 +153,59 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { } private static Color getStatusColor(int totalCreated, long ghostThreads) { - double rawResRate = totalCreated > 0 ? ((double) (totalCreated - ghostThreads) / totalCreated) * 100 : -1; - - if (rawResRate >= 70) return Color.GREEN; - if (rawResRate >= 30) return Color.YELLOW; - if (rawResRate >= 0) return Color.RED; + double rawResRate = + totalCreated > 0 ? ((double) (totalCreated - ghostThreads) / totalCreated) * 100 + : -1; + + if (rawResRate >= 70) + return Color.GREEN; + if (rawResRate >= 30) + return Color.YELLOW; + if (rawResRate >= 0) + return Color.RED; return Color.GRAY; } private String getTopTag(DSLContext context, Instant start, OrderField order) { - return context.select(HELP_THREADS.TAGS).from(HELP_THREADS) - .where(HELP_THREADS.CREATED_AT.ge(start)).and(HELP_THREADS.TAGS.ne("none")) - .groupBy(HELP_THREADS.TAGS).orderBy(order).limit(1) - .fetchOptional(HELP_THREADS.TAGS).orElse("N/A"); + return context.select(HELP_THREADS.TAGS) + .from(HELP_THREADS) + .where(HELP_THREADS.CREATED_AT.ge(start)) + .and(HELP_THREADS.TAGS.ne("none")) + .groupBy(HELP_THREADS.TAGS) + .orderBy(order) + .limit(1) + .fetchOptional(HELP_THREADS.TAGS) + .orElse("N/A"); } private String getPeakHour(DSLContext context, Instant start) { return context.select(field("strftime('%H', {0})", String.class, HELP_THREADS.CREATED_AT)) - .from(HELP_THREADS).where(HELP_THREADS.CREATED_AT.ge(start)) - .groupBy(field("strftime('%H', {0})", String.class, HELP_THREADS.CREATED_AT)) - .orderBy(count().desc()).limit(1).fetchOptional(Record1::value1) - .map(hour -> { - int h = Integer.parseInt(hour); - return "%02d:00 - %02d:00 UTC".formatted(h, (h + 1) % 24); - }).orElse("N/A"); + .from(HELP_THREADS) + .where(HELP_THREADS.CREATED_AT.ge(start)) + .groupBy(field("strftime('%H', {0})", String.class, HELP_THREADS.CREATED_AT)) + .orderBy(count().desc()) + .limit(1) + .fetchOptional(Record1::value1) + .map(hour -> { + int h = Integer.parseInt(hour); + return "%02d:00 - %02d:00 UTC".formatted(h, (h + 1) % 24); + }) + .orElse("N/A"); } private String smartFormat(Double seconds) { - if (seconds < 0) return "N/A"; - if (seconds < 60) return "%.0f secs".formatted(seconds); - if (seconds < 3600) return "%.1f mins".formatted(seconds / 60.0); - if (seconds < 86400) return "%.1f hrs".formatted(seconds / 3600.0); + if (seconds < 0) + return "N/A"; + if (seconds < 60) + return "%.0f secs".formatted(seconds); + if (seconds < 3600) + return "%.1f mins".formatted(seconds / 60.0); + if (seconds < 86400) + return "%.1f hrs".formatted(seconds / 3600.0); return "%.1f days".formatted(seconds / 86400.0); } private String formatDouble(Object val) { return val instanceof Number num ? "%.2f".formatted(num.doubleValue()) : "0.00"; } -} \ No newline at end of file +} From 31f419066dfee5b0a6c1ced3af43ec1068cfbd57 Mon Sep 17 00:00:00 2001 From: Ankit Yadav Date: Sun, 21 Dec 2025 16:50:57 +0530 Subject: [PATCH 4/5] refactor: helper for duration in seconds calculation spotless fixes, helper for calculating duration in seconds between datetime fields to be more compliant with code quality tests --- .../features/help/HelpThreadStatsCommand.java | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java index 9b3bd08788..ebea078445 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java @@ -5,6 +5,7 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; import org.jooq.DSLContext; +import org.jooq.Field; import org.jooq.OrderField; import org.jooq.Record1; @@ -38,6 +39,14 @@ public class HelpThreadStatsCommand extends SlashCommandAdapter { public static final String COMMAND_NAME = "help-thread-stats"; public static final String DURATION_OPTION = "duration-option"; + private static final String TOTAL_CREATED_FIELD = "total_created"; + private static final String OPEN_NOW_ALIAS = "open_now"; + private static final String GHOST_NOW_ALIAS = "ghost_count"; + private static final String AVERAGE_PARTICIPANTS_ALIAS = "avg_parts"; + private static final String AVERAGE_MESSAGE_COUNT_ALIAS = "avg_msgs"; + private static final String AVERAGE_THREAD_DURATION_IN_SECONDS_ALIAS = "avg_sec"; + private static final String MINIMUM_THREAD_DURATION_IN_SECONDS_ALIAS = "min_sec"; + private static final String MAXIMUM_THREAD_DURATION_IN_SECONDS_ALIAS = "max_sec"; private final Database database; @@ -72,36 +81,33 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { database.read(context -> { var statsRecord = context - .select(count().as("total_created"), count() + .select(count().as(TOTAL_CREATED_FIELD), count() .filterWhere( HELP_THREADS.TICKET_STATUS.eq(HelpSystemHelper.TicketStatus.ACTIVE.val)) - .as("open_now"), - count().filterWhere(HELP_THREADS.PARTICIPANTS.eq(1)).as("ghost_count"), - avg(HELP_THREADS.PARTICIPANTS).as("avg_parts"), - avg(HELP_THREADS.MESSAGE_COUNT).as("avg_msgs"), - avg(field("unixepoch({0}) - unixepoch({1})", Double.class, - HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)) - .as("avg_sec"), - min(field("unixepoch({0}) - unixepoch({1})", Double.class, - HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)) - .as("min_sec"), - max(field("unixepoch({0}) - unixepoch({1})", Double.class, - HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)) - .as("max_sec")) + .as(OPEN_NOW_ALIAS), + count().filterWhere(HELP_THREADS.PARTICIPANTS.eq(1)).as(GHOST_NOW_ALIAS), + avg(HELP_THREADS.PARTICIPANTS).as(AVERAGE_PARTICIPANTS_ALIAS), + avg(HELP_THREADS.MESSAGE_COUNT).as(AVERAGE_MESSAGE_COUNT_ALIAS), + avg(durationInSeconds(HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)) + .as(AVERAGE_THREAD_DURATION_IN_SECONDS_ALIAS), + min(durationInSeconds(HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)) + .as(MINIMUM_THREAD_DURATION_IN_SECONDS_ALIAS), + max(durationInSeconds(HELP_THREADS.CLOSED_AT, HELP_THREADS.CREATED_AT)) + .as(MAXIMUM_THREAD_DURATION_IN_SECONDS_ALIAS)) .from(HELP_THREADS) .where(HELP_THREADS.CREATED_AT.ge(startDate)) .fetchOne(); - if (statsRecord == null || statsRecord.get("total_created", Integer.class) == 0) { + if (statsRecord == null || statsRecord.get(TOTAL_CREATED_FIELD, Integer.class) == 0) { event.getHook() .editOriginal("No stats available for the last " + days + " days.") .queue(); return null; } - int totalCreated = statsRecord.get("total_created", Integer.class); - int openThreads = statsRecord.get("open_now", Integer.class); - long ghostThreads = statsRecord.get("ghost_count", Number.class).longValue(); + int totalCreated = statsRecord.get(TOTAL_CREATED_FIELD, Integer.class); + int openThreads = statsRecord.get(OPEN_NOW_ALIAS, Integer.class); + long ghostThreads = statsRecord.get(GHOST_NOW_ALIAS, Number.class).longValue(); double rawResRate = totalCreated > 0 ? ((double) (totalCreated - ghostThreads) / totalCreated) * 100 @@ -124,27 +130,32 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { Objects.requireNonNull(event.getGuild()).getIconUrl()); embed.addField("📝 THREAD ACTIVITY", - "Created: `%d`\nCurrently Open: `%d`\nResponse Rate: %.1f%%\nPeak Hours: `%s`" + "Created: `%d`%nCurrently Open: `%d`%nResponse Rate: %.1f%%%nPeak Hours: `%s`" .formatted(totalCreated, openThreads, rawResRate, peakHourRange), false); embed.addField("💬 ENGAGEMENT", - "Avg Messages: `%s`\nAvg Helpers: `%s`\nUnanswered (Ghost): `%d`".formatted( - formatDouble(Objects.requireNonNull(statsRecord.get("avg_msgs"))), - formatDouble(Objects.requireNonNull(statsRecord.get("avg_parts"))), + "Avg Messages: `%s`%nAvg Helpers: `%s`%nUnanswered (Ghost): `%d`".formatted( + formatDouble(Objects + .requireNonNull(statsRecord.get(AVERAGE_MESSAGE_COUNT_ALIAS))), + formatDouble(Objects + .requireNonNull(statsRecord.get(AVERAGE_PARTICIPANTS_ALIAS))), ghostThreads), false); embed.addField("🏷️ TAG ACTIVITY", - "Most Used: `%s`\nMost Active: `%s`\nNeeds Love: `%s`".formatted(highVolumeTag, + "Most Used: `%s`%nMost Active: `%s`%nNeeds Love: `%s`".formatted(highVolumeTag, highActivityTag, lowActivityTag), false); embed.addField("⚡ RESOLUTION SPEED", - "Average: `%s`\nFastest: `%s`\nSlowest: `%s`".formatted( - smartFormat(statsRecord.get("avg_sec", Double.class)), - smartFormat(statsRecord.get("min_sec", Double.class)), - smartFormat(statsRecord.get("max_sec", Double.class))), + "Average: `%s`%nFastest: `%s`%nSlowest: `%s`".formatted( + smartFormat(statsRecord.get(AVERAGE_THREAD_DURATION_IN_SECONDS_ALIAS, + Double.class)), + smartFormat(statsRecord.get(MINIMUM_THREAD_DURATION_IN_SECONDS_ALIAS, + Double.class)), + smartFormat(statsRecord.get(MAXIMUM_THREAD_DURATION_IN_SECONDS_ALIAS, + Double.class))), false); event.getHook().editOriginalEmbeds(embed.build()).queue(); @@ -208,4 +219,12 @@ private String smartFormat(Double seconds) { private String formatDouble(Object val) { return val instanceof Number num ? "%.2f".formatted(num.doubleValue()) : "0.00"; } + + /** + * Calculates the duration in seconds between two timestamp fields. Uses SQLite unixepoch for + * conversion. + */ + private Field durationInSeconds(Field end, Field start) { + return field("unixepoch({0}) - unixepoch({1})", Double.class, end, start); + } } From d9aec8f4b4cbbe0c9d8809c6b81437b024068dd1 Mon Sep 17 00:00:00 2001 From: Ankit Yadav Date: Sun, 21 Dec 2025 17:14:57 +0530 Subject: [PATCH 5/5] refactor: emojis and whitespace constants change emojis to unicode character constants with descriptive names, single/blank line of space to be more intentional and clear --- .../features/help/HelpThreadStatsCommand.java | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java index ebea078445..35760386c8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadStatsCommand.java @@ -26,7 +26,7 @@ * forum's activity over a specific duration. *

* Example usage: - * + * *

  * {@code
  * /help-thread-stats duration-option: 7 Days
@@ -48,6 +48,15 @@ public class HelpThreadStatsCommand extends SlashCommandAdapter {
     private static final String MINIMUM_THREAD_DURATION_IN_SECONDS_ALIAS = "min_sec";
     private static final String MAXIMUM_THREAD_DURATION_IN_SECONDS_ALIAS = "max_sec";
 
+    private static final String EMOJI_CHART = "\uD83D\uDCCA";
+    private static final String EMOJI_MEMO = "\uD83D\uDCDD";
+    private static final String EMOJI_SPEECH_BUBBLE = "\uD83D\uDCAC";
+    private static final String EMOJI_LABEL = "\uD83C\uDFF7\uFE0F";
+    private static final String EMOJI_LIGHTNING = "\u26A1";
+
+    private static final String EMBED_BLANK_LINE = "\u200B";
+    private static final String WHITESPACE = " ";
+
     private final Database database;
 
     /**
@@ -121,20 +130,20 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
 
             String peakHourRange = getPeakHour(context, startDate);
 
-            EmbedBuilder embed =
-                    new EmbedBuilder().setTitle("📊 Help Thread Stats (Last " + days + " Days)")
-                        .setColor(getStatusColor(totalCreated, ghostThreads))
-                        .setTimestamp(Instant.now())
-                        .setDescription("\u200B")
-                        .setFooter("Together Java Community Stats",
-                                Objects.requireNonNull(event.getGuild()).getIconUrl());
+            EmbedBuilder embed = new EmbedBuilder()
+                .setTitle(EMOJI_CHART + " Help Thread Stats (Last " + days + " Days)")
+                .setColor(getStatusColor(totalCreated, ghostThreads))
+                .setTimestamp(Instant.now())
+                .setDescription(EMBED_BLANK_LINE)
+                .setFooter("Together Java Community Stats",
+                        Objects.requireNonNull(event.getGuild()).getIconUrl());
 
-            embed.addField("📝 THREAD ACTIVITY",
+            embed.addField(EMOJI_MEMO + WHITESPACE + "THREAD ACTIVITY",
                     "Created: `%d`%nCurrently Open: `%d`%nResponse Rate: %.1f%%%nPeak Hours: `%s`"
                         .formatted(totalCreated, openThreads, rawResRate, peakHourRange),
                     false);
 
-            embed.addField("💬 ENGAGEMENT",
+            embed.addField(EMOJI_SPEECH_BUBBLE + WHITESPACE + "ENGAGEMENT",
                     "Avg Messages: `%s`%nAvg Helpers: `%s`%nUnanswered (Ghost): `%d`".formatted(
                             formatDouble(Objects
                                 .requireNonNull(statsRecord.get(AVERAGE_MESSAGE_COUNT_ALIAS))),
@@ -143,12 +152,12 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
                             ghostThreads),
                     false);
 
-            embed.addField("🏷️ TAG ACTIVITY",
+            embed.addField(EMOJI_LABEL + WHITESPACE + "TAG ACTIVITY",
                     "Most Used: `%s`%nMost Active: `%s`%nNeeds Love: `%s`".formatted(highVolumeTag,
                             highActivityTag, lowActivityTag),
                     false);
 
-            embed.addField("⚡ RESOLUTION SPEED",
+            embed.addField(EMOJI_LIGHTNING + WHITESPACE + "RESOLUTION SPEED",
                     "Average: `%s`%nFastest: `%s`%nSlowest: `%s`".formatted(
                             smartFormat(statsRecord.get(AVERAGE_THREAD_DURATION_IN_SECONDS_ALIAS,
                                     Double.class)),