Přejít na obsah
Aktuální kupón:sleva 10 %Uplatnit v objednávce

První bot v discord.js: slash příkaz krok za krokem

Napíšete bota v discord.js 14 se slash příkazy /ping a /pozdrav, zaregistrujete příkazy přes REST, naučíte se odpovídat do 3 sekund a bota spustíte u StormHostu.

  • pokročilý
  • ~30 min
  • Aktualizováno 1. 10. 2026

Napíšeme bota v knihovně discord.js se dvěma slash příkazy: /ping a /pozdrav s parametrem. Každý řádek vysvětlíme a nakonec bota spustíme u StormHostu.

Co budete potřebovat

  • Node.js ve verzi LTS v počítači (stažení na nodejs.org (otevře se v novém okně)) a editor kódu, například Visual Studio Code
  • Aplikaci v Discord Developer Portalu, token a bota pozvaného na váš testovací server se scope applications.commands (návod)
  • Discord bota u StormHostu pro trvalý provoz
  • Asi 30 minut

Jak to funguje#

Bot má se slash příkazy tři části:

  1. Definice příkazů – název, popis a parametry příkazu. Discordu je oznámíte jednou samostatným skriptem přes REST API (registrace).
  2. Běžící bot – připojí se k Discordu (gateway) a čeká na události.
  3. Obsluha interakce – když někdo příkaz použije, Discord pošle botovi interakci a bot na ni musí do 3 sekund odpovědět. Jinak uživatel uvidí, že aplikace neodpověděla.

Registrace a běh bota jsou oddělené: příkazy stačí zaregistrovat znovu jen tehdy, když změníte jejich definici (název, popis, parametry). Změny v kódu odpovědi registraci nepotřebují.

Založení projektu#

  1. Vytvořte složku muj-bot a otevřete v ní terminál (ve VS Code Ctrl+`).

  2. Založte projekt a nainstalujte knihovny:

    bash
    npm init -y
    npm install discord.js dotenv
    • npm init -y – vytvoří package.json s výchozími hodnotami (-y = na všechny otázky odpoví „ano“).
    • npm install discord.js dotenv – nainstaluje discord.js a dotenv (načítání souboru .env) a zapíše je do dependencies.
  3. V package.json nastavte "main": "index.js" a "type": "commonjs". Ukázky používají require(). Význam polí vysvětluje návod nahrání a spuštění bota.

  4. Vytvořte soubor .env:

    .envini
    DISCORD_TOKEN=<TOKEN_BOTA>
    CLIENT_ID=<APPLICATION_ID>
    GUILD_ID=<ID_SERVERU>

    Za <TOKEN_BOTA> dosaďte token bota, za <APPLICATION_ID> Application ID ze stránky General Information v Developer Portalu a za <ID_SERVERU> ID vašeho testovacího Discord serveru. Kde je najít, popisuje návod token a intenty.

  5. Vytvořte .gitignore se dvěma řádky node_modules a .env.

Výsledná struktura:

text
muj-bot/
├── .env
├── .gitignore
├── package.json
├── package-lock.json
├── index.js
├── deploy-commands.js
└── commands/
    └── utility/
        ├── ping.js
        └── pozdrav.js

Příkazy#

Každý příkaz má vlastní soubor se dvěma částmi: data (definice pro Discord) a execute (co se stane po použití).

commands/utility/ping.jsJavaScript
const { SlashCommandBuilder, MessageFlags } = require('discord.js');

module.exports = {
  data: new SlashCommandBuilder()
    .setName('ping')
    .setDescription('Odpoví Pong! a ukáže odezvu bota.'),

  async execute(interaction) {
    await interaction.reply({
      content: `Pong! Odezva: ${interaction.client.ws.ping} ms`,
      flags: MessageFlags.Ephemeral,
    });
  },
};
ŘádekCo dělá
require('discord.js')Načte z knihovny třídu SlashCommandBuilder (stavba definice příkazu) a MessageFlags (příznaky zpráv).
.setName('ping')Název příkazu, který uživatel píše za lomítko. 1–⁠32 znaků, jen malá písmena, číslice, - a _, bez mezer.
.setDescription('…')Popis v nabídce příkazů. Povinný, 1–⁠100 znaků.
async execute(interaction)Funkce, kterou bot zavolá při použití příkazu. interaction nese vše o použití (kdo, kde, parametry).
interaction.reply({ … })Odpověď na příkaz. Musí přijít do 3 sekund.
interaction.client.ws.pingOdezva spojení bota s Discordem v milisekundách. Hned po startu může být -1, než se změří.
flags: MessageFlags.EphemeralOdpověď uvidí jen ten, kdo příkaz použil. Ostatní v kanálu ji nevidí.

Příkaz s parametrem:

commands/utility/pozdrav.jsJavaScript
const { SlashCommandBuilder } = require('discord.js');

module.exports = {
  data: new SlashCommandBuilder()
    .setName('pozdrav')
    .setDescription('Pozdraví zadaného člověka.')
    .addStringOption((option) =>
      option.setName('jmeno').setDescription('Koho pozdravit').setRequired(true),
    ),

  async execute(interaction) {
    const jmeno = interaction.options.getString('jmeno');
    await interaction.reply(`Ahoj, ${jmeno}! Zdraví tě ${interaction.user.username}.`);
  },
};
  • .addStringOption(…) – přidá textový parametr. Discord ho uživateli nabídne jako pole jmeno:.
  • .setRequired(true) – parametr je povinný, bez něj příkaz nejde odeslat.
  • interaction.options.getString('jmeno') – přečte hodnotu parametru.
  • interaction.user.username – uživatelské jméno toho, kdo příkaz použil.
  • interaction.reply('…') – veřejná odpověď do kanálu.

Další typy parametrů: addIntegerOption (celé číslo), addBooleanOption (ano/ne), addUserOption (člen serveru), addChannelOption (kanál), addRoleOption (role).

Registrace příkazů#

Skript deploy-commands.js načte definice všech příkazů a pošle je Discordu. Spouštíte ho ručně, jen když přidáte příkaz nebo změníte jeho definici.

deploy-commands.jsJavaScript
const fs = require('node:fs');
const path = require('node:path');
require('dotenv').config({ path: path.join(__dirname, '.env'), quiet: true });
const { REST, Routes } = require('discord.js');

const commands = [];
const foldersPath = path.join(__dirname, 'commands');
for (const folder of fs.readdirSync(foldersPath)) {
  const commandsPath = path.join(foldersPath, folder);
  for (const file of fs.readdirSync(commandsPath).filter((f) => f.endsWith('.js'))) {
    const command = require(path.join(commandsPath, file));
    if ('data' in command && 'execute' in command) {
      commands.push(command.data.toJSON());
    } else {
      console.log(`[WARNING] Soubor ${file} nemá "data" nebo "execute".`);
    }
  }
}

const rest = new REST().setToken(process.env.DISCORD_TOKEN);

(async () => {
  try {
    console.log(`Registruji příkazy: ${commands.length}`);
    const data = await rest.put(
      Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
      { body: commands },
    );
    console.log(`Hotovo, zaregistrováno příkazů: ${data.length}`);
  } catch (error) {
    console.error(error);
  }
})();
ČástCo dělá
fs.readdirSync(foldersPath)Projde podsložky commands (například utility) a v nich soubory .js.
command.data.toJSON()Převede definici příkazu do tvaru, který čeká Discord API.
new REST().setToken(…)Klient REST API Discordu přihlášený tokenem bota. Nepřipojuje se jako bot, jen pošle požadavek.
Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID)Adresa pro příkazy jednoho serveru (guild).
rest.put(…, { body: commands })Nahradí všechny příkazy na serveru tímto seznamem. Příkaz, který v seznamu chybí, z Discordu zmizí.

Spusťte ho v počítači ve složce projektu:

bash
node deploy-commands.js
Výstuptext
Registruji příkazy: 2
Hotovo, zaregistrováno příkazů: 2

Serverové, nebo globální příkazy?#

DruhRouteKde fungujíKdy použít
Serverové (guild)Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID)Jen na serveru GUILD_ID.Vývoj a testování, bot pro jeden server.
GlobálníRoutes.applicationCommands(CLIENT_ID)Na všech serverech, kde je bot pozvaný se scope applications.commands, a ve výchozím stavu i v soukromých zprávách.Hotový bot pro víc serverů.

Když máte stejný příkaz zaregistrovaný serverově i globálně, uvidíte ho na testovacím serveru dvakrát. Před přechodem na globální registraci serverové příkazy smažte: spusťte serverovou registraci s prázdným seznamem (body: []).

Discord omezuje vytváření příkazů na 200 za den na jeden server. Registrační skript proto nespouštějte při každém startu bota.

Hlavní soubor bota#

index.jsJavaScript
const fs = require('node:fs');
const path = require('node:path');
require('dotenv').config({ path: path.join(__dirname, '.env'), quiet: true });
const { Client, Collection, Events, GatewayIntentBits, MessageFlags } = require('discord.js');

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

// Načtení příkazů ze složky commands
client.commands = new Collection();
const foldersPath = path.join(__dirname, 'commands');
for (const folder of fs.readdirSync(foldersPath)) {
  const commandsPath = path.join(foldersPath, folder);
  for (const file of fs.readdirSync(commandsPath).filter((f) => f.endsWith('.js'))) {
    const command = require(path.join(commandsPath, file));
    if ('data' in command && 'execute' in command) {
      client.commands.set(command.data.name, command);
    }
  }
}

client.once(Events.ClientReady, (readyClient) => {
  console.log(`Ready! Logged in as ${readyClient.user.tag}`);
});

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand()) return;

  const command = interaction.client.commands.get(interaction.commandName);
  if (!command) {
    console.error(`Příkaz ${interaction.commandName} neexistuje.`);
    return;
  }

  try {
    await command.execute(interaction);
  } catch (error) {
    console.error(error);
    const odpoved = { content: 'Při provádění příkazu nastala chyba.', flags: MessageFlags.Ephemeral };
    if (interaction.replied || interaction.deferred) {
      await interaction.followUp(odpoved);
    } else {
      await interaction.reply(odpoved);
    }
  }
});

client.login(process.env.DISCORD_TOKEN);
ČástCo dělá
new Client({ intents: [GatewayIntentBits.Guilds] })Vytvoří bota. Intent Guilds stačí pro slash příkazy a není privilegovaný. Čtení běžných zpráv by vyžadovalo GuildMessages a MessageContent.
client.commands = new Collection()Seznam příkazů podle názvu. Collection je rozšířená Map z discord.js.
client.commands.set(command.data.name, command)Uloží příkaz pod jeho název, například ping.
client.once(Events.ClientReady, …)Spustí se jednou po úspěšném přihlášení. readyClient.user.tag je jméno bota.
client.on(Events.InteractionCreate, …)Spustí se při každé interakci (příkaz, tlačítko, formulář).
interaction.isChatInputCommand()Propustí jen slash příkazy, ostatní interakce ignoruje.
client.commands.get(interaction.commandName)Najde soubor příkazu podle názvu, který uživatel použil.
try { … } catch (error) { … }Chyba v příkazu nesmí shodit bota. Uživatel dostane chybovou odpověď a chyba se vypíše do konzole.
interaction.replied / interaction.deferredKdyž už bot odpověděl nebo odpověď odložil, další zprávu musí poslat přes followUp.
client.login(process.env.DISCORD_TOKEN)Přihlásí bota tokenem z .env.

Bota vyzkoušejte v počítači:

bash
node index.js
Výstuptext
Ready! Logged in as MujBot#1234

Na Discordu napište /ping a vyberte příkaz svého bota.

Odpověď na déle trvající příkaz#

Když příkaz potřebuje víc než 3 sekundy (dotaz do databáze, volání jiného API), nejdřív odpověď odložte a výsledek pošlete později:

JavaScript
async execute(interaction) {
  await interaction.deferReply();
  const vysledek = await nejakaDlouhaPrace();
  await interaction.editReply(`Hotovo: ${vysledek}`);
}
  • interaction.deferReply() – okamžitě potvrdí příjem. Uživatel vidí „bot přemýšlí“. Pro odpověď viditelnou jen jemu použijte deferReply({ flags: MessageFlags.Ephemeral }).
  • interaction.editReply(…) – nahradí „přemýšlí“ skutečnou odpovědí. Odloženou odpověď musíte poslat do 15 minut.
  • nejakaDlouhaPrace() – vaše funkce, ukázkový název.

Odpověď s embedem#

Embed je zpráva s barevným proužkem, nadpisem a poli. Hodí se pro přehledné informace. Příkaz /server ukáže údaje o serveru:

commands/utility/server.jsJavaScript
const { SlashCommandBuilder, EmbedBuilder, InteractionContextType } = require('discord.js');

module.exports = {
  data: new SlashCommandBuilder()
    .setName('server')
    .setDescription('Informace o tomto Discord serveru.')
    .setContexts(InteractionContextType.Guild),

  async execute(interaction) {
    const guild = interaction.guild;
    const embed = new EmbedBuilder()
      .setColor(0xe11d48)
      .setTitle(guild.name)
      .addFields(
        { name: 'Členů', value: String(guild.memberCount), inline: true },
        { name: 'Založen', value: guild.createdAt.toLocaleDateString('cs-CZ'), inline: true },
      )
      .setTimestamp();
    await interaction.reply({ embeds: [embed] });
  },
};
ČástCo dělá
.setContexts(InteractionContextType.Guild)Příkaz půjde použít jen na serveru, ne v soukromých zprávách (tam by interaction.guild neexistoval).
new EmbedBuilder()Začne stavět embed.
.setColor(0xe11d48)Barva proužku jako šestnáctkové číslo (tady červená).
.setTitle(guild.name)Nadpis, tady název serveru.
.addFields({ name, value, inline })Pole s popiskem a hodnotou. value musí být text, proto String(…). inline: true dá pole vedle sebe.
guild.memberCountPočet členů serveru. Funguje s intentem Guilds, bez privilegovaného intentu.
.setTimestamp()Do patičky přidá aktuální čas.
interaction.reply({ embeds: [embed] })Pošle embed. Do jedné zprávy jde vložit víc embedů v poli.

Po přidání souboru spusťte znovu node deploy-commands.js, nový příkaz se musí zaregistrovat.

Kdo smí příkaz použít#

Výchozí oprávnění nastavíte přímo v definici příkazu:

commands/moderace/smazat.jsJavaScript
const { SlashCommandBuilder, PermissionFlagsBits, InteractionContextType, MessageFlags } = require('discord.js');

module.exports = {
  data: new SlashCommandBuilder()
    .setName('smazat')
    .setDescription('Smaže poslední zprávy v kanálu.')
    .addIntegerOption((o) =>
      o.setName('pocet').setDescription('Kolik zpráv (1–100)').setMinValue(1).setMaxValue(100).setRequired(true),
    )
    .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
    .setContexts(InteractionContextType.Guild),

  async execute(interaction) {
    const pocet = interaction.options.getInteger('pocet');
    const smazane = await interaction.channel.bulkDelete(pocet, true);
    await interaction.reply({ content: `Smazáno zpráv: ${smazane.size}`, flags: MessageFlags.Ephemeral });
  },
};
  • .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) – příkaz ve výchozím stavu uvidí jen členové s oprávněním Spravovat zprávy. Admin serveru to může v nastavení serveru (Integrace) změnit.
  • .setMinValue(1) a .setMaxValue(100) – Discord nedovolí zadat číslo mimo rozsah.
  • interaction.channel.bulkDelete(pocet, true) – hromadně smaže zprávy. true přeskočí zprávy starší než 14 dní, které hromadně smazat nejde.
  • Bot sám potřebuje oprávnění Manage Messages v kanálu, jinak skončí DiscordAPIError[50013]: Missing Permissions.

Členy s oprávněním Administrator žádné nastavení příkazu nezastaví. Příkazy jen pro vás (například eval) proto registrujte jen na soukromém testovacím serveru.

Událost: uvítání nového člena#

Kromě příkazů reaguje bot na události. Uvítání nového člena potřebuje privilegovaný intent Server Members Intent: zapněte ho v Developer Portalu na stránce Bot a doplňte ho do kódu.

events/guildMemberAdd.jsJavaScript
const { Events } = require('discord.js');

module.exports = {
  name: Events.GuildMemberAdd,
  once: false,
  async execute(member) {
    const kanal = member.guild.systemChannel;
    if (!kanal) return;
    await kanal.send(`Vítej na serveru, ${member}!`);
  },
};

Do index.js upravte intenty a přidejte načítání událostí (za načítání příkazů):

index.jsJavaScript
const client = new Client({
  intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
});

// … načítání příkazů …

const eventsPath = path.join(__dirname, 'events');
for (const file of fs.readdirSync(eventsPath).filter((f) => f.endsWith('.js'))) {
  const event = require(path.join(eventsPath, file));
  if (event.once) {
    client.once(event.name, (...args) => event.execute(...args));
  } else {
    client.on(event.name, (...args) => event.execute(...args));
  }
}
  • GatewayIntentBits.GuildMembers – Discord pak posílá události příchodu a odchodu členů. Bez zapnutého intentu v Developer Portalu bot hlásí Used disallowed intents.
  • member.guild.systemChannel – systémový kanál serveru (kam Discord píše uvítání). Když ho server nemá, bot nic nepošle.
  • ${member} – vloží zmínku nového člena (@jméno).
  • client.once / client.on – událost s once: true se zpracuje jen jednou (start bota), ostatní pokaždé.

Struktura většího bota#

  • Příkazy po kategoriích ve složkách commands/moderace, commands/zabava … Načítání výše je projde všechny.
  • Události ve složce events, každá ve vlastním souboru s name, once a execute (viz výše). Do events můžete přesunout i obsluhu ClientReady a InteractionCreate.
  • Konfigurace (ID kanálů, rolí) do config.json, tajné údaje do .env.
  • Data (body, varování) do databáze, ne do proměnných v paměti, které se restartem ztratí.

Spuštění u StormHostu#

  1. Registraci příkazů (node deploy-commands.js) spusťte v počítači. Na serveru to není potřeba.
  2. Bota v počítači vypněte (Ctrl+C). Dvě spuštěné kopie se stejným tokenem si navzájem kradou interakce.
  3. Nahrajte index.js, package.json, package-lock.json a složku commands se všemi podsložkami do záložky Soubory. Pokud jste přidali uvítání nového člena, nahrajte i složku events. Bez ní bot při startu spadne s chybou ENOENT: no such file or directory. .env vytvořte přímo na serveru. Postup: nahrání a spuštění bota.
  4. V záložce Nastavení zkontrolujte hlavní soubor index.js.
  5. V záložce Konzole klikněte na Spustit.

Ověření#

  • V Konzoli je Ready! Logged in as … a bot je na Discordu online.
  • Po napsání / na testovacím serveru nabídka obsahuje ping a pozdrav s ikonou vašeho bota.
  • /ping odpoví zprávou viditelnou jen vám, /pozdrav jmeno:Jan veřejnou zprávou.

Když něco nefunguje#

Příkazy se v nabídce nezobrazují#

  • Neproběhla registrace: spusťte node deploy-commands.js a zkontrolujte výpis Hotovo, zaregistrováno příkazů: ….
  • GUILD_ID je ID jiného serveru, než na kterém zkoušíte.
  • Bot byl pozvaný bez scope applications.commands. Pozvěte ho znovu odkazem s bot i applications.commands.
  • Discord si nabídku pamatuje. Restartujte aplikaci Discord (Ctrl+R).

Registrace skončí DiscordAPIError[50001]: Missing Access#

Bot není na serveru GUILD_ID, nebo na něm nemá scope applications.commands. Zkontrolujte ID serveru a bota pozvěte znovu.

Discord píše „The application did not respond“ (aplikace neodpověděla)#

Bot neodpověděl do 3 sekund. Buď neběží (zkontrolujte Konzoli), nebo příkaz trvá dlouho a chybí deferReply(), nebo v příkazu nastala chyba před odpovědí (najdete ji v Konzoli).

DiscordAPIError[10062]: Unknown interaction#

Odpověď přišla pozdě (po 3 sekundách) nebo běží dvě kopie bota se stejným tokenem a interakci už zpracovala ta druhá. Použijte deferReply() a nechte běžet jen jednu kopii.

DiscordAPIError[40060]: Interaction has already been acknowledged#

Na jednu interakci jste zavolali reply() dvakrát. Druhou zprávu pošlete přes followUp(), nebo po deferReply() použijte editReply().

[WARNING] Soubor … nemá "data" nebo "execute".#

Soubor ve složce commands neexportuje obě části. Zkontrolujte module.exports = { data, execute } a překlepy.

require is not defined in ES module scope nebo Cannot use import statement outside a module#

Míchání CommonJS a ES modulů. Ukázky jsou pro CommonJS: v package.json musí být "type": "commonjs" (nebo pole type chybí). Podrobnosti v návodu logy a chyby.

Shrnutí

  • Slash příkaz = definice (SlashCommandBuilder), registrace (deploy-commands.js přes REST) a obsluha (Events.InteractionCreate).
  • Registraci spouštějte jen při změně definice. Při vývoji serverově (applicationGuildCommands), hotového bota globálně (applicationCommands).
  • Na interakci odpovězte do 3 sekund. Delší práci zahajte deferReply() a dokončete editReply().
  • Chyby v příkazech chytejte přes try/catch, ať jedna chyba neshodí bota. Nikdy nespouštějte dvě kopie se stejným tokenem.

Hledání v návodech