├── LICENSE ├── README.md ├── package.json └── src ├── config.js ├── events ├── interactionCreate.js ├── messageCreate.js └── ready.js ├── index.js └── utils └── utils.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 thesleax 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎫 Discord Modals Simple Ticket Bot 2 | 3 | This is a bot infrastructure I created in my spare time. Initially, I made it for my own server, but I decided to share it with the community in hopes that it will be useful to others. Happy coding! 4 | 5 | ## 🚀 Setup Guide 6 | Now, there's a video tutorial to help you set up the bot! Watch it here: [YouTube](https://youtu.be/gv94bxO-jo0) 7 | 8 | ### Prerequisites 9 | 📌 You must have **Node.js** installed on your computer. If you haven't installed it yet, download it from [here](https://nodejs.org). 10 | 11 | ### Installation Steps 12 | 1️⃣ Open a terminal and run: 13 | ```sh 14 | npm install -g yarn 15 | ``` 16 | This will install Yarn globally. 17 | 18 | 2️⃣ Navigate to the bot’s directory, open a terminal, and install the required modules: 19 | ```sh 20 | yarn install 21 | ``` 22 | 23 | 3️⃣ Open the **config.js** file and fill in the necessary details. 24 | 25 | 4️⃣ Once everything is set up, start the bot by running: 26 | ```sh 27 | node src/index.js 28 | ``` 29 | Alternatively, you can create a `.bat` file with the command above to start the bot without opening a terminal. 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ticket-bot", 3 | "version": "0.0.2", 4 | "description": "Discord Modals Simple Ticket Bot", 5 | "type": "module", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "start": "node src/index.js" 9 | }, 10 | "author": "thesleax", 11 | "license": "MIT", 12 | "dependencies": { 13 | "discord.js": "14.18.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import Discord from "discord.js"; 2 | const { ButtonStyle, TextInputStyle } = Discord; 3 | 4 | export default { 5 | PREFIX: "", 6 | TOKEN: "", 7 | PRESENCE: { 8 | NAME: "", 9 | TYPE: "PLAYING" /* 'PLAYING', 'STREAMING', 'LISTENING', 'WATCHING', 'COMPETING' */, 10 | STATUS: "dnd" /* 'online', 'idle', 'dnd', 'offline' */, 11 | }, 12 | GUILD_ID: "", 13 | TICKET: { 14 | CHANNEL: "", 15 | CATEGORY: "", 16 | ARCHIVE_CATEGORY: "", 17 | MESSAGE: "Click to create ticket!", 18 | STAFF_ROLES: [] /* ["ROLE_ID", "ROLE_ID"] */, 19 | BUTTONS: [ 20 | { 21 | STYLE: ButtonStyle.Success, 22 | LABEL: "Confirm Ticket", 23 | EMOTE: "✅", 24 | ID: "successTicket", 25 | DISABLED: false, 26 | }, 27 | { 28 | STYLE: ButtonStyle.Secondary, 29 | LABEL: "Archive Ticket", 30 | EMOTE: "🎫", 31 | ID: "archiveTicket", 32 | DISABLED: false, 33 | }, 34 | { 35 | STYLE: ButtonStyle.Danger, 36 | LABEL: "Delete Ticket", 37 | EMOTE: "🎟️", 38 | ID: "deleteTicket", 39 | DISABLED: false, 40 | }, 41 | ], 42 | QUESTIONS: [ 43 | { 44 | ID: "name", 45 | LABEL: "What is your name?", 46 | STYLE: TextInputStyle.Short, 47 | MIN_LENGTH: 1, 48 | MAX_LENGTH: 16, 49 | PLACE_HOLDER: "You can write your name.", 50 | REQUIRED: true, 51 | }, 52 | { 53 | ID: "age", 54 | LABEL: "How old are you?", 55 | STYLE: TextInputStyle.Short, 56 | MIN_LENGTH: 1, 57 | MAX_LENGTH: 2, 58 | PLACE_HOLDER: "You can write your age.", 59 | REQUIRED: true, 60 | }, 61 | ], 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /src/events/interactionCreate.js: -------------------------------------------------------------------------------- 1 | import Utils from "../utils/utils.js"; 2 | import Config from "../config.js"; 3 | import Discord from "discord.js"; 4 | const { 5 | ButtonBuilder, 6 | ActionRowBuilder, 7 | PermissionFlagsBits, 8 | InteractionType, 9 | ChannelType, 10 | MessageFlags, 11 | } = Discord; 12 | 13 | export default (Bot) => { 14 | Bot.on("interactionCreate", async (interaction) => { 15 | if (interaction.type === InteractionType.ModalSubmit) { 16 | if (interaction.customId === "ticket") { 17 | let Questions = Config.TICKET.QUESTIONS.map((x) => x.LABEL); 18 | 19 | let fields = []; 20 | 21 | [interaction.fields].map((z) => 22 | z.fields.map((x) => { 23 | fields.push(x); 24 | }) 25 | ); 26 | 27 | let Value = fields.map((x) => x.value); 28 | let Output = Value.map((x, i) => ({ 29 | Questions: Questions[i], 30 | Value: x, 31 | })); 32 | let Content = Output.map( 33 | (x, index) => 34 | `\n\`Question ${index + 1}:\` **${x.Questions}** \n\`Reply:\` **${ 35 | x.Value 36 | }**` 37 | ).join("\n"); 38 | 39 | const Channel = interaction.guild.channels.cache.find( 40 | (x) => x.name === "ticket" + "-" + interaction.user.id 41 | ); 42 | 43 | await interaction.deferReply({ flags: MessageFlags.Ephemeral }); 44 | 45 | if (Channel) { 46 | interaction.followUp({ 47 | content: `You already have a ticket request.`, 48 | flags: MessageFlags.Ephemeral, 49 | }); 50 | } else { 51 | let PermissionsArray = [ 52 | { 53 | id: interaction.user.id, 54 | allow: [ 55 | PermissionFlagsBits.ViewChannel, 56 | PermissionFlagsBits.ReadMessageHistory, 57 | ], 58 | deny: [PermissionFlagsBits.SendMessages], 59 | }, 60 | { 61 | id: interaction.guild.id, 62 | deny: [PermissionFlagsBits.ViewChannel], 63 | }, 64 | ]; 65 | 66 | Config.TICKET.STAFF_ROLES.map((x) => { 67 | PermissionsArray.push({ 68 | id: x, 69 | allow: [ 70 | PermissionFlagsBits.ViewChannel, 71 | PermissionFlagsBits.ReadMessageHistory, 72 | PermissionFlagsBits.SendMessages, 73 | ], 74 | }); 75 | }); 76 | 77 | interaction.guild.channels 78 | .create({ 79 | name: "ticket" + "-" + interaction.user.id, 80 | type: ChannelType.GuildText, 81 | parent: Config.TICKET.CATEGORY, 82 | permissionOverwrites: PermissionsArray, 83 | }) 84 | .then(async (Channel) => { 85 | interaction.followUp({ 86 | content: 87 | "Hey! Your ticket request has been successfully created.", 88 | flags: MessageFlags.Ephemeral, 89 | }); 90 | 91 | Channel.send({ 92 | embeds: [ 93 | Utils.embed( 94 | `Ticket Creator Member Information: \n${interaction.user} (\`${interaction.user.id}\`) \n${Content}`, 95 | interaction.guild, 96 | Bot, 97 | interaction.user 98 | ), 99 | ], 100 | components: [Utils.ticketButton()], 101 | }); 102 | }); 103 | } 104 | } 105 | } 106 | 107 | if (!interaction.isButton()) return; 108 | 109 | if (interaction.customId === "ticket") { 110 | await interaction.showModal(Utils.modal()); 111 | } 112 | 113 | if (interaction.customId === "successTicket") { 114 | if ( 115 | !Config.TICKET.STAFF_ROLES.some((x) => 116 | interaction.member.roles.cache.has(x) 117 | ) && 118 | ![interaction.guild.ownerId].includes(interaction.user.id) 119 | ) { 120 | await interaction.deferReply({ flags: MessageFlags.Ephemeral }); 121 | 122 | interaction.followUp({ 123 | content: `Only authorities can use the ticket validation system.`, 124 | flags: MessageFlags.Ephemeral, 125 | }); 126 | 127 | return; 128 | } else { 129 | await interaction.update({ 130 | components: [ 131 | new ActionRowBuilder({ 132 | components: [ 133 | ButtonBuilder.from( 134 | interaction.message.components[0].components[0] 135 | ).setDisabled(true), 136 | ButtonBuilder.from( 137 | interaction.message.components[0].components[1] 138 | ), 139 | ButtonBuilder.from( 140 | interaction.message.components[0].components[2] 141 | ), 142 | ], 143 | }), 144 | ], 145 | }); 146 | 147 | interaction.channel.permissionOverwrites.edit( 148 | interaction.channel.name.replace("ticket-", ""), 149 | { SendMessages: true } 150 | ); 151 | 152 | interaction.followUp({ 153 | content: `The ticket has been successfully approved.`, 154 | flags: MessageFlags.Ephemeral, 155 | }); 156 | 157 | interaction.channel.send({ 158 | content: `Heyy! <@!${interaction.channel.name.replace( 159 | "ticket-", 160 | "" 161 | )}>, ticket has been successfully approved by the authorities.`, 162 | }); 163 | 164 | return; 165 | } 166 | } 167 | 168 | if (interaction.customId === "archiveTicket") { 169 | await interaction.deferReply({ flags: MessageFlags.Ephemeral }); 170 | 171 | if ( 172 | !Config.TICKET.STAFF_ROLES.some((x) => 173 | interaction.member.roles.cache.has(x) 174 | ) && 175 | ![interaction.guild.ownerId].includes(interaction.user.id) 176 | ) 177 | return interaction.followUp({ 178 | content: `Only authorities can use the ticket archive system.`, 179 | flags: MessageFlags.Ephemeral, 180 | }); 181 | 182 | if (interaction.channel.parentId === Config.TICKET.ARCHIVE_CATEGORY) 183 | return interaction.followUp({ 184 | content: `This ticket is already archived.`, 185 | flags: MessageFlags.Ephemeral, 186 | }); 187 | 188 | let Parent = interaction.guild.channels.cache.get( 189 | Config.TICKET.ARCHIVE_CATEGORY 190 | ); 191 | 192 | interaction.channel.permissionOverwrites.delete( 193 | interaction.channel.name.replace("ticket-", "") 194 | ); 195 | 196 | interaction.channel 197 | .setParent(Parent.id, { lockPermissions: false }) 198 | .then(async (x) => { 199 | x.setName(interaction.channel.name.replace("ticket", "archive")); 200 | 201 | interaction.message.edit({ 202 | embeds: [ 203 | Utils.embed( 204 | interaction.message.embeds.map((x) => x.description).join(""), 205 | interaction.guild, 206 | Bot, 207 | "" 208 | ), 209 | ], 210 | components: [], 211 | }); 212 | 213 | interaction.followUp({ 214 | content: `Ticket successfully archived.`, 215 | flags: MessageFlags.Ephemeral, 216 | }); 217 | }); 218 | } 219 | 220 | if (interaction.customId === "deleteTicket") { 221 | await interaction.deferReply({ flags: MessageFlags.Ephemeral }); 222 | 223 | let User = interaction.channel.name.replace("ticket-", ""); 224 | 225 | if ([User].includes(interaction.user.id)) { 226 | if ( 227 | interaction.message.components[0].components[0].data.disabled === true 228 | ) 229 | return interaction.followUp({ 230 | content: `The support request has been approved by the authorities, you can no longer delete it.`, 231 | flags: MessageFlags.Ephemeral, 232 | }); 233 | } else { 234 | if ( 235 | !Config.TICKET.STAFF_ROLES.some((x) => 236 | interaction.member.roles.cache.has(x) 237 | ) && 238 | ![interaction.guild.ownerId].includes(interaction.user.id) 239 | ) 240 | return; 241 | } 242 | 243 | interaction.followUp({ 244 | content: `Your request has been received successfully after \`5 seconds\` the channel will be deleted automatically.`, 245 | flags: MessageFlags.Ephemeral, 246 | }); 247 | 248 | setTimeout(() => { 249 | interaction.channel.delete().catch(() => { 250 | return undefined; 251 | }); 252 | }, 1000 * 5); 253 | } 254 | }); 255 | }; 256 | -------------------------------------------------------------------------------- /src/events/messageCreate.js: -------------------------------------------------------------------------------- 1 | import Utils from "../utils/utils.js"; 2 | import Config from "../config.js"; 3 | import Discord from "discord.js"; 4 | const { ButtonStyle, PermissionsBitField } = Discord; 5 | 6 | export default (Bot) => { 7 | Bot.on("messageCreate", (message) => { 8 | const Prefix = message.content.toLowerCase().startsWith(Config.PREFIX); 9 | 10 | if (!Prefix && !message.guild) return; 11 | 12 | const Args = message.content.split(" ").slice(1); 13 | const Command = message.content.split(" ")[0].slice(Config.PREFIX.length); 14 | 15 | if (Command === "ticket") { 16 | if ( 17 | !message.member.permissions.has(PermissionsBitField.Flags.Administrator) 18 | ) 19 | return message.reply({ 20 | content: "You are not permissions to use this command.", 21 | }); 22 | 23 | message.delete().catch(() => { 24 | return undefined; 25 | }); 26 | 27 | let TicketChannel = message.guild.channels.cache.get( 28 | Config.TICKET.CHANNEL 29 | ); 30 | 31 | if (!TicketChannel) 32 | return message.reply({ 33 | content: "Please write ticket channel ID in config file.", 34 | }); 35 | 36 | TicketChannel.send({ 37 | embeds: [Utils.embed(Config.TICKET.MESSAGE, message.guild, Bot, "")], 38 | components: [ 39 | Utils.button( 40 | ButtonStyle.Primary, 41 | "Open Ticket!", 42 | "🎫", 43 | "ticket", 44 | false 45 | ), 46 | ], 47 | }); 48 | 49 | return message.channel.send(`Sended the message to ${TicketChannel}`); 50 | } 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /src/events/ready.js: -------------------------------------------------------------------------------- 1 | import { ActivityType } from "discord.js"; 2 | import Config from "../config.js"; 3 | 4 | export default (Bot) => { 5 | Bot.on("ready", () => { 6 | Bot.user.setPresence({ 7 | status: Config.PRESENCE.STATUS || "online", 8 | activities: [ 9 | { 10 | name: Config.PRESENCE.NAME, 11 | type: ActivityType[Config.PRESENCE.TYPE] || ActivityType.Playing, 12 | }, 13 | ], 14 | }); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Client, GatewayIntentBits } from "discord.js"; 2 | const Bot = (global.bot = new Client({ 3 | intents: [ 4 | GatewayIntentBits.Guilds, 5 | GatewayIntentBits.GuildMessages, 6 | GatewayIntentBits.GuildMembers, 7 | GatewayIntentBits.MessageContent, 8 | ], 9 | })); 10 | 11 | import Utils from "./utils/utils.js"; 12 | 13 | Utils.event(Bot); 14 | Utils.login(Bot); 15 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | import Fs from "fs"; 2 | import Discord from "discord.js"; 3 | const { 4 | EmbedBuilder, 5 | ActionRowBuilder, 6 | ButtonBuilder, 7 | TextInputBuilder, 8 | ModalBuilder, 9 | Colors, 10 | } = Discord; 11 | import Config from "../config.js"; 12 | class Utils { 13 | static login(Bot) { 14 | Bot.login(Config.TOKEN ? Config.TOKEN : process.env.TOKEN) 15 | .then(() => console.log("[BOT] Ticket bot active.")) 16 | .catch((err) => console.log("" + err)); 17 | } 18 | 19 | static event(Bot) { 20 | Fs.readdirSync("./src/events").forEach(async (file) => { 21 | const Event = await import(`../events/${file}`).then((x) => x); 22 | 23 | Event.default(Bot); 24 | }); 25 | } 26 | 27 | static embed(Content, Guild, Bot, User) { 28 | const Embed = new EmbedBuilder() 29 | .setAuthor({ 30 | name: `${Guild.name} Ticket System`, 31 | iconURL: Guild.iconURL({ dynamic: true }), 32 | }) 33 | .setDescription(Content) 34 | .setColor(Colors.DarkNavy) 35 | .setFooter({ 36 | text: Bot.user.username, 37 | iconURL: Bot.user.avatarURL({ dynamic: true }), 38 | }) 39 | .setTimestamp() 40 | .setThumbnail( 41 | User 42 | ? User.avatarURL({ dynamic: true }) 43 | : Guild.iconURL({ dynamic: true }) 44 | ); 45 | 46 | return Embed; 47 | } 48 | 49 | static button(Style, Label, Emoji, Id, Disabled) { 50 | const Row = new ActionRowBuilder().addComponents( 51 | new ButtonBuilder() 52 | .setCustomId(Id) 53 | .setLabel(Label) 54 | .setStyle(Style) 55 | .setEmoji(Emoji) 56 | .setDisabled(Disabled) 57 | ); 58 | 59 | return Row; 60 | } 61 | 62 | static ticketButton() { 63 | let Buttons = []; 64 | 65 | Config.TICKET.BUTTONS.map((x) => { 66 | const Button = new ButtonBuilder() 67 | .setCustomId(x.ID) 68 | .setLabel(x.LABEL) 69 | .setStyle(x.STYLE) 70 | .setEmoji(x.EMOTE) 71 | .setDisabled(x.DISABLED); 72 | 73 | Buttons.push(Button); 74 | }); 75 | 76 | let Row = new ActionRowBuilder().addComponents(Buttons); 77 | 78 | return Row; 79 | } 80 | 81 | static modal() { 82 | let Inputs = []; 83 | 84 | Config.TICKET.QUESTIONS.map((v) => { 85 | const Input = new TextInputBuilder() 86 | .setCustomId(v.ID) 87 | .setLabel(v.LABEL) 88 | .setStyle(v.STYLE) 89 | .setMinLength(v.MIN_LENGTH) 90 | .setMaxLength(v.MAX_LENGTH) 91 | .setPlaceholder(v.PLACE_HOLDER) 92 | .setRequired(v.REQUIRED); 93 | 94 | Inputs.push(Input); 95 | }); 96 | 97 | let Modals = new ModalBuilder() 98 | .setCustomId("ticket") 99 | .setTitle("Ticket Creation Request"); 100 | 101 | let Row = []; 102 | Inputs.map((x) => Row.push(new ActionRowBuilder().addComponents([x]))); 103 | Modals.addComponents(Row); 104 | 105 | return Modals; 106 | } 107 | } 108 | 109 | export default Utils; 110 | --------------------------------------------------------------------------------