Create a Discord Bot to clear chat history

Updated: Sep 5, 2020

Since I feel comfortable writing bots for discord, I thought I could give it another try but with a different scenario. Sometimes a chat get filled with lots of messages and you want to clean it without deleting it entirely. In this guide we will implement a bot to solve this small task.

Before we start, be sure, that you have already a structured project, like described in this article about creating a first bot. The structure of this project differs from the former bot, but don’t worry, we will take a closer look at the composition and the differences.

First we take a look at the pom.xml. To get the latest version of the JDA artifact, we need an additional repository entry. In this project we also added a proper logging, so we won’t get any warnings due to a missing SLF4J configuration and to store the logs persistent. To store the logs permanently, I’ve decided to use Application Insights. AI is a free service of the Azure Cloud.

    <repositories>
        <repository>
            <id>jcenter</id>
            <name>jcenter-bintray</name>
            <url>https://jcenter.bintray.com</url>
        </repository>
    </repositories> 
        <dependency>
            <groupId>net.dv8tion</groupId>
            <artifactId>JDA</artifactId>
            <version>4.1.1_144</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.7</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.7</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.7</version>
        </dependency>
        <dependency>
            <groupId>com.microsoft.azure</groupId>
            <artifactId>applicationinsights-logging-log4j2</artifactId>
            <version>2.6.0</version>
        </dependency> 

As you can see, we use log4j2 as the logger implementation. Now we need to configure where the data should be logged and on which severity level. Therefor we need a configuration file, like log4j2.xml in the root directory of our resource folder. The configuration consists of two loggers, one to append it to the console and another to append it to the remote storage. If you want use another representation of the configuration, see here all supported formats. The name of a logger is also a filter. The aiAppender will only log information classes with a qualified name starting with „de.klemensmorbe.discord“. The consoleAppender will log everything but only at level error and lower.

<Configuration packages="com.microsoft.applicationinsights.log4j.v2">
    <Appenders>
        <Console name="consoleAppender" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
        <ApplicationInsightsAppender name="aiAppender" instrumentationKey="94c01a69-eeb9-4fb3-8752-6caa2330d78e"/>
    </Appenders>
    <Loggers>
        <Logger name="de.klemensmorbe.discord" level="trace">
            <AppenderRef ref="aiAppender"/>
        </Logger>
        <Root level="error">
            <AppenderRef ref="consoleAppender"/>
        </Root>
    </Loggers>
</Configuration> 

Ok, let’s start with the implementation. First we need a main method as our entry point. In this method we will initialize the JDA instance, add the authentication token for the bot, add all listeners needed (one) and set the online status of the bot. In my case, I get the token as a system environment variable. You could also read the input arguments or read it from a Java KeyStore, It’s just a question of taste.

public class ClearEntryPoint {

    private static final Logger LOGGER = LoggerFactory.getLogger(ClearEntryPoint.class);

    public static void main(final String[] ignore) throws Exception {
        LOGGER.info("clear bot started");

        try {
            final var token = System.getenv("bot_token");

            JDABuilder.createDefault(token)
                    .addEventListeners(new ClearListener())
                    .setActivity(Activity.playing("Type !clear"))
                    .setStatus(OnlineStatus.ONLINE)
                    .build();

            LOGGER.info("clear bot initialized");
        } catch (final Exception e) {
            LOGGER.error("clear bot initialization failed", e);
        }

    }
} 

The second thing we should do is to declare a new class for the listener and extend from the abstract class ListenerAdapter and override the OnMessageReceived method.

public class ClearListener extends ListenerAdapter {

    @Override
    public void onMessageReceived(MessageReceivedEvent event) { }

} 

For the listener part, we need a bit more logic. This bot reads all input messages from public channels or private ones. To start processing, there are a few things that we should check:

  1. is the writing user also a bot?

  2. is the text starting with !clear

  3. is the message in a public channel or a private one?

  4. does the channel exist?

  5. does the user have sufficient permissions?

if (event.getAuthor().isBot()) {
            return;
} 
String content = event.getMessage().getContentRaw();
if (!content.startsWith(CLEAR_COMMAND)) {
            return;
} 
if (event.getMember() != null && !event.getMember().hasPermission(Permission.MESSAGE_MANAGE)) {
            final var message = "insufficient permission";
            event.getChannel().sendMessage(message).queue();
            LOGGER.info(message);
            return;
} 
if (event.getMember() == null) {
            final var message = "cannot delete from private channels";
            event.getChannel().sendMessage(message).queue();
            LOGGER.info(message);
            return;
} 

Now we checked for any illegal argument (except unknown channel). Now we can take the input argument and parse it. The argument may consist of one or two parts, the command !clear or !clear $channelName. If the arguments is only !clear, we should try to clear the channel it was written in. If the argument has two words, we should look for the channel and try to delete the messages there.

final var commandLine = content.split(DELIMITER);

if (commandLine.length == 1) {
            event.getChannel().getIterableHistory().forEach(message -> message.delete().queue());
            event.getChannel().sendMessage(CLEARED_CHANNEL_MESSAGE).queue();
            LOGGER.info(CLEARED_CHANNEL_MESSAGE);
            try {
                TimeUnit.SECONDS.sleep(WAIT_BEFORE_DELETION);
            } catch (final InterruptedException ignore) {
            }
            event.getChannel().getIterableHistory().forEach(message -> message.delete().queue());
        } else {
            final var channelName = commandLine[1];
            final var channels = event.getGuild().getTextChannelsByName(channelName, true);
            channels.stream()
                    .findFirst()
                    .ifPresentOrElse(textChannel -> {
                        textChannel.getIterableHistory().forEach(message -> message.delete().queue());
                        textChannel.sendMessage(CLEARED_CHANNEL_MESSAGE).queue();
                        LOGGER.info(CLEARED_CHANNEL_MESSAGE);
                        try {
                            TimeUnit.SECONDS.sleep(WAIT_BEFORE_DELETION);
                        } catch (final InterruptedException ignore) {
                        }
                        textChannel.getIterableHistory().forEach(message -> message.delete().queue());
                    }, () -> event.getChannel().sendMessage(String.format("no channel with name '%s' found", channelName)).queue());
} 

That is it. We are done. The bot can now be started, and we can interact with it. Create a channel to test it. Type in some messages and watch them disappear. Remember that we added some delay before the task triggers. Thanks to this I was able to take a picture before the actual deletion process.

In this small project I decided to delete all messages by iterating over the history of the channel. We could also create a copy of this channel with no history and delete the other one, the outcome would be the same. In future we could add even more features to this bot. In the current state the bot deletes everything in a channel, we could also enhance the functionality so the user can add a number, how many lines should be deleted or how many lines should be ignored from the start of the channel history. we could also omit sticky channel messages, so they don’t get lost. There are many options, try it out.

For the lazy people, I provide a link to the repository, so you can see the entire code. Be aware that I hosted the bots using azure docker instances, so don’t get confused by the Dockerfile and other configuration files.



Recent Posts

See All