Создание своего Discord-бота на Node.js

В этой теме мы напишем небольшого Discord-бота на Node.js с использованием slash-комманд, кнопок и выпадающих меню, который будет показывать курс валют.

В этой статье для создания бота мы будем использовать библиотеку discord.js

Подготовка рабочей среды 

Шаг 1.1: Установка Node.js

Для начала работы нам нужно установить ноду. Переходим на сайт Node.js и скачиваем LTS-версию (можно и последнюю).

Установка не сложная, просто нажимаете далее. Можно выбрать свою папку установки.

Шаг 1.2: Установка редактора

Существует множество различных редакторов, можно использовать даже блокнот, НО я рекомендую вам эти программы:

  • Visual Studio Code – быстрый редактор, поддерживает плагины, множество языков, имеет встроенный терминал. В этой теме я буду использовать именно этот редактор.
  • Atom – простой, быстрый, не нагружающий систему редактор, поддерживает плагины.

Шаг 1.3: Создание проекта
Сейчас нам нужно создать папку с любым названием, желательно на латинице и без пробелов. Затем необходимо открыть созданную нами папку через выбранный редактор.

Далее необходимо открыть терминал. В Visual Studio Code это делается так:

Теперь проверим установлена ли нода, команда 

node -v

 должна выдать что-то примерно 

v16.13.0

Если у вас какая-либо ошибка, попробуйте перезагрузить компьютер, либо переустановить ноду.
Сейчас нам нужно инициировать наш проект. В терминале нам необходимо инициировать NPM-модуль (в нашем случае, наш проект):

# С вопросами для настройки
npm init
# Без вопросов
npm init --yes

Получение токена

Шаг 2.1: Регистрация приложения

Для начала нам нужно зарегистрировать приложение на странице Приложений:

Вводим желаемое имя, и жмём Create.

Шаг 2.2: Добавление бота

Сейчас нужно добавить бота к нашему приложению:

Шаг 2.3: Получение токена

Добавив бота нам необходимо получить его токен:

Этот токен пригодится нам в дальнейшем

Установка библиотек 

Шаг 3: Установка всех библиотек

Получив токен мы приступаем к установке библиотек. В терминале необходимо выполнить команду:

npm i discord.js dotenv axios

Всё! Мы установили все библиотеки. Переходим к следующему шагу.

Структура проекта 

Шаг 4.1: Создание папок

Чтобы всё было понятно, вкратце объясню какие папки и файл вам нужно создать и что в них находится:

  • index.js – наш главный файл, в котором мы объявим все важные нам переменные и вызовем необходимые функции
  • .env – файл с нашим токеном
  • src – папка со всеми ресурсами проекта. В корне этой папки мы будем хранить различные утилиты
  • src/events – папка слушателей ивентов (прослушиватель slash-комманд)
  • src/interactions – папка с обработчиками (обработчик slash-комманды)

Шаг 4.2: Настройка файла .env

Нам необходимо настроить этот файл, чтобы наш бот мог получить из него данные.
BOT_TOKEN – ваш токен который вы получили в шаге 2.3
API_URL – url адрес api которое мы будем использовать для получения курса валют.

BOT_TOKEN=Токен полученный в шаге 2.3
API_URL=https://api.privatbank.ua/p24api/pubinfo?json&exchange&coursid=5

Если вы будете заливать код на github (хоть в приватный проект) убедитесь, что файл .env находится в .gitignore,
чтобы ваш токен никто не узнал.
Кстати если вы нечаянно выложите свой токен в публичный проект, Discord сразу же оповестит вас об этом и сбросит ваш токен на новый.

Шаг 4.3: Добавление утилит

Итак добавим нашу первую утилиту которая будет генерировать рандомный индивидульный ID

В папке src создадим файл с названием snowflake.js
В нём находится код для генерации ID. Мы будем использовать snowflake, генерируются они по этой схеме:

Использовать мы их будем для обозначения кнопок и выпадающих меню в нашем кеше (объясню позже):

// Переменная которая постоянно прибавляется и
// обнуляется достигая значение 4095, чтобы не сгенерировались одинаковые ID
let i = 0;
// Экспортируем нашу функцию для доступа извне
module.exports = () => {
    // Инкрементируем нашу переменную i чтобы избежать одинаковые ID
    if(i == 0xFFF) i = 0;
    else i++;
    // Первая часть Snowflake
    let first = (BigInt(Date.now()) - 1420070400000n).toString(2);
    // Вторая часть
    let second = "0000100000";
    // Третья часть, как раз наша переменная i
    let third = i.toString(2).padStart(12, "0");
    // Возвращаем в виде строки
    return BigInt("0b"+first+second+third).toString();
}

Готово.

Теперь нужно создать файл currency.js в папке src, в нём будет функция которая обращается к API
для получения курса валют:

//Импортируем библиотеку axios
const axios = require('axios');
 
// Экспортируем нашу функцию для доступа извне
// Async позволяет сделать нашу функцию асинхронной
module.exports = async () => {
    // Делаем запрос к API указанному нами в файле .env
    const res = await axios.get(process.env.API_URL);
    // Проверяем успешно ли прошёл запрос, иначе выбрасываем ошибку
    if(!res || res.status !== 200 || !res.data) throw new Error('[CURRENCY] Ошибка при получении курса валют');
    // Возвращаем результат
    return res.data;
}

Асинхронная функция – функция, выполнение которой можно подождать, и получить ответ.

Главный файл 

Шаг 5.1: index.js

Приступим к настройке созданного нами файла index.js в корне проекта. Для этого вставим туда этот код:

// Импортируем и настраиваем библиотеку dotenv, чтобы импортировать наш .env файл
require('dotenv').config();
// Импортируем библиотеку discord.js для бота
const Discord = require('discord.js');
// Создаём объект бота
const bot = new Discord.Client({ intents: 31997 });
// Создаём пользовательский объект чтобы нам было удобно хранить глобальную информацию
bot.info = {
    interactions: {}, // Объект для наших будущих обработчиков
    cache: {} // Объект для кеша
};
// Импортируем нашу утилиту currency.js,
// чтобы при включении бота автоматически узнать все доступные курсы валют
// Делаем это в виде функции чтобы упростить обновление
function updateCurrency(){
    require("./src/currency")().then(result => { // Метод .then позволяет подождать и получить результат из async-функции
        bot.info.currencies = result; // Устанавливаем результат в наш пользовательский объект
    });
}
// Вызываем обновление
updateCurrency();
// Создаём интервал на обновление курса каждые 30 минут
setInterval(updateCurrency, 1000 * 60 * 30);
 
bot.login(process.env.BOT_TOKEN) // Авторизовываем бота с нашим токеном из .env
.catch(e => console.log("[BOT] Произошла ошибка:\n" + e)); // Ловим ошибки во время авторизации (у async-функции login)

Вам, наверное, интересно, что же за intents мы указали в Discord.Client ? Это фишка Discord, которая позволяет избавить нашего бота от лишней информации, которую мы не будем использовать. В число 31997 я указал все публичные интенты.

Существуют также приватные интенты, которые можно включить в настройках бота, они нужны для дополнительной информации, которая по умолчанию не доступна.

Загрузчик (Loader) 

Шаг 6.1: Создание загрузчика

Т.к. мы хотим чтобы наш бот автоматически определял всех наших слушателей и обработчиков, нам нужно создать загрузчик.
Для этого в папке src создадим файл loader.js со следующим содержимым:

// Импортируем нативную библиотеку node.js для работы с файловой системой
const fs = require("fs");
// Экспортируем функцию loadEvents для доступа извне
module.exports.loadEvents = (bot) => { // Наша функция получает параметр bot
    //Получаем все файлы в папке src/events
    // Здесь необходимо указывать путь относительно корневой папки проекта
    let dir = fs.readdirSync("./src/events");
    //Если папка пустая логируем это
    if(!dir[0]) return console.log("[LOADER] Ивенты не найдены!");
 
    //Счётчик успешно загруженных ивентов
    let suc = 0;
    //Загрузка ивентов
    dir.forEach(eventName => {
        // Ловим ошибки у синхронной функции
        try{
            // Импортируем наш слушатель
            let event = require("./events/" + eventName);
            // Вызываем функцию init (узнаете позже)
            event.init(bot);
            // Делаем +1 к счётчику
            suc++;
        }catch(e){
            //Логирование ошибки
            console.log("[LOADER] Ошибка при загрузке ивента " + eventName + ":\n" + e);
        }
    });
 
    //Информация
    console.log("[LOADER] Загружено " + suc + " ивентов из " + dir.length + "!");
}
 
// Экспортируем функцию loadEvents для доступа извне
module.exports.loadInteractions = (bot) => {// Наша функция получает параметр bot
    //Получаем все файлы в папке src/events
    // Здесь необходимо указывать путь относительно корневой папки проекта
    let dir = fs.readdirSync("./src/interactions");
    //Если папка пустая логируем это
    if(!dir[0]) return console.log("[LOADER] Интеракции не найдены!");
 
    //Счётчик успешно загруженных интеракций
    let suc = 0;
    //Загрузка интеракций
    dir.forEach(interactionName => {
        // Ловим ошибки у синхронной функции
        try{
            // Импортируем наш обработчик
            let interaction = require("./interactions/" + interactionName);
            // Добавляем наш обработчик в пользовательский объект для удобства
            // Что такое customId вы узнаете позже
            bot.info.interactions[interaction.customId] = interaction;
            // Делаем +1 к счётчику
            suc++;
        }catch(e){
            //Логирование ошибки
            console.log("[LOADER] Ошибка при загрузке интеракции " + interactionName + ":\n" + e);
        }
    });
    //Информация
    console.log("[LOADER] Загружено " + suc + " интеракций из " + dir.length + "!");
}

Готово. Перейдём к следующему шагу.

Шаг 6.2: Вызов загрузчика из index.js

В нашем главном файле index.js примерно в середине (над bot.login…) нужно добавить следующие строки:

// Импортируем наш загрузик
 
const Loader = require("./src/loader");
// Загружаем слушатели
Loader.loadEvents(bot);
// Загружаем обработчики
Loader.loadInteractions(bot);

Шаг 7.1: Ивент ready

Создаём файл ready.js в папке src/events со следующим содержимым:

// Экспортируем функцию для доступа извне
// Наша функция принимает параметр bot
// Далее у bot вызывает метод on, он слушает ивенты и выполняет функцию которую мы указываем через запятую
module.exports.init = (bot) => bot.on('ready', () => {
    console.log("[BOT] Готов к работе!"); // Лог в консоль
});

Сейчас мы создали наш первый слушатель, который слушает ивент ready который сообщает о том, что наш бот готов к работе.

Перейдём к следующему ивенту

Шаг 7.2: Ивент interactionCreate

Создаём файл interactionCreatejs в папке src/events со следующим содержимым:

// Экспортируем функцию для доступа извне
// Далее у bot она вызвает метод on, слушает ивент interactionCreate, и передаём асинхронную функцию
// в прослушиватель, которая принимает параметр interaction от ивента
module.exports.init = (bot) => bot.on('interactionCreate', async interaction => {
    //Если наша интеракция slash-команда
    if(interaction.isCommand()){
        // Ищём наш обработчик в пользовательском объекте, в который мы записали наши обработчики через загрузчик
        let handler = bot.info.interactions[interaction.commandName];
        // Если обработчик мы не нашли, пропускаем
        if(!handler) return;
        // А если нашли, вызываем метод .run и передаём туда параметры bot и interaction
        handler.run(bot, interaction);
        // Если наша интеракция это кнопка или выпадающее меню
    }else if(interaction.isButton() || interaction.isSelectMenu()){
        // Вы дальше узнаете как мы записали айди интеракции в кеш
        // Теперь мы ищем в кеше наш обработчик кнопок/выпадающих меню по айди,
        // который ранее мы записали и передали в Discord
        let cache = bot.info.cache[interaction.customId];
        // Если не нашли, пропускаем
        if(!cache) return;
        // Проверяем в кеше, какие пользователи могут использовать кнопку/выпадающее меню
        // Если пользователь вызвавший интеракцию не может использовать, пропускаем
        if(cache.users && !cache.users.includes(interaction.user.id)) return;
        // Ищем обработчик в пользовательском объекте, в который мы записали наши обработчики через загрузчик
        let handler = bot.info.interactions[cache.customId];
        // Если нет обработчика, пропускаем
        if(!handler) return;
        handler.run(bot, interaction, cache);
    }
});

Шаг 7.3: Утилита очистки кеша

Чтобы легко очистить кеш имея объект сообщения нам нужна простая утилита.
В папке src создаём файл clearCache.js со следующим содержимым:

// Экспортируем для доступа извне
// Наша функция принимает bot и interaction
module.exports = (bot, interaction) => {
    // Получаем message из interaction, а из message - components
    // Методом forEach перебираем все components
    interaction.message.components.forEach(e => { // Теперь каждый из компонентов объявлен как e
        // Компоненты сообщений могут быть только ActionRow, которые в свою очередь обязаны иметь свои компоненты
        e.components.forEach(e2 => { // Перебираем их, и объявляем как e2
            // Ищем в кеше значения с таким же id и удаляем
            if(bot.info.cache[e2.customId]) delete bot.info.cache[e2.customId];
        });
    });
}

Шаг 8.1: Теперь нам необходимо зарегистрировать нашу команду

Для начала заходим на страницу Приложений, выбираем там нашего бота и копируем Application ID

Теперь нам нужно вставить ID в ссылку для API запроса:

https://discord.com/api/v9/applications/<Скопированный ID>/commands
 
# Например
https://discord.com/api/v9/applications/894996856684286032/commands

Шаг 8.2: Body

Теперь сформулируем тело нашего запроса:

{
    "name": "convert",
    "type": 1,
    "description": "Конвертер валют"
}

Итак давайте я объясню подробнее

  • name – название нашей команды
  • type – тип интеракции, 1 – slash-команда
  • description – описание команды

Шаг 8.3: Выполнение http-запроса

Открываем любую программу для создания http-запросов, например Postman. Я буду использовать Advanced REST client.
Здесь нам нужно всё настроить:

  • Выбрать тип запроса – POST
  • Вставить ссылку из шага 7.1
  • Добавить хедер Content-Type со значением application/json
  • Добавить хедер Authorization со значением Bot <токен бота из шага 2.3>
  • Затем открываем вкладку Body
  • Во вкладке Body нам нужно вставить наше тело из шага 7.2
  • И нажать кнопку Send

Готово! В ответе мы должны были получить объект с копией нашего тела и дополнительной информацией

Если у вас какая-либо ошибка, попробуйте повторить всё начиная с шага 7.1

Создание обработчиков 

Шаг 9.1: Создание обработчика команды

Для начала нам нужно создать обработчик самой команды. Создадим файл convert.js в папке src/interactions
Добавим туда следующее содержимое:

// Импортируем библиотеку discord.js и наши утилиты snowflake, currency
 
const Discord = require("discord.js");
const snowflake = require("../snowflake");
const currency = require("../currency");
 
// Экспортируем объект нашего обработчика
module.exports = {
    // В этом поле хранится имя нашей команды
    customId: "convert",
    // В этом поле вспомогательная функция interactionType
    // Она нужна, чтобы слушатель из src/events/interactionCreate.js
    // Автоматически понимал подходит ли эта команда к типу интеракции
    interactionType: (i) => i.isCommand(), //isCommand возвращает true если интеракция это slash-комманда
    // Функция run принимает аргументы bot и interaction. Является главной функцией обработчика
    run(bot, interaction){
        // Создадим эмбед для ответа на команду
        let embed = new Discord.MessageEmbed()
            .setColor("RANDOM") // Установим цвет
            .setTitle("Конвертация") // Заголовок
            // Футер с ником и аватаркой пользователя вызвавшего команду
            .setFooter(interaction.user.tag, interaction.user.displayAvatarURL());
 
        // Создадим ID для кнопки
        let buttonId = snowflake();
        // Создадим ID для выпадающего меню
        let selectMenuId = snowflake();
 
        // Получим список курсов из пользовательского объекта
        // И используем метод map чтобы обработать значение каждого курса как нам надо
        let currencies = bot.info.currencies.map(e => ({
            label: e.base_ccy + " -> " + e.ccy, // Заголовок варианта из выпадающего меню
            // Значение варианта из выпадающего меню обязательно должно быть строкой
            // Поэтому наш курс мы превратим в строку формата JSON
            value: JSON.stringify(e)
        }));
 
        // Создадим компоненты (кнопки и меню) которые будут внизу сообщения
        let components = [
            // Каждый элемент или несколько элементов (до 5) обязаны быть внутри ActionRow
            // Создаём ActionRow и добавляем в него компоненты
            new Discord.MessageActionRow().addComponents(
                // Создаём выпадающее меню, с плейсхолдером "Курс", и customId который мы объявили чуть выше
                // Затем добавляем варианты курса (addOptions) которые мы сделали чуть выше с помошью .map
                new Discord.MessageSelectMenu({ placeholder: "Курс", customId: selectMenuId }).addOptions(currencies)
            ),
            // Создаём ActionRow для кнопки которая будет под меню и добавляем компоненты
            new Discord.MessageActionRow().addComponents(
                // Создаём кнопку красного цвета (style: "DANGER") с заголовком "Отменить"
                // И customId который мы объявили чуть выше
                new Discord.MessageButton({ label: "Отменить", customId: buttonId, style: "DANGER" })
            )
        ];
            // Добавляем в кеш нашу кнопку и меню, чтобы обработчик interactionCreate
            // Понял с каким именем он должен искать обработчик
            // И какие пользователи могут нажимать на кнопку или выбирать вариант в меню
            bot.info.cache[buttonId] = {
                users: [interaction.user.id],
                customId: "delete"
            }
 
            bot.info.cache[selectMenuId] = {
                users: [interaction.user.id],
                customId: "convert-select"
            }
 
            // Отвечаем на команду нашим эмбедом и компонентами
            interaction.reply({ embeds: [embed], components });
    }
}

Сохраняем. И идём дальше

Шаг 9.2: Создание обработчика кнопки Отменить

В папке src/interactions создаём файл delete.js со следующим содержимым:

// Импортируем библиотеку discord.js и нашу утилиту clearCache
 
const Discord = require("discord.js");
const clearCache = require("../clearCache");
 
// Экспортируем обработчик
module.exports = {
    // Вы уже знаете, что в этом поле хранится название обработчика
    customId: "delete",
    // А здесь вспомогательная функция, но здесь isButton говорит о том, что это обработчик кнопки
    interactionType: (i) => i.isButton(),
    // Функция принимает bot, interaction и вдобавок cache (который получил наш слушатель interactionCreate)
    run(bot, interaction, cache){
            // Передаём в функцию-утилиту clearCache объект бота (в котором наш пользовательский объект с кешем)
            // И объект интеракции, чтобы утилита нашла все компоненты и удалила их из кеша
            clearCache(bot, interaction);
            // Создаём новый эмбед
            let embed = new Discord.MessageEmbed()
                .setColor("#ff0000") // Ставим красный цвет
                .setTitle("Отменено!") // Ставим заголовок
            // Редактируем сообщение на котором была нажата кнопка Отменить
            interaction.update({
                embeds: [embed], // Устанавливаем новый эмбед
                components: [] // Убираем всё компоненты (кнопки и меню)
            })
    }
}

Шаг 9.3: Обработчик выпадающего меню

И наконец, это наш последний файл и последний код!
В папке src/interactions создаём файл convertSelect.js со следующим содержимым:

// Вы уже это видели
 
const Discord = require("discord.js");
const clearCache = require("../clearCache");
 
module.exports = {
    // Название обработчика
    customId: "convert-select",
    // Вспомогательная функция-определитель, isSelectMenu - является ли интеракция (i) выпадающим меню
    interactionType: (i) => i.isSelectMenu(),
    // Снова главная функция принимающая объект бота, интеракции и кеш.
    run(bot, interaction, cache){
        // Очищаем кеш
        clearCache(bot, interaction);
 
        // Получаем выбранное значение (.values[0]) и парсим JSON-строку в JS-объект
        let exchangeRate = JSON.parse(interaction.values[0]);
 
        // Создаём эмбед
        let embed = new Discord.MessageEmbed()
            .setColor("RANDOM") // Ставим рандомный цвет
            .setTitle("Конвертация") // Ставим заголовок
            // Устанавливаем описание эмбеда.
            // Первая строка - Курс (заголовок нашего варианта из меню)
            // Вторая строка - цена продажи валюты
            // Третья строка - цена покупки валюты
            .setDescription("`" + exchangeRate.base_ccy + " -> " + exchangeRate.ccy + "`\n" +
            "Покупка: `" + exchangeRate.buy + "`\n" +
            "Продажа: `" + exchangeRate.sale + "`")
            // Футер с ником и аватаркой пользователя выбравшего вариант из меню
            .setFooter(interaction.user.tag, interaction.user.displayAvatarURL());
 
            // Редактируем сообщение на котором выбран вариант
            interaction.update({
                embeds: [embed], // Ставим наш новый эмбед
                components: [] // Убираем компоненты
            })
    }
}

Это был обработчик для выпадающего меню, а мы перейдём к заключительным действиям!

Приглашение и запуск бота 

Шаг 10.1: Приглашение бота на сервер

Заходим на сайт Калькулятора прав и делаем следующие действия:

  • Выбираем право Administrator
  • Вставляем наш Application ID из шага 7.2 в поле Client ID
  • В поле Scope нам нужно вставить bot applications.commands
  • Теперь копируем ссылку из поля Link и переходим по ней

Здесь мы выбираем нужный нам сервер, жмём Продолжить и проходим капчу. Готово!

Шаг 10.2: Запуск бота

В терминале нашего проекта нам нужно прописать команду 

node index.js

.
В терминале должно было вывестись так:

[LOADER] Загружено 2 ивентов из 2!
[LOADER] Загружено 3 интеракций из 3!
[BOT] Готов к работе!

Как нам сообщил [BOT], он готов к работе!

Если произошли какие-то ошибки, попробуйте разобрать где они произошли и исправить руководствуясь кодом из этой статьи.

Проверим работу нашего бота:

Ура! Бот работает, значит мы всё сделали правильно!

1 Comment

  • C:\Users\Паша\Desktop\Тест\node_modules\axios\dist\node\axios.cjs:836
    AxiosError.call(axiosError, error.message, code, config, request, response);
    ^
    AxiosError: connect ECONNREFUSED ::1:80
    at AxiosError.from (C:\Users\Паша\Desktop\Тест\node_modules\axios\dist\node\axios.cjs:836:14)
    at RedirectableRequest.handleRequestError (C:\Users\Паша\Desktop\Тест\node_modules\axios\dist\node\axios.cjs:3010:25)
    at RedirectableRequest.emit (node:events:513:28)
    at eventHandlers. (C:\Users\Паша\Desktop\Тест\node_modules\follow-redirects\index.js:14:24)
    at ClientRequest.emit (node:events:513:28)
    at Socket.socketErrorListener (node:_http_client:502:9)
    at Socket.emit (node:events:513:28)
    at emitErrorNT (node:internal/streams/destroy:151:8)
    at emitErrorCloseNT (node:internal/streams/destroy:116:3)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {

    Всё перепробовал всё также…

    Серега
    Posted 28.06.2023

Leave a comment

Discord

Наш дискорд сервер

Telegram

Мы в Telegram

AWKA.IO © 2012-2025 Все права защищены. Пользовательское соглашение  |  Контакты  |  О нас | Отказ от ответственности | Полное или частичное копирование материалов сайта без согласования с редакцией запрещено.|

Мы используем cookie, чтобы собирать статистику и делать контент более интересным. Также cookie используются для отображения более релевантной рекламы. Вы можете прочитать подробнее о cookie-файлах и изменить настройки вашего браузера.


Перейти к верхней панели