Ticket bot (JavaScript)
Opens a private support channel for each member who runs /ticket, with a button to close it.
Node.js 22
♥ 0 likes
Environment variables you'll need
| Key | What it is |
|---|---|
DISCORD_TOKEN | Your bot's token from the Discord Developer Portal. Paste it on the Secrets tab; we store it encrypted. |
TICKET_CATEGORY_ID | Optional. Category the ticket channels are created in |
SUPPORT_ROLE_ID | Optional. Role that can see and close every ticket |
Files
README.md
# Ticket bot
A JavaScript bot built on discord.js. Your bot token is set on the Secrets tab as
DISCORD_TOKEN and reaches the bot as an environment variable.
## Commands
- `/ticket [reason]` opens a private channel only you, the bot and the support role can see. One open ticket per member.
- The Close button (or `/close` inside the ticket) deletes the channel.
## Setup
1. No privileged gateway intents are needed. Leave Message Content, Server Members
and Presence off in the Discord Developer Portal.
2. Invite the bot with the `bot` and `applications.commands` scopes and these
permissions: View Channels, Manage Channels, Send Messages, Read Message History.
3. Set these values on the Secrets tab. The ones marked optional can stay empty:
- `TICKET_CATEGORY_ID`: Optional. Category the ticket channels are created in.
- `SUPPORT_ROLE_ID`: Optional. Role that can see and close every ticket.
4. Press Deploy. The console shows a line with "Logged in as" once the bot is online.
index.js
const {
ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, Client, Events,
GatewayIntentBits, MessageFlags, OverwriteType, PermissionFlagsBits, SlashCommandBuilder,
} = require('discord.js');
const envId = (name) => (/^\d+$/.test((process.env[name] || '').trim()) ? process.env[name].trim() : null);
const CATEGORY_ID = envId('TICKET_CATEGORY_ID');
const SUPPORT_ROLE_ID = envId('SUPPORT_ROLE_ID');
const CLOSE_ID = 'ticket:close';
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const commands = [
new SlashCommandBuilder().setName('ticket').setDescription('Open a private support ticket')
.addStringOption((o) => o.setName('reason').setDescription('What do you need help with?')),
new SlashCommandBuilder().setName('close').setDescription('Close this ticket'),
].map((c) => c.toJSON());
const ticketName = (userId) => `ticket-${userId}`;
function hasRole(member, roleId) {
if (!member || !roleId) return false;
return Array.isArray(member.roles) ? member.roles.includes(roleId) : member.roles.cache.has(roleId);
}
async function openTicket(interaction) {
const { guild, user } = interaction;
const name = ticketName(user.id);
const existing = guild.channels.cache.find((c) => c.name === name);
if (existing) {
await interaction.reply({ content: `You already have an open ticket: ${existing}`, flags: MessageFlags.Ephemeral });
return;
}
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const member = [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory];
const overwrites = [
{ id: guild.roles.everyone.id, type: OverwriteType.Role, deny: [PermissionFlagsBits.ViewChannel] },
{ id: user.id, type: OverwriteType.Member, allow: [...member, PermissionFlagsBits.AttachFiles] },
{ id: client.user.id, type: OverwriteType.Member, allow: [...member, PermissionFlagsBits.ManageChannels] },
];
if (SUPPORT_ROLE_ID && guild.roles.cache.has(SUPPORT_ROLE_ID)) {
overwrites.push({ id: SUPPORT_ROLE_ID, type: OverwriteType.Role, allow: member });
}
const category = CATEGORY_ID ? guild.channels.cache.get(CATEGORY_ID) : null;
const channel = await guild.channels.create({
name,
type: ChannelType.GuildText,
parent: category && category.type === ChannelType.GuildCategory ? category.id : null,
topic: `Ticket for ${user.tag} (${user.id})`,
permissionOverwrites: overwrites,
reason: `Ticket opened by ${user.tag}`,
});
const reason = interaction.options.getString('reason');
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId(CLOSE_ID).setLabel('Close ticket').setStyle(ButtonStyle.Danger),
);
await channel.send({
content: `${user} opened a ticket.${reason ? `\nReason: ${reason}` : ''}\n`
+ 'Support will be with you soon. Press the button or use /close when done.',
components: [row],
allowedMentions: { users: [user.id] },
});
await interaction.editReply(`Your ticket is ready: ${channel}`);
}
async function closeTicket(interaction) {
const { channel, member, user } = interaction;
if (!channel || !channel.name || !channel.name.startsWith('ticket-')) {
await interaction.reply({ content: 'This only works inside a ticket channel.', flags: MessageFlags.Ephemeral });
return;
}
const allowed = channel.name === ticketName(user.id)
|| hasRole(member, SUPPORT_ROLE_ID)
|| interaction.memberPermissions.has(PermissionFlagsBits.ManageChannels);
if (!allowed) {
await interaction.reply({ content: 'Only the ticket owner or support staff can close this ticket.', flags: MessageFlags.Ephemeral });
return;
}
await interaction.reply('Closing this ticket.');
await channel.delete(`Ticket closed by ${user.tag}`);
}
client.once(Events.ClientReady, async (c) => {
console.log(`Logged in as ${c.user.tag} (ready)`);
try {
await c.application.commands.set(commands);
console.log('Slash commands registere
package.json
{
"name": "ticket-bot-js",
"private": true,
"main": "index.js",
"engines": { "node": ">=22" },
"dependencies": { "discord.js": "^14.0.0" }
}