├── .gitattributes ├── package.json ├── src ├── utils │ └── logUtils.ts ├── events │ └── ServerListener.ts └── commands │ ├── General │ └── CommandPing.ts │ ├── Command.ts │ └── index.ts ├── README.md ├── .gitignore └── app.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@types/node": "^20.11.7" 4 | }, 5 | "dependencies": { 6 | "discord.js": "^14.14.1", 7 | "dotenv": "^16.4.1", 8 | "glob": "^10.3.10" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/logUtils.ts: -------------------------------------------------------------------------------- 1 | export function startLog(tag: string, prefix: string, commands: number, slash: number) { 2 | console.clear(); 3 | console.log("Autenticado em: " + tag); 4 | console.log("Prefixo selecionado: " + prefix); 5 | console.log(""); 6 | console.log("Commands: " + `${commands} registered`) 7 | console.log("Slash Commands: " + `${slash} registered`) 8 | console.log(""); 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Discord Bot Base 2 | 3 | To run this project, you will need to add the following environment variables to your .env file 4 | 5 | `TOKEN` = Your discord bot token\ 6 | `PREFIX` = Prefix that your bot will use to respond\ 7 | `MAIN_GUILD_ID` = Server ID that slash commands will be added to 8 | 9 | ### Exemplo de `.env` 10 | 11 | ```env 12 | TOKEN=TOKEN_AQUI 13 | PREFIX=! 14 | MAIN_GUILD_ID=778802467604791337 15 | ``` 16 | 17 | ### How to install and run 18 | 19 | CMD:\ 20 | `npm i -g typescript && npm i -g ts-node && npm install` 21 | 22 | POWERSHELL:\ 23 | `npm i -g typescript ; npm i -g ts-node ; npm install` 24 | -------------------------------------------------------------------------------- /src/events/ServerListener.ts: -------------------------------------------------------------------------------- 1 | import Discord, { AuditLogEvent, Message, Options, TextChannel } from "discord.js"; 2 | 3 | export class ServerListener { 4 | constructor() { } 5 | 6 | async guildMemberAdd( 7 | client: Discord.Client, 8 | guild: Discord.Guild, 9 | guildMember: Discord.GuildMember 10 | ): Promise { 11 | const generalChat = client.channels.cache.get("id_do_canal"); 12 | if (generalChat instanceof TextChannel) { 13 | generalChat.send({ content: `Bem vindo ${guildMember}!` }) 14 | } 15 | } 16 | 17 | async guildMemberRemove( 18 | client: Discord.Client, 19 | guildMember: Discord.GuildMember | Discord.PartialGuildMember 20 | ): Promise { 21 | const logChannel = client.channels.cache.get("id_do_canal"); 22 | if (logChannel instanceof TextChannel) { 23 | logChannel.send({ content: `${guildMember} saiu do servidor!` }) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/commands/General/CommandPing.ts: -------------------------------------------------------------------------------- 1 | import { Client, CommandInteraction, Guild, GuildTextBasedChannel, PermissionsBitField, User } from 'discord.js'; 2 | import { Command } from "../Command"; 3 | export class Cmd extends Command { 4 | 5 | constructor() { 6 | super("ping", { "pt-BR": "ping" }, "Get 'pong' response.", [PermissionsBitField.Flags.SendMessages], [/*user_bot_permissions*/], [PermissionsBitField.Flags.SendMessages], ["p"]); 7 | this.options = []; 8 | this.enabled = true; 9 | this.description_localizations = { "pt-BR": "Receba 'pong' como resposta." } 10 | } 11 | 12 | async execute(client: Client, guild: Guild, user: User, channel: GuildTextBasedChannel, data: CommandInteraction | string[]) { 13 | 14 | const isSlash = data instanceof CommandInteraction; 15 | 16 | if (isSlash) { 17 | data.reply({ content: 'pong', ephemeral: true }); 18 | return; 19 | } 20 | 21 | channel.send({ content: 'pong' }); 22 | } 23 | } -------------------------------------------------------------------------------- /src/commands/Command.ts: -------------------------------------------------------------------------------- 1 | import {ApplicationCommandOption, Client, CommandInteraction, Guild, GuildTextBasedChannel, User } from 'discord.js'; 2 | 3 | export abstract class Command { 4 | name: string; 5 | name_localizations: any; 6 | description: string; 7 | user_permissions: bigint[]; 8 | user_bot_permissions: string[]; 9 | bot_permissions: bigint[]; 10 | cooldownToUse: number; 11 | cooldownType: string; 12 | enabled: boolean; 13 | slash: boolean; 14 | description_localizations: any; 15 | options: ApplicationCommandOption[]; 16 | aliases: string[]; 17 | 18 | constructor( 19 | name: string, 20 | name_localizations: any, 21 | description: string, 22 | user_permissions: bigint[], 23 | user_bot_permissions: string[], 24 | bot_permissions: bigint[], 25 | aliases: string[], 26 | ) { 27 | this.name = name; 28 | this.name_localizations = name_localizations; 29 | this.description = description; 30 | this.user_permissions = user_permissions; 31 | this.user_bot_permissions = user_bot_permissions; 32 | this.bot_permissions = bot_permissions; 33 | this.cooldownToUse = 0; 34 | this.cooldownType = 'global'; 35 | this.enabled = true; 36 | this.slash = true; 37 | this.description_localizations = {}; 38 | this.options = []; 39 | this.aliases = aliases; 40 | } 41 | 42 | abstract execute(client: Client, guild: Guild | null, user: User, channel: GuildTextBasedChannel | null, data: CommandInteraction | string[]): Promise | void; 43 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | .yarn/cache 124 | .yarn/unplugged 125 | .yarn/build-state.yml 126 | .yarn/install-state.gz 127 | .pnp.* 128 | -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | import { ActivityType, BitFieldResolvable, ChannelType, Client, GatewayIntentsString, IntentsBitField, PermissionsBitField } from "discord.js"; 2 | import { BotCommands } from "./src/commands/index"; 3 | import 'dotenv/config' 4 | import { startLog } from "./src/utils/logUtils"; 5 | import { ServerListener } from "./src/events/ServerListener"; 6 | import fs from 'fs'; 7 | 8 | async function main() { 9 | const client = new Client({ 10 | intents: Object.keys(IntentsBitField.Flags) as BitFieldResolvable 11 | }); 12 | 13 | await client.login(process.env.TOKEN); 14 | 15 | const serverListener = new ServerListener(); 16 | const botCommands = new BotCommands(client); 17 | 18 | client.on("messageCreate", async (message) => { 19 | if (message && message.guildId && message.guild?.available === true) { 20 | if ( 21 | message.channel && 22 | message.channel.isTextBased() && 23 | !message.channel.isDMBased() 24 | ) { 25 | const permissions = message.channel.permissionsFor( 26 | client.user?.id || "" 27 | ); 28 | 29 | if ( 30 | !permissions || 31 | !permissions.has(PermissionsBitField.Flags.SendMessages) || 32 | !message.channel.viewable 33 | ) { 34 | return; 35 | } 36 | 37 | botCommands.onMessage( 38 | client, 39 | message.channel, 40 | message.member, 41 | message 42 | ); 43 | } 44 | } 45 | }); 46 | 47 | client.on("guildMemberRemove", (guildMember) => { 48 | if (guildMember.guild.available === true) { 49 | serverListener.guildMemberRemove(guildMember.client, guildMember); 50 | } 51 | }); 52 | 53 | client.on("guildMemberAdd", (guildMember) => { 54 | if (guildMember.guild.available === true) { 55 | serverListener.guildMemberAdd(guildMember.client, guildMember.guild, guildMember); 56 | } 57 | }); 58 | 59 | client.on("interactionCreate", (interaction) => { 60 | if (interaction && interaction.guild && interaction.guild.available === true) { 61 | botCommands.handleInteractionCommand(interaction); 62 | } 63 | }); 64 | 65 | client.on("ready", async () => { 66 | client.user?.setPresence({ activities: [{ name: `fxrst`, type: ActivityType.Listening }], status: 'idle' }); 67 | }) 68 | 69 | startLog(client.user?.tag || "", process.env.PREFIX || "!", botCommands._commands.size, botCommands._slashs.length); 70 | } 71 | 72 | process.on("uncaughtException", function (err: Error) { 73 | console.error(err); 74 | fs.appendFileSync("./errors.txt", err.stack + "\n\n", { encoding: 'utf8' }); 75 | }); 76 | 77 | main(); 78 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationCommandOption, 3 | ApplicationCommandType, 4 | Channel, 5 | Client, 6 | Collection, 7 | CommandInteraction, 8 | Guild, 9 | GuildMember, 10 | GuildTextBasedChannel, 11 | Interaction, 12 | Message, 13 | REST, 14 | Routes, 15 | Snowflake, 16 | User, 17 | } from "discord.js"; 18 | import * as glob from "glob"; 19 | import * as path from "path"; 20 | import { Command } from "./Command"; 21 | import "dotenv/config"; 22 | 23 | const rest = new REST().setToken(process.env.TOKEN || ""); 24 | 25 | type SlashCommand = { 26 | id?: Snowflake; 27 | type?: ApplicationCommandType; 28 | application_id: Snowflake; 29 | guild_id?: Snowflake; 30 | name: string; 31 | name_localizations?: any; 32 | description: string; 33 | description_localizations?: any; 34 | options: ApplicationCommandOption[]; 35 | default_member_permissions?: string; 36 | dm_permission?: boolean; 37 | default_permission?: boolean; 38 | nsfw?: boolean; 39 | version?: Snowflake; 40 | }; 41 | 42 | export class BotCommands { 43 | private readonly _client: Client; 44 | _commands: Collection; 45 | _slashs: SlashCommand[]; 46 | 47 | constructor(client: Client) { 48 | this._commands = new Collection(); 49 | this._client = client; 50 | this._slashs = []; 51 | this.loadCommands(); 52 | } 53 | 54 | async handleInteractionCommand(interaction: Interaction) { 55 | if (!interaction.isCommand()) { 56 | return; 57 | } 58 | 59 | if (interaction.guildId) { 60 | if ( 61 | interaction.channel && 62 | interaction.channel.isTextBased() && 63 | !interaction.channel.isDMBased() 64 | ) { 65 | this.handleCommand( 66 | interaction.client, 67 | interaction.guild, 68 | interaction.user, 69 | interaction.channel, 70 | interaction.commandName, 71 | interaction 72 | ); 73 | } 74 | } 75 | } 76 | 77 | loadCommands() { 78 | const commandFiles = glob.sync("**/*.ts", { 79 | cwd: path.join(__dirname, ""), 80 | }); 81 | 82 | for (const file of commandFiles) { 83 | if (file == "Command.ts" || file == "index.ts") continue; 84 | const { Cmd } = require(path.join(__dirname, file)); 85 | if (typeof Cmd == "function") { 86 | const command: Command = new Cmd(); 87 | if (command.enabled === true) { 88 | this._commands.set(command.name, command); 89 | if (command.slash) { 90 | this._slashs.push({ 91 | name: command.name, 92 | name_localizations: command.name_localizations, 93 | description_localizations: command.description_localizations, 94 | application_id: this._client.user?.id || "", 95 | guild_id: process.env.MAIN_GUILD_ID || "", 96 | description: command.description, 97 | options: command.options, 98 | }); 99 | } 100 | } 101 | } 102 | } 103 | 104 | rest.put( 105 | Routes.applicationGuildCommands( 106 | this._client.user?.id || "", 107 | process.env.MAIN_GUILD_ID || "" 108 | ), 109 | { body: this._slashs } 110 | ); 111 | } 112 | 113 | handleCommand( 114 | client: Client, 115 | guild: Guild | null, 116 | user: User, 117 | channel: GuildTextBasedChannel | null, 118 | commandName: string, 119 | data: string[] | CommandInteraction 120 | ) { 121 | let command = this._commands.get(commandName); 122 | 123 | if (!command) { 124 | this._commands.forEach((cmd) => { 125 | if (cmd.aliases && cmd.aliases.includes(commandName)) { 126 | command = cmd; 127 | } 128 | }); 129 | } 130 | 131 | if (!command) { 132 | return; 133 | } 134 | 135 | if (channel && !channel.isDMBased() && channel.isTextBased()) { 136 | const permissionsBot = channel.permissionsFor(client.user?.id || ""); 137 | if (!permissionsBot || !channel.viewable) { 138 | return; 139 | } 140 | if ( 141 | Array.isArray(command.bot_permissions) && 142 | command.bot_permissions.length > 0 143 | ) { 144 | for (const permission of command.bot_permissions) { 145 | if (!permissionsBot.has(permission)) { 146 | // Missing "permission" to execute command (BOT) 147 | return; 148 | } 149 | } 150 | } 151 | 152 | // Permissions for who Executed 153 | const permissionsUser = channel.permissionsFor(user.id); 154 | if (!permissionsUser) { 155 | return; 156 | } 157 | if ( 158 | Array.isArray(command.user_permissions) && 159 | command.user_permissions.length > 0 160 | ) { 161 | for (const permission of command.user_permissions) { 162 | if (!permissionsBot.has(permission)) { 163 | // Missing "permission" to execute command (USER) 164 | return; 165 | } 166 | } 167 | } 168 | } 169 | 170 | command.execute(client, guild, user, channel, data); 171 | } 172 | 173 | async onMessage( 174 | client: Client, 175 | channel: Channel, 176 | member: GuildMember | null, 177 | message: Message 178 | ): Promise { 179 | const content = message.content; 180 | const prefix = process.env.PREFIX || "!"; 181 | if (!content.toLowerCase().startsWith(prefix.toLowerCase())) { 182 | return; 183 | } 184 | 185 | const args = message.content 186 | .slice(prefix.length) 187 | .trim() 188 | .split(/ +/g); 189 | const commandName = args.shift()?.toLowerCase(); 190 | 191 | if (commandName) { 192 | if (channel.isTextBased() && !channel.isDMBased()) { 193 | message.delete().catch(() => { }); 194 | this.handleCommand( 195 | client, 196 | message.guild, 197 | message.author, 198 | channel, 199 | commandName, 200 | args 201 | ); 202 | } 203 | } 204 | } 205 | } --------------------------------------------------------------------------------