3. Код
Небольшой бот для заметок: /note buy milk сохраняет заметку, /notes показывает ваши последние пять.
package.json
{
"name": "my-grammy-bot",
"private": true,
"main": "index.js",
"dependencies": {
"grammy": "^1.46.0",
"mysql2": "^3.24.6"
}
}
index.js
const { Bot } = require("grammy");
const mysql = require("mysql2/promise");
// One pool for the whole bot. The values come from the Environment pane.
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
connectionLimit: 5,
});
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN);
bot.command("note", async (ctx) => {
const text = ctx.match.trim();
if (!text) return ctx.reply("Write your note after the command, for example: /note buy milk");
if (text.length > 500) return ctx.reply("A note can be up to 500 characters.");
await pool.execute("INSERT INTO notes (user_id, text) VALUES (?, ?)", [ctx.from.id, text]);
await ctx.reply("Saved. Send /notes to see your last five.");
});
bot.command("notes", async (ctx) => {
const [rows] = await pool.execute(
"SELECT text FROM notes WHERE user_id = ? ORDER BY id DESC LIMIT 5",
[ctx.from.id],
);
if (rows.length === 0) return ctx.reply("You have no notes yet.");
await ctx.reply(rows.map((row) => `- ${row.text}`).join("\n"));
});
// Without this, an error in one command stops the whole bot.
bot.catch((err) => {
console.error(`Error while handling update ${err.ctx.update.update_id}:`, err.error);
});
async function main() {
await pool.query(
"CREATE TABLE IF NOT EXISTS notes (" +
"id INT AUTO_INCREMENT PRIMARY KEY, " +
"user_id BIGINT NOT NULL, " +
"text VARCHAR(500) NOT NULL, " +
"created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"KEY user_id (user_id))",
);
console.log("Database ready");
// bot.start() only returns when the bot stops, so the ready line goes in onStart.
await bot.start({ onStart: (me) => console.log(`Logged in as @${me.username}`) });
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Что он делает: main() создаёт таблицу notes , если её нет, и выводит Database ready до того, как bot.start() начнёт запрашивать у Telegram сообщения. Пул создаётся один раз, в начале файла, и его использует каждая команда: pool.execute() берёт соединение, выполняет запрос и возвращает соединение. ctx.match содержит текст после команды: grammY убирает пробелы перед ним и сохраняет остальное как набрано, включая переводы строк. bot.catch() здесь важен: в документации grammY сказано, что без обработчика ошибок ошибка в хендлере останавливает бота, запущенного через bot.start(), а ошибка базы данных как раз такая.
Плейсхолдеры ? отправляют заметку в базу отдельно от SQL, поэтому заметка вроде '); DROP TABLE notes; -- сохраняется как обычный текст. В нашем тесте она сохранилась как строка 2, а таблица осталась на месте. Никогда не собирайте SQL строкой-шаблоном.
Храните ID Telegram как BIGINT
В документации Bot API Telegram сказано, что ID пользователя или чата «может содержать более 32 значащих бит», но не более 52, так что 64-битного целого достаточно. На странице об ID даны диапазоны: ID пользователей доходят до 1.099.511.627.775, а ID групп, супергрупп и каналов отрицательные. Супергруппы и каналы лежат в диапазоне от -1.000.000.000.001 до -1.997.852.516.352 (например, -1001234567890). Тип INT в MySQL заканчивается на 2.147.483.647. В нашем тесте, с тем же строгим режимом SQL, что и на нашем сервере баз данных, сохранить ID пользователя 7.000.000.001 в столбце INT не удалось. mysql2 вернул код ошибки ER_WARN_DATA_OUT_OF_RANGE (errno 1264) с таким сообщением:
Out of range value for column 'user_id' at row 1
Столбец BIGINT его сохранил, как и -1001234567890 . Используйте обычный BIGINT, а не BIGINT UNSIGNED, чтобы подходили и ID чатов.
4. Запустите
- Загрузите
index.js и package.json в разделе Файлы. Не загружайте node_modules.
- Нажмите Запустить и откройте Консоль. При запуске устанавливаются пакеты из
package.json, затем появляются [PlayNHost] Starting your bot, Database ready и Logged in as @your_bot. Эта последняя строка переключает статус в разделе Обзор на Онлайн в Telegram.
- В Telegram отправьте своему боту
/note buy milk, затем /notes. Чтобы посмотреть строки, откройте страницу базы и нажмите Открыть в phpMyAdmin.
В нашем тесте замена Telegram отправила эти команды и получила эти ответы:
| Отправлено | Ответ |
/note buy milk | Saved. Send /notes to see your last five. |
/note | Write your note after the command, for example: /note buy milk |
/note и 501 символ | A note can be up to 500 characters. |
/notes | Сохранённые заметки, сначала новые |
/notes от второго пользователя | You have no notes yet. |
Много сообщений одновременно
В документации grammY сказано, что bot.start() обрабатывает все обновления последовательно: одна команда завершается до начала следующей. В нашем тесте после ответов на 8 команд у бота было 1 открытое соединение. connectionLimit: 5 важен, когда ваш код выполняет запросы параллельно или когда вы переходите на grammY runner, который обрабатывает обновления одновременно: пул никогда не открывает больше 5 соединений, а лишний запрос ждёт свободного.
Неудачный запрос не останавливает бота
В нашем тесте мы удалили таблицу, пока бот работал, и отправили /notes. bot.catch() вывел Error while handling update 500001: Error: Table '...notes' doesn't exist с code: 'ER_NO_SUCH_TABLE', бот не отправил ответ на эту команду и продолжил работать: когда таблица вернулась, следующая команда /note сохранилась. Тот, кто отправил команду, ничего не видит, поэтому, если запрос может завершиться ошибкой, оберните его в хендлере в try/catch и ответьте коротким сообщением.
Тихая ночь не ломает пул
Наш сервер баз данных закрывает соединение, простаивавшее 8 часов. Бот, который так долго не получает сообщений, например ночью, обнаруживает, что соединения в пуле закрыты. Пул mysql2 справляется с этим сам. Мы проверили это с лимитом простоя, сниженным до 5 секунд: после 7 тихих секунд следующая команда /note сохранилась без ошибки.
Лимиты, важные для вашего пула
- 20 соединений одновременно на пользователя базы. Учитывается каждый процесс, использующий тот же логин. Соединение 21 отклоняется с ошибкой
ER_USER_LIMIT_REACHED. В нашем тесте пул с connectionLimit: 30 завершился этой ошибкой, когда одновременно выполнялись 30 запросов. Пул из 5 держит бота заметно ниже лимита.
- Запрос, который выполняется дольше 120 секунд, останавливается. MariaDB завершает его с ошибкой 1969,
Query execution was interrupted (max_statement_time exceeded). В нашем более раннем тесте, на учётной записи с такой же настройкой, это произошло через 120,03 секунды. Запросы бота обычно занимают миллисекунды, так что это касается только запроса, вышедшего из-под контроля.
- 500 МБ на базу данных. Страница Базы данных показывает, сколько занято. При 500 МБ запись останавливается, пока вы не удалите строки.
- Резервные копии сервера не включают базу данных. Время от времени экспортируйте её из phpMyAdmin.
Ошибки и что они значат
Вот коды ошибок, которые вернул mysql2 в нашем тесте:
ER_ACCESS_DENIED_ERROR ... Access denied for user: неверный DB_USER или DB_PASSWORD. После Сбросить пароль на карточке Подключение старый пароль сразу перестаёт работать: замените DB_PASSWORD и перезапустите бота.
ECONNREFUSED: по этому хосту и порту соединение никто не принял. Проверьте DB_HOST и DB_PORT. Отсутствующий DB_HOST тоже приводит сюда, потому что mysql2 тогда подключается к localhost, где никакая база не запущена: переменной нет в разделе Окружение, или бота не перезапустили после её добавления.
ENOTFOUND: DB_HOST написан с ошибкой, поэтому такого имени не существует.
ETIMEDOUT: никто не ответил вовремя. Что бы ни говорило сообщение, подключение с вашего компьютера не удастся: база принимает подключения только с серверов PlayNHost и из phpMyAdmin.
ER_USER_LIMIT_REACHED ... has exceeded the 'max_user_connections' resource (current value: 20): открыто слишком много соединений, см. лимиты выше. Например, вторая копия бота или пул, создаваемый на каждое сообщение.
ER_WARN_DATA_OUT_OF_RANGE (errno 1264, Out of range value for column 'user_id' at row 1): столбец с ID имеет тип INT. Измените его на BIGINT.
ER_NO_SUCH_TABLE (errno 1146): таблицы нет. Код выше создаёт её при запуске; если вы создали её вручную, проверьте имя.
Неверный токен проявляется после части с базой данных. С нашим поддельным токеном против настоящего Telegram бот вывел Database ready, а затем остановился примерно через четверть секунды с ошибкой GrammyError: Call to 'deleteWebhook' failed! (401: Unauthorized). Скопируйте токен заново у @BotFather и вставьте его на странице Бот , в разделе Telegram. Другие ошибки Telegram: Как исправить ошибку 409 Conflict у Telegram-бота.
Сколько памяти он использует
В нашем тесте работающий бот использовал 67,6 МБ памяти (resident) после ответов на 8 команд, с открытым пулом, в контейнере с лимитом 512 МБ, как у нашего самого маленького тарифа для ботов. В нашем более раннем тесте хостинга один grammY занимал 60 МБ (тот бот остановился на отклонённом токене, не обработав ни одного сообщения), так что разница, около 8 МБ, приходится на mysql2, пул и обработанные команды. Установка grammY и mysql2 через npm с нуля достигла пика 140 МБ, включая файловый кэш.
То же самое с другой библиотекой
Нужен сервер для вашего Telegram-бота с включённой базой данных? Посмотреть тарифы для Telegram-ботов.