1
1
package org .togetherjava .tjbot .commands .tophelper ;
2
2
3
+ import com .github .freva .asciitable .AsciiTable ;
4
+ import com .github .freva .asciitable .Column ;
5
+ import com .github .freva .asciitable .ColumnData ;
3
6
import com .github .freva .asciitable .HorizontalAlign ;
4
7
import net .dv8tion .jda .api .entities .Member ;
5
8
import net .dv8tion .jda .api .events .interaction .SlashCommandEvent ;
6
9
import net .dv8tion .jda .api .interactions .Interaction ;
7
10
import org .jetbrains .annotations .NotNull ;
11
+ import org .jetbrains .annotations .Nullable ;
8
12
import org .jooq .Records ;
9
13
import org .jooq .impl .DSL ;
14
+ import org .slf4j .Logger ;
15
+ import org .slf4j .LoggerFactory ;
10
16
import org .togetherjava .tjbot .commands .SlashCommandAdapter ;
11
17
import org .togetherjava .tjbot .commands .SlashCommandVisibility ;
12
18
import org .togetherjava .tjbot .db .Database ;
13
- import org .togetherjava .tjbot .db .generated .tables .HelpChannelMessages ;
14
19
20
+ import java .time .Instant ;
21
+ import java .time .Period ;
15
22
import java .util .Collection ;
16
23
import java .util .List ;
17
24
import java .util .Map ;
25
+ import java .util .function .Function ;
26
+ import java .util .function .IntFunction ;
18
27
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 ;
19
31
20
32
/**
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}.
22
36
*/
23
37
public final class TopHelpersCommand extends SlashCommandAdapter {
38
+ private static final Logger logger = LoggerFactory .getLogger (TopHelpersCommand .class );
24
39
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 ;
34
41
35
42
private final Database database ;
36
43
37
44
/**
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
41
47
*/
42
48
public TopHelpersCommand (@ NotNull Database database ) {
43
49
super (COMMAND_NAME , "Lists top helpers for the last 30 days" , SlashCommandVisibility .GUILD );
@@ -46,51 +52,89 @@ public TopHelpersCommand(@NotNull Database database) {
46
52
47
53
@ Override
48
54
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 );
68
133
}
69
134
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 ) {
77
136
}
78
137
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 ) {
95
139
}
96
140
}
0 commit comments