2929import java .time .LocalDateTime ;
3030import java .time .OffsetDateTime ;
3131import java .time .format .DateTimeFormatter ;
32+ import java .util .Collections ;
33+ import java .util .HashMap ;
3234import java .util .List ;
3335import java .util .Map ;
3436import java .util .Map .Entry ;
37+ import java .util .concurrent .CompletableFuture ;
38+ import java .util .concurrent .CopyOnWriteArrayList ;
3539import java .util .concurrent .ExecutorService ;
40+ import java .util .concurrent .atomic .AtomicBoolean ;
3641import java .util .stream .Collectors ;
3742
3843/**
4247public class PurgeCommand extends ModerateCommand {
4348 private static final Path ARCHIVE_DIR = Path .of ("purgeArchives" );
4449 private final ExecutorService asyncPool ;
50+
51+ private final Map <Long , List <RunningPurge >> currentPurges = Collections .synchronizedMap (new HashMap <>());
4552
4653 /**
4754 * The constructor of this class, which sets the corresponding {@link net.dv8tion.jda.api.interactions.commands.build.SlashCommandData}.
@@ -51,8 +58,8 @@ public class PurgeCommand extends ModerateCommand {
5158 public PurgeCommand (BotConfig botConfig , ExecutorService asyncPool ) {
5259 super (botConfig );
5360 this .asyncPool = asyncPool ;
54- setModerationSlashCommandData (Commands .slash ("purge" , "Deletes messages from a channel." )
55- .addOption (OptionType .INTEGER , "amount" , "Number of messages to remove." , true )
61+ setModerationSlashCommandData (Commands .slash ("purge" , "Bulk-deletes messages from a channel. Use /purge 0 to stop all purges ." )
62+ .addOption (OptionType .INTEGER , "amount" , "Number of messages to remove. Set this to 0 in order to stop all running purges. " , true )
5663 .addOption (OptionType .USER , "user" , "The user whose messages to remove. If left blank, messages from any user are removed." , false )
5764 .addOption (OptionType .BOOLEAN , "archive" , "Whether the removed messages should be saved in an archive. This defaults to true, if left blank." , false )
5865 );
@@ -69,10 +76,34 @@ protected ReplyCallbackAction handleModerationCommand(@NotNull SlashCommandInter
6976 Long amount = (amountOption == null ) ? 1 : amountOption .getAsLong ();
7077 User user = (userOption == null ) ? null : userOption .getAsUser ();
7178 int maxAmount = config .getPurgeMaxMessageCount ();
72- if (amount == null || amount < 1 || amount > maxAmount ) {
79+ if (amount == null || amount > maxAmount ) {
7380 return Responses .warning (event , "Invalid amount. Should be between 1 and " + maxAmount + ", inclusive." );
7481 }
75- asyncPool .submit (() -> this .purge (amount , user , event .getUser (), archive , event .getChannel (), config .getLogChannel ()));
82+ if (amount == 0 ) {
83+ List <RunningPurge > purges = currentPurges .get (event .getGuild ().getIdLong ());
84+ if (purges == null ) {
85+ return Responses .warning (event , "Cannot stop purge as no purge is currently running." );
86+ } else {
87+ int count = 0 ;
88+ for (RunningPurge purge : purges ) {
89+ if (purge .cancelled ().compareAndSet (false , true )) {
90+ count ++;
91+ }
92+ }
93+ return Responses .success (event , "Purge stopped" , count + " purge(s) have been stopped." );
94+ }
95+ }
96+ RunningPurge runningPurge = new RunningPurge (event .getIdLong (), new AtomicBoolean ());
97+ CompletableFuture <Void > future = CompletableFuture .runAsync (
98+ () -> this .purge (amount , user , event .getUser (), archive , event .getChannel (), config .getLogChannel (), runningPurge .cancelled ()),
99+ asyncPool );
100+ currentPurges
101+ .computeIfAbsent (event .getGuild ().getIdLong (), l -> new CopyOnWriteArrayList <>())
102+ .add (runningPurge );
103+ future .whenComplete ((success , failure ) ->
104+ currentPurges .get (event .getGuild ().getIdLong ())
105+ .remove (runningPurge )
106+ );
76107 StringBuilder sb = new StringBuilder ();
77108 sb .append (amount > 1 ? "Up to " + amount + " messages " : "1 message " );
78109 if (user != null ) {
@@ -92,20 +123,42 @@ protected ReplyCallbackAction handleModerationCommand(@NotNull SlashCommandInter
92123 * @param archive Whether to create an archive file for the purge.
93124 * @param channel The channel to remove messages from.
94125 * @param logChannel The channel to write log messages to during the purge.
126+ * @param cancelled {@code true} indicates the purge is cancelled, else {@code false}
95127 */
96- private void purge (long amount , @ Nullable User user , User initiatedBy , boolean archive , MessageChannel channel , TextChannel logChannel ) {
128+ private void purge (long amount , @ Nullable User user , User initiatedBy , boolean archive , MessageChannel channel , TextChannel logChannel , AtomicBoolean cancelled ) {
97129 MessageHistory history = channel .getHistory ();
98130 String timestamp = LocalDateTime .now ().format (DateTimeFormatter .ofPattern ("yyyy-MM-dd_HH-mm-ss" ));
99131 String file = String .format ("purge_%s_%s.txt" , channel .getName (), timestamp );
100132 PrintWriter archiveWriter = archive ? createArchiveWriter (channel , logChannel , file ) : null ;
101- List <Message > messages ;
102133 OffsetDateTime startTime = OffsetDateTime .now ();
103134 long count = 0 ;
104135 logChannel .sendMessageFormat ("Starting purge of channel %s, initiated by %s" , channel .getAsMention (), initiatedBy .getAsMention ())
105136 .queue ();
137+ count = performDeletion (amount , user , channel , logChannel , history , archiveWriter , count , cancelled );
138+ if (archiveWriter != null ) {
139+ archiveWriter .close ();
140+ }
141+ MessageCreateAction action = logChannel .sendMessage (String .format (
142+ "Purge of channel %s has completed. %d messages have been removed, and the purge took %s." ,
143+ channel .getAsMention (),
144+ count ,
145+ new TimeUtils ().formatDurationToNow (startTime )
146+ ));
147+ if (archive ) {
148+ action .addFiles (FileUpload .fromData (ARCHIVE_DIR .resolve (file ).toFile ()));
149+ }
150+ action .queue ();
151+ }
152+
153+ private long performDeletion (long amount , User user , MessageChannel channel , TextChannel logChannel ,
154+ MessageHistory history , PrintWriter archiveWriter , long count , AtomicBoolean cancelled ) {
106155 int lastEmptyIterations = 0 ;
156+ List <Message > messages ;
107157 do {
108158 messages = history .retrievePast ((int ) Math .min (100 , user ==null ? amount : Math .max (amount , 10 ))).complete ();
159+ if (cancelled .get ()) {
160+ return count ;
161+ }
109162 if (!messages .isEmpty ()) {
110163 int messagesRemoved = removeMessages (messages , user , archiveWriter , amount - count );
111164 count += messagesRemoved ;
@@ -114,27 +167,15 @@ private void purge(long amount, @Nullable User user, User initiatedBy, boolean a
114167 messagesRemoved ,
115168 channel .getAsMention (),
116169 count
117- )).queue ();
170+ )).complete ();
118171 if (messagesRemoved == 0 ) {
119172 lastEmptyIterations ++;
120173 }else {
121174 lastEmptyIterations = 0 ;
122175 }
123176 }
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 ();
177+ } while (!cancelled .get () && !messages .isEmpty () && amount > count && lastEmptyIterations <= 20 );
178+ return count ;
138179 }
139180
140181 /**
@@ -162,7 +203,7 @@ private int removeMessages(List<Message> messages, @Nullable User user, @Nullabl
162203 for (Message msg : msgs ) {
163204 archiveMessage (archiveWriter , msg );
164205 }
165- entry .getKey ().purgeMessages (messages );
206+ entry .getKey ().purgeMessages (msgs );
166207 }
167208 }
168209 return count ;
@@ -208,4 +249,8 @@ private void archiveMessage(PrintWriter writer, Message message) {
208249 message .getContentRaw ()
209250 );
210251 }
252+
253+ private record RunningPurge (long id , AtomicBoolean cancelled ) {
254+
255+ }
211256}
0 commit comments