Napíšeme bota v knižnici discord.js s dvoma slash príkazmi: /ping a /pozdrav s parametrom. Každý riadok vysvetlíme a nakoniec bota spustíme u StormHostu.
Čo budete potrebovať
- Node.js vo verzii LTS v počítači (stiahnutie na nodejs.org (otvorí sa v novom okne)) a editor kódu, napríklad Visual Studio Code
- Aplikáciu v Discord Developer Portali, token a bota pozvaného na váš testovací server so scope
applications.commands(návod) - Discord bota u StormHostu na trvalú prevádzku
- Približne 30 minút
Ako to funguje#
Bot má so slash príkazmi tri časti:
- Definícia príkazov – názov, opis a parametre príkazu. Discordu ich oznámite raz samostatným skriptom cez REST API (registrácia).
- Bežiaci bot – pripojí sa k Discordu (gateway) a čaká na udalosti.
- Obsluha interakcie – keď niekto príkaz použije, Discord pošle botovi interakciu a bot na ňu musí do 3 sekúnd odpovedať. Inak používateľ uvidí, že aplikácia neodpovedala.
Registrácia a beh bota sú oddelené: príkazy stačí zaregistrovať znova iba vtedy, keď zmeníte ich definíciu (názov, opis, parametre). Zmeny v kóde odpovede registráciu nepotrebujú.
Založenie projektu#
Vytvorte priečinok
muj-bota otvorte v ňom terminál (vo VS Code Ctrl+`).Založte projekt a nainštalujte knižnice:
bash npm init -y npm install discord.js dotenvnpm init -y– vytvorípackage.jsons predvolenými hodnotami (-y= na všetky otázky odpovie „áno“).npm install discord.js dotenv– nainštaluje discord.js a dotenv (načítanie súboru.env) a zapíše ich dodependencies.
V
package.jsonnastavte"main": "index.js"a"type": "commonjs". Ukážky používajúrequire(). Význam polí vysvetľuje návod nahranie a spustenie bota.Vytvorte súbor
.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 zo stránky General Information v Developer Portali a za<ID_SERVERU>ID vášho testovacieho Discord servera. Kde ich nájsť, opisuje návod token a intenty.Vytvorte
.gitignores dvoma riadkaminode_modulesa.env.
Výsledná štruktúra:
muj-bot/
├── .env
├── .gitignore
├── package.json
├── package-lock.json
├── index.js
├── deploy-commands.js
└── commands/
└── utility/
├── ping.js
└── pozdrav.jsPríkazy#
Každý príkaz má vlastný súbor s dvoma časťami: data (definícia pre Discord) a execute (čo sa stane po použití).
const { SlashCommandBuilder, MessageFlags } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Odpovie Pong! a ukáže odozvu bota.'),
async execute(interaction) {
await interaction.reply({
content: `Pong! Odozva: ${interaction.client.ws.ping} ms`,
flags: MessageFlags.Ephemeral,
});
},
};| Riadok | Čo robí |
|---|---|
require('discord.js') | Načíta z knižnice triedu SlashCommandBuilder (stavba definície príkazu) a MessageFlags (príznaky správ). |
.setName('ping') | Názov príkazu, ktorý používateľ píše za lomku. 1–32 znakov, iba malé písmená, číslice, - a _, bez medzier. |
.setDescription('…') | Opis v ponuke príkazov. Povinný, 1–100 znakov. |
async execute(interaction) | Funkcia, ktorú bot zavolá pri použití príkazu. interaction nesie všetko o použití (kto, kde, parametre). |
interaction.reply({ … }) | Odpoveď na príkaz. Musí prísť do 3 sekúnd. |
interaction. | Odozva spojenia bota s Discordom v milisekundách. Hneď po štarte môže byť -1, kým sa nezmeria. |
flags: MessageFlags.Ephemeral | Odpoveď uvidí iba ten, kto príkaz použil. Ostatní v kanáli ju nevidia. |
Príkaz s parametrom:
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('pozdrav')
.setDescription('Pozdraví zadaného človeka.')
.addStringOption((option) =>
option.setName('jmeno').setDescription('Koho pozdraviť').setRequired(true),
),
async execute(interaction) {
const jmeno = interaction.options.getString('jmeno');
await interaction.reply(`Ahoj, ${jmeno}! Zdraví ťa ${interaction.user.username}.`);
},
};.addStringOption(…)– pridá textový parameter. Discord ho používateľovi ponúkne ako polejmeno:..setRequired(true)– parameter je povinný, bez neho príkaz nejde odoslať.interaction.– prečíta hodnotu parametra.options. getString( 'jmeno') interaction.– používateľské meno toho, kto príkaz použil.user. username interaction.reply('…')– verejná odpoveď do kanála.
Ďalšie typy parametrov: addIntegerOption (celé číslo), addBooleanOption (áno/nie), addUserOption (člen servera), addChannelOption (kanál), addRoleOption (rola).
Registrácia príkazov#
Skript deploy-commands.js načíta definície všetkých príkazov a pošle ich Discordu. Spúšťate ho ručne, iba keď pridáte príkaz alebo zmeníte jeho definíciu.
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] Súbor ${file} nemá "data" alebo "execute".`);
}
}
}
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
(async () => {
try {
console.log(`Registrujem príkazy: ${commands.length}`);
const data = await rest.put(
Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
{ body: commands },
);
console.log(`Hotovo, zaregistrovaných príkazov: ${data.length}`);
} catch (error) {
console.error(error);
}
})();| Časť | Čo robí |
|---|---|
fs. | Prejde podpriečinky commands (napríklad utility) a v nich súbory .js. |
command.data.toJSON() | Prevedie definíciu príkazu do tvaru, ktorý očakáva Discord API. |
new REST().setToken(…) | Klient REST API Discordu prihlásený tokenom bota. Nepripája sa ako bot, iba pošle požiadavku. |
Routes. | Adresa pre príkazy jedného servera (guild). |
rest.put(…, { body: commands }) | Nahradí všetky príkazy na serveri týmto zoznamom. Príkaz, ktorý v zozname chýba, z Discordu zmizne. |
Spustite ho v počítači v priečinku projektu:
node deploy-commands.jsRegistrujem príkazy: 2
Hotovo, zaregistrovaných príkazov: 2Serverové, alebo globálne príkazy?#
| Druh | Route | Kde fungujú | Kedy použiť |
|---|---|---|---|
| Serverové (guild) | Routes. | Iba na serveri GUILD_ID. | Vývoj a testovanie, bot pre jeden server. |
| Globálne | Routes. | Na všetkých serveroch, kde je bot pozvaný so scope applications.commands, a v predvolenom stave aj v súkromných správach. | Hotový bot pre viac serverov. |
Keď máte rovnaký príkaz zaregistrovaný serverovo aj globálne, uvidíte ho na testovacom serveri dvakrát. Pred prechodom na globálnu registráciu serverové príkazy zmažte: spustite serverovú registráciu s prázdnym zoznamom (body: []).
Discord obmedzuje vytváranie príkazov na 200 za deň na jeden server. Registračný skript preto nespúšťajte pri každom štarte bota.
Hlavný súbor 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čítanie príkazov z priečinka 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(`Príkaz ${interaction.commandName} neexistuje.`);
return;
}
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
const odpoved = { content: 'Pri vykonávaní prí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);| Časť | Čo robí |
|---|---|
new Client({ intents: [GatewayIntentBits. | Vytvorí bota. Intent Guilds stačí pre slash príkazy a nie je privilegovaný. Čítanie bežných správ by vyžadovalo GuildMessages a MessageContent. |
client.commands = new Collection() | Zoznam príkazov podľa názvu. Collection je rozšírená Map z discord.js. |
client. | Uloží príkaz pod jeho názov, napríklad ping. |
client. | Spustí sa raz po úspešnom prihlásení. readyClient.user.tag je meno bota. |
client. | Spustí sa pri každej interakcii (príkaz, tlačidlo, formulár). |
interaction. | Prepustí iba slash príkazy, ostatné interakcie ignoruje. |
client. | Nájde súbor príkazu podľa názvu, ktorý používateľ použil. |
try { … } catch (error) { … } | Chyba v príkaze nesmie zhodiť bota. Používateľ dostane chybovú odpoveď a chyba sa vypíše do konzoly. |
interaction.replied / interaction.deferred | Keď už bot odpovedal alebo odpoveď odložil, ďalšiu správu musí poslať cez followUp. |
client. | Prihlási bota tokenom z .env. |
Bota vyskúšajte v počítači:
node index.jsReady! Logged in as MujBot#1234Na Discorde napíšte /ping a vyberte príkaz svojho bota.
Odpoveď na dlhšie trvajúci príkaz#
Keď príkaz potrebuje viac než 3 sekundy (dopyt do databázy, volanie iného API), najprv odpoveď odložte a výsledok pošlite neskôr:
async execute(interaction) {
await interaction.deferReply();
const vysledek = await nejakaDlouhaPrace();
await interaction.editReply(`Hotovo: ${vysledek}`);
}interaction.deferReply()– okamžite potvrdí príjem. Používateľ vidí „bot premýšľa“. Pre odpoveď viditeľnú iba jemu použitedeferReply({ flags: MessageFlags.Ephemeral }).interaction.editReply(…)– nahradí „premýšľa“ skutočnou odpoveďou. Odloženú odpoveď musíte poslať do 15 minút.nejakaDlouhaPrace()– vaša funkcia, ukážkový názov.
Odpoveď s embedom#
Embed je správa s farebným prúžkom, nadpisom a poľami. Hodí sa na prehľadné informácie. Príkaz /server ukáže údaje o serveri:
const { SlashCommandBuilder, EmbedBuilder, InteractionContextType } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('server')
.setDescription('Informácie o tomto Discord serveri.')
.setContexts(InteractionContextType.Guild),
async execute(interaction) {
const guild = interaction.guild;
const embed = new EmbedBuilder()
.setColor(0xe11d48)
.setTitle(guild.name)
.addFields(
{ name: 'Členov', value: String(guild.memberCount), inline: true },
{ name: 'Založený', value: guild.createdAt.toLocaleDateString('sk-SK'), inline: true },
)
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};| Časť | Čo robí |
|---|---|
.setContexts( | Príkaz pôjde použiť iba na serveri, nie v súkromných správach (tam by interaction.guild neexistoval). |
new EmbedBuilder() | Začne stavať embed. |
.setColor(0xe11d48) | Farba prúžku ako šestnástkové číslo (tu červená). |
.setTitle(guild.name) | Nadpis, tu názov servera. |
.addFields({ name, value, inline }) | Pole s popisom a hodnotou. value musí byť text, preto String(…). inline: true dá polia vedľa seba. |
guild.memberCount | Počet členov servera. Funguje s intentom Guilds, bez privilegovaného intentu. |
.setTimestamp() | Do pätičky pridá aktuálny čas. |
interaction.reply({ embeds: [embed] }) | Pošle embed. Do jednej správy je možné vložiť viac embedov v poli. |
Po pridaní súboru spustite znova node deploy-commands.js, nový príkaz sa musí zaregistrovať.
Kto smie príkaz použiť#
Predvolené oprávnenia nastavíte priamo v definícii príkazu:
const { SlashCommandBuilder, PermissionFlagsBits, InteractionContextType, MessageFlags } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('smazat')
.setDescription('Zmaže posledné správy v kanáli.')
.addIntegerOption((o) =>
o.setName('pocet').setDescription('Koľko sprá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: `Zmazaných správ: ${smazane.size}`, flags: MessageFlags.Ephemeral });
},
};.setDefaultMemberPermissions(– príkaz v predvolenom stave uvidia iba členovia s oprávnením Spravovať správy. Admin servera to môže v nastaveniach servera (Integrácie) zmeniť.PermissionFlagsBits. ManageMessages) .setMinValue(1)a.setMaxValue(100)– Discord nedovolí zadať číslo mimo rozsahu.interaction.– hromadne zmaže správy.channel. bulkDelete( pocet, true) truepreskočí správy staršie než 14 dní, ktoré hromadne zmazať nejde.- Bot sám potrebuje oprávnenie Manage Messages v kanáli, inak skončí
DiscordAPIError[50013]: Missing Permissions.
Členov s oprávnením Administrator žiadne nastavenie príkazu nezastaví. Príkazy iba pre vás (napríklad eval) preto registrujte iba na súkromnom testovacom serveri.
Udalosť: uvítanie nového člena#
Okrem príkazov reaguje bot na udalosti. Uvítanie nového člena potrebuje privilegovaný intent Server Members Intent: zapnite ho v Developer Portali na stránke 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(`Vitaj na serveri, ${member}!`);
},
};V index.js upravte intenty a pridajte načítanie udalostí (za načítanie príkazov):
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
});
// … načítanie príkazov …
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 potom posiela udalosti príchodu a odchodu členov. Bez zapnutého intentu v Developer Portali bot hlásiGuildMembers Used disallowed intents.member.– systémový kanál servera (kam Discord píše uvítania). Keď ho server nemá, bot nič nepošle.guild. systemChannel ${member}– vloží zmienku nového člena (@meno).client.once/client.on– udalosť sonce: truesa spracuje iba raz (štart bota), ostatné zakaždým.
Štruktúra väčšieho bota#
- Príkazy podľa kategórií v priečinkoch
commands/moderace,commands/zabava… Načítanie vyššie ich prejde všetky. - Udalosti v priečinku
events, každá vo vlastnom súbore sname,onceaexecute(pozri vyššie). Doeventsmôžete presunúť aj obsluhuClientReadyaInteractionCreate. - Konfigurácia (ID kanálov, rolí) do
config.json, tajné údaje do.env. - Dáta (body, varovania) do databázy, nie do premenných v pamäti, ktoré sa reštartom stratia.
Spustenie u StormHostu#
- Registráciu príkazov (
node deploy-commands.js) spustite v počítači. Na serveri to nie je potrebné. - Bota v počítači vypnite (Ctrl+C). Dve spustené kópie s rovnakým tokenom si navzájom kradnú interakcie.
- Nahrajte
index.js,package.json,package-lock.jsona priečinokcommandsso všetkými podpriečinkami do záložky Súbory. Ak ste pridali uvítanie nového člena, nahrajte aj priečinokevents. Bez neho bot pri štarte spadne s chybouENOENT: no such file or directory..envvytvorte priamo na serveri. Postup: nahranie a spustenie bota. - V záložke Nastavenia skontrolujte hlavný súbor
index.js. - V záložke Konzola kliknite na Spustiť.
Overenie#
- V Konzole je
Ready! Logged in as …a bot je na Discorde online. - Po napísaní
/na testovacom serveri ponuka obsahujepingapozdravs ikonou vášho bota. /pingodpovie správou viditeľnou iba vám,/pozdrav jmeno:Janverejnou správou.
Keď niečo nefunguje#
Príkazy sa v ponuke nezobrazujú#
- Neprebehla registrácia: spustite
node deploy-commands.jsa skontrolujte výpisHotovo, zaregistrovaných príkazov: …. GUILD_IDje ID iného servera, než na ktorom skúšate.- Bot bol pozvaný bez scope
applications.commands. Pozvite ho znova odkazom sbotajapplications.commands. - Discord si ponuku pamätá. Reštartujte aplikáciu Discord (Ctrl+R).
Registrácia skončí DiscordAPIError[50001]: Missing Access#
Bot nie je na serveri GUILD_ID, alebo na ňom nemá scope applications.commands. Skontrolujte ID servera a bota pozvite znova.
Discord píše „The application did not respond“ (aplikácia neodpovedala)#
Bot neodpovedal do 3 sekúnd. Buď nebeží (skontrolujte Konzolu), alebo príkaz trvá dlho a chýba deferReply(), alebo v príkaze nastala chyba pred odpoveďou (nájdete ju v Konzole).
DiscordAPIError[10062]: Unknown interaction#
Odpoveď prišla neskoro (po 3 sekundách) alebo bežia dve kópie bota s rovnakým tokenom a interakciu už spracovala tá druhá. Použite deferReply() a nechajte bežať iba jednu kópiu.
DiscordAPIError[40060]: Interaction has already been acknowledged#
Na jednu interakciu ste zavolali reply() dvakrát. Druhú správu pošlite cez followUp(), alebo po deferReply() použite editReply().
[WARNING] Súbor … nemá "data" alebo "execute".#
Súbor v priečinku commands neexportuje obe časti. Skontrolujte module.exports = { data, execute } a preklepy.
require is not defined in ES module scope alebo Cannot use import statement outside a module#
Miešanie CommonJS a ES modulov. Ukážky sú pre CommonJS: v package.json musí byť "type": "commonjs" (alebo pole type chýba). Podrobnosti v návode logy a chyby.
Zhrnutie
- Slash príkaz = definícia (
SlashCommandBuilder), registrácia (deploy-commands.jscez REST) a obsluha (Events.InteractionCreate). - Registráciu spúšťajte iba pri zmene definície. Pri vývoji serverovo (
applicationGuildCommands), hotového bota globálne (applicationCommands). - Na interakciu odpovedzte do 3 sekúnd. Dlhšiu prácu začnite
deferReply()a dokončiteeditReply(). - Chyby v príkazoch chytajte cez
try/catch, aby jedna chyba nezhodila bota. Nikdy nespúšťajte dve kópie s rovnakým tokenom.