77import net .dv8tion .jda .api .entities .User ;
88import net .dv8tion .jda .api .entities .channel .concrete .TextChannel ;
99import net .dv8tion .jda .api .events .interaction .command .SlashCommandInteractionEvent ;
10+ import net .dv8tion .jda .api .interactions .InteractionHook ;
1011import org .slf4j .Logger ;
1112import org .slf4j .LoggerFactory ;
1213
1617import org .togetherjava .tjbot .features .tophelper .TopHelpersService ;
1718import org .togetherjava .tjbot .features .utils .Colors ;
1819
20+ import java .time .OffsetDateTime ;
1921import java .util .Comparator ;
2022import java .util .HashMap ;
2123import java .util .List ;
2224import java .util .Map ;
2325import java .util .StringJoiner ;
26+ import java .util .concurrent .ConcurrentHashMap ;
2427import java .util .regex .Pattern ;
2528
2629/**
@@ -41,6 +44,9 @@ public final class LeaderboardCommand extends SlashCommandAdapter {
4144
4245 private final Config config ;
4346
47+ private final Map <Long , Map <Long , Integer >> winsByGuild = new ConcurrentHashMap <>();
48+ private final Map <Long , OffsetDateTime > lastFetchedPerGuild = new ConcurrentHashMap <>();
49+
4450 public LeaderboardCommand (Config config ) {
4551 super (COMMAND_NAME , "Show the all-time top helpers leaderboard" , CommandVisibility .GUILD );
4652 this .config = config ;
@@ -71,58 +77,87 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
7177 return ;
7278 }
7379
74- hallOfFame . getIterableHistory (). takeAsync ( HISTORY_LIMIT ). thenAccept ( messages -> {
75- Map < Long , Integer > winsByUser = countWins ( messages );
80+ long guildId = guild . getIdLong ();
81+ InteractionHook hook = event . getHook ( );
7682
77- List < Map . Entry < Long , Integer >> sorted = winsByUser . entrySet ()
78- . stream ()
79- . sorted ( Map . Entry . <Long , Integer >comparingByValue ( Comparator . reverseOrder ()))
80- . limit ( TOP_LIMIT )
81- . toList ( );
83+ if (! winsByGuild . containsKey ( guildId )) {
84+ hallOfFame . getIterableHistory (). takeAsync ( HISTORY_LIMIT ). thenAccept ( messages -> {
85+ Map <Long , Integer > wins = new HashMap <>();
86+ countWinsInto ( messages , wins );
87+ winsByGuild . put ( guildId , new ConcurrentHashMap <>( wins ) );
8288
83- if (sorted .isEmpty ()) {
84- event .getHook ().editOriginal ("No top helper data found." ).queue ();
85- return ;
86- }
89+ if (!messages .isEmpty ()) {
90+ lastFetchedPerGuild .put (guildId , messages .getFirst ().getTimeCreated ());
91+ }
92+
93+ sendLeaderboard (guild , wins , hook );
94+ }).exceptionally (error -> {
95+ logger .error ("Failed to read hall of fame channel" , error );
96+ hook .editOriginal ("Failed to read the hall of fame channel." ).queue ();
97+ return null ;
98+ });
99+ } else {
100+ OffsetDateTime lastFetched = lastFetchedPerGuild .get (guildId );
101+ Map <Long , Integer > cachedWins = winsByGuild .get (guildId );
102+
103+ hallOfFame .getIterableHistory ()
104+ .takeWhileAsync (HISTORY_LIMIT , msg -> msg .getTimeCreated ().isAfter (lastFetched ))
105+ .thenAccept (newMessages -> {
106+ if (!newMessages .isEmpty ()) {
107+ countWinsInto (newMessages , cachedWins );
108+ lastFetchedPerGuild .put (guildId , newMessages .getFirst ().getTimeCreated ());
109+ }
110+ sendLeaderboard (guild , cachedWins , hook );
111+ })
112+ .exceptionally (error -> {
113+ logger .error ("Failed to read hall of fame channel" , error );
114+ hook .editOriginal ("Failed to read the hall of fame channel." ).queue ();
115+ return null ;
116+ });
117+ }
118+ }
87119
88- List <Long > ids = sorted .stream ().map (Map .Entry ::getKey ).toList ();
120+ private void sendLeaderboard (Guild guild , Map <Long , Integer > wins , InteractionHook hook ) {
121+ List <Map .Entry <Long , Integer >> sorted = wins .entrySet ()
122+ .stream ()
123+ .sorted (Map .Entry .<Long , Integer >comparingByValue (Comparator .reverseOrder ()))
124+ .limit (TOP_LIMIT )
125+ .toList ();
89126
90- guild .retrieveMembersByIds (ids ).onSuccess (members -> {
91- Map <Long , Member > memberById = TopHelpersService .mapUserIdToMember (members );
127+ if (sorted .isEmpty ()) {
128+ hook .editOriginal ("No top helper data found." ).queue ();
129+ return ;
130+ }
92131
93- StringJoiner description = new StringJoiner ("\n " );
94- for (int i = 0 ; i < sorted .size (); i ++) {
95- Map .Entry <Long , Integer > entry = sorted .get (i );
96- Member member = memberById .get (entry .getKey ());
97- String name = TopHelpersService .getUsernameDisplay (member );
98- int wins = entry .getValue ();
99- description .add ("%s **%s** — %d month%s" .formatted (rankPrefix (i ), name , wins ,
100- wins == 1 ? "" : "s" ));
101- }
132+ List <Long > ids = sorted .stream ().map (Map .Entry ::getKey ).toList ();
102133
103- EmbedBuilder embed = new EmbedBuilder ().setTitle ("🏆 Top Helpers — Hall of Fame" )
104- .setDescription (description .toString ())
105- .setColor (Colors .SUCCESS_COLOR )
106- .setFooter ("Times awarded Top Helper" );
134+ guild .retrieveMembersByIds (ids ).onSuccess (members -> {
135+ Map <Long , Member > memberById = TopHelpersService .mapUserIdToMember (members );
107136
108- event .getHook ().editOriginalEmbeds (embed .build ()).queue ();
137+ StringJoiner description = new StringJoiner ("\n " );
138+ for (int i = 0 ; i < sorted .size (); i ++) {
139+ Map .Entry <Long , Integer > entry = sorted .get (i );
140+ Member member = memberById .get (entry .getKey ());
141+ String name = TopHelpersService .getUsernameDisplay (member );
142+ int winCount = entry .getValue ();
143+ description .add ("%s **%s** — %d month%s" .formatted (rankPrefix (i ), name , winCount ,
144+ winCount == 1 ? "" : "s" ));
145+ }
109146
110- }). onError ( error -> {
111- logger . error ( "Failed to retrieve members for leaderboard" , error );
112- event . getHook ( )
113- . editOriginal ( "Failed to load member data, please try again." )
114- . queue ();
115- } );
147+ EmbedBuilder embed = new EmbedBuilder (). setTitle ( "🏆 Top Helpers — Hall of Fame" )
148+ . setDescription ( description . toString ())
149+ . setColor ( Colors . SUCCESS_COLOR )
150+ . setFooter ( "Times awarded Top Helper" );
151+
152+ hook . editOriginalEmbeds ( embed . build ()). queue ( );
116153
117- }).exceptionally (error -> {
118- logger .error ("Failed to read hall of fame channel" , error );
119- event .getHook ().editOriginal ("Failed to read the hall of fame channel." ).queue ();
120- return null ;
154+ }).onError (error -> {
155+ logger .error ("Failed to retrieve members for leaderboard" , error );
156+ hook .editOriginal ("Failed to load member data, please try again." ).queue ();
121157 });
122158 }
123159
124- private static Map <Long , Integer > countWins (List <Message > messages ) {
125- Map <Long , Integer > wins = new HashMap <>();
160+ private static void countWinsInto (List <Message > messages , Map <Long , Integer > wins ) {
126161 for (Message message : messages ) {
127162 String content = message .getContentRaw ();
128163 if (!content .toLowerCase ().contains ("top helper" )) {
@@ -132,7 +167,6 @@ private static Map<Long, Integer> countWins(List<Message> messages) {
132167 wins .merge (user .getIdLong (), 1 , Integer ::sum );
133168 }
134169 }
135- return wins ;
136170 }
137171
138172 private static String rankPrefix (int zeroBasedIndex ) {
0 commit comments