Skip to content

Commit 3998789

Browse files
committed
Improved top-helpers code
* changed the layout of the methods * fixed style issues * fixed various design issues * streamlined the code more the logic itself merely changed
1 parent 6e93a24 commit 3998789

File tree

3 files changed

+141
-153
lines changed

3 files changed

+141
-153
lines changed

application/src/main/java/org/togetherjava/tjbot/commands/tophelper/PresentationUtils.java

Lines changed: 0 additions & 65 deletions
This file was deleted.
Lines changed: 100 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,49 @@
11
package org.togetherjava.tjbot.commands.tophelper;
22

3+
import com.github.freva.asciitable.AsciiTable;
4+
import com.github.freva.asciitable.Column;
5+
import com.github.freva.asciitable.ColumnData;
36
import com.github.freva.asciitable.HorizontalAlign;
47
import net.dv8tion.jda.api.entities.Member;
58
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
69
import net.dv8tion.jda.api.interactions.Interaction;
710
import org.jetbrains.annotations.NotNull;
11+
import org.jetbrains.annotations.Nullable;
812
import org.jooq.Records;
913
import org.jooq.impl.DSL;
14+
import org.slf4j.Logger;
15+
import org.slf4j.LoggerFactory;
1016
import org.togetherjava.tjbot.commands.SlashCommandAdapter;
1117
import org.togetherjava.tjbot.commands.SlashCommandVisibility;
1218
import org.togetherjava.tjbot.db.Database;
13-
import org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages;
1419

20+
import java.time.Instant;
21+
import java.time.Period;
1522
import java.util.Collection;
1623
import java.util.List;
1724
import java.util.Map;
25+
import java.util.function.Function;
26+
import java.util.function.IntFunction;
1827
import java.util.stream.Collectors;
28+
import java.util.stream.IntStream;
29+
30+
import static org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages.HELP_CHANNEL_MESSAGES;
1931

2032
/**
21-
* Command to retrieve top helpers for last 30 days.
33+
* Command that displays the top helpers of a given time range.
34+
*
35+
* Top helpers are measured by their message count in help channels, as set by {@link TopHelpersMessageListener}.
2236
*/
2337
public final class TopHelpersCommand extends SlashCommandAdapter {
38+
private static final Logger logger = LoggerFactory.getLogger(TopHelpersCommand.class);
2439
private static final String COMMAND_NAME = "top-helpers";
25-
26-
public static final String PLAINTEXT_MESSAGE_TEMPLATE = "```\n%s\n```";
27-
private static final String COUNT_OPTION = "count";
28-
private static final String NO_ENTRIES = "No entries";
29-
30-
private static final int HELPER_LIMIT = 30;
31-
32-
private record TopHelperRow(Integer serialId, Long userId, Long messageCount) {
33-
}
40+
private static final int TOP_HELPER_LIMIT = 20;
3441

3542
private final Database database;
3643

3744
/**
38-
* Initializes TopHelpers with a database.
39-
*
40-
* @param database the database to store the key-value pairs in
45+
* Creates a new instance.
46+
* @param database the database containing the message counts of top helpers
4147
*/
4248
public TopHelpersCommand(@NotNull Database database) {
4349
super(COMMAND_NAME, "Lists top helpers for the last 30 days", SlashCommandVisibility.GUILD);
@@ -46,51 +52,89 @@ public TopHelpersCommand(@NotNull Database database) {
4652

4753
@Override
4854
public void onSlashCommand(@NotNull SlashCommandEvent event) {
49-
long guildId = event.getGuild().getIdLong();
50-
database.readAndConsume(context -> {
51-
List<TopHelperRow> records = context.with("TOPHELPERS")
52-
.as(DSL
53-
.select(HelpChannelMessages.HELP_CHANNEL_MESSAGES.AUTHOR_ID,
54-
DSL.count().as("COUNT"))
55-
.from(HelpChannelMessages.HELP_CHANNEL_MESSAGES)
56-
.where(HelpChannelMessages.HELP_CHANNEL_MESSAGES.GUILD_ID.eq(guildId))
57-
.groupBy(HelpChannelMessages.HELP_CHANNEL_MESSAGES.AUTHOR_ID)
58-
.orderBy(DSL.count().desc())
59-
.limit(HELPER_LIMIT))
60-
.select(DSL.rowNumber()
61-
.over(DSL.orderBy(DSL.field(DSL.name("COUNT")).desc()))
62-
.as("#"), DSL.field(DSL.name("AUTHOR_ID"), Long.class),
63-
DSL.field(DSL.name("COUNT"), Long.class))
64-
.from(DSL.table(DSL.name("TOPHELPERS")))
65-
.fetch(Records.mapping(TopHelperRow::new));
66-
generateResponse(event, records);
67-
});
55+
List<TopHelperResult> topHelpers =
56+
computeTopHelpersDescending(event.getGuild().getIdLong());
57+
58+
if (topHelpers.isEmpty()) {
59+
event.reply("No entries for the selected time range.").queue();
60+
}
61+
event.deferReply().queue();
62+
63+
List<Long> topHelperIds = topHelpers.stream().map(TopHelperResult::authorId).toList();
64+
event.getGuild()
65+
.retrieveMembersByIds(topHelperIds)
66+
.onError(error -> handleError(error, event))
67+
.onSuccess(members -> handleTopHelpers(topHelpers, members, event));
68+
}
69+
70+
private @NotNull List<TopHelperResult> computeTopHelpersDescending(long guildId) {
71+
return database.read(context -> context.select(HELP_CHANNEL_MESSAGES.AUTHOR_ID, DSL.count())
72+
.from(HELP_CHANNEL_MESSAGES)
73+
.where(HELP_CHANNEL_MESSAGES.GUILD_ID.eq(guildId)
74+
.and(HELP_CHANNEL_MESSAGES.SENT_AT
75+
.greaterOrEqual(Instant.now().minus(Period.ofDays(30)))))
76+
.groupBy(HELP_CHANNEL_MESSAGES.AUTHOR_ID)
77+
.orderBy(DSL.count().desc())
78+
.limit(TOP_HELPER_LIMIT)
79+
.fetch(Records.mapping(TopHelperResult::new)));
80+
}
81+
82+
private static void handleError(@NotNull Throwable error, @NotNull Interaction event) {
83+
logger.warn("Failed to compute top-helpers", error);
84+
event.getHook().editOriginal("Sorry, something went wrong.").queue();
85+
}
86+
87+
private static void handleTopHelpers(@NotNull Collection<TopHelperResult> topHelpers,
88+
@NotNull Collection<? extends Member> members, @NotNull Interaction event) {
89+
Map<Long, Member> userIdToMember =
90+
members.stream().collect(Collectors.toMap(Member::getIdLong, Function.identity()));
91+
92+
List<List<String>> topHelpersDataTable = topHelpers.stream()
93+
.map(topHelper -> topHelperToDataRow(topHelper,
94+
userIdToMember.get(topHelper.authorId())))
95+
.toList();
96+
97+
String message = "```java%n%s%n```".formatted(dataTableToString(topHelpersDataTable));
98+
99+
event.getHook().editOriginal(message).queue();
100+
}
101+
102+
private static @NotNull List<String> topHelperToDataRow(@NotNull TopHelperResult topHelper,
103+
@Nullable Member member) {
104+
String id = Long.toString(topHelper.authorId());
105+
String name = member == null ? "UNKNOWN_USER" : member.getEffectiveName();
106+
String messageCount = Integer.toString(topHelper.messageCount());
107+
108+
return List.of(id, name, messageCount);
109+
}
110+
111+
private static @NotNull String dataTableToString(@NotNull Collection<List<String>> dataTable) {
112+
return dataTableToAsciiTable(dataTable,
113+
List.of(new ColumnSetting("Id", HorizontalAlign.RIGHT),
114+
new ColumnSetting("Name", HorizontalAlign.RIGHT),
115+
new ColumnSetting("Message count (30 days)", HorizontalAlign.RIGHT)));
116+
}
117+
118+
private static @NotNull String dataTableToAsciiTable(
119+
@NotNull Collection<List<String>> dataTable,
120+
@NotNull List<ColumnSetting> columnSettings) {
121+
IntFunction<String> headerToAlignment = i -> columnSettings.get(i).headerName();
122+
IntFunction<HorizontalAlign> indexToAlignment = i -> columnSettings.get(i).alignment();
123+
124+
IntFunction<ColumnData<List<String>>> indexToColumn =
125+
i -> new Column().header(headerToAlignment.apply(i))
126+
.dataAlign(indexToAlignment.apply(i))
127+
.with(row -> row.get(i));
128+
129+
List<ColumnData<List<String>>> columns =
130+
IntStream.range(0, columnSettings.size()).mapToObj(indexToColumn).toList();
131+
132+
return AsciiTable.getTable(AsciiTable.BASIC_ASCII_NO_DATA_SEPARATORS, dataTable, columns);
68133
}
69134

70-
private static @NotNull String prettyFormatOutput(@NotNull List<List<String>> dataFrame) {
71-
return String.format(PLAINTEXT_MESSAGE_TEMPLATE,
72-
dataFrame.isEmpty() ? NO_ENTRIES
73-
: PresentationUtils.dataFrameToAsciiTable(dataFrame,
74-
new String[] {"#", "Name", "Message Count (in the last 30 days)"},
75-
new HorizontalAlign[] {HorizontalAlign.RIGHT, HorizontalAlign.LEFT,
76-
HorizontalAlign.RIGHT}));
135+
private record TopHelperResult(long authorId, int messageCount) {
77136
}
78137

79-
private static void generateResponse(@NotNull Interaction event,
80-
@NotNull Collection<TopHelperRow> records) {
81-
List<Long> userIds = records.stream().map(TopHelperRow::userId).toList();
82-
event.getGuild().retrieveMembersByIds(userIds).onSuccess(members -> {
83-
Map<Long, String> activeUserIdToEffectiveNames = members.stream()
84-
.collect(Collectors.toMap(Member::getIdLong, Member::getEffectiveName));
85-
List<List<String>> topHelpersDataframe = records.stream()
86-
.map(topHelperRow -> List.of(topHelperRow.serialId.toString(),
87-
activeUserIdToEffectiveNames.getOrDefault(topHelperRow.userId,
88-
// Any user who is no more a part of the guild is marked as
89-
// [UNKNOWN]
90-
"[UNKNOWN]"),
91-
topHelperRow.messageCount.toString()))
92-
.toList();
93-
event.reply(prettyFormatOutput(topHelpersDataframe)).queue();
94-
});
138+
private record ColumnSetting(String headerName, HorizontalAlign alignment) {
95139
}
96140
}

application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,30 @@
77
import org.togetherjava.tjbot.commands.MessageReceiverAdapter;
88
import org.togetherjava.tjbot.config.Config;
99
import org.togetherjava.tjbot.db.Database;
10-
import org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages;
1110

1211
import java.time.Instant;
13-
import java.time.temporal.ChronoUnit;
12+
import java.time.Period;
1413
import java.util.regex.Pattern;
1514

15+
import static org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages.HELP_CHANNEL_MESSAGES;
16+
1617
/**
17-
* Listener responsible for persistence of text message metadata.
18+
* Listener that receives all sent help messages and puts them into the database for
19+
* {@link TopHelpersCommand} to pick them up.
20+
*
21+
* Also runs a cleanup routine to get rid of old entries. In general, it manages the database data
22+
* to determine top-helpers.
1823
*/
1924
public final class TopHelpersMessageListener extends MessageReceiverAdapter {
2025
private static final Logger logger = LoggerFactory.getLogger(TopHelpersMessageListener.class);
21-
22-
private static final int MESSAGE_METADATA_ARCHIVAL_DAYS = 30;
26+
private static final Period DELETE_MESSAGE_RECORDS_AFTER = Period.ofDays(90);
2327

2428
private final Database database;
2529

2630
/**
27-
* Creates a new message metadata listener, using the given database.
31+
* Creates a new listener to receive all message sent in help channels.
2832
*
29-
* @param database the database to store message metadata.
33+
* @param database to store message meta-data in
3034
*/
3135
public TopHelpersMessageListener(@NotNull Database database) {
3236
super(Pattern.compile(Config.getInstance().getHelpChannelPattern()));
@@ -35,31 +39,36 @@ public TopHelpersMessageListener(@NotNull Database database) {
3539

3640
@Override
3741
public void onMessageReceived(@NotNull GuildMessageReceivedEvent event) {
38-
var channel = event.getChannel();
39-
if (!event.getAuthor().isBot() && !event.isWebhookMessage()) {
40-
var messageId = event.getMessage().getIdLong();
41-
var guildId = event.getGuild().getIdLong();
42-
var channelId = channel.getIdLong();
43-
var userId = event.getAuthor().getIdLong();
44-
var createTimestamp = event.getMessage().getTimeCreated().toInstant();
45-
database.write(dsl -> {
46-
dsl.newRecord(HelpChannelMessages.HELP_CHANNEL_MESSAGES)
47-
.setMessageId(messageId)
48-
.setGuildId(guildId)
49-
.setChannelId(channelId)
50-
.setAuthorId(userId)
51-
.setSentAt(createTimestamp)
52-
.insert();
53-
int noOfRowsDeleted = dsl.deleteFrom(HelpChannelMessages.HELP_CHANNEL_MESSAGES)
54-
.where(HelpChannelMessages.HELP_CHANNEL_MESSAGES.SENT_AT
55-
.le(Instant.now().minus(MESSAGE_METADATA_ARCHIVAL_DAYS, ChronoUnit.DAYS)))
56-
.execute();
57-
if (noOfRowsDeleted > 0) {
58-
logger.debug(
59-
"{} old records have been deleted based on archival criteria of {} days.",
60-
noOfRowsDeleted, MESSAGE_METADATA_ARCHIVAL_DAYS);
61-
}
62-
});
42+
if (event.getAuthor().isBot() || event.isWebhookMessage()) {
43+
return;
44+
}
45+
46+
addMessageRecord(event);
47+
// TODO Use a routine that runs every 4 hours for the deletion instead
48+
deleteOldMessageRecords();
49+
}
50+
51+
private void addMessageRecord(@NotNull GuildMessageReceivedEvent event) {
52+
database.write(context -> context.newRecord(HELP_CHANNEL_MESSAGES)
53+
.setMessageId(event.getMessage().getIdLong())
54+
.setGuildId(event.getGuild().getIdLong())
55+
.setChannelId(event.getChannel().getIdLong())
56+
.setAuthorId(event.getAuthor().getIdLong())
57+
.setSentAt(event.getMessage().getTimeCreated().toInstant())
58+
.insert());
59+
}
60+
61+
private void deleteOldMessageRecords() {
62+
int recordsDeleted =
63+
database.writeAndProvide(context -> context.deleteFrom(HELP_CHANNEL_MESSAGES)
64+
.where(HELP_CHANNEL_MESSAGES.SENT_AT
65+
.lessOrEqual(Instant.now().minus(DELETE_MESSAGE_RECORDS_AFTER)))
66+
.execute());
67+
68+
if (recordsDeleted > 0) {
69+
logger.debug(
70+
"{} old help message records have been deleted because they are older than {}.",
71+
recordsDeleted, DELETE_MESSAGE_RECORDS_AFTER);
6372
}
6473
}
6574
}

0 commit comments

Comments
 (0)