Discord Bot
This section will cover adding support for a Discord bot to be able to see our task list, and add, delete, and view specific tasks. For context the @sodacore/discord
plugin wraps the discord.js
library so expect to see some familiar code if you've used that library before.
Setting up the Discord bot.
Going back to the ./src/main.ts
file, we can now uncomment the Discord plugin code we commented out at the start.
// Install the Discord plugin.
app.use(new DiscordPlugin({
token: env.DISCORD_TOKEN,
clientId: env.DISCORD_CLIENT_ID,
guildId: env.DISCORD_GUILD_ID,
}));
Make sure you have the environment variables set up in your
.env
file.
Creating the controller.
To setup the Discord bot, we need to create a controller that will define our command and handle the commands, to do this, create a new file called ./src/command/tasks.ts
with the following code:
import type { PrismaClient } from '../../prisma/prisma';
import { Command, Interaction, On, SlashCommandBuilder } from '@sodacore/discord';
import { Inject } from '@sodacore/di';
import { ChatInputCommandInteraction, Colors, EmbedBuilder, MessageFlags } from 'discord.js';
@Command(
new SlashCommandBuilder()
.setName('tasks')
.setDescription('Task based management commands.')
.addSubcommand(subcommand =>
subcommand
.setName('list')
.setDescription('List all tasks.'),
)
.addSubcommand(subcommand =>
subcommand
.setName('view')
.setDescription('View a specific task.')
.addNumberOption(option =>
option.setName('id')
.setDescription('The ID of the task to view.')
.setRequired(true)
.setAutocomplete(true),
),
)
.addSubcommand(subcommand =>
subcommand
.setName('create')
.setDescription('Create a new task.')
.addStringOption(option =>
option.setName('title')
.setDescription('The title of the task.')
.setRequired(true)
)
.addStringOption(option =>
option.setName('description')
.setDescription('The description of the task.')
.setRequired(true)
),
)
.addSubcommand(subcommand =>
subcommand
.setName('delete')
.setDescription('Delete a task.')
.addNumberOption(option =>
option
.setName('id')
.setDescription('The ID of the task to delete.')
.setRequired(true)
.setAutocomplete(true),
),
),
)
export class TaskCommand {
@Inject('prisma') private prisma!: PrismaClient;
@On.SubCommand('create')
public async onCreate(
@Interaction() interaction: ChatInputCommandInteraction,
) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const title = interaction.options.getString('title', true);
const description = interaction.options.getString('description', true);
const task = await this.prisma.todos.create({
data: {
title,
description,
},
});
return `Task created with ID: ${task.id}`;
}
@On.SubCommand('list')
public async onList() {
const tasks = await this.prisma.todos.findMany({
select: {
id: true,
title: true,
description: true,
completed: true,
},
orderBy: {
id: 'asc',
},
});
if (tasks.length === 0) {
return 'No tasks found.';
}
const embed = new EmbedBuilder()
.setTitle('Task List')
.setDescription(tasks.map(task => `**${task.id}**: ${task.title.replace('\n', ' ')} [${task.completed ? '✅' : '❌'}]\n${task.description ? task.description.replace('\n', ' ') : '__No Description__'}`).join('\n\n'))
.setColor(Colors.Aqua);
return { embeds: [embed] };
}
@On.SubCommand('view')
public async onView(
@Interaction() interaction: ChatInputCommandInteraction,
) {
const id = interaction.options.getNumber('id', true);
const task = await this.prisma.todos.findUnique({
where: { id },
select: {
id: true,
title: true,
description: true,
completed: true,
createdAt: true,
updatedAt: true,
},
});
if (!task) {
return interaction.reply({ content: `Task with ID ${id} not found.` });
}
const embed = new EmbedBuilder()
.setTitle(`Task #${task.id} - ${task.title}`)
.setDescription(task.description || '__No Description__')
.addFields(
{ name: 'Status', value: task.completed ? '✅ Completed' : '❌ Not Completed', inline: false },
{ name: 'Created At', value: task.createdAt.toISOString(), inline: false },
{ name: 'Updated At', value: task.updatedAt.toISOString(), inline: false },
)
.setColor(Colors.Aqua);
return interaction.reply({ embeds: [embed] });
}
@On.SubCommand('delete')
public async onDelete(
@Interaction() interaction: ChatInputCommandInteraction,
) {
const id = interaction.options.getNumber('id', true);
const task = await this.prisma.todos.findUnique({
where: { id },
});
if (!task) {
return interaction.reply({ content: `Task with ID ${id} not found.`, flags: MessageFlags.Ephemeral });
}
await this.prisma.todos.delete({
where: { id },
});
return interaction.reply({ content: `Task with ID ${id} has been deleted.`, flags: MessageFlags.Ephemeral });
}
@On.Autocomplete('id', 'view')
public async onViewIdAutocomplete() {
const tasks = await this.prisma.todos.findMany({
select: {
id: true,
title: true,
},
orderBy: {
id: 'asc',
},
});
return tasks.map(task => ({
name: String(task.title).length > 50 ? `${task.title.slice(0, 47)}...` : task.title,
value: task.id,
}));
}
@On.Autocomplete('id', 'delete')
public async onDeleteIdAutocomplete() {
const tasks = await this.prisma.todos.findMany({
select: {
id: true,
title: true,
},
orderBy: {
id: 'asc',
},
});
return tasks.map(task => ({
name: String(task.title).length > 50 ? `${task.title.slice(0, 47)}...` : task.title,
value: task.id,
}));
}
}
As you can see, the way our commands are structured, is that the decorator actually accepts a SlashCommandBuilder
instance, which allows us to define our command, subcommands, etc.
As you can see, we have created 4 sub commands for list, view, create and delete.
We then also set both view and delete to have an autocomplete option for the id
parameter, which will return a list of tasks with their IDs and titles.
The @On.SubCommand()
decorator will handle the sub command request, and the @Interaction()
decorator will inject the interaction object, which is the Discord interaction that triggered the command, depending on the type of interaction, it will be a ChatInputCommandInteraction
for slash commands.
Registering the slash commands.
As you can imagine, we need to actually register our slash commands with Discord, the easiest way to do this is using our Sodacore CLI, this will allow you to run commands via the command line, using the built-in scripts from the Discord plugin. You can learn about scripts in the Core section.
Start the application using
bun dev
to ensure the CLI can connect to it.
Install our CLI package if you haven't already:
bun add --global @sodacore/cli
Then you can run the following command:
sodacore
We shall assume that the CLI is running on the same machine as your application.
Using the CLI menu, select the Add a new connection
option, the hostname would be localhost (it's the default), the port is the default unless you have changed it, then set the password you defined in the .env
file, which is SODACORE_PASSWORD
and lastly give it a name.
Once you have done this, you can then be sent back to the menu, here you will see a new menu option called: Access: YOUR_NAME_HERE
click this, it will attempt to connect to your application, once it has connected, you should see some command options you can run, select the register one for Discord, and follow it, if you select register to guild, then the guild ID it will pre-suggest/insert is the one you defined in your .env
file, from here you can simply follow it through, once completed, it will tell the Bun application to register it's commands with Discord, and then you can go to your Discord server and test the commands out.
In the future, you can manage your slash commands all from the CLI, just ensure your application is running.
Conclusion.
That's it, you now have a fully working, pointless Sodacore application, with various plugins integrated. Go forth!
If you have questions about this guide, or something doesn't work, then feel free to raise a bug.
For more specific information regarding the plugins, and framework, look at our Package specific documentation: Packages Overview or read our API Reference.
For the full demo code, you can find it on GitHub.