Skip to content

Commit 6184eaf

Browse files
Merge pull request #458 from danthe1st/uniform-qotw-leaderboard
2 parents e4832e4 + 137572e commit 6184eaf

File tree

7 files changed

+136
-37
lines changed

7 files changed

+136
-37
lines changed

src/main/java/net/discordjug/javabot/api/routes/leaderboard/qotw/QOTWLeaderboardController.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import net.discordjug.javabot.api.routes.CaffeineCache;
77
import net.discordjug.javabot.api.routes.leaderboard.qotw.model.QOTWUserData;
88
import net.discordjug.javabot.systems.qotw.QOTWPointsService;
9+
import net.discordjug.javabot.systems.qotw.model.QOTWAccount;
910
import net.discordjug.javabot.util.Pair;
1011
import net.dv8tion.jda.api.JDA;
1112
import net.dv8tion.jda.api.entities.Guild;
13+
import net.dv8tion.jda.api.entities.User;
1214

1315
import org.springframework.beans.factory.annotation.Autowired;
1416
import org.springframework.http.HttpStatus;
@@ -26,7 +28,7 @@
2628
*/
2729
@RestController
2830
public class QOTWLeaderboardController extends CaffeineCache<Pair<Long, Integer>, List<QOTWUserData>> {
29-
private static final int PAGE_AMOUNT = 8;
31+
private static final int PAGE_AMOUNT = 10;
3032
private final JDA jda;
3133
private final QOTWPointsService pointsService;
3234

@@ -65,11 +67,23 @@ public ResponseEntity<List<QOTWUserData>> getQOTWLeaderboard(
6567
}
6668
List<QOTWUserData> members = getCache().getIfPresent(new Pair<>(guild.getIdLong(), page));
6769
if (members == null || members.isEmpty()) {
68-
members = pointsService.getTopAccounts(PAGE_AMOUNT, page).stream()
69-
.map(p -> QOTWUserData.of(p, jda.retrieveUserById(p.getUserId()).complete()))
70+
List<QOTWAccount> topAccounts = pointsService.getTopAccounts(PAGE_AMOUNT, page);
71+
members = topAccounts.stream()
72+
.map(account -> new Pair<>(account, jda.retrieveUserById(account.getUserId()).complete()))
73+
.filter(pair -> guild.isMember(pair.second()))
74+
.map(pair -> createAPIAccount(pair.first(), pair.second(), topAccounts, page))
7075
.toList();
7176
getCache().put(new Pair<>(guild.getIdLong(), page), members);
7277
}
7378
return new ResponseEntity<>(members, HttpStatus.OK);
7479
}
80+
81+
private QOTWUserData createAPIAccount(QOTWAccount account, User user, List<QOTWAccount> topAccounts, int page) {
82+
return QOTWUserData.of(
83+
account,
84+
user,
85+
//this can be inaccurate for later pages with multiple users having the same score on the previous page
86+
//specifically, it counts all users on previous pages as strictly higher in the leaderboard
87+
pointsService.getQOTWRank(account.getUserId(), topAccounts)+(page-1)*PAGE_AMOUNT);
88+
}
7589
}

src/main/java/net/discordjug/javabot/api/routes/leaderboard/qotw/model/QOTWUserData.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,25 @@
1717
@EqualsAndHashCode(callSuper = false)
1818
public class QOTWUserData extends UserData {
1919
private QOTWAccount account;
20+
private int rank;
2021

2122
/**
2223
* Creates a new {@link QOTWUserData} instance.
2324
*
2425
* @param account The {@link QOTWAccount} to use.
2526
* @param user A nullable {@link User}.
27+
* @param rank The position of the user in the QOTW leaderboard
2628
* @return The {@link QOTWUserData}.
2729
*/
28-
public static @NotNull QOTWUserData of(@NotNull QOTWAccount account, @Nullable User user) {
30+
public static @NotNull QOTWUserData of(@NotNull QOTWAccount account, @Nullable User user, int rank) {
2931
QOTWUserData data = new QOTWUserData();
3032
data.setUserId(account.getUserId());
3133
if (user != null) {
3234
data.setUserName(user.getName());
3335
data.setDiscriminator(user.getDiscriminator());
3436
data.setEffectiveAvatarUrl(user.getEffectiveAvatarUrl());
3537
}
38+
data.setRank(rank);
3639
data.setAccount(account);
3740
return data;
3841
}

src/main/java/net/discordjug/javabot/systems/qotw/QOTWPointsService.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,36 @@ public QOTWAccount getOrCreateAccount(long userId) throws DataAccessException {
5555
public int getQOTWRank(long userId) {
5656
try{
5757
List<QOTWAccount> accounts = pointsRepository.sortByPoints(getCurrentMonth());
58-
return accounts.stream()
59-
.map(QOTWAccount::getUserId)
60-
.toList()
61-
.indexOf(userId) + 1;
58+
int currentRank = getQOTWRank(userId, accounts);
59+
return currentRank;
6260
} catch (DataAccessException e) {
6361
ExceptionLogger.capture(e, getClass().getSimpleName());
6462
return -1;
6563
}
6664
}
6765

66+
/**
67+
* Gets the given user's QOTW-Rank given a {@link List} of QOTW accounts.
68+
* @param userId The user whose rank should be returned.
69+
* @param accounts A list of QOTW accounts in descending order with respect to the score. This should at least contain the user (if they are ranked) and all users before them.
70+
* @return The QOTW-Rank as an integer.
71+
*/
72+
public int getQOTWRank(long userId, List<QOTWAccount> accounts) {
73+
long lastScore = -1;
74+
int currentRank = 1;
75+
for (int i = 0; i < accounts.size(); i++) {
76+
QOTWAccount account = accounts.get(i);
77+
if (account.getPoints() != lastScore) {
78+
currentRank = i + 1;
79+
lastScore = account.getPoints();
80+
}
81+
if (account.getUserId() == userId) {
82+
return currentRank;
83+
}
84+
}
85+
return -1;
86+
}
87+
6888
/**
6989
* Gets the given user's QOTW-Points.
7090
*
@@ -89,7 +109,7 @@ public long getPoints(long userId) {
89109
*/
90110
public List<Pair<QOTWAccount, Member>> getTopMembers(int n, Guild guild) {
91111
try {
92-
List<QOTWAccount> accounts = pointsRepository.sortByPoints(getCurrentMonth());
112+
List<QOTWAccount> accounts = pointsRepository.getTopAccounts(getCurrentMonth(),1,(int)Math.ceil(n*1.5));
93113
return accounts.stream()
94114
.map(s -> new Pair<>(s, guild.getMemberById(s.getUserId())))
95115
.filter(p->p.first().getPoints() > 0)
@@ -106,7 +126,7 @@ public List<Pair<QOTWAccount, Member>> getTopMembers(int n, Guild guild) {
106126
* Gets the specified amount of {@link QOTWAccount}s, sorted by their points.
107127
*
108128
* @param amount The amount to retrieve.
109-
* @param page The page to get.
129+
* @param page The page to get, starting at 1.
110130
* @return An unmodifiable {@link List} of {@link QOTWAccount}s.
111131
*/
112132
public List<QOTWAccount> getTopAccounts(int amount, int page) {

src/main/java/net/discordjug/javabot/systems/qotw/dao/QuestionPointsRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,13 @@ public List<QOTWAccount> sortByPoints(LocalDate startDate) throws DataAccessExce
9292
* Gets a specified amount of {@link QOTWAccount}s.
9393
*
9494
* @param startDate the minimum date points are considered
95-
* @param page The page.
95+
* @param page The page, starting at 1.
9696
* @param size The amount of {@link QOTWAccount}s to return.
9797
* @return A {@link List} containing the specified amount of {@link QOTWAccount}s.
9898
* @throws DataAccessException If an error occurs.
9999
*/
100100
public List<QOTWAccount> getTopAccounts(LocalDate startDate, int page, int size) throws DataAccessException {
101-
return jdbcTemplate.query("SELECT user_id, SUM(points) FROM qotw_points WHERE obtained_at >= ? AND points > 0 GROUP BY user_id ORDER BY SUM(points) DESC LIMIT ? OFFSET ?",
101+
return jdbcTemplate.query("SELECT user_id, SUM(points) FROM qotw_points WHERE obtained_at >= ? AND points > 0 GROUP BY user_id ORDER BY SUM(points) DESC, user_id ASC LIMIT ? OFFSET ?",
102102
(rs,row)->this.read(rs),
103103
startDate, size, Math.max(0, (page * size) - size));
104104
}

src/main/java/net/discordjug/javabot/systems/qotw/jobs/QOTWChampionJob.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void execute() {
3535
LocalDate month=LocalDate.now().minusMonths(1);
3636
Role qotwChampionRole = botConfig.get(guild).getQotwConfig().getQOTWChampionRole();
3737
if (qotwChampionRole != null) {
38-
pointsRepository.getTopAccounts(month, 0, 1)
38+
pointsRepository.getTopAccounts(month, 1, 1)
3939
.stream()
4040
.findFirst()
4141
.ifPresent(best -> {

src/main/java/net/discordjug/javabot/systems/user_commands/leaderboard/QOTWLeaderboardSubcommand.java

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@
1111

1212
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
1313
import net.discordjug.javabot.systems.qotw.QOTWPointsService;
14-
import net.discordjug.javabot.systems.qotw.dao.QuestionPointsRepository;
1514
import net.discordjug.javabot.systems.qotw.model.QOTWAccount;
1615
import net.discordjug.javabot.util.ExceptionLogger;
1716
import net.discordjug.javabot.util.Pair;
1817
import net.discordjug.javabot.util.UserUtils;
1918
import net.dv8tion.jda.api.EmbedBuilder;
20-
import net.dv8tion.jda.api.entities.Guild;
2119
import net.dv8tion.jda.api.entities.Member;
2220
import net.dv8tion.jda.api.entities.Message;
2321
import net.dv8tion.jda.api.entities.MessageEmbed;
@@ -35,29 +33,27 @@ public class QOTWLeaderboardSubcommand extends SlashCommand.Subcommand {
3533

3634
private final QOTWPointsService pointsService;
3735
private final ExecutorService asyncPool;
38-
private final QuestionPointsRepository qotwPointsRepository;
3936

4037
/**
4138
* The constructor of this class, which sets the corresponding {@link SubcommandData}.
4239
* @param pointsService The {@link QOTWPointsService} managing {@link QOTWAccount}s
4340
* @param asyncPool The thread pool for asynchronous operations
44-
* @param qotwPointsRepository Dao object that represents the QOTW_POINTS SQL Table.
4541
*/
46-
public QOTWLeaderboardSubcommand(QOTWPointsService pointsService, ExecutorService asyncPool, QuestionPointsRepository qotwPointsRepository) {
42+
public QOTWLeaderboardSubcommand(QOTWPointsService pointsService, ExecutorService asyncPool) {
4743
setCommandData(new SubcommandData("qotw", "The QOTW Points Leaderboard."));
4844
this.pointsService=pointsService;
4945
this.asyncPool = asyncPool;
50-
this.qotwPointsRepository = qotwPointsRepository;
5146
}
5247

5348
@Override
5449
public void execute(SlashCommandInteractionEvent event) {
5550
event.deferReply().queue();
5651
asyncPool.submit(() -> {
5752
try {
58-
WebhookMessageCreateAction<Message> action = event.getHook().sendMessageEmbeds(buildLeaderboardRankEmbed(event.getMember()));
53+
List<Pair<QOTWAccount,Member>> topMembers = pointsService.getTopMembers(DISPLAY_COUNT, event.getGuild());
54+
WebhookMessageCreateAction<Message> action = event.getHook().sendMessageEmbeds(buildLeaderboardRankEmbed(event.getMember(), topMembers));
5955
// check whether the image may already been cached
60-
byte[] array = LeaderboardCreator.attemptLoadFromCache(getCacheName(), ()->generateLeaderboard(event.getGuild()));
56+
byte[] array = LeaderboardCreator.attemptLoadFromCache(getCacheName(topMembers), ()->generateLeaderboard(topMembers));
6157
action.addFiles(FileUpload.fromData(new ByteArrayInputStream(array), Instant.now().getEpochSecond() + ".png")).queue();
6258
} catch (IOException e) {
6359
ExceptionLogger.capture(e, getClass().getSimpleName());
@@ -68,11 +64,12 @@ public void execute(SlashCommandInteractionEvent event) {
6864
/**
6965
* Builds the Leaderboard Rank {@link MessageEmbed}.
7066
*
67+
* @param topMembers the accounts with the top QOTW users
7168
* @param member The member which executed the command.
7269
* @return A {@link MessageEmbed} object.
7370
*/
74-
private MessageEmbed buildLeaderboardRankEmbed(Member member) {
75-
int rank = pointsService.getQOTWRank(member.getIdLong());
71+
private MessageEmbed buildLeaderboardRankEmbed(Member member, List<Pair<QOTWAccount, Member>> topMembers) {
72+
int rank = findRankOfMember(member, topMembers);
7673
String rankSuffix = switch (rank % 10) {
7774
case 1 -> "st";
7875
case 2 -> "nd";
@@ -90,49 +87,53 @@ private MessageEmbed buildLeaderboardRankEmbed(Member member) {
9087
.build();
9188
}
9289

90+
private int findRankOfMember(Member member, List<Pair<QOTWAccount, Member>> topMembers) {
91+
return pointsService.getQOTWRank(member.getIdLong(),
92+
topMembers
93+
.stream()
94+
.map(Pair::first)
95+
.toList());
96+
}
97+
9398
/**
9499
* Draws a single "user card".
95100
*
96101
* @param leaderboardCreator handling actual drawing.
97102
* @param member The member.
98103
* @param service The {@link QOTWPointsService}.
104+
* @param topMembers the accounts with the top QOTW users
99105
* @throws IOException If an error occurs.
100106
*/
101-
private void drawUserCard(LeaderboardCreator leaderboardCreator, @NotNull Member member, QOTWPointsService service) throws IOException {
102-
leaderboardCreator.drawLeaderboardEntry(member, UserUtils.getUserTag(member.getUser()), service.getPoints(member.getIdLong()), service.getQOTWRank(member.getIdLong()));
107+
private void drawUserCard(LeaderboardCreator leaderboardCreator, @NotNull Member member, QOTWPointsService service, List<Pair<QOTWAccount, Member>> topMembers) throws IOException {
108+
leaderboardCreator.drawLeaderboardEntry(member, UserUtils.getUserTag(member.getUser()), service.getPoints(member.getIdLong()), findRankOfMember(member, topMembers));
103109
}
104110

105111
/**
106112
* Draws and constructs the leaderboard image.
107113
*
108-
* @param guild The current guild.
114+
* @param topMembers the accounts with the top QOTW users
109115
* @return The finished image as a {@link ByteArrayInputStream}.
110116
* @throws IOException If an error occurs.
111117
*/
112-
private @NotNull byte[] generateLeaderboard(Guild guild) throws IOException {
113-
List<Pair<QOTWAccount, Member>> topMembers = pointsService.getTopMembers(DISPLAY_COUNT, guild);
114-
118+
private @NotNull byte[] generateLeaderboard(List<Pair<QOTWAccount, Member>> topMembers) throws IOException {
115119
try(LeaderboardCreator creator = new LeaderboardCreator(Math.min(DISPLAY_COUNT, topMembers.size()), "QuestionOfTheWeekHeader")){
116120
for (Pair<QOTWAccount, Member> pair : topMembers) {
117-
drawUserCard(creator, pair.second(), pointsService);
121+
drawUserCard(creator, pair.second(), pointsService, topMembers);
118122
}
119-
return creator.getImageBytes(getCacheName(), "qotw_leaderboard");
123+
return creator.getImageBytes(getCacheName(topMembers), "qotw_leaderboard");
120124
}
121125
}
122126

123127
/**
124128
* Builds the cached image's name.
125129
*
130+
* @param topMembers the accounts with the top QOTW users
126131
* @return The image's cache name.
127132
*/
128-
private @NotNull String getCacheName() {
133+
private @NotNull String getCacheName(List<Pair<QOTWAccount, Member>> topMembers) {
129134
try {
130-
List<QOTWAccount> accounts = qotwPointsRepository.sortByPoints(QOTWPointsService.getCurrentMonth())
131-
.stream()
132-
.limit(DISPLAY_COUNT)
133-
.toList();
134135
StringBuilder sb = new StringBuilder("qotw_leaderboard_");
135-
accounts.forEach(account -> sb.append(String.format(":%s:%s", account.getUserId(), account.getPoints())));
136+
topMembers.forEach(account -> sb.append(String.format(":%s:%s", account.first().getUserId(), account.first().getPoints())));
136137
return sb.toString();
137138
} catch (DataAccessException e) {
138139
ExceptionLogger.capture(e, getClass().getSimpleName());
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package net.discordjug.javabot.systems.qotw;
2+
3+
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
6+
import java.util.List;
7+
8+
import org.junit.jupiter.api.Test;
9+
10+
import net.discordjug.javabot.systems.qotw.model.QOTWAccount;
11+
12+
class QOTWPointsServiceTest {
13+
14+
private QOTWPointsService pointsService = new QOTWPointsService(null);
15+
16+
@Test
17+
void testGetQOTWRankNotPresent() {
18+
assertEquals(-1, pointsService.getQOTWRank(0, List.of()));
19+
assertEquals(-1, pointsService.getQOTWRank(0, List.of(createAccount(1, 1))));
20+
}
21+
22+
@Test
23+
void testNormalQOTWRank() {
24+
assertEquals(1, pointsService.getQOTWRank(1, List.of(createAccount(1, 1))));
25+
assertEquals(2, pointsService.getQOTWRank(2, List.of(
26+
createAccount(1, 2),
27+
createAccount(2, 1)
28+
)));
29+
}
30+
31+
@Test
32+
void testQOTWRankWithTiesBefore() {
33+
assertEquals(3, pointsService.getQOTWRank(1, List.of(
34+
createAccount(2, 2),
35+
createAccount(3, 2),
36+
createAccount(1, 1)
37+
)));
38+
}
39+
40+
@Test
41+
void testQOTWRankWithTiesAtSamePosition() {
42+
assertEquals(2, pointsService.getQOTWRank(1, List.of(
43+
createAccount(2, 2),
44+
createAccount(3, 1),
45+
createAccount(1, 1)
46+
)));
47+
assertEquals(2, pointsService.getQOTWRank(1, List.of(
48+
createAccount(2, 2),
49+
createAccount(1, 1),
50+
createAccount(3, 1)
51+
)));
52+
}
53+
54+
private QOTWAccount createAccount(long userId, int score) {
55+
QOTWAccount account = new QOTWAccount();
56+
account.setUserId(userId);
57+
account.setPoints(score);
58+
return account;
59+
}
60+
61+
}

0 commit comments

Comments
 (0)