66import xyz .dynxsty .dih4jda .interactions .components .ButtonHandler ;
77import net .dv8tion .jda .api .EmbedBuilder ;
88import net .dv8tion .jda .api .entities .Guild ;
9+ import net .dv8tion .jda .api .entities .Member ;
910import net .dv8tion .jda .api .entities .MessageEmbed ;
1011import net .dv8tion .jda .api .entities .Role ;
11- import net .dv8tion .jda .api .entities .User ;
1212import net .dv8tion .jda .api .events .interaction .command .SlashCommandInteractionEvent ;
1313import net .dv8tion .jda .api .events .interaction .component .ButtonInteractionEvent ;
1414import net .dv8tion .jda .api .interactions .commands .OptionMapping ;
1515import net .dv8tion .jda .api .interactions .commands .OptionType ;
16+ import net .dv8tion .jda .api .interactions .commands .build .OptionData ;
1617import net .dv8tion .jda .api .interactions .commands .build .SubcommandData ;
1718import net .dv8tion .jda .api .interactions .components .ActionRow ;
1819import net .dv8tion .jda .api .interactions .components .buttons .Button ;
20+ import net .dv8tion .jda .api .utils .FileUpload ;
21+ import net .dv8tion .jda .api .utils .messages .MessageEditBuilder ;
1922import net .javadiscord .javabot .annotations .AutoDetectableComponentHandler ;
2023import net .javadiscord .javabot .systems .help .dao .HelpAccountRepository ;
21- import net .javadiscord .javabot .systems .help .model . HelpAccount ;
24+ import net .javadiscord .javabot .systems .help .dao . HelpTransactionRepository ;
2225import net .javadiscord .javabot .util .ExceptionLogger ;
2326import net .javadiscord .javabot .util .Pair ;
2427import net .javadiscord .javabot .util .Responses ;
2528import org .jetbrains .annotations .Contract ;
2629import org .jetbrains .annotations .NotNull ;
2730import org .springframework .dao .DataAccessException ;
2831
32+ import java .io .IOException ;
2933import java .util .List ;
34+ import java .util .Objects ;
3035import java .util .concurrent .ExecutorService ;
36+ import java .util .function .BiFunction ;
3137
3238/**
3339 * <h3>This class represents the /leaderboard help-experience command.</h3>
3440 */
3541@ AutoDetectableComponentHandler ("experience-leaderboard" )
3642public class ExperienceLeaderboardSubcommand extends SlashCommand .Subcommand implements ButtonHandler {
37- private static final int PAGE_SIZE = 5 ;
43+ /**
44+ * prefix contained in the image cache.
45+ */
46+ public static final String CACHE_PREFIX = "xp_leaderboard" ;
47+ private static final int PAGE_SIZE = 10 ;
48+
3849 private final ExecutorService asyncPool ;
3950 private final HelpAccountRepository helpAccountRepository ;
51+ private final HelpTransactionRepository helpTransactionRepository ;
4052
4153 /**
4254 * The constructor of this class, which sets the corresponding {@link SubcommandData}.
4355 * @param helpAccountRepository Dao object that represents the HELP_ACCOUNT SQL Table.
4456 * @param asyncPool the main thread pool for asynchronous operations
57+ * @param helpTransactionRepository Dao object that represents the HELP_TRANSACTIONS SQL Table.
4558 */
46- public ExperienceLeaderboardSubcommand (HelpAccountRepository helpAccountRepository , ExecutorService asyncPool ) {
59+ public ExperienceLeaderboardSubcommand (HelpAccountRepository helpAccountRepository , ExecutorService asyncPool , HelpTransactionRepository helpTransactionRepository ) {
4760 this .asyncPool = asyncPool ;
4861 this .helpAccountRepository = helpAccountRepository ;
62+ this .helpTransactionRepository = helpTransactionRepository ;
4963 setCommandData (new SubcommandData ("help-experience" , "The Help Experience Leaderboard." )
5064 .addOption (OptionType .INTEGER , "page" , "The page of results to show. By default it starts at 1." , false )
65+ .addOptions (new OptionData (OptionType .STRING , "type" , "Type of the help-XP headerboard" , false )
66+ .addChoice ("total" , LeaderboardType .TOTAL .name ())
67+ .addChoice ("last 30 days" , LeaderboardType .MONTH .name ()))
5168 );
5269 }
5370
5471 @ Override
5572 public void handleButton (@ NotNull ButtonInteractionEvent event , Button button ) {
5673 event .deferEdit ().queue ();
5774 String [] id = ComponentIdBuilder .split (event .getComponentId ());
75+ LeaderboardType type ;
76+ if (id .length > 3 ) {
77+ type = LeaderboardType .valueOf (id [3 ]);
78+ } else {
79+ type = LeaderboardType .TOTAL ;
80+ }
5881 asyncPool .execute (() -> {
5982 try {
6083 int page = Integer .parseInt (id [2 ]);
@@ -64,60 +87,120 @@ public void handleButton(@NotNull ButtonInteractionEvent event, Button button) {
6487 } else {
6588 page ++;
6689 }
67- int maxPage = helpAccountRepository .getTotalAccounts () / PAGE_SIZE ;
90+ int totalAccounts = switch (type ) {
91+ case MONTH -> helpTransactionRepository .getNumberOfUsersWithHelpXPInLastMonth ();
92+ case TOTAL -> helpAccountRepository .getTotalAccounts ();
93+ };
94+ int maxPage = totalAccounts / PAGE_SIZE ;
6895 if (page <= 0 ) {
6996 page = maxPage ;
7097 }
7198 if (page > maxPage ) {
7299 page = 1 ;
73100 }
101+ Pair <MessageEmbed , FileUpload > messageInfo = buildExperienceLeaderboard (event .getGuild (), page , type );
74102 event .getHook ()
75- .editOriginalEmbeds ( buildExperienceLeaderboard ( event . getGuild (), helpAccountRepository , page ))
76- .setComponents (buildPageControls (page )).queue ();
77- } catch (DataAccessException e ) {
103+ .editOriginal ( new MessageEditBuilder (). setEmbeds ( messageInfo . first ()). setAttachments ( messageInfo . second ()). build ( ))
104+ .setComponents (buildPageControls (page , type )).queue ();
105+ } catch (DataAccessException | IOException e ) {
78106 ExceptionLogger .capture (e , ExperienceLeaderboardSubcommand .class .getSimpleName ());
79107 }
80108 });
81109 }
82110
83- private static @ NotNull MessageEmbed buildExperienceLeaderboard (Guild guild , @ NotNull HelpAccountRepository dao , int page ) throws DataAccessException {
84- int maxPage = dao .getTotalAccounts () / PAGE_SIZE ;
85- List <HelpAccount > accounts = dao .getAccounts (Math .min (page , maxPage ), PAGE_SIZE );
111+
112+ private @ NotNull Pair <MessageEmbed , FileUpload > buildExperienceLeaderboard (Guild guild , int page , LeaderboardType type ) throws DataAccessException , IOException {
113+ return switch (type ) {
114+ case TOTAL -> buildGenericExperienceLeaderboard (page , helpAccountRepository .getTotalAccounts (),
115+ "total Leaderboard of help experience" ,
116+ helpAccountRepository ::getAccounts , (position , account ) -> {
117+ Pair <Role , Double > currentRole = account .getCurrentExperienceGoal (guild );
118+ return createUserData (guild , position , account .getExperience (), account .getUserId (), currentRole .first () != null ? currentRole .first ().getAsMention () + ": " : "" );
119+ });
120+ case MONTH -> buildGenericExperienceLeaderboard (page , helpTransactionRepository .getNumberOfUsersWithHelpXPInLastMonth (),
121+ """
122+ help experience leaderboard from the last 30 days
123+ This leaderboard does not include experience decay.
124+ """ ,
125+ helpTransactionRepository ::getTotalTransactionWeightsInLastMonth , (position , xpInfo ) -> {
126+ return createUserData (guild , position , (double ) xpInfo .second (), xpInfo .first (), "" );
127+ });
128+ };
129+ }
130+
131+ private <T > @ NotNull Pair <MessageEmbed , FileUpload > buildGenericExperienceLeaderboard (int page , int totalAccounts , String description ,
132+ BiFunction <Integer , Integer , List <T >> accountsReader , BiFunction <Integer , T , UserData > fieldExtractor ) throws DataAccessException , IOException {
133+
134+ int maxPage = totalAccounts / PAGE_SIZE ;
135+ int actualPage = Math .max (1 , Math .min (page , maxPage ));
136+ List <T > accounts = accountsReader .apply (actualPage , PAGE_SIZE );
137+
86138 EmbedBuilder builder = new EmbedBuilder ()
87139 .setTitle ("Experience Leaderboard" )
140+ .setDescription (description )
88141 .setColor (Responses .Type .DEFAULT .getColor ())
89- .setFooter (String .format ("Page %s/%s" , Math .min (page , maxPage ), maxPage ));
90- accounts .forEach (account -> {
91- Pair <Role , Double > currentRole = account .getCurrentExperienceGoal (guild );
92- User user = guild .getJDA ().getUserById (account .getUserId ());
93- builder .addField (
94- String .format ("**%s.** %s" , (accounts .indexOf (account ) + 1 ) + (page - 1 ) * PAGE_SIZE , user == null ? account .getUserId () : UserUtils .getUserTag (user )),
95- String .format ("%s`%.0f XP`\n " , currentRole .first () != null ? currentRole .first ().getAsMention () + ": " : "" , account .getExperience ()),
96- false );
142+ .setFooter (String .format ("Page %s/%s" , actualPage , maxPage ));
143+
144+ String pageCachePrefix = CACHE_PREFIX + "_" + page ;
145+ String cacheName = pageCachePrefix + "_" + accounts .hashCode ();
146+ byte [] bytes = LeaderboardCreator .attemptLoadFromCache (cacheName , ()->{
147+ try (LeaderboardCreator creator = new LeaderboardCreator (accounts .size (), null )){
148+ for (int i = 0 ; i < accounts .size (); i ++) {
149+ int position = (i + 1 ) + (actualPage - 1 ) * PAGE_SIZE ;
150+ UserData userInfo = fieldExtractor .apply (position , accounts .get (i ));
151+ creator .drawLeaderboardEntry (userInfo .member (), userInfo .displayName (), userInfo .xp (), position );
152+ }
153+ return creator .getImageBytes (cacheName , pageCachePrefix );
154+ }
97155 });
98- return builder .build ();
156+ builder .setImage ("attachment://leaderboard.png" );
157+ return new Pair <MessageEmbed , FileUpload >(builder .build (), FileUpload .fromData (bytes , "leaderboard.png" ));
158+ }
159+
160+ private UserData createUserData (Guild guild , Integer position , double experience , long userId , String prefix ) {
161+ Member member = guild .getMemberById (userId );
162+ String displayName ;
163+ if (member == null ) {
164+ displayName = String .valueOf (userId );
165+ } else {
166+ displayName = UserUtils .getUserTag (member .getUser ());
167+ }
168+ return new UserData (member , displayName , (long )experience );
99169 }
100170
101171 @ Contract ("_ -> new" )
102- private static @ NotNull ActionRow buildPageControls (int currentPage ) {
172+ private static @ NotNull ActionRow buildPageControls (int currentPage , LeaderboardType type ) {
103173 return ActionRow .of (
104- Button .primary (ComponentIdBuilder .build ("experience-leaderboard" , "left" , currentPage ), "Prev" ),
105- Button .primary (ComponentIdBuilder .build ("experience-leaderboard" , "right" , currentPage ), "Next" )
174+ Button .primary (ComponentIdBuilder .build ("experience-leaderboard" , "left" , currentPage , type . name () ), "Prev" ),
175+ Button .primary (ComponentIdBuilder .build ("experience-leaderboard" , "right" , currentPage , type . name () ), "Next" )
106176 );
107177 }
108178
109179 @ Override
110180 public void execute (@ NotNull SlashCommandInteractionEvent event ) {
111181 int page = event .getOption ("page" , 1 , OptionMapping ::getAsInt );
182+ LeaderboardType type = event .getOption ("type" , LeaderboardType .TOTAL , o ->LeaderboardType .valueOf (o .getAsString ()));
112183 event .deferReply ().queue ();
113184 asyncPool .execute (() -> {
114185 try {
115- event .getHook ().sendMessageEmbeds (buildExperienceLeaderboard (event .getGuild (), helpAccountRepository , page ))
116- .setComponents (buildPageControls (page ))
186+ Pair <MessageEmbed , FileUpload > messageInfo = buildExperienceLeaderboard (event .getGuild (), page , type );
187+ event .getHook ().sendMessageEmbeds (messageInfo .first ())
188+ .addFiles (messageInfo .second ())
189+ .setComponents (buildPageControls (page , type ))
117190 .queue ();
118- }catch (DataAccessException e ) {
191+ }catch (DataAccessException | IOException e ) {
119192 ExceptionLogger .capture (e , ExperienceLeaderboardSubcommand .class .getSimpleName ());
120193 }
121194 });
122195 }
196+
197+ private enum LeaderboardType {
198+ TOTAL , MONTH
199+ }
200+
201+ private record UserData (Member member , String displayName , long xp ) {
202+ UserData {
203+ Objects .requireNonNull (displayName );
204+ }
205+ }
123206}
0 commit comments