77import org .jooq .DSLContext ;
88import org .jooq .OrderField ;
99import org .jooq .Record1 ;
10+
1011import org .togetherjava .tjbot .db .Database ;
1112import org .togetherjava .tjbot .features .CommandVisibility ;
1213import org .togetherjava .tjbot .features .SlashCommandAdapter ;
2021import static org .togetherjava .tjbot .db .generated .Tables .HELP_THREADS ;
2122
2223/**
23- * Implements the '/help-thread-stats' command which provides analytical insights into the
24- * help forum's activity over a specific duration.
24+ * Implements the '/help-thread-stats' command which provides analytical insights into the help
25+ * forum's activity over a specific duration.
2526 * <p>
2627 * Example usage:
28+ *
2729 * <pre>
2830 * {@code
2931 * /help-thread-stats duration-option: 7 Days
3032 * }
3133 * </pre>
3234 * <p>
33- * The command aggregates data such as response rates, engagement metrics (messages/helpers),
34- * tag popularity, and resolution speeds.
35+ * The command aggregates data such as response rates, engagement metrics (messages/helpers), tag
36+ * popularity, and resolution speeds.
3537 */
3638public class HelpThreadStatsCommand extends SlashCommandAdapter {
3739 public static final String COMMAND_NAME = "help-thread-stats" ;
@@ -47,12 +49,13 @@ public class HelpThreadStatsCommand extends SlashCommandAdapter {
4749 public HelpThreadStatsCommand (Database database ) {
4850 super (COMMAND_NAME , "Display Help Thread Statistics" , CommandVisibility .GUILD );
4951
50- OptionData durationOption = new OptionData (OptionType .INTEGER , DURATION_OPTION , "The time range for statistics" , false )
51- .addChoice ("1 Day" , 1 )
52- .addChoice ("7 Days" , 7 )
53- .addChoice ("30 Days" , 30 )
54- .addChoice ("90 Days" , 90 )
55- .addChoice ("180 Days" , 180 );
52+ OptionData durationOption = new OptionData (OptionType .INTEGER , DURATION_OPTION ,
53+ "The time range for statistics" , false )
54+ .addChoice ("1 Day" , 1 )
55+ .addChoice ("7 Days" , 7 )
56+ .addChoice ("30 Days" , 30 )
57+ .addChoice ("90 Days" , 90 )
58+ .addChoice ("180 Days" , 180 );
5659
5760 getData ().addOptions (durationOption );
5861 this .database = database ;
@@ -68,105 +71,141 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
6871 event .deferReply ().queue ();
6972
7073 database .read (context -> {
71- var statsRecord = context .select (
72- count ().as ("total_created" ),
73- count ().filterWhere (HELP_THREADS .TICKET_STATUS .eq (HelpSystemHelper .TicketStatus .ACTIVE .val )).as ("open_now" ),
74- count ().filterWhere (HELP_THREADS .PARTICIPANTS .eq (1 )).as ("ghost_count" ),
75- avg (HELP_THREADS .PARTICIPANTS ).as ("avg_parts" ),
76- avg (HELP_THREADS .MESSAGE_COUNT ).as ("avg_msgs" ),
77- avg (field ("unixepoch({0}) - unixepoch({1})" , Double .class , HELP_THREADS .CLOSED_AT , HELP_THREADS .CREATED_AT )).as ("avg_sec" ),
78- min (field ("unixepoch({0}) - unixepoch({1})" , Double .class , HELP_THREADS .CLOSED_AT , HELP_THREADS .CREATED_AT )).as ("min_sec" ),
79- max (field ("unixepoch({0}) - unixepoch({1})" , Double .class , HELP_THREADS .CLOSED_AT , HELP_THREADS .CREATED_AT )).as ("max_sec" )
80- )
81- .from (HELP_THREADS )
82- .where (HELP_THREADS .CREATED_AT .ge (startDate ))
83- .fetchOne ();
74+ var statsRecord = context
75+ .select (count ().as ("total_created" ), count ()
76+ .filterWhere (
77+ HELP_THREADS .TICKET_STATUS .eq (HelpSystemHelper .TicketStatus .ACTIVE .val ))
78+ .as ("open_now" ),
79+ count ().filterWhere (HELP_THREADS .PARTICIPANTS .eq (1 )).as ("ghost_count" ),
80+ avg (HELP_THREADS .PARTICIPANTS ).as ("avg_parts" ),
81+ avg (HELP_THREADS .MESSAGE_COUNT ).as ("avg_msgs" ),
82+ avg (field ("unixepoch({0}) - unixepoch({1})" , Double .class ,
83+ HELP_THREADS .CLOSED_AT , HELP_THREADS .CREATED_AT ))
84+ .as ("avg_sec" ),
85+ min (field ("unixepoch({0}) - unixepoch({1})" , Double .class ,
86+ HELP_THREADS .CLOSED_AT , HELP_THREADS .CREATED_AT ))
87+ .as ("min_sec" ),
88+ max (field ("unixepoch({0}) - unixepoch({1})" , Double .class ,
89+ HELP_THREADS .CLOSED_AT , HELP_THREADS .CREATED_AT ))
90+ .as ("max_sec" ))
91+ .from (HELP_THREADS )
92+ .where (HELP_THREADS .CREATED_AT .ge (startDate ))
93+ .fetchOne ();
8494
8595 if (statsRecord == null || statsRecord .get ("total_created" , Integer .class ) == 0 ) {
86- event .getHook ().editOriginal ("No stats available for the last " + days + " days." ).queue ();
96+ event .getHook ()
97+ .editOriginal ("No stats available for the last " + days + " days." )
98+ .queue ();
8799 return null ;
88100 }
89101
90102 int totalCreated = statsRecord .get ("total_created" , Integer .class );
91103 int openThreads = statsRecord .get ("open_now" , Integer .class );
92104 long ghostThreads = statsRecord .get ("ghost_count" , Number .class ).longValue ();
93105
94- double rawResRate = totalCreated > 0 ? ((double ) (totalCreated - ghostThreads ) / totalCreated ) * 100 : 0 ;
106+ double rawResRate =
107+ totalCreated > 0 ? ((double ) (totalCreated - ghostThreads ) / totalCreated ) * 100
108+ : 0 ;
95109
96110 String highVolumeTag = getTopTag (context , startDate , count ().desc ());
97- String highActivityTag = getTopTag (context , startDate , avg (HELP_THREADS .MESSAGE_COUNT ).desc ());
98- String lowActivityTag = getTopTag (context , startDate , avg (HELP_THREADS .MESSAGE_COUNT ).asc ());
111+ String highActivityTag =
112+ getTopTag (context , startDate , avg (HELP_THREADS .MESSAGE_COUNT ).desc ());
113+ String lowActivityTag =
114+ getTopTag (context , startDate , avg (HELP_THREADS .MESSAGE_COUNT ).asc ());
99115
100116 String peakHourRange = getPeakHour (context , startDate );
101117
102- EmbedBuilder embed = new EmbedBuilder ()
103- .setTitle ("📊 Help Thread Stats (Last " + days + " Days)" )
104- .setColor (getStatusColor (totalCreated , ghostThreads ))
105- .setTimestamp (Instant .now ())
106- .setDescription ("\u200B " )
107- .setFooter ("Together Java Community Stats" , Objects .requireNonNull (event .getGuild ()).getIconUrl ());
118+ EmbedBuilder embed =
119+ new EmbedBuilder ().setTitle ("📊 Help Thread Stats (Last " + days + " Days)" )
120+ .setColor (getStatusColor (totalCreated , ghostThreads ))
121+ .setTimestamp (Instant .now ())
122+ .setDescription ("\u200B " )
123+ .setFooter ("Together Java Community Stats" ,
124+ Objects .requireNonNull (event .getGuild ()).getIconUrl ());
108125
109126 embed .addField ("📝 THREAD ACTIVITY" ,
110127 "Created: `%d`\n Currently Open: `%d`\n Response Rate: %.1f%%\n Peak Hours: `%s`"
111- .formatted (totalCreated , openThreads , rawResRate , peakHourRange ), false );
128+ .formatted (totalCreated , openThreads , rawResRate , peakHourRange ),
129+ false );
112130
113131 embed .addField ("💬 ENGAGEMENT" ,
114132 "Avg Messages: `%s`\n Avg Helpers: `%s`\n Unanswered (Ghost): `%d`" .formatted (
115133 formatDouble (Objects .requireNonNull (statsRecord .get ("avg_msgs" ))),
116134 formatDouble (Objects .requireNonNull (statsRecord .get ("avg_parts" ))),
117- ghostThreads ), false );
135+ ghostThreads ),
136+ false );
118137
119138 embed .addField ("🏷️ TAG ACTIVITY" ,
120- "Most Used: `%s`\n Most Active: `%s`\n Needs Love: `%s`" .formatted (
121- highVolumeTag , highActivityTag , lowActivityTag ), false );
139+ "Most Used: `%s`\n Most Active: `%s`\n Needs Love: `%s`" .formatted (highVolumeTag ,
140+ highActivityTag , lowActivityTag ),
141+ false );
122142
123143 embed .addField ("⚡ RESOLUTION SPEED" ,
124144 "Average: `%s`\n Fastest: `%s`\n Slowest: `%s`" .formatted (
125145 smartFormat (statsRecord .get ("avg_sec" , Double .class )),
126146 smartFormat (statsRecord .get ("min_sec" , Double .class )),
127- smartFormat (statsRecord .get ("max_sec" , Double .class ))), false );
147+ smartFormat (statsRecord .get ("max_sec" , Double .class ))),
148+ false );
128149
129150 event .getHook ().editOriginalEmbeds (embed .build ()).queue ();
130151 return null ;
131152 });
132153 }
133154
134155 private static Color getStatusColor (int totalCreated , long ghostThreads ) {
135- double rawResRate = totalCreated > 0 ? ((double ) (totalCreated - ghostThreads ) / totalCreated ) * 100 : -1 ;
136-
137- if (rawResRate >= 70 ) return Color .GREEN ;
138- if (rawResRate >= 30 ) return Color .YELLOW ;
139- if (rawResRate >= 0 ) return Color .RED ;
156+ double rawResRate =
157+ totalCreated > 0 ? ((double ) (totalCreated - ghostThreads ) / totalCreated ) * 100
158+ : -1 ;
159+
160+ if (rawResRate >= 70 )
161+ return Color .GREEN ;
162+ if (rawResRate >= 30 )
163+ return Color .YELLOW ;
164+ if (rawResRate >= 0 )
165+ return Color .RED ;
140166 return Color .GRAY ;
141167 }
142168
143169 private String getTopTag (DSLContext context , Instant start , OrderField <?> order ) {
144- return context .select (HELP_THREADS .TAGS ).from (HELP_THREADS )
145- .where (HELP_THREADS .CREATED_AT .ge (start )).and (HELP_THREADS .TAGS .ne ("none" ))
146- .groupBy (HELP_THREADS .TAGS ).orderBy (order ).limit (1 )
147- .fetchOptional (HELP_THREADS .TAGS ).orElse ("N/A" );
170+ return context .select (HELP_THREADS .TAGS )
171+ .from (HELP_THREADS )
172+ .where (HELP_THREADS .CREATED_AT .ge (start ))
173+ .and (HELP_THREADS .TAGS .ne ("none" ))
174+ .groupBy (HELP_THREADS .TAGS )
175+ .orderBy (order )
176+ .limit (1 )
177+ .fetchOptional (HELP_THREADS .TAGS )
178+ .orElse ("N/A" );
148179 }
149180
150181 private String getPeakHour (DSLContext context , Instant start ) {
151182 return context .select (field ("strftime('%H', {0})" , String .class , HELP_THREADS .CREATED_AT ))
152- .from (HELP_THREADS ).where (HELP_THREADS .CREATED_AT .ge (start ))
153- .groupBy (field ("strftime('%H', {0})" , String .class , HELP_THREADS .CREATED_AT ))
154- .orderBy (count ().desc ()).limit (1 ).fetchOptional (Record1 ::value1 )
155- .map (hour -> {
156- int h = Integer .parseInt (hour );
157- return "%02d:00 - %02d:00 UTC" .formatted (h , (h + 1 ) % 24 );
158- }).orElse ("N/A" );
183+ .from (HELP_THREADS )
184+ .where (HELP_THREADS .CREATED_AT .ge (start ))
185+ .groupBy (field ("strftime('%H', {0})" , String .class , HELP_THREADS .CREATED_AT ))
186+ .orderBy (count ().desc ())
187+ .limit (1 )
188+ .fetchOptional (Record1 ::value1 )
189+ .map (hour -> {
190+ int h = Integer .parseInt (hour );
191+ return "%02d:00 - %02d:00 UTC" .formatted (h , (h + 1 ) % 24 );
192+ })
193+ .orElse ("N/A" );
159194 }
160195
161196 private String smartFormat (Double seconds ) {
162- if (seconds < 0 ) return "N/A" ;
163- if (seconds < 60 ) return "%.0f secs" .formatted (seconds );
164- if (seconds < 3600 ) return "%.1f mins" .formatted (seconds / 60.0 );
165- if (seconds < 86400 ) return "%.1f hrs" .formatted (seconds / 3600.0 );
197+ if (seconds < 0 )
198+ return "N/A" ;
199+ if (seconds < 60 )
200+ return "%.0f secs" .formatted (seconds );
201+ if (seconds < 3600 )
202+ return "%.1f mins" .formatted (seconds / 60.0 );
203+ if (seconds < 86400 )
204+ return "%.1f hrs" .formatted (seconds / 3600.0 );
166205 return "%.1f days" .formatted (seconds / 86400.0 );
167206 }
168207
169208 private String formatDouble (Object val ) {
170209 return val instanceof Number num ? "%.2f" .formatted (num .doubleValue ()) : "0.00" ;
171210 }
172- }
211+ }
0 commit comments