Przejdź do treści
Aktualny kupon:10% zniżkiUżyj w zamówieniu

Twój pierwszy bot discord.js: polecenie slash krok po kroku

Napisz bota discord.js 14 z poleceniami slash /ping i /pozdrav, zarejestruj polecenia przez REST, naucz się odpowiadać w ciągu 3 sekund i uruchom bota w StormHost.

  • zaawansowany
  • ~30 min
  • Zaktualizowano 9.10.2026

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:

  1. Definicje poleceń – nazwa, opis i parametry polecenia. Informujesz o nich Discord jednorazowo, osobnym skryptem przez REST API (rejestracja).
  2. Działający bot – łączy się z Discordem (gateway) i czeka na zdarzenia.
  3. 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#

  1. Utwórz folder muj-bot i otwórz w nim terminal (w VS Code: Ctrl+`).

  2. Utwórz projekt i zainstaluj biblioteki:

    bash
    npm init -y
    npm install discord.js dotenv
    • npm init -y – tworzy package.json z 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 do dependencies.
  3. W package.json ustaw "main": "index.js" i "type": "commonjs". Przykłady używają require(). Pola objaśniamy w poradniku przesyłanie i uruchomienie bota.

  4. 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.

  5. Utwórz plik .gitignore z dwiema liniami: node_modules i .env.

Wynikowa struktura:

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

Polecenia#

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).

commands/utility/ping.jsJavaScript
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,
    });
  },
};
LiniaCo 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.client.ws.pingOpóź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.EphemeralOdpowiedź widzi tylko osoba, która użyła polecenia. Inni na kanale jej nie widzą.

Polecenie z parametrem:

commands/utility/pozdrav.jsJavaScript
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 pole jmeno:.
  • .setRequired(true) – parametr jest wymagany; bez niego nie da się wysłać polecenia.
  • interaction.options.getString('jmeno') – odczytuje wartość parametru.
  • interaction.user.username – nazwa użytkownika osoby, która użyła polecenia.
  • 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ę.

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] 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.readdirSync(foldersPath)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.applicationGuildCommands(CLIENT_ID, GUILD_ID)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:

bash
node deploy-commands.js
Wyniktext
Rejestruję polecenia: 2
Gotowe, zarejestrowane polecenia: 2

Polecenia serwerowe czy globalne?#

TypTrasaGdzie działająKiedy używać
Serwerowe (guild)Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID)Tylko na serwerze GUILD_ID.Rozwój i testowanie, bot dla jednego serwera.
GlobalneRoutes.applicationCommands(CLIENT_ID)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#

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] });

// 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.Guilds] })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.commands.set(command.data.name, command)Zapisuje polecenie pod jego nazwą, na przykład ping.
client.once(Events.ClientReady, …)Uruchamia się raz po udanym zalogowaniu. readyClient.user.tag to nazwa bota.
client.on(Events.InteractionCreate, …)Uruchamia się przy każdej interakcji (polecenie, przycisk, formularz).
interaction.isChatInputCommand()Przepuszcza tylko polecenia slash i ignoruje inne interakcje.
client.commands.get(interaction.commandName)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.deferredGdy bot już odpowiedział lub odroczył odpowiedź, każdą kolejną wiadomość musi wysłać przez followUp.
client.login(process.env.DISCORD_TOKEN)Loguje bota tokenem z .env.

Wypróbuj bota na komputerze:

bash
node index.js
Wyniktext
Ready! Logged in as MujBot#1234

Na 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:

JavaScript
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żyj deferReply({ 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:

commands/utility/server.jsJavaScript
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(InteractionContextType.Guild)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.memberCountLiczba 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:

commands/moderace/smazat.jsJavaScript
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(PermissionFlagsBits.ManageMessages) – domyślnie polecenie widzą tylko członkowie z uprawnieniem Zarządzanie wiadomościami. Administrator serwera może to zmienić w ustawieniach serwera (Integracje).
  • .setMinValue(1) i .setMaxValue(100) – Discord nie przyjmie liczby spoza zakresu.
  • interaction.channel.bulkDelete(pocet, true) – usuwa wiadomości hurtowo. true pomija 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.

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(`Witaj na serwerze, ${member}!`);
  },
};

W index.js zaktualizuj intenty i dodaj wczytywanie zdarzeń (po wczytaniu poleceń):

index.jsJavaScript
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.GuildMembers – 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łasza Used disallowed intents.
  • member.guild.systemChannel – kanał systemowy serwera (tam, gdzie Discord publikuje wiadomości powitalne). Jeśli serwer go nie ma, bot nic nie wysyła.
  • ${member} – wstawia wzmiankę o nowym członku (@nazwa).
  • client.once / client.on – zdarzenie z once: true jest 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 z name, once i execute (zobacz wyżej). Do events możesz przenieść także obsługę ClientReady i InteractionCreate.
  • 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#

  1. Uruchom rejestrację poleceń (node deploy-commands.js) na komputerze. Na serwerze nie musisz jej uruchamiać.
  2. Zatrzymaj bota na komputerze (Ctrl+C). Dwie działające kopie z tym samym tokenem podkradają sobie nawzajem interakcje.
  3. W zakładce Pliki prześlij index.js, package.json, package-lock.json oraz folder commands ze wszystkimi podfolderami. Jeśli dodano powitanie nowego członka, prześlij też folder events. Bez niego bot zawiesi się przy starcie z błędem ENOENT: no such file or directory. Plik .env utwórz bezpośrednio na serwerze. Kroki: przesyłanie i uruchomienie bota.
  4. W zakładce Ustawienia sprawdź, czy plikiem głównym jest index.js.
  5. 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 pokazuje ping i pozdrav z ikoną Twojego bota.
  • /ping odpowiada wiadomością widoczną tylko dla Ciebie, /pozdrav jmeno:Jan wiadomością publiczną.

Rozwiązywanie problemów#

Polecenia nie pokazują się w menu#

  • Rejestracja nie została uruchomiona: uruchom node deploy-commands.js i sprawdź, czy pojawia się wyjście Gotowe, zarejestrowane polecenia: ….
  • GUILD_ID to ID innego serwera niż ten, na którym testujesz.
  • Bot został zaproszony bez scope applications.commands. Zaproś go ponownie linkiem, który ma zarówno bot, jak i applications.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.js przez 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 przez editReply().
  • 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.

Wyszukiwanie w poradnikach