Napiszemy bota w bibliotece discord.js z dwoma poleceniami slash: /ping i /pozdrav („pozdrów”) z parametrem. Objaśnimy każdą linię, a na koniec uruchomimy bota w StormHost.
Czego potrzebujesz
- Node.js LTS na komputerze (pobierz z nodejs.org (otwiera się w nowym oknie)) i edytor kodu, na przykład Visual Studio Code
- Aplikacja w Discord Developer Portal, token i bot zaproszony na serwer testowy ze scope
applications.commands(poradnik) - Bot Discord w StormHost, aby działał bez przerwy
- Około 30 minut
Jak to działa#
Bot z poleceniami slash ma trzy części:
- Definicje poleceń – nazwa, opis i parametry polecenia. Informujesz o nich Discord jednorazowo, osobnym skryptem przez REST API (rejestracja).
- Działający bot – łączy się z Discordem (gateway) i czeka na zdarzenia.
- Obsługa interakcji – gdy ktoś użyje polecenia, Discord wysyła botowi interakcję, a bot musi odpowiedzieć w ciągu 3 sekund. W przeciwnym razie użytkownik widzi, że aplikacja nie odpowiedziała.
Rejestracja i uruchomienie bota to osobne sprawy: polecenia rejestrujesz ponownie tylko wtedy, gdy zmienisz ich definicję (nazwę, opis, parametry). Zmiany w kodzie odpowiedzi nie wymagają nowej rejestracji.
Zakładanie projektu#
Utwórz folder
muj-boti otwórz w nim terminal (w VS Code: Ctrl+`).Utwórz projekt i zainstaluj biblioteki:
bash npm init -y npm install discord.js dotenvnpm init -y– tworzypackage.jsonz wartościami domyślnymi (-y= odpowiada „tak” na każde pytanie).npm install discord.js dotenv– instaluje discord.js i dotenv (wczytuje plik.env) i dodaje je dodependencies.
W
package.jsonustaw"main": "index.js"i"type": "commonjs". Przykłady używająrequire(). Pola objaśniamy w poradniku przesyłanie i uruchomienie bota.Utwórz plik
.env:.envini DISCORD_TOKEN=<TOKEN_BOTA> CLIENT_ID=<APPLICATION_ID> GUILD_ID=<ID_SERWERA>Zamiast
<TOKEN_BOTA>wstaw token bota, zamiast<APPLICATION_ID>Application ID ze strony General Information w Developer Portal, a zamiast<ID_SERWERA>ID swojego testowego serwera Discord. Gdzie je znaleźć, opisuje poradnik token i intenty.Utwórz plik
.gitignorez dwiema liniami:node_modulesi.env.
Wynikowa struktura:
muj-bot/
├── .env
├── .gitignore
├── package.json
├── package-lock.json
├── index.js
├── deploy-commands.js
└── commands/
└── utility/
├── ping.js
└── pozdrav.jsPolecenia#
Każde polecenie ma własny plik z dwiema częściami: data (definicja dla Discorda) i execute (co się dzieje, gdy ktoś go użyje).
const { SlashCommandBuilder, MessageFlags } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Odpowiada Pong! i pokazuje opóźnienie bota.'),
async execute(interaction) {
await interaction.reply({
content: `Pong! Opóźnienie: ${interaction.client.ws.ping} ms`,
flags: MessageFlags.Ephemeral,
});
},
};| Linia | Co robi |
|---|---|
require('discord.js') | Wczytuje z biblioteki klasę SlashCommandBuilder (buduje definicję polecenia) i MessageFlags (flagi wiadomości). |
.setName('ping') | Nazwa polecenia, którą użytkownik wpisuje po ukośniku. 1–32 znaki, tylko małe litery, cyfry, - i _, bez spacji. |
.setDescription('…') | Opis w menu poleceń. Wymagany, 1–100 znaków. |
async execute(interaction) | Funkcja, którą bot wywołuje po użyciu polecenia. interaction zawiera wszystko na jego temat (kto, gdzie, parametry). |
interaction.reply({ … }) | Odpowiedź na polecenie. Musi dotrzeć w ciągu 3 sekund. |
interaction. | Opóźnienie połączenia bota z Discordem w milisekundach („Opóźnienie” w odpowiedzi). Tuż po starcie może wynosić -1, dopóki nie zostanie zmierzone. |
flags: MessageFlags.Ephemeral | Odpowiedź widzi tylko osoba, która użyła polecenia. Inni na kanale jej nie widzą. |
Polecenie z parametrem:
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('pozdrav')
.setDescription('Pozdrawia wskazaną osobę.')
.addStringOption((option) =>
option.setName('jmeno').setDescription('Kogo pozdrowić').setRequired(true),
),
async execute(interaction) {
const jmeno = interaction.options.getString('jmeno');
await interaction.reply(`Cześć, ${jmeno}! Pozdrawia Cię ${interaction.user.username}.`);
},
};.addStringOption(…)– dodaje parametr tekstowy (jmeno= imię). Discord pokazuje go użytkownikowi jako polejmeno:..setRequired(true)– parametr jest wymagany; bez niego nie da się wysłać polecenia.interaction.– odczytuje wartość parametru.options. getString( 'jmeno') interaction.– nazwa użytkownika osoby, która użyła polecenia.user. username interaction.reply('…')– publiczna odpowiedź na kanale.
Inne typy parametrów: addIntegerOption (liczba całkowita), addBooleanOption (tak/nie), addUserOption (członek serwera), addChannelOption (kanał), addRoleOption (rola).
Rejestracja poleceń#
Skrypt deploy-commands.js wczytuje definicje wszystkich poleceń i wysyła je do Discorda. Uruchamiasz go ręcznie, tylko gdy dodasz polecenie lub zmienisz jego definicję.
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] W pliku ${file} brakuje "data" lub "execute".`);
}
}
}
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
(async () => {
try {
console.log(`Rejestruję polecenia: ${commands.length}`);
const data = await rest.put(
Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
{ body: commands },
);
console.log(`Gotowe, zarejestrowane polecenia: ${data.length}`);
} catch (error) {
console.error(error);
}
})();| Część | Co robi |
|---|---|
fs. | Przechodzi przez podfoldery commands (na przykład utility) i pliki .js w nich. |
command.data.toJSON() | Konwertuje definicję polecenia na format, którego oczekuje Discord API. |
new REST().setToken(…) | Klient Discord REST API uwierzytelniony tokenem bota. Nie łączy się jako bot, tylko wysyła żądanie. |
Routes. | Trasa dla poleceń jednego serwera (guild). |
rest.put(…, { body: commands }) | Zastępuje wszystkie polecenia na serwerze tą listą. Polecenie, którego nie ma na liście, znika z Discorda. |
Uruchom go na komputerze w folderze projektu:
node deploy-commands.jsRejestruję polecenia: 2
Gotowe, zarejestrowane polecenia: 2Polecenia serwerowe czy globalne?#
| Typ | Trasa | Gdzie działają | Kiedy używać |
|---|---|---|---|
| Serwerowe (guild) | Routes. | Tylko na serwerze GUILD_ID. | Rozwój i testowanie, bot dla jednego serwera. |
| Globalne | Routes. | Na każdym serwerze, na który bot został zaproszony ze scope applications.commands, a domyślnie także w wiadomościach prywatnych. | Gotowy bot dla wielu serwerów. |
Jeśli to samo polecenie masz zarejestrowane jednocześnie dla serwera i globalnie, na serwerze testowym zobaczysz je dwukrotnie. Przed przejściem na rejestrację globalną usuń polecenia serwerowe: uruchom rejestrację serwerową z pustą listą (body: []).
Discord ogranicza tworzenie poleceń do 200 dziennie na serwer. Dlatego nie uruchamiaj skryptu rejestracji przy każdym starcie bota.
Plik główny 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] });
// Wczytanie poleceń z folderu 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(`Polecenie ${interaction.commandName} nie istnieje.`);
return;
}
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
const odpoved = { content: 'Podczas wykonywania tego polecenia wystąpił błąd.', flags: MessageFlags.Ephemeral };
if (interaction.replied || interaction.deferred) {
await interaction.followUp(odpoved);
} else {
await interaction.reply(odpoved);
}
}
});
client.login(process.env.DISCORD_TOKEN);| Część | Co robi |
|---|---|
new Client({ intents: [GatewayIntentBits. | Tworzy bota. Intent Guilds wystarcza do poleceń slash i nie jest uprzywilejowany. Czytanie zwykłych wiadomości wymagałoby GuildMessages i MessageContent. |
client.commands = new Collection() | Lista poleceń według nazwy. Collection to rozszerzona Map z discord.js. |
client. | Zapisuje polecenie pod jego nazwą, na przykład ping. |
client. | Uruchamia się raz po udanym zalogowaniu. readyClient.user.tag to nazwa bota. |
client. | Uruchamia się przy każdej interakcji (polecenie, przycisk, formularz). |
interaction. | Przepuszcza tylko polecenia slash i ignoruje inne interakcje. |
client. | Znajduje plik polecenia po nazwie, której użył użytkownik. |
try { … } catch (error) { … } | Błąd w poleceniu nie może zawiesić bota. Użytkownik dostaje odpowiedź o błędzie, a błąd trafia do konsoli. |
interaction.replied / interaction.deferred | Gdy bot już odpowiedział lub odroczył odpowiedź, każdą kolejną wiadomość musi wysłać przez followUp. |
client. | Loguje bota tokenem z .env. |
Wypróbuj bota na komputerze:
node index.jsReady! Logged in as MujBot#1234Na Discordzie wpisz /ping i wybierz polecenie swojego bota.
Odpowiedź na dłużej trwające polecenie#
Gdy polecenie potrzebuje więcej niż 3 sekundy (zapytanie do bazy danych, wywołanie innego API), najpierw odrocz odpowiedź, a wynik wyślij później:
async execute(interaction) {
await interaction.deferReply();
const vysledek = await nejakaDlouhaPrace();
await interaction.editReply(`Gotowe: ${vysledek}`);
}interaction.deferReply()– od razu potwierdza interakcję. Użytkownik widzi, że bot „myśli”. Dla odpowiedzi widocznej tylko dla niego użyjdeferReply({ flags: MessageFlags.Ephemeral }).interaction.editReply(…)– zastępuje „myślenie” właściwą odpowiedzią. Odroczoną odpowiedź musisz wysłać w ciągu 15 minut.nejakaDlouhaPrace()– Twoja własna funkcja; nazwa jest tylko przykładem.
Odpowiedź z embedem#
Embed to wiadomość z kolorowym paskiem, tytułem i polami. Dobrze nadaje się do przejrzystego prezentowania informacji. Polecenie /server pokazuje szczegóły serwera:
const { SlashCommandBuilder, EmbedBuilder, InteractionContextType } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('server')
.setDescription('Informacje o tym serwerze Discord.')
.setContexts(InteractionContextType.Guild),
async execute(interaction) {
const guild = interaction.guild;
const embed = new EmbedBuilder()
.setColor(0xe11d48)
.setTitle(guild.name)
.addFields(
{ name: 'Członkowie', value: String(guild.memberCount), inline: true },
{ name: 'Utworzono', value: guild.createdAt.toLocaleDateString('pl-PL'), inline: true },
)
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};| Część | Co robi |
|---|---|
.setContexts( | Polecenia można użyć tylko na serwerze, nie w wiadomościach prywatnych (tam interaction.guild nie istniałoby). |
new EmbedBuilder() | Rozpoczyna budowanie embedu. |
.setColor(0xe11d48) | Kolor paska jako liczba szesnastkowa (tutaj czerwony). |
.setTitle(guild.name) | Tytuł, tutaj nazwa serwera. |
.addFields({ name, value, inline }) | Pola z etykietą i wartością. value musi być tekstem, stąd String(…). inline: true ustawia pola obok siebie. |
guild.memberCount | Liczba członków serwera. Działa z intentem Guilds, bez uprzywilejowanego intentu. |
.setTimestamp() | Dodaje bieżący czas do stopki. |
interaction.reply({ embeds: [embed] }) | Wysyła embed. W jednej wiadomości możesz umieścić kilka embedów jako tablicę. |
Po dodaniu pliku uruchom ponownie node deploy-commands.js; nowe polecenie trzeba zarejestrować.
Kto może użyć polecenia#
Domyślne uprawnienia ustawiasz bezpośrednio w definicji polecenia:
const { SlashCommandBuilder, PermissionFlagsBits, InteractionContextType, MessageFlags } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('smazat')
.setDescription('Usuwa najnowsze wiadomości na kanale.')
.addIntegerOption((o) =>
o.setName('pocet').setDescription('Ile wiadomości (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: `Usunięte wiadomości: ${smazane.size}`, flags: MessageFlags.Ephemeral });
},
};.setDefaultMemberPermissions(– domyślnie polecenie widzą tylko członkowie z uprawnieniem Zarządzanie wiadomościami. Administrator serwera może to zmienić w ustawieniach serwera (Integracje).PermissionFlagsBits. ManageMessages) .setMinValue(1)i.setMaxValue(100)– Discord nie przyjmie liczby spoza zakresu.interaction.– usuwa wiadomości hurtowo.channel. bulkDelete( pocet, true) truepomija wiadomości starsze niż 14 dni, których nie można usuwać hurtowo.- Sam bot potrzebuje na kanale uprawnienia Zarządzanie wiadomościami, inaczej zgłosi
DiscordAPIError[50013]: Missing Permissions.
Żadne ustawienie polecenia nie zatrzyma członków z uprawnieniem Administrator. Polecenia przeznaczone tylko dla Ciebie (na przykład eval) rejestruj więc wyłącznie na prywatnym serwerze testowym.
Zdarzenie: powitanie nowego członka#
Oprócz poleceń bot reaguje na zdarzenia. Powitanie nowego członka wymaga uprzywilejowanego Server Members Intent: włącz go w Developer Portal na stronie Bot i dodaj do kodu.
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(`Witaj na serwerze, ${member}!`);
},
};W index.js zaktualizuj intenty i dodaj wczytywanie zdarzeń (po wczytaniu poleceń):
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
});
// … wczytywanie poleceń …
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 wysyła wtedy zdarzenia dołączenia i opuszczenia serwera przez członków. Jeśli intent nie jest włączony w Developer Portal, bot zgłaszaGuildMembers Used disallowed intents.member.– kanał systemowy serwera (tam, gdzie Discord publikuje wiadomości powitalne). Jeśli serwer go nie ma, bot nic nie wysyła.guild. systemChannel ${member}– wstawia wzmiankę o nowym członku (@nazwa).client.once/client.on– zdarzenie zonce: truejest obsługiwane tylko raz (start bota), pozostałe za każdym razem.
Struktura większego bota#
- Polecenia pogrupowane według kategorii w folderach, takich jak
commands/moderace,commands/zabava… Powyższy kod wczytujący przechodzi przez wszystkie. - Zdarzenia w folderze
events, każde w osobnym pliku zname,onceiexecute(zobacz wyżej). Doeventsmożesz przenieść także obsługęClientReadyiInteractionCreate. - Konfiguracja (ID kanałów i ról) w
config.json, dane poufne w.env. - Dane (punkty, ostrzeżenia) w bazie danych, a nie w zmiennych w pamięci, które giną przy restarcie.
Uruchomienie w StormHost#
- Uruchom rejestrację poleceń (
node deploy-commands.js) na komputerze. Na serwerze nie musisz jej uruchamiać. - Zatrzymaj bota na komputerze (Ctrl+C). Dwie działające kopie z tym samym tokenem podkradają sobie nawzajem interakcje.
- W zakładce Pliki prześlij
index.js,package.json,package-lock.jsonoraz foldercommandsze wszystkimi podfolderami. Jeśli dodano powitanie nowego członka, prześlij też folderevents. Bez niego bot zawiesi się przy starcie z błędemENOENT: no such file or directory. Plik.envutwórz bezpośrednio na serwerze. Kroki: przesyłanie i uruchomienie bota. - W zakładce Ustawienia sprawdź, czy plikiem głównym jest
index.js. - W zakładce Konsola kliknij Start.
Weryfikacja#
- Konsola pokazuje
Ready! Logged in as …, a bot jest online na Discordzie. - Gdy na serwerze testowym wpiszesz
/, menu pokazujepingipozdravz ikoną Twojego bota. /pingodpowiada wiadomością widoczną tylko dla Ciebie,/pozdrav jmeno:Janwiadomością publiczną.
Rozwiązywanie problemów#
Polecenia nie pokazują się w menu#
- Rejestracja nie została uruchomiona: uruchom
node deploy-commands.jsi sprawdź, czy pojawia się wyjścieGotowe, zarejestrowane polecenia: …. GUILD_IDto ID innego serwera niż ten, na którym testujesz.- Bot został zaproszony bez scope
applications.commands. Zaproś go ponownie linkiem, który ma zarównobot, jak iapplications.commands. - Discord buforuje menu. Przeładuj aplikację Discord (Ctrl+R).
Rejestracja kończy się błędem DiscordAPIError[50001]: Missing Access#
Bota nie ma na serwerze GUILD_ID albo nie ma tam scope applications.commands. Sprawdź ID serwera i zaproś bota ponownie.
Discord pisze „Aplikacja nie odpowiedziała”#
Bot nie odpowiedział w ciągu 3 sekund. Albo nie działa (sprawdź Konsolę), albo polecenie trwa długo i brakuje deferReply(), albo w poleceniu przed odpowiedzią wystąpił błąd (znajdziesz go w Konsoli).
DiscordAPIError[10062]: Unknown interaction#
Odpowiedź przyszła za późno (po 3 sekundach) albo działają dwie kopie bota z tym samym tokenem i druga już obsłużyła interakcję. Użyj deferReply() i zostaw uruchomioną tylko jedną kopię.
DiscordAPIError[40060]: Interaction has already been acknowledged#
Wywołano reply() dwa razy na jednej interakcji. Drugą wiadomość wyślij przez followUp() albo użyj editReply() po deferReply().
[WARNING] W pliku … brakuje "data" lub "execute".#
Plik w folderze commands nie eksportuje obu części. Sprawdź module.exports = { data, execute } i poszukaj literówek.
require is not defined in ES module scope lub Cannot use import statement outside a module#
Mieszasz CommonJS i moduły ES. Przykłady są dla CommonJS: package.json musi mieć "type": "commonjs" (albo w ogóle nie mieć pola type). Szczegóły w poradniku logi i błędy.
Podsumowanie
- Polecenie slash = definicja (
SlashCommandBuilder), rejestracja (deploy-commands.jsprzez REST) i obsługa (Events.InteractionCreate). - Rejestrację uruchamiaj tylko przy zmianie definicji. Podczas rozwoju rejestruj dla serwera (
applicationGuildCommands); gotowego bota rejestruj globalnie (applicationCommands). - Na interakcję odpowiadaj w ciągu 3 sekund. Dłuższą pracę rozpocznij od
deferReply()i zakończ przezeditReply(). - Błędy w poleceniach przechwytuj przez
try/catch, aby jeden błąd nie zawiesił bota. Nigdy nie uruchamiaj dwóch kopii z tym samym tokenem.