A library for interacting with the Discord API at a very low level.
MIT License
A library for interacting with the Discord API at a very low level.
Eris provides simple access to the Discord API without smothering data in layers of needless, slow wrappers. It also avoids forcing users into particular design choices (futures, lambdas.)
The library is designed to be:
No third-party libraries were harmed in the making of this.
Get started with this introduction guide.
<repository>
<id>kenzie</id>
<name>Kenzie's Repository</name>
<url>https://repo.kenzie.mx/releases</url>
</repository>
<dependency>
<groupId>mx.kenzie</groupId>
<artifactId>eris</artifactId>
<version>1.0.0</version>
</dependency>
The default design pattern has two features: listeners and entities. Both of these are optional, and advanced users can use a different design pattern that better suits their project.
Listeners can anticipate Discord events (dispatches.) Advanced users can listen to raw gateway payloads and handle the data manually.
final Bot bot = new Bot("token");
bot.registerListener(Ready.class, ready -> {
System.out.println("The bot has successfully logged in!");
System.out.println("It is called " + ready.user.getTag());
});
Entities represent Discord objects (users, guilds, members, etc.) The entity structure corresponds directly to its raw data.
Entities have exposed, mutable fields rather than methods. This is a design choice:
Most entities have a Snowflake
ID by which they can be retrieved.
final User user = api.getUser(196709350469795841L);
final User user = api.getUser("196709350469795841");
// Both ID formats are supported.
Entities are Lazy
.
Their data may not be immediately available after creation, but the object can still be used.
To ensure that a Lazy
entity has finished acquiring its data, use the entity.await()
method.
This will block the current thread. An alternative entity.<CompletableFuture>whenReady()
method is available.
final User user = api.getUser(196709350469795841L);
assert user.id != null; // This property is already available.
user.await(); // Wait for all data to be loaded.
assert user.name != null; // This property is now available.
final User pending = api.getUser(196709350469795841L);
pending.<User>whenReady().thenAccept(user -> System.out.println(user.username + " is ready!"));
This dual structure allows programs to use the most suitable design pattern.
The Lazy
framework has an ancillary benefit: it has the smallest possible time-requirement.
final User user = api.getUser(012345678910L);
final Guild guild = api.getGuild(109876543210L);
for (int x = 0; x < 10000; x++) {
// do something slow here, e.g. RegEx
// both Guild + User are populating in the background
}
user.await();
guild.await();
// this has taken the smallest possible time for both
// Guild + User to be ready
This allows you to have minimal wait-times for multiple entities to be resolved without needing to use Java's complex CompletableFuture
s.
It is safe to cache or store Discord entities. After retrieving a stored entity it should be updated before being used to make sure all data is accurate.
A Discord entity may need to be updated if:
Guild
request) and want more data.Guild
data from a file.)If you ask Discord to PATCH
(change) an entity it will be updated automatically on completion.
For beginners, it is better practice to ask the DiscordAPI
to do it for you.
This API object can be obtained from your Bot
instance.
For advanced users it is safe to manually create most Entity
objects manually, however you will need to request their data from Discord.
Before requesting the data you must set its snowflake id
field.
Please note that any helper methods on the entity (e.g. channel.send(...)
) will not function until you update the entity or attach a DiscordAPI
object.
Many utility methods will accept a generic I
entity, e.g. api.getBan(IGuild, IUser)
.
These generic I
entities have two special rules:
api.getUser(...)
without await
ing it first.These objects are not strictly checked outside a test environment. Most methods will specify which types are permitted.
Almost all data must be acquired from Discord's API before being used.
Although Eris does not force developers to use a particular format or method, some are recommended for simplicity or safety.
Most Discord entities are provided as Lazy
objects.
You can see here for an introduction to how these work.
When using a Lazy
entity the data may be acquired using lazy.await()
which will block the current thread until all data is received.
You may also wait for the data using any method that calls await
internally, e.g. lazy.successful()
.
Note: be careful about using
lazy.await()
on objects that were not acquired directly from the API - these may have no completion goal and so will block indefinitely.
To check whether Lazy
acquisition is successful, a lazy.successful()
block method is provided.
If there is an error it may be found and thrown/read using lazy.error()
.
Rather than using the provided Lazy
API to read data in-situ, it is possible to use Java's CompletableFuture
system instead.
These may be used from a Lazy
object directly (e.g. lazy.whenReady()...
) however this is not advised in pooled environments since it busy-waits on a background thread.
Alternatively, the data can be acquired from the API's request
methods directly. Since Eris contains the Argo JSON library, the InputStream
can be read and converted.
This is not advised for beginner users, since the data will have to be marshalled correctly.
Events are triggered by incoming payloads from the Discord websocket. These are sent when something happens that your application 1) intends to listen to and 2) has the privilege to listen to.
Your application will not receive events if it was not registered with the correct intents or if it does not have privilege for those intents.
The event object received by listeners is designed to be easy-to-use.
All events are Payload
s corresponding to their JSON key/value structure.
Some events are designed to correspond directly with entities, such as Channel Create
.
The CreateChannel
object directly extends the Channel
entity class, and is fully usable as a channel object.
This makes it easy to construct a listener with behaviour around this.
bot.registerListener(CreateChannel.class, channel -> {
channel.send(new Message("hello there"));
});
This is the list of Discord events and corresponding event classes in this library.
Note: Discord (unhelpfully) sends some of these events as payloads - the event class will be left blank.
Discord Event Name | Event Class |
---|---|
Hello | |
Ready | Ready |
Resumed | Resumed |
Reconnect | |
Invalid Session | |
Application Command Permissions Update | UpdateCommandPermissions |
Auto Moderation Rule Create | CreateModerationRule |
Auto Moderation Rule Update | UpdateModerationRule |
Auto Moderation Rule Delete | DeleteModerationRule |
Auto Moderation Action Execution | ExecuteRule |
Channel Create | CreateChannel |
Channel Update | UpdateChannel |
Channel Delete | DeleteChannel |
Channel Pins Update | UpdateChannelPins |
Thread Create | CreateThread |
Thread Update | UpdateThread |
Thread Delete | DeleteThread |
Thread List Sync | ThreadListSync |
Thread Member Update | UpdateThreadMember |
Thread Members Update | UpdateThreadMembers |
Guild Create | IdentifyGuild |
Guild Update | UpdateGuild |
Guild Delete | DeleteGuild |
Guild Ban Add | AddGuildBan |
Guild Ban Remove | RemoveGuildBan |
Guild Emojis Update | UpdateGuildEmojis |
Guild Stickers Update | UpdateGuildStickers |
Guild Integrations Update | UpdateGuildIntegrations |
Guild Member Add | AddGuildMember |
Guild Member Remove | RemoveGuildMember |
Guild Member Update | UpdateGuildMember |
Guild Members Chunk | IdentifyGuildMembers |
Guild Role Create | CreateGuildRole |
Guild Role Update | UpdateGuildRole |
Guild Role Delete | DeleteGuildRole |
Guild Scheduled Event Create | CreateScheduledEvent |
Guild Scheduled Event Update | UpdateScheduledEvent |
Guild Scheduled Event Delete | DeleteScheduledEvent |
Guild Scheduled Event User Add | ScheduledEventAddUser |
Guild Scheduled Event User Remove | ScheduledEventRemoveUser |
Integration Create | CreateIntegration |
Integration Update | UpdateIntegration |
Integration Delete | DeleteIntegration |
Interaction Create | Interaction |
Invite Create | CreateInvite |
Invite Delete | DeleteInvite |
Message Create | ReceiveMessage |
Message Update | UpdateMessage |
Message Delete | DeleteMessage |
Message Delete Bulk | BulkDeleteMessage |
Message Reaction Add | AddMessageReaction |
Message Reaction Remove | RemoveMessageReaction |
Message Reaction Remove All | RemoveAllMessageReactions |
Message Reaction Remove Emoji | RemoveEmojiMessageReactions |
Presence Update | UpdatePresence |
Stage Instance Create | CreateStage |
Stage Instance Update | UpdateStage |
Stage Instance Delete | DeleteStage |
Typing Start | StartTyping |
User Update | UpdateUser |
Voice State Update | UpdateVoiceState |
Voice Server Update | UpdateVoiceServer |
Webhooks Update | UpdateWebhooks |
Listeners can anticipate Discord events (dispatches.) Advanced users can listen to raw gateway payloads and handle the data manually.
final Bot bot = new Bot("token");
bot.registerListener(Ready.class, ready -> {
System.out.println("The bot has successfully logged in!");
System.out.println("It is called " + ready.user.getTag());
});
It will be necessary to handle data - and its corresponding entities - in bulk.
An example of this would be acquiring large chunks of the member list or message history.
While Discord does limit these to X results per call (e.g. 100
for messages) this is still a large chunk of data to read into memory all at once.
Reading 100
Message
s into memory would use: ~16 bytes for the Message object ~8 bytes of primitive field data ~72 bytes of addresses for message data (assuming these are already cached) ++ String data from each message = 12kb of memory, excluding message content.
Ideally, we do not want to be using 12kb+ of memory at a time.
Fortunately, bulk calls provide three ways of dealing with their data.
If all 100 messages need to be in memory at once (e.g. sort/search/store/etc.) they can be acquired as a LazyList
.
final LazyList<Message> messages = channel.getMessages()
.limit(100).get();
messages.await();
messages...
This will use the most memory, but all messages will be available at once.
Pros | Cons |
---|---|
Can act on all the messages. | All of the messages go into memory. |
Able to iterate all the messages at once. | Have to wait for all messages to arrive before reading one. |
Can do other tasks while the messages are arriving. |
If the messages do not need to be consumed immediately, they may be dealt with in the background using a consumer.
channel.getMessages().limit(100)
.forEach(message -> System.out.println(message.content));
This will use the least memory: once an entity is assembled from JSON it is immediately consumed. After the consumer has finished - providing the user is not storing a strong reference - the entity object is destroyed. However, there is a short delay (nanoseconds) between messages since they are consumed while the data stream is still incoming.
Pros | Cons |
---|---|
Low-memory: objects are discarded after the consumer is finished. | Can act on only one message at a time. |
Fast: consumers are run as the data is read. | Cannot look ahead/behind at other messages. |
Background: doesn't block the current thread. | Slow consumers will slow down the incoming data. |
The parser must wait for each consumer to finish. |
Note: this is not a standard for-each.
The message helper object can be treated as an Iterable
(like a list) and looped.
for (final Message message : channel.getMessages().limit(100)) {
System.out.println(message.content);
}
This is the fastest and lowest-memory approach, but it blocks the current thread.
Messages are passed across a transferring queue and then iterated. This allows the messages to be read on the current thread.
Pros | Cons |
---|---|
Low-memory: objects are discarded after each iteration. | Cannot look ahead/behind at other messages. |
Low-CPU: no heavy consumers or lambdas required. | |
In-situ: entities are in the current method. | |
Speedy: incoming entities are queued in the background. |
Unlike other discord libraries, Eris operates only a very limited cache. Eris is designed to function in low-memory environments, so caching large amounts of potentially-unnecessary data would be inappropriate.
Some ID-based entities (Users, Guilds, Channels) are cached when requested. This cache will be maintained only as long as the user keeps a reference to that entity somewhere. This is to prevent having to wait to reacquire entities in multiple places at the same time. In order to use this cache longer-term, simply store the entity references somewhere within your own project to stop them being garbage-collected from the cache.
Every time a cached entity is requested, the cached version will be returned but a call will be sent to the Discord API for an updated copy.
You do not need to await
the updated copy before using the entity, however in some cases you may wish to if the cached copy is old.
Some API methods allow a user to provide their own copy of an entity. This copy may be different from the cached version. If the method returns the same type of entity it will always return the cached version (pending the result from Discord's API.) The user-provided entity may supplant the cached version and be returned if:
Channel
vs Thread
.)Discord's message component system encourages chaining multiple actions together.
E.g. command -> modal -> message -> button press -> result
However, Discord's API is structured around regular listeners and callbacks. To avoid users needing to create 5+ temporary listeners for a single interaction chain, the API provides a helper process for these "one-and-done" callbacks.
The one-and-done response system is not designed for regular or repeatable interactions like commands. It is appropriate for interactions which are:
It is not appropriate for interactions which are:
Some interactive components have an expectResult
method.
This tells the API you are intending to use the component for a one-and-done response.
Some components have a static auto
creator method that will pre-trigger this.
An example of responding to a button is below.
final Button button = Button.auto("My Button");
final Message message; // store for later
channel.send(message = new Message("Hello!", button);
button.await(50000); // wait for somebody to press the button
// a time-out is always appropriate
// if the user does not press the button this would hang indefinitely
if (button.cancelled()) return; // the user didn't press the button or the interaction expired
final Interaction press = button.result(); // this is the interaction event
press.respond(new Message("You pressed the button!").withFlag(MessageFlags.EPHEMERAL));
// this can be triggered only once
message.delete(); // get rid of the button so nobody else presses it!
In this example a button-message is sent to a channel. The first time a user presses the button, the bot will respond. After the bot has responded, the button will be deleted. If nobody presses the button within the 50-second time window, the interaction will not respond.
This complex example shows creating a modal and processing its result.
bot.registerCommand(Command.slash("bean", "My bean command."), interaction -> {
final Modal modal = Modal.auto("My Modal", new TextInput("text", "Write something!"));
interaction.respond(modal);
modal.await(30000);
if (modal.cancelled()) return;
final Interaction result = modal.result();
final String text = result.data.getInputValue("text");
final Button button = Button.auto("My Button");
result.respond(new Message("You wrote: `" + text + "`", button).withFlag(MessageFlags.EPHEMERAL));
button.await(50000);
if (button.cancelled()) return;
final Interaction press = button.result();
press.respond(new Message("You pressed the button!").withFlag(MessageFlags.EPHEMERAL));
});
In this example, a global command is registered. When the command is run, it sends a unique modal to the user asking for a text response. The interaction waits 30 seconds for the user to fill in the text. A message is sent to the user telling them what they wrote, with a button. If the user presses the button it will respond with a new message.
Some Discord API endpoints support attachments. As the file sizes could be greater than 100mb (with increased file limits) these files cannot be read normally.
This is a dangerous process since there are no limits or security checks on off-heap memory. Currently, you may read files only if they are smaller than your available RAM. Accidentally reading a multi-gigabyte file would crash your application since it cannot allocate that much memory.
Since Discord's file limit is fairly small, there is no reason to add an artificial restriction.
Users are not forced into using the provided API methods or even the Entity
data objects.
The API provides ways to send raw requests to Discord and read their data as an InputStream
.
You may also unregister the payload-listener that creates wrapped events and interpret payloads directly from the gateway socket.