Basic Bot Tutorial
Making your first bot is very simple. This tutorial will walk you through how to make a basic bot that responds to a ping command and listens to some other events.
This tutorial assumes a basic familiarity with reactive programming and Reactor. Refer to Reactive (Reactor) Tutorial for information on that topic.
What is a bot?
But first, it will be helpful to understand roughly how Discord bots work.
A "bot" is simply a program running on a computer that can interact with Discord. Discord4J provides a way to do those interactions in Java. We take care of the low-level details like maintaining a websocket connection and making HTTP requests to Discord's web API.
When "logged in," a bot has an active websocket connection with Discord, which will send events to the bot. For example, when a message gets sent in a channel the bot has access to, Discord will notify the bot by sending an event with all of the relevant information about the message. The bot can interact with Discord primarily by making HTTP requests. For example, that is how the bot can send a message.
Using just those two mechanisms (receiving events from Discord via a websocket, and interacting with Discord via HTTP), you can create complex programs that do all sorts of things in Discord. In this tutorial, you will learn how to create a very simple, but extensible, bot which will send the message "Pong!" any time someone sends "!ping".
Download / Install
You first need to set up a project with Discord4J as a dependency. Refer to the Quickstart page for details on how to do that with Maven or Gradle. If you're unfamiliar with those tools, refer to the documentation for your specific IDE.
IntelliJ
Eclipse
Constructing the client
As mentioned previously, Discord4J provides higher-level APIs for interacting with Discord. The "entry point" for
any kind of interaction with Discord is the DiscordClient
. So, the first thing we need to do in our program is
construct one. The easiest way to do that is simply using the create
method.
More advanced configuration of the client can be done with DiscordClientBuilder
. The default configuration is fine
for the purposes of this tutorial.
import discord4j.core.DiscordClient;
public class MyBot {
public static void main(String[] args) {
DiscordClient client = DiscordClient.create("TOKEN");
}
}
Replace TOKEN
with your bot's authentication token. This is like a username and password that a normal user
would use to authenticate with Discord. It can be found in the "Bot" settings page of the developer portal.
Logging In
A DiscordClient
has an important caveat: it presents all of the operations a bot can do while not logged in.
Remember, when a bot is "logged in," it has an active connection to Discord that allows it to receive real-time
information. While there are many legitimate use cases for just having a non-connected DiscordClient
, most bots
will instead require a GatewayDiscordClient
.
To obtain a GatewayDiscordClient
, we will use the withGateway
method of DiscordClient
. This method allows us to
say the bot should do when it logs in. We can use the method like so:
Mono<Void> login = client.withGateway((GatewayDiscordClient gateway) -> Mono.empty());
login.block();
We can see that withGateway
returns a Mono<Void>
which we have named login
. Upon subscribing to this Mono,
Discord4J will establish a connection with Discord and perform the action specified by the argument to withGateway
(i.e., it will invoke the function we passed to withGateway
). In this case, we immediately return Mono.empty()
,
so that action is essentially "do nothing." You can confirm this by running the following updated program and observing
that the bot comes online in Discord, and the program runs indefinitely.
import discord4j.core.DiscordClient;
import discord4j.core.GatewayDiscordClient;
import reactor.core.publisher.Mono;
public class MyBot {
public static void main(String[] args) {
DiscordClient client = DiscordClient.create("TOKEN");
Mono<Void> login = client.withGateway((GatewayDiscordClient gateway) -> Mono.empty());
login.block();
}
}
Because most bots require a GatewayDiscordClient
over a DiscordClient
, you will often see the former referred to as
just "client."
Responding to events
A bot doing nothing but being online is pretty boring, so let's change that. Recall that Discord sends real-time
information to connected clients via events. To listen to these events, we can use the on
method of
GatewayDiscordClient
.
This method has several overloads, but the most useful (and safest, refer to the javadocs for more info) is the one
which takes a Function<E, Publisher<T>>
for the second parameter. That function is invoked every time the specified
event is received from Discord. The Publisher that it returns determines how the event is handled.
Let's look at some concrete examples.
ReadyEvent
First, let's listen to ReadyEvent
. Quoting from the javadoc, this event is "dispatched when an initial connection to
the Discord gateway has been established." We can rewrite our withGateway
invocation to instead listen to this event
and print some information from it.
Mono<Void> login = client.withGateway((GatewayDiscordClient gateway) ->
gateway.on(ReadyEvent.class, event ->
Mono.fromRunnable(() -> {
final User self = event.getSelf();
System.out.printf("Logged in as %s#%s%n", self.getUsername(), self.getDiscriminator());
})));
login.block();
With this code, every occurrence of ReadyEvent
is handled by printing the username and discriminator of the "self
user" which is the user associated with the bot account.
fromRunnable
is used because the handler isn't reactive. As we'll see later, this usually isn't the case.
When you run the program now, you should see the printed "Logged in as ..."
line once the bot logs in. In English,
this new code essentially says "when logged in, listen to the ready event, and when it gets dispatched, print 'logged
in as ....'"
MessageCreateEvent
Next we'll see how to create our !ping
command. Remember, we want the bot to respond to "!ping" with "pong!".
The setup for listening to the event is exactly the same as with ReadyEvent
. The only difference is the event we want
to listen to. In this case that's MessageCreateEvent
. Again quoting from the javadoc, this event is "dispatched when a
message is sent in a message channel."
Mono<Void> login = client.withGateway((GatewayDiscordClient gateway) ->
gateway.on(MessageCreateEvent.class, event -> {
// ... todo ...
}));
We need to fill in the "todo" with code that checks if the message we received is "!ping" and then responds with "pong!" if it is. This is fairly straightforward. The main thing here is just knowing which methods will give you access to the information you want. Like any library, this familiarity comes with experience.
A pretty good strategy for exploring a new library is to just type someVariable.
and seeing what suggestions your IDE
has. For example, without any prior knowledge, you could find that you can get the received message in the event with
event.getMessage()
and the content of that message with event.getMessage().getContent()
.
The final code with "todo" filled in looks like this.
Mono<Void> login = client.withGateway((GatewayDiscordClient gateway) ->
gateway.on(MessageCreateEvent.class, event -> {
Message message = event.getMessage();
if (message.getContent().equalsIgnoreCase("!ping")) {
return message.getChannel()
.flatMap(channel -> channel.createMessage("pong!"));
}
return Mono.empty();
}));
We check if the message content equals (ignoring case) "!ping", and if it does, we handle the event by creating a
message in the same channel with the content "pong!". Otherwise, we handle the event by doing nothing (returning
Mono.empty()
).
At this point, you can probably see how this could be extended to support many more commands. Try adding another one!
And that's it! If you run the program and send "!ping" in a channel the bot can see, it will dutifully respond "pong!".
Multiple Events
You might have noticed that the MessageCreateEvent
example overwrote the ReadyEvent
one. But what if we want to do
both? Our bot should both print "Logged in as..." on start up and respond to "!ping" with "pong!". We can easily do
this with a couple of small modifications and the and
method.
Mono<Void> login = client.withGateway((GatewayDiscordClient gateway) -> {
// ReadyEvent example
Mono<Void> printOnLogin = gateway.on(ReadyEvent.class, event ->
Mono.fromRunnable(() -> {
final User self = event.getSelf();
System.out.printf("Logged in as %s#%s%n", self.getUsername(), self.getDiscriminator());
}))
.then();
// MessageCreateEvent example
Mono<Void> handlePingCommand = gateway.on(MessageCreateEvent.class, event -> {
Message message = event.getMessage();
if (message.getContent().equalsIgnoreCase("!ping")) {
return message.getChannel()
.flatMap(channel -> channel.createMessage("pong!"));
}
return Mono.empty();
}).then();
// combine them!
return printOnLogin.and(handlePingCommand);
});
The first change you might notice is the addition of then()
to both handlers. We didn't discuss is previously, but
gateway.on()
returns a Flux<T>
where T
is the return type of the event handler function. This allows you to
continue operating on the results of the handler. In this case, we don't need to do that, so we can simply ignore
any results with then()
.
Now that both of the handlers are Mono<Void>
, we can simply combine them with and
. The code can be fluently read as
"when logged in, print on login and handle the ping command."