mirror of
https://github.com/Sheldan/abstracto.git
synced 2026-03-06 08:25:31 +00:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<#include "purge_status_update_message">
|
||||
@@ -0,0 +1 @@
|
||||
<#include "no_message_found_exception_message">
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package dev.sheldan.abstracto.core.command.execution;
|
||||
|
||||
public enum ResultState {
|
||||
ERROR, SUCCESSFUL, IGNORED, CONDITION
|
||||
ERROR, SUCCESSFUL, IGNORED, CONDITION, SELF_DESTRUCT
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Deletes the last n messages in the channel
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
Amount of messages do delete.
|
||||
@@ -0,0 +1 @@
|
||||
The member to delete messages of.
|
||||
@@ -0,0 +1 @@
|
||||
purge <amount> [member]
|
||||
@@ -0,0 +1 @@
|
||||
${currentlyDeleted} out of ${totalToDelete} messages deleted.
|
||||
@@ -0,0 +1 @@
|
||||
No more messages found to delete.
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user