Goal: Create a full session system with start, end, and vote auto-start.
In this lesson, you will build a session system that allows staff to start sessions, end sessions, and allow players to vote to automatically start a session when enough votes are reached.
The Session model stores session data like:
active session status, who started it, when it started, and vote data.
This allows the bot to remember session data even if the bot restarts.
Starts a session. The bot sets the session to active, saves who started it, stores the start time, and sends a session started message in the session channel.
Ends the current active session. The bot sets the session to inactive, clears session data, and sends a session ended message.
Starts a vote where users can press a button to vote yes. If enough votes are reached, the session automatically starts.
When a user presses the vote button, their vote is added. If they press again, their vote is removed (toggle system). The bot updates the vote count and checks if the required votes are reached.
If an expiry time is set, the vote will automatically end after the specified number of minutes if the required votes are not reached.
This system uses Discord Components V2 such as
ContainerBuilder,
TextDisplayBuilder,
and ButtonBuilder.
Every message using these must include
MessageFlags.IsComponentsV2.
Create: commands/session.js
const { SlashCommandBuilder } = require("@discordjs/builders");
const {
ContainerBuilder,
SeparatorSpacingSize,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
MessageFlags,
} = require("discord.js");
const mongoose = require("mongoose");
const Session = require("../models/Session");
const config = require("../config.json");
const SESSION_CHANNEL_ID = "";
const SESSION_PING_ROLE_ID = "";
const ALLOWED_ROLES = ""; // who can execute a /session command
if (mongoose.connection.readyState === 0) {
mongoose.connect(config.MONGOURI);
}
async function getOrCreateSession(guildId) {
let session = await Session.findone({ guildId });
if (!session) session = await Session.create({ guildId })
return session;
}
function buildVoteComponents(yesCount, disabled = false) {
const container = new ContainerBuilder()
.addTextDisplayComponents(
(t) => t.setContent("## Session Vote"),
(t) =>
t.setContent(
"Vote yes to request that a session is started."
)
)
.addSeparatorComponents((s) =>
s.setSpacing(SeparatorSpacingSize.Small)
)
.addTextDisplayComponents(
(t) => t.setContent(`**Yes:** ${yesCount}`)
);
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("session_vote_yes")
.setLabel(`Yes (${yesCount})`)
.setStyle(ButtonStyle.Success)
.setDisabled(disabled)
);
return { container, row };
}
module.exports = {
dev: true, // visible only to guilds in config.json
guilds: [''], // whitelist by guild ID
roles: ALLOWED_ROLES, // requires user to have listed role IDs
users: [''], // whitelist by user ID
cooldown: 0, // cooldown time in seconds
userPerms: [''], // required user permissions
clientPerms: [''], // required bot permissions
alias: '', // Single Alias
aliases: [''], // Mulitple Aliases
defer: false, // defer without being ephemeral | true or false
cache: true, // the next time you run this command it will jump straight to the bottom without delay. | true or false
data: new SlashCommandBuilder()
.setName('session') // Name of your slash command
.setDescription('Session management.')
.addSubcommand((sub) =>
sub.setName('start').setDescription('Starts a session')
)
.addSubcommand((sub) =>
sub
.setName("vote")
.setDescription("Start a session vote")
.addIntegerOption((opt) =>
opt
.setName("requiredvotes")
.setDescription("Votes needed to auto-start the session")
.setRequired(false)
.setMinValue(1)
)
.addIntegerOption((opt) =>
opt
.setName("expiry")
.setDescription(
"Minutes until the votes expires"
)
.setRequired(false)
.setMinValue(1)
)
),
async execute(interaction) {
// Command logic here
const sub = interaction.options.getSubCommand();
const guildId = interaction.guildId;
const session = await getOrCreateSession(guildId);
const channel =
await interaction.client.channels.fetch(SESSION_CHANNEL_ID)
if (sub === "start") {
if (session.active) {
return interaction.editReply({
components: [
new ContainerBuilder()
.addTextDisplayComponents((t) =>
t.setContent("A session is already active.")
)
],
flags: MessageFlags.IsComponentsV2,
});
}
session.active = true;
session.startedAt = new Date();
session.startedBy = interaction.user.id;
session.vote.active = false;
await session.save();
await channel.send({
content: `<@&${SESSION_PING_ROLE_ID}>`,
components: [
new ContainerBuilder()
.addTextDisplayComponents(
(t) => t.setContent("## Session Started"),
(t) =>
t.setContent(
`A session has been started by <@${interaction.user.id}>!`
)
),
],
flags: MessageFlags.IsComponentsV2,
});
return interaction.editReply({
components: [
new ContainerBuilder()
.addTextDisplayComponents((t) =>
t.setContent("Session started successfully.")
)
],
flags: MessageFlags.IsComponentsV2,
});
}
if (sub === "end") {
if (!session.active) {
return interaction.editReply({
components: [
new ContainerBuilder()
.addTextDisplayComponents((t) =>
t.setContent(
"There is no active session to end."
)
),
],
flags: MessageFlags.IsComponentsV2,
});
}
session.active = false;
session.startedAt = null;
session.startedBy = null;
session.vote.active = false;
await session.save();
await channel.send({
components: [
new ContainerBuilder()
.addTextDisplayComponents(
(t) => t.setContent('## Session Ended'),
(t) =>
t.setContent(
`The session has been ended by <@${interaction.user.id}>.`
)
),
],
flags: MessageFlags.IsComponentsV2,
});
return interaction.editReply({
components: [
new ContainerBuilder()
.addTextDisplayComponents((t) =>
t.setContent("Session ended succesfully")
),
],
flags: MessageFlags.IsComponentsV2,
});
}
if (sub === "vote") {
if (session.vote.active) {
return interaction.editReply({
components: [
new ContainerBuilder()
.addTextDisplayComponents((t) =>
t.setContent(
"A vote is already in progress."
)
)
],
flags: MessageFlags.IsComponentsV2
});
}
const requiredVotes =
interaction.options.getInteger("requiredvotes") ?? null;
const expiry =
interaction.options.getInteger("expiry") ?? null;
const expiresAt = expiry
? new Date(Date.now() + expiry * 60 * 1000)
: null;
const { container, row } = buildVoteComponents(0);
const voteMessage = await channel.send({
components: [container, row],
flags: MessageFlags.IsComponentsV2,
});
session.vote.active = true;
session.vote.messageId = voteMessage.id;
session.vote.channelId = SESSION_CHANNEL_ID;
session.vote.requiredVotes = requiredVotes;
session.vote.yesVotes = [];
session.vote.expiresAt = expiresAt;
session.vote.startedBy = interaction.user.id;
await session.save();
if (expiresAt) {
setTimeout(async () => {
const current = await Session.findone({ guildId });
if (!current?.vote?.active) return;
current.vote.active = false;
await current.save();
}, expiry * 60 * 1000);
}
return interaction.editReply({
components: [
new ContainerBuilder()
.addTextDisplayComponents((t) =>
t.setContent(
`Vote started ${requiredVotes ? ` - **${requiredVotes}** yes votes needed to auto-start` : ''}${expiry ? ` - expires in **${expiry}m**` : ''}.`
)
)
],
flags: MessageFlags.IsComponentsV2,
});
}
},
};
const {
ContainerBuilder,
SeparatorSpacingSize,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
MessageFlags,
} = require("discord.js")
const mongoose = require("mongoose");
const Session = require("../models/Session");
const config = require("../config.json");
const { execute } = require("../commands/session");
const SESSION_CHANNEL_ID = "";
const SESSION_PING_ROLE_ID = "";
if (mongoose.connection.readyState === 0) {
mongoose.connect(config.MONGOURI);
}
function buildVoteComponents(yesCount, disabled = false) {
const container = new ContainerBuilder()
.addTextDisplayComponents(
(t) => t.setContent("## Session Vote"),
(t) =>
t.setContent("Vote yes to ask that a session is started.")
)
.addSeparatorComponents((s) =>
s.setSpacing(SeparatorSpacingSize.Small)
)
.addTextDisplayComponents(
(t) => t.setContent(`**Yes** ${yesCount}`)
);
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("session_vote_yes")
.setLabel(`Yes (${yesCount})`)
.setStyle(ButtonStyle.Success)
.setDisabled(disabled)
);
return { container, row };
}
module.exports = {
customIds: ["session_vote_yes"],
async execute(interaction) {
const guildId = interaction.guildId;
const userId = interaction.user.id;
const session = await Session.findOne({ guildId });
if (!session?.vote?.active) {
return interaction.reply({
components: [
new ContainerBuilder()
.addTextDisplayComponents((t) =>
t.setContent("There is no active vote.")
),
],
flags: MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral,
});
}
// Toggling the vote to be off if you already voted yes
const alreadyVoted = session.vote.yesVotes.includes(userId);
if (alreadyVoted) {
session.vote.yesVotes = session.vote.yesVotes.filter(
(id) => id !== userId
);
} else {
session.vote.yesVotes.push(userId);
}
const yesCount = session.vote.yesVotes.length;
const requiredVotes = session.vote.requiredVotes;
const thresholdHit =
requiredVotes !== null && yesCount >= requiredVotes;
if (thresholdHit) {
session.vote.active = false;
}
await session.save();
const { container, row } = buildVoteComponents(yesCount, thresholdHit);
await interaction.update({
components: [container, row],
flags: MessageFlags.IsComponentsV2,
});
if (thresholdHit && !session.active) {
session.active = true;
session.startedAt = new Date();
session.startedBy = interaction.client.user.id;
await session.save();
const channel =
await interaction.client.channels.fetch(SESSION_CHANNEL_ID);
await channel.send({
content: `<@&{SESSION_PING_ROLE_ID}>`,
components: [
new ContainerBuilder()
.addTextDisplayComponents(
(t) => t.setContent("## Session Started"),
(t) =>
t.setContent(
`The vote passed with **${yesCount}** votes - a session has been automatically started.`
)
)
],
flags: MessageFlags.IsComponentsV2,
});
}
},
}
node index.js