Skip to content

Commit

Permalink
Improved top-helpers code
Browse files Browse the repository at this point in the history
* changed the layout of the methods
* fixed style issues
* fixed various design issues
* streamlined the code more

the logic itself merely changed
  • Loading branch information
Zabuzard committed Jan 27, 2022
1 parent 6e93a24 commit 3a76f0c
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 153 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,43 +1,51 @@
package org.togetherjava.tjbot.commands.tophelper;

import com.github.freva.asciitable.AsciiTable;
import com.github.freva.asciitable.Column;
import com.github.freva.asciitable.ColumnData;
import com.github.freva.asciitable.HorizontalAlign;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.interactions.Interaction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jooq.Records;
import org.jooq.impl.DSL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.togetherjava.tjbot.commands.SlashCommandAdapter;
import org.togetherjava.tjbot.commands.SlashCommandVisibility;
import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages;

import java.time.Instant;
import java.time.Period;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages.HELP_CHANNEL_MESSAGES;

/**
* Command to retrieve top helpers for last 30 days.
* Command that displays the top helpers of a given time range.
*
* Top helpers are measured by their message count in help channels, as set by
* {@link TopHelpersMessageListener}.
*/
public final class TopHelpersCommand extends SlashCommandAdapter {
private static final Logger logger = LoggerFactory.getLogger(TopHelpersCommand.class);
private static final String COMMAND_NAME = "top-helpers";

public static final String PLAINTEXT_MESSAGE_TEMPLATE = "```\n%s\n```";
private static final String COUNT_OPTION = "count";
private static final String NO_ENTRIES = "No entries";

private static final int HELPER_LIMIT = 30;

private record TopHelperRow(Integer serialId, Long userId, Long messageCount) {
}
private static final int TOP_HELPER_LIMIT = 20;

private final Database database;

/**
* Initializes TopHelpers with a database.
*
* @param database the database to store the key-value pairs in
* Creates a new instance.
*
* @param database the database containing the message counts of top helpers
*/
public TopHelpersCommand(@NotNull Database database) {
super(COMMAND_NAME, "Lists top helpers for the last 30 days", SlashCommandVisibility.GUILD);
Expand All @@ -46,51 +54,89 @@ public TopHelpersCommand(@NotNull Database database) {

@Override
public void onSlashCommand(@NotNull SlashCommandEvent event) {
long guildId = event.getGuild().getIdLong();
database.readAndConsume(context -> {
List<TopHelperRow> records = context.with("TOPHELPERS")
.as(DSL
.select(HelpChannelMessages.HELP_CHANNEL_MESSAGES.AUTHOR_ID,
DSL.count().as("COUNT"))
.from(HelpChannelMessages.HELP_CHANNEL_MESSAGES)
.where(HelpChannelMessages.HELP_CHANNEL_MESSAGES.GUILD_ID.eq(guildId))
.groupBy(HelpChannelMessages.HELP_CHANNEL_MESSAGES.AUTHOR_ID)
.orderBy(DSL.count().desc())
.limit(HELPER_LIMIT))
.select(DSL.rowNumber()
.over(DSL.orderBy(DSL.field(DSL.name("COUNT")).desc()))
.as("#"), DSL.field(DSL.name("AUTHOR_ID"), Long.class),
DSL.field(DSL.name("COUNT"), Long.class))
.from(DSL.table(DSL.name("TOPHELPERS")))
.fetch(Records.mapping(TopHelperRow::new));
generateResponse(event, records);
});
List<TopHelperResult> topHelpers =
computeTopHelpersDescending(event.getGuild().getIdLong());

if (topHelpers.isEmpty()) {
event.reply("No entries for the selected time range.").queue();
}
event.deferReply().queue();

List<Long> topHelperIds = topHelpers.stream().map(TopHelperResult::authorId).toList();
event.getGuild()
.retrieveMembersByIds(topHelperIds)
.onError(error -> handleError(error, event))
.onSuccess(members -> handleTopHelpers(topHelpers, members, event));
}

private @NotNull List<TopHelperResult> computeTopHelpersDescending(long guildId) {
return database.read(context -> context.select(HELP_CHANNEL_MESSAGES.AUTHOR_ID, DSL.count())
.from(HELP_CHANNEL_MESSAGES)
.where(HELP_CHANNEL_MESSAGES.GUILD_ID.eq(guildId)
.and(HELP_CHANNEL_MESSAGES.SENT_AT
.greaterOrEqual(Instant.now().minus(Period.ofDays(30)))))
.groupBy(HELP_CHANNEL_MESSAGES.AUTHOR_ID)
.orderBy(DSL.count().desc())
.limit(TOP_HELPER_LIMIT)
.fetch(Records.mapping(TopHelperResult::new)));
}

private static void handleError(@NotNull Throwable error, @NotNull Interaction event) {
logger.warn("Failed to compute top-helpers", error);
event.getHook().editOriginal("Sorry, something went wrong.").queue();
}

private static void handleTopHelpers(@NotNull Collection<TopHelperResult> topHelpers,
@NotNull Collection<? extends Member> members, @NotNull Interaction event) {
Map<Long, Member> userIdToMember =
members.stream().collect(Collectors.toMap(Member::getIdLong, Function.identity()));

List<List<String>> topHelpersDataTable = topHelpers.stream()
.map(topHelper -> topHelperToDataRow(topHelper,
userIdToMember.get(topHelper.authorId())))
.toList();

String message = "```java%n%s%n```".formatted(dataTableToString(topHelpersDataTable));

event.getHook().editOriginal(message).queue();
}

private static @NotNull List<String> topHelperToDataRow(@NotNull TopHelperResult topHelper,
@Nullable Member member) {
String id = Long.toString(topHelper.authorId());
String name = member == null ? "UNKNOWN_USER" : member.getEffectiveName();
String messageCount = Integer.toString(topHelper.messageCount());

return List.of(id, name, messageCount);
}

private static @NotNull String dataTableToString(@NotNull Collection<List<String>> dataTable) {
return dataTableToAsciiTable(dataTable,
List.of(new ColumnSetting("Id", HorizontalAlign.RIGHT),
new ColumnSetting("Name", HorizontalAlign.RIGHT),
new ColumnSetting("Message count (30 days)", HorizontalAlign.RIGHT)));
}

private static @NotNull String dataTableToAsciiTable(
@NotNull Collection<List<String>> dataTable,
@NotNull List<ColumnSetting> columnSettings) {
IntFunction<String> headerToAlignment = i -> columnSettings.get(i).headerName();
IntFunction<HorizontalAlign> indexToAlignment = i -> columnSettings.get(i).alignment();

IntFunction<ColumnData<List<String>>> indexToColumn =
i -> new Column().header(headerToAlignment.apply(i))
.dataAlign(indexToAlignment.apply(i))
.with(row -> row.get(i));

List<ColumnData<List<String>>> columns =
IntStream.range(0, columnSettings.size()).mapToObj(indexToColumn).toList();

return AsciiTable.getTable(AsciiTable.BASIC_ASCII_NO_DATA_SEPARATORS, dataTable, columns);
}

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

private static void generateResponse(@NotNull Interaction event,
@NotNull Collection<TopHelperRow> records) {
List<Long> userIds = records.stream().map(TopHelperRow::userId).toList();
event.getGuild().retrieveMembersByIds(userIds).onSuccess(members -> {
Map<Long, String> activeUserIdToEffectiveNames = members.stream()
.collect(Collectors.toMap(Member::getIdLong, Member::getEffectiveName));
List<List<String>> topHelpersDataframe = records.stream()
.map(topHelperRow -> List.of(topHelperRow.serialId.toString(),
activeUserIdToEffectiveNames.getOrDefault(topHelperRow.userId,
// Any user who is no more a part of the guild is marked as
// [UNKNOWN]
"[UNKNOWN]"),
topHelperRow.messageCount.toString()))
.toList();
event.reply(prettyFormatOutput(topHelpersDataframe)).queue();
});
private record ColumnSetting(String headerName, HorizontalAlign alignment) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,30 @@
import org.togetherjava.tjbot.commands.MessageReceiverAdapter;
import org.togetherjava.tjbot.config.Config;
import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.time.Period;
import java.util.regex.Pattern;

import static org.togetherjava.tjbot.db.generated.tables.HelpChannelMessages.HELP_CHANNEL_MESSAGES;

/**
* Listener responsible for persistence of text message metadata.
* Listener that receives all sent help messages and puts them into the database for
* {@link TopHelpersCommand} to pick them up.
*
* Also runs a cleanup routine to get rid of old entries. In general, it manages the database data
* to determine top-helpers.
*/
public final class TopHelpersMessageListener extends MessageReceiverAdapter {
private static final Logger logger = LoggerFactory.getLogger(TopHelpersMessageListener.class);

private static final int MESSAGE_METADATA_ARCHIVAL_DAYS = 30;
private static final Period DELETE_MESSAGE_RECORDS_AFTER = Period.ofDays(90);

private final Database database;

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

@Override
public void onMessageReceived(@NotNull GuildMessageReceivedEvent event) {
var channel = event.getChannel();
if (!event.getAuthor().isBot() && !event.isWebhookMessage()) {
var messageId = event.getMessage().getIdLong();
var guildId = event.getGuild().getIdLong();
var channelId = channel.getIdLong();
var userId = event.getAuthor().getIdLong();
var createTimestamp = event.getMessage().getTimeCreated().toInstant();
database.write(dsl -> {
dsl.newRecord(HelpChannelMessages.HELP_CHANNEL_MESSAGES)
.setMessageId(messageId)
.setGuildId(guildId)
.setChannelId(channelId)
.setAuthorId(userId)
.setSentAt(createTimestamp)
.insert();
int noOfRowsDeleted = dsl.deleteFrom(HelpChannelMessages.HELP_CHANNEL_MESSAGES)
.where(HelpChannelMessages.HELP_CHANNEL_MESSAGES.SENT_AT
.le(Instant.now().minus(MESSAGE_METADATA_ARCHIVAL_DAYS, ChronoUnit.DAYS)))
.execute();
if (noOfRowsDeleted > 0) {
logger.debug(
"{} old records have been deleted based on archival criteria of {} days.",
noOfRowsDeleted, MESSAGE_METADATA_ARCHIVAL_DAYS);
}
});
if (event.getAuthor().isBot() || event.isWebhookMessage()) {
return;
}

addMessageRecord(event);
// TODO Use a routine that runs every 4 hours for the deletion instead
deleteOldMessageRecords();
}

private void addMessageRecord(@NotNull GuildMessageReceivedEvent event) {
database.write(context -> context.newRecord(HELP_CHANNEL_MESSAGES)
.setMessageId(event.getMessage().getIdLong())
.setGuildId(event.getGuild().getIdLong())
.setChannelId(event.getChannel().getIdLong())
.setAuthorId(event.getAuthor().getIdLong())
.setSentAt(event.getMessage().getTimeCreated().toInstant())
.insert());
}

private void deleteOldMessageRecords() {
int recordsDeleted =
database.writeAndProvide(context -> context.deleteFrom(HELP_CHANNEL_MESSAGES)
.where(HELP_CHANNEL_MESSAGES.SENT_AT
.lessOrEqual(Instant.now().minus(DELETE_MESSAGE_RECORDS_AFTER)))
.execute());

if (recordsDeleted > 0) {
logger.debug(
"{} old help message records have been deleted because they are older than {}.",
recordsDeleted, DELETE_MESSAGE_RECORDS_AFTER);
}
}
}

0 comments on commit 3a76f0c

Please sign in to comment.