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:
- Definice příkazů – název, popis a parametry příkazu. Discordu je oznámíte jednou samostatným skriptem přes REST API (registrace).
- Běžící bot – připojí se k Discordu (gateway) a čeká na události.
- 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#
Vytvořte složku
muj-bota otevřete v ní terminál (ve VS Code Ctrl+`).Založte projekt a nainstalujte knihovny:
bash npm init -y npm install discord.js dotenvnpm init -y– vytvořípackage.jsons 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 dodependencies.
V
package.jsonnastavte"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.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.Vytvořte
.gitignorese dvěma řádkynode_modulesa.env.
Výsledná struktura:
muj-bot/
├── .env
├── .gitignore
├── package.json
├── package-lock.json
├── index.js
├── deploy-commands.js
└── commands/
└── utility/
├── ping.js
└── pozdrav.jsPříkazy#
Každý příkaz má vlastní soubor se dvěma částmi: data (definice pro Discord) a execute (co se stane po použití).
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,
});
},
};| Řádek | Co 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. | Odezva spojení bota s Discordem v milisekundách. Hned po startu může být -1, než se změří. |
flags: MessageFlags.Ephemeral | Odpověď uvidí jen ten, kdo příkaz použil. Ostatní v kanálu ji nevidí. |
Příkaz s parametrem:
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 polejmeno:..setRequired(true)– parametr je povinný, bez něj příkaz nejde odeslat.interaction.– přečte hodnotu parametru.options. getString( 'jmeno') interaction.– uživatelské jméno toho, kdo příkaz použil.user. username 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.
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);
}
})();| Část | Co dělá |
|---|---|
fs. | 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. | 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:
node deploy-commands.jsRegistruji příkazy: 2
Hotovo, zaregistrováno příkazů: 2Serverové, nebo globální příkazy?#
| Druh | Route | Kde fungují | Kdy použít |
|---|---|---|---|
| Serverové (guild) | Routes. | Jen na serveru GUILD_ID. | Vývoj a testování, bot pro jeden server. |
| Globální | Routes. | 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#
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);| Část | Co dělá |
|---|---|
new Client({ intents: [GatewayIntentBits. | 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. | Uloží příkaz pod jeho název, například ping. |
client. | Spustí se jednou po úspěšném přihlášení. readyClient.user.tag je jméno bota. |
client. | Spustí se při každé interakci (příkaz, tlačítko, formulář). |
interaction. | Propustí jen slash příkazy, ostatní interakce ignoruje. |
client. | 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.deferred | Když už bot odpověděl nebo odpověď odložil, další zprávu musí poslat přes followUp. |
client. | Přihlásí bota tokenem z .env. |
Bota vyzkoušejte v počítači:
node index.jsReady! Logged in as MujBot#1234Na 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:
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žijtedeferReply({ 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:
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] });
},
};| Část | Co dělá |
|---|---|
.setContexts( | 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.memberCount | Poč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:
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(– 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.PermissionFlagsBits. ManageMessages) .setMinValue(1)a.setMaxValue(100)– Discord nedovolí zadat číslo mimo rozsah.interaction.– hromadně smaže zprávy.channel. bulkDelete( pocet, true) truepř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.
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ů):
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.– Discord pak posílá události příchodu a odchodu členů. Bez zapnutého intentu v Developer Portalu bot hlásíGuildMembers Used disallowed intents.member.– systémový kanál serveru (kam Discord píše uvítání). Když ho server nemá, bot nic nepošle.guild. systemChannel ${member}– vloží zmínku nového člena (@jméno).client.once/client.on– událost sonce: truese 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 sname,onceaexecute(viz výše). Doeventsmůžete přesunout i obsluhuClientReadyaInteractionCreate. - 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#
- Registraci příkazů (
node deploy-commands.js) spusťte v počítači. Na serveru to není potřeba. - Bota v počítači vypněte (Ctrl+C). Dvě spuštěné kopie se stejným tokenem si navzájem kradou interakce.
- Nahrajte
index.js,package.json,package-lock.jsona složkucommandsse všemi podsložkami do záložky Soubory. Pokud jste přidali uvítání nového člena, nahrajte i složkuevents. Bez ní bot při startu spadne s chybouENOENT: no such file or directory..envvytvořte přímo na serveru. Postup: nahrání a spuštění bota. - V záložce Nastavení zkontrolujte hlavní soubor
index.js. - 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 obsahujepingapozdravs ikonou vašeho bota. /pingodpoví zprávou viditelnou jen vám,/pozdrav jmeno:Janveřejnou zprávou.
Když něco nefunguje#
Příkazy se v nabídce nezobrazují#
- Neproběhla registrace: spusťte
node deploy-commands.jsa zkontrolujte výpisHotovo, zaregistrováno příkazů: …. GUILD_IDje ID jiného serveru, než na kterém zkoušíte.- Bot byl pozvaný bez scope
applications.commands. Pozvěte ho znovu odkazem sbotiapplications.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.jspř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četeeditReply(). - Chyby v příkazech chytejte přes
try/catch, ať jedna chyba neshodí bota. Nikdy nespouštějte dvě kopie se stejným tokenem.