added purge command to delete messages via bulk delete

adapted api of status message service
added concept of self destruct command results, these will cause the command message to be deleted
This commit is contained in:
Sheldan
2020-06-03 19:22:31 +02:00
parent 072072dffc
commit 1091e66013
23 changed files with 361 additions and 1 deletions

View File

@@ -0,0 +1,67 @@
package dev.sheldan.abstracto.moderation.commands;
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.HelpInfo;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureEnum;
import dev.sheldan.abstracto.core.utils.ExceptionUtils;
import dev.sheldan.abstracto.moderation.config.ModerationModule;
import dev.sheldan.abstracto.moderation.config.features.ModerationFeatures;
import dev.sheldan.abstracto.moderation.service.PurgeService;
import dev.sheldan.abstracto.templating.service.TemplateService;
import net.dv8tion.jda.api.entities.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class Purge extends AbstractConditionableCommand {
@Autowired
private PurgeService purgeService;
@Autowired
private TemplateService templateService;
@Autowired
private ExceptionUtils exceptionUtils;
@Override
public CommandResult execute(CommandContext commandContext) {
Integer amountOfMessages = (Integer) commandContext.getParameters().getParameters().get(0);
Member memberToPurgeMessagesOf = null;
if(commandContext.getParameters().getParameters().size() == 2) {
memberToPurgeMessagesOf = (Member) commandContext.getParameters().getParameters().get(1);
}
CompletableFuture<Void> future = purgeService.purgeMessagesInChannel(amountOfMessages, commandContext.getChannel(), commandContext.getMessage(), memberToPurgeMessagesOf);
future.whenComplete((aVoid, throwable) -> exceptionUtils.handleExceptionIfTemplatable(throwable, commandContext.getChannel()));
return CommandResult.fromSelfDestruct();
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
parameters.add(Parameter.builder().name("amount").type(Integer.class).templated(true).build());
parameters.add(Parameter.builder().name("member").type(Member.class).optional(true).templated(true).build());
HelpInfo helpInfo = HelpInfo.builder().templated(true).build();
return CommandConfiguration.builder()
.name("purge")
.module(ModerationModule.MODERATION)
.templated(true)
.causesReaction(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureEnum getFeature() {
return ModerationFeatures.MODERATION;
}
}

View File

@@ -0,0 +1,148 @@
package dev.sheldan.abstracto.moderation.service;
import dev.sheldan.abstracto.core.service.MessageService;
import dev.sheldan.abstracto.moderation.exception.NoMessageFoundException;
import dev.sheldan.abstracto.moderation.models.template.commands.PurgeStatusUpdateModel;
import dev.sheldan.abstracto.templating.model.MessageToSend;
import dev.sheldan.abstracto.templating.service.TemplateService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageHistory;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.utils.MiscUtil;
import net.dv8tion.jda.api.utils.TimeUtil;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@Component
@Slf4j
public class PurgeServiceBean implements PurgeService {
@Autowired
private MessageService messageService;
@Autowired
private TemplateService templateService;
@Override
public CompletableFuture<Void> purgeMessagesInChannel(Integer amountToDelete, TextChannel channel, Long startId, Member purgedMember) {
return purgeMessages(amountToDelete, channel, startId, purgedMember, amountToDelete, 0, 0L);
}
@NotNull
private CompletableFuture<Void> purgeMessages(Integer amountToDelete, TextChannel channel, Long startId, Member purgedMember, Integer totalCount, Integer currentCount, Long statusMessageId) {
CompletableFuture<Void> deletionFuture = new CompletableFuture<>();
int toDeleteInThisIteration;
int messageLimit = 100;
if(amountToDelete >= messageLimit){
toDeleteInThisIteration = messageLimit;
} else {
toDeleteInThisIteration = amountToDelete % messageLimit;
}
CompletableFuture<MessageHistory> historyFuture = channel.getHistoryBefore(startId, toDeleteInThisIteration).submit();
CompletableFuture<Long> statusMessageFuture = getOrCreatedStatusMessage(channel, totalCount, statusMessageId);
CompletableFuture<Void> coreFuture = CompletableFuture.allOf(historyFuture, statusMessageFuture);
coreFuture.thenAccept(voidParam -> {
try {
List<Message> retrievedHistory = historyFuture.get().getRetrievedHistory();
List<Message> messagesToDeleteNow = filterMessagesToDelete(retrievedHistory, purgedMember);
Long currentStatusMessageId = statusMessageFuture.get();
if(messagesToDeleteNow.size() == 0) {
deletionFuture.completeExceptionally(new NoMessageFoundException());
channel.deleteMessageById(currentStatusMessageId).queueAfter(5, TimeUnit.SECONDS);
return;
}
Message latestMessage = messagesToDeleteNow.get(messagesToDeleteNow.size() - 1);
log.trace("Deleting {} messages directly", messagesToDeleteNow.size());
int newCurrentCount = currentCount + messagesToDeleteNow.size();
int newAmountToDelete = amountToDelete - messageLimit;
Consumer<Void> consumer = deletionConsumer(newAmountToDelete, channel, purgedMember, totalCount, newCurrentCount, deletionFuture, currentStatusMessageId, latestMessage);
if (messagesToDeleteNow.size() > 1) {
bulkDeleteMessages(channel, deletionFuture, messagesToDeleteNow, consumer);
} else if (messagesToDeleteNow.size() == 1) {
messagesToDeleteNow.get(0).delete().queue(consumer, deletionFuture::completeExceptionally);
}
} catch (Exception e) {
log.warn("Failed to purge messages.", e);
deletionFuture.completeExceptionally(e);
}
}).exceptionally(throwable -> {
log.warn("Failed to fetch messages.", throwable);
return null;
});
return CompletableFuture.allOf(coreFuture, deletionFuture);
}
private void bulkDeleteMessages(TextChannel channel, CompletableFuture<Void> deletionFuture, List<Message> messagesToDeleteNow, Consumer<Void> consumer) {
try {
channel.deleteMessages(messagesToDeleteNow).queue(consumer, deletionFuture::completeExceptionally);
} catch (IllegalArgumentException e) {
channel.sendMessage(e.getMessage()).queue();
log.warn("Failed to bulk delete, message was most likely too old to delete by bulk.", e);
deletionFuture.complete(null);
}
}
private CompletableFuture<Long> getOrCreatedStatusMessage(TextChannel channel, Integer totalCount, Long statusMessageId) {
CompletableFuture<Long> statusMessageFuture;
if(statusMessageId == 0) {
PurgeStatusUpdateModel model = PurgeStatusUpdateModel.builder().currentlyDeleted(0).totalToDelete(totalCount).build();
MessageToSend messageToSend = templateService.renderTemplateToMessageToSend("purge_status_update", model);
statusMessageFuture = messageService.createStatusMessageId(messageToSend, channel);
} else {
statusMessageFuture = CompletableFuture.completedFuture(statusMessageId);
}
return statusMessageFuture;
}
private List<Message> filterMessagesToDelete(List<Message> retrievedHistory, Member purgedMember) {
long twoWeeksAgo = TimeUtil.getDiscordTimestamp((System.currentTimeMillis() - (14 * 24 * 60 * 60 * 1000)));
List<Message> messagesToDeleteNow = new ArrayList<>();
for (Message messageObj : retrievedHistory) {
if (MiscUtil.parseSnowflake(messageObj.getId()) > twoWeeksAgo) {
if(purgedMember != null) {
if(messageObj.getAuthor().getIdLong() == purgedMember.getIdLong()) {
messagesToDeleteNow.add(messageObj);
}
} else {
messagesToDeleteNow.add(messageObj);
}
}
}
return messagesToDeleteNow;
}
private Consumer<Void> deletionConsumer(Integer amountToDelete, TextChannel channel, Member purgedMember, Integer totalCount, Integer currentCount, CompletableFuture<Void> deletionFuture, Long currentStatusMessageId, Message earliestMessage) {
return aVoid -> {
if (amountToDelete > 1) {
purgeMessages(amountToDelete, channel, earliestMessage.getIdLong(), purgedMember, totalCount, currentCount, currentStatusMessageId).thenAccept(aVoid1 ->
deletionFuture.complete(null)
);
} else {
channel.deleteMessageById(currentStatusMessageId).queueAfter(5, TimeUnit.SECONDS);
deletionFuture.complete(null);
}
log.info("Setting status for {} out of {}", currentCount, totalCount);
PurgeStatusUpdateModel finalUpdateModel = PurgeStatusUpdateModel.builder().currentlyDeleted(currentCount).totalToDelete(totalCount).build();
MessageToSend finalUpdateMessage = templateService.renderTemplateToMessageToSend("purge_status_update", finalUpdateModel);
messageService.updateStatusMessage(channel, currentStatusMessageId, finalUpdateMessage);
};
}
@Override
public CompletableFuture<Void> purgeMessagesInChannel(Integer count, TextChannel channel, Message origin, Member purgingRestriction) {
return purgeMessagesInChannel(count, channel, origin.getIdLong(), purgingRestriction);
}
}

View File

@@ -0,0 +1,20 @@
package dev.sheldan.abstracto.moderation.exception;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.templating.Templatable;
public class NoMessageFoundException extends AbstractoRunTimeException implements Templatable {
public NoMessageFoundException() {
super("");
}
@Override
public String getTemplateName() {
return "no_message_found_exception";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.moderation.models.template.commands;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class PurgeStatusUpdateModel {
private Integer currentlyDeleted;
private Integer totalToDelete;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.moderation.service;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import java.util.concurrent.CompletableFuture;
public interface PurgeService {
CompletableFuture<Void> purgeMessagesInChannel(Integer count, TextChannel channel, Long messageId, Member purgingRestriction);
CompletableFuture<Void> purgeMessagesInChannel(Integer count, TextChannel channel, Message origin, Member purgingRestriction);
}

View File

@@ -0,0 +1,18 @@
package dev.sheldan.abstracto.core.command.post;
import dev.sheldan.abstracto.core.command.Command;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.execution.ResultState;
import dev.sheldan.abstracto.core.command.service.PostCommandExecution;
import org.springframework.stereotype.Component;
@Component
public class SelfDestructPostExecution implements PostCommandExecution {
@Override
public void execute(CommandContext commandContext, CommandResult commandResult, Command command) {
if(commandResult.getResult().equals(ResultState.SELF_DESTRUCT)) {
commandContext.getMessage().delete().queue();
}
}
}

View File

@@ -83,11 +83,26 @@ public class MessageServiceBean implements MessageService {
return channelService.sendMessageToSendToAChannel(messageToSend, channel).get(0);
}
@Override
public CompletableFuture<Message> createStatusMessage(MessageToSend messageToSend, MessageChannel channel) {
return channelService.sendMessageToSendToChannel(messageToSend, channel).get(0);
}
@Override
public CompletableFuture<Long> createStatusMessageId(MessageToSend messageToSend, MessageChannel channel) {
return channelService.sendMessageToSendToChannel(messageToSend, channel).get(0).thenApply(ISnowflake::getIdLong);
}
@Override
public void updateStatusMessage(AChannel channel, Long messageId, MessageToSend messageToSend) {
channelService.editMessageInAChannel(messageToSend, channel, messageId);
}
@Override
public void updateStatusMessage(MessageChannel channel, Long messageId, MessageToSend messageToSend) {
channelService.editMessageInAChannel(messageToSend, channel, messageId);
}
@Override
public void sendMessageToUser(AUserInAServer userInAServer, String text, TextChannel feedbackChannel) {
Member memberInServer = botService.getMemberInServer(userInAServer);

View File

@@ -19,6 +19,10 @@ public class CommandResult {
return CommandResult.builder().result(ResultState.SUCCESSFUL).build();
}
public static CommandResult fromSelfDestruct() {
return CommandResult.builder().result(ResultState.SELF_DESTRUCT).build();
}
public static CommandResult fromError(String message){
return CommandResult.builder().result(ResultState.ERROR).message(message).build();
}

View File

@@ -1,5 +1,5 @@
package dev.sheldan.abstracto.core.command.execution;
public enum ResultState {
ERROR, SUCCESSFUL, IGNORED, CONDITION
ERROR, SUCCESSFUL, IGNORED, CONDITION, SELF_DESTRUCT
}

View File

@@ -4,6 +4,7 @@ import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.templating.model.MessageToSend;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.User;
@@ -16,7 +17,10 @@ public interface MessageService {
List<CompletableFuture<Void>> addReactionsToMessageWithFuture(List<String> emoteKeys, Long serverId, Message message);
CompletableFuture<Void> deleteMessageInChannelInServer(Long serverId, Long channelId, Long messageId);
CompletableFuture<Message> createStatusMessage(MessageToSend messageToSend, AChannel channel);
CompletableFuture<Message> createStatusMessage(MessageToSend messageToSend, MessageChannel channel);
CompletableFuture<Long> createStatusMessageId(MessageToSend messageToSend, MessageChannel channel);
void updateStatusMessage(AChannel channel, Long messageId, MessageToSend messageToSend);
void updateStatusMessage(MessageChannel channel, Long messageId, MessageToSend messageToSend);
void sendMessageToUser(AUserInAServer userInAServer, String text, TextChannel feedbackChannel);
void sendMessageToUser(User user, String text, TextChannel feedbackChannel);
}

View File

@@ -0,0 +1,27 @@
package dev.sheldan.abstracto.core.utils;
import dev.sheldan.abstracto.templating.Templatable;
import dev.sheldan.abstracto.templating.service.TemplateService;
import net.dv8tion.jda.api.entities.MessageChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class ExceptionUtils {
@Autowired
private TemplateService templateService;
@Transactional
public void handleExceptionIfTemplatable(Throwable throwable, MessageChannel channel) {
if(throwable != null) {
if(throwable.getCause() instanceof Templatable) {
String exceptionText = templateService.renderTemplatable((Templatable) throwable.getCause());
channel.sendMessage(exceptionText).queue();
} else {
channel.sendMessage(throwable.getCause().getMessage()).queue();
}
}
}
}

View File

@@ -29,6 +29,11 @@ Change the slow mode in a channel::
* Usage: `slowmode <duration> [channel]`
* Description: This command sets the slow mode in the `channel` to the given `duration`. This command uses duration parsing. The `channel` is optional and if none is provided, the current channel is used.
* Example: `slowMode 1h2m3s #general` in order to set the slow mode in channel `general` to 1 hour 2 minutes and 3 seconds (the #general is a user mention)
Purging messages in a channel::
* Usage: `purge <messageCount> [member]`
* Description: Deletes the last `messageCount` messages in the current channel. If a `member` is provided as parameter, only the messages by this member
will be deleted. The deletion of this messages will *not* be logged by the logging mechanism. The messages to be deleted need to be from within the last 2 weeks, but there is no limit on how much messages can be deleted besides that.
While the command is ongoing, a status update message will be shown indicating how far the command is. This message will be deleted after the command is done.
=== Warning

View File

@@ -0,0 +1,6 @@
Deletes the last n messages in the channel. The messages are allowed to be at most 2 weeks old.
If a member is provided as a parameter, only messages by this member are deleted, but at most n messages are considered.
For example, if you execute the command with 200 messages and specify member User#1234, and User#1234 does not have any messages
in the last 200 message, no message will be deleted.
While the command is going on a status message indicating how many messages are currently being deleted is shown.
If messages older than two weeks are found, the command will stop and post an error message.

View File

@@ -0,0 +1 @@
${currentlyDeleted} out of ${totalToDelete} messages deleted.

View File

@@ -114,6 +114,11 @@ public class TemplateServiceBean implements TemplateService {
.build();
}
@Override
public MessageToSend renderTemplateToMessageToSend(String key, Object model) {
return MessageToSend.builder().message(renderTemplate(key, model)).build();
}
private void createFieldsForEmbed(List<EmbedBuilder> embedBuilders, EmbedConfiguration configuration) {
for (int i = 0; i < configuration.getFields().size(); i++) {
EmbedField field = configuration.getFields().get(i);

View File

@@ -18,6 +18,14 @@ public interface TemplateService {
*/
MessageToSend renderEmbedTemplate(String key, Object model);
/**
* Renders the given template directly into a {@link MessageToSend} but the template only represents a text message.
* @param key The key of the string template to be rendered.
* @param model The model used to render the template
* @return A {@link MessageToSend} instance only containing the string property.
*/
MessageToSend renderTemplateToMessageToSend(String key, Object model);
/**
* Renders the template identified by the key with the given {@link HashMap} used as model and returns the value as a string
* @param key The key of the template to be rendered.