2929import java .time .LocalDateTime ;
3030import java .time .OffsetDateTime ;
3131import java .time .format .DateTimeFormatter ;
32+ import java .util .Collections ;
33+ import java .util .HashMap ;
34+ import java .util .HashSet ;
3235import java .util .List ;
3336import java .util .Map ;
3437import java .util .Map .Entry ;
38+ import java .util .Set ;
39+ import java .util .concurrent .CompletableFuture ;
3540import java .util .concurrent .ExecutorService ;
41+ import java .util .concurrent .atomic .AtomicBoolean ;
3642import java .util .stream .Collectors ;
3743
3844/**
4248public class PurgeCommand extends ModerateCommand {
4349 private static final Path ARCHIVE_DIR = Path .of ("purgeArchives" );
4450 private final ExecutorService asyncPool ;
51+
52+ private final Map <Long , Set <RunningPurge >> currentPurges = Collections .synchronizedMap (new HashMap <>());
4553
4654 /**
4755 * The constructor of this class, which sets the corresponding {@link net.dv8tion.jda.api.interactions.commands.build.SlashCommandData}.
@@ -51,8 +59,8 @@ public class PurgeCommand extends ModerateCommand {
5159 public PurgeCommand (BotConfig botConfig , ExecutorService asyncPool ) {
5260 super (botConfig );
5361 this .asyncPool = asyncPool ;
54- setModerationSlashCommandData (Commands .slash ("purge" , "Deletes messages from a channel." )
55- .addOption (OptionType .INTEGER , "amount" , "Number of messages to remove." , true )
62+ setModerationSlashCommandData (Commands .slash ("purge" , "Bulk-deletes messages from a channel. Use /purge 0 to stop all purges ." )
63+ .addOption (OptionType .INTEGER , "amount" , "Number of messages to remove. Set this to 0 in order to stop all running purges. " , true )
5664 .addOption (OptionType .USER , "user" , "The user whose messages to remove. If left blank, messages from any user are removed." , false )
5765 .addOption (OptionType .BOOLEAN , "archive" , "Whether the removed messages should be saved in an archive. This defaults to true, if left blank." , false )
5866 );
@@ -69,10 +77,34 @@ protected ReplyCallbackAction handleModerationCommand(@NotNull SlashCommandInter
6977 Long amount = (amountOption == null ) ? 1 : amountOption .getAsLong ();
7078 User user = (userOption == null ) ? null : userOption .getAsUser ();
7179 int maxAmount = config .getPurgeMaxMessageCount ();
72- if (amount == null || amount < 1 || amount > maxAmount ) {
80+ if (amount == null || amount > maxAmount ) {
7381 return Responses .warning (event , "Invalid amount. Should be between 1 and " + maxAmount + ", inclusive." );
7482 }
75- asyncPool .submit (() -> this .purge (amount , user , event .getUser (), archive , event .getChannel (), config .getLogChannel ()));
83+ if (amount == 0 ) {
84+ Set <RunningPurge > purges = currentPurges .get (event .getGuild ().getIdLong ());
85+ if (purges == null ) {
86+ return Responses .warning (event , "Cannot stop purge as no purge is currently running." );
87+ } else {
88+ int count = 0 ;
89+ for (RunningPurge purge : purges ) {
90+ if (purge .cancelled ().compareAndSet (false , true )) {
91+ count ++;
92+ }
93+ }
94+ return Responses .success (event , "Purge stopped" , count + " purge(s) have been stopped." );
95+ }
96+ }
97+ RunningPurge runningPurge = new RunningPurge (event .getIdLong (), new AtomicBoolean ());
98+ CompletableFuture <Void > future = CompletableFuture .runAsync (
99+ () -> this .purge (amount , user , event .getUser (), archive , event .getChannel (), config .getLogChannel (), runningPurge .cancelled ()),
100+ asyncPool );
101+ currentPurges
102+ .computeIfAbsent (event .getGuild ().getIdLong (), l -> Collections .synchronizedSet (new HashSet <>()))
103+ .add (runningPurge );
104+ future .whenComplete ((success , failure ) ->
105+ currentPurges .get (event .getGuild ().getIdLong ())
106+ .remove (runningPurge )
107+ );
76108 StringBuilder sb = new StringBuilder ();
77109 sb .append (amount > 1 ? "Up to " + amount + " messages " : "1 message " );
78110 if (user != null ) {
@@ -92,20 +124,42 @@ protected ReplyCallbackAction handleModerationCommand(@NotNull SlashCommandInter
92124 * @param archive Whether to create an archive file for the purge.
93125 * @param channel The channel to remove messages from.
94126 * @param logChannel The channel to write log messages to during the purge.
127+ * @param cancelled {@code true} indicates the purge is cancelled, else {@code false}
95128 */
96- private void purge (long amount , @ Nullable User user , User initiatedBy , boolean archive , MessageChannel channel , TextChannel logChannel ) {
129+ private void purge (long amount , @ Nullable User user , User initiatedBy , boolean archive , MessageChannel channel , TextChannel logChannel , AtomicBoolean cancelled ) {
97130 MessageHistory history = channel .getHistory ();
98131 String timestamp = LocalDateTime .now ().format (DateTimeFormatter .ofPattern ("yyyy-MM-dd_HH-mm-ss" ));
99132 String file = String .format ("purge_%s_%s.txt" , channel .getName (), timestamp );
100133 PrintWriter archiveWriter = archive ? createArchiveWriter (channel , logChannel , file ) : null ;
101- List <Message > messages ;
102134 OffsetDateTime startTime = OffsetDateTime .now ();
103135 long count = 0 ;
104136 logChannel .sendMessageFormat ("Starting purge of channel %s, initiated by %s" , channel .getAsMention (), initiatedBy .getAsMention ())
105137 .queue ();
138+ count = performDeletion (amount , user , channel , logChannel , history , archiveWriter , count , cancelled );
139+ if (archiveWriter != null ) {
140+ archiveWriter .close ();
141+ }
142+ MessageCreateAction action = logChannel .sendMessage (String .format (
143+ "Purge of channel %s has completed. %d messages have been removed, and the purge took %s." ,
144+ channel .getAsMention (),
145+ count ,
146+ new TimeUtils ().formatDurationToNow (startTime )
147+ ));
148+ if (archive ) {
149+ action .addFiles (FileUpload .fromData (ARCHIVE_DIR .resolve (file ).toFile ()));
150+ }
151+ action .queue ();
152+ }
153+
154+ private long performDeletion (long amount , User user , MessageChannel channel , TextChannel logChannel ,
155+ MessageHistory history , PrintWriter archiveWriter , long count , AtomicBoolean cancelled ) {
106156 int lastEmptyIterations = 0 ;
157+ List <Message > messages ;
107158 do {
108159 messages = history .retrievePast ((int ) Math .min (100 , user ==null ? amount : Math .max (amount , 10 ))).complete ();
160+ if (cancelled .get ()) {
161+ return count ;
162+ }
109163 if (!messages .isEmpty ()) {
110164 int messagesRemoved = removeMessages (messages , user , archiveWriter , amount - count );
111165 count += messagesRemoved ;
@@ -114,27 +168,15 @@ private void purge(long amount, @Nullable User user, User initiatedBy, boolean a
114168 messagesRemoved ,
115169 channel .getAsMention (),
116170 count
117- )).queue ();
171+ )).complete ();
118172 if (messagesRemoved == 0 ) {
119173 lastEmptyIterations ++;
120174 }else {
121175 lastEmptyIterations = 0 ;
122176 }
123177 }
124- } while (!messages .isEmpty () && amount > count && lastEmptyIterations <= 20 );
125- if (archiveWriter != null ) {
126- archiveWriter .close ();
127- }
128- MessageCreateAction action = logChannel .sendMessage (String .format (
129- "Purge of channel %s has completed. %d messages have been removed, and the purge took %s." ,
130- channel .getAsMention (),
131- count ,
132- new TimeUtils ().formatDurationToNow (startTime )
133- ));
134- if (archive ) {
135- action .addFiles (FileUpload .fromData (ARCHIVE_DIR .resolve (file ).toFile ()));
136- }
137- action .queue ();
178+ } while (!cancelled .get () && !messages .isEmpty () && amount > count && lastEmptyIterations <= 20 );
179+ return count ;
138180 }
139181
140182 /**
@@ -162,7 +204,7 @@ private int removeMessages(List<Message> messages, @Nullable User user, @Nullabl
162204 for (Message msg : msgs ) {
163205 archiveMessage (archiveWriter , msg );
164206 }
165- entry .getKey ().purgeMessages (messages );
207+ entry .getKey ().purgeMessages (msgs );
166208 }
167209 }
168210 return count ;
@@ -208,4 +250,11 @@ private void archiveMessage(PrintWriter writer, Message message) {
208250 message .getContentRaw ()
209251 );
210252 }
253+
254+ private record RunningPurge (long id , AtomicBoolean cancelled ) {
255+ @ Override
256+ public final int hashCode () {
257+ return (int ) id ;
258+ }
259+ }
211260}
0 commit comments