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 أبدًا بسلسلة قالب.
خزّن معرّفات Telegram بنوع BIGINT
تقول وثائق Bot API الخاصة بـ Telegram إن معرّف المستخدم أو المحادثة «قد يحتوي على أكثر من 32 بتًا ذا دلالة» لكن بحد أقصى 52، لذا فإن عددًا صحيحًا بحجم 64 بت آمن. وتعطي صفحتها عن المعرّفات النطاقات: تصل معرّفات المستخدمين حتى 1,099,511,627,775، بينما معرّفات المجموعات والمجموعات الخارقة والقنوات سالبة. وتمتد المجموعات الخارقة والقنوات من -1,000,000,000,001 إلى -1,997,852,516,352 (مثلًا -1001234567890). ويتوقف النوع INT في MySQL عند 2,147,483,647. في اختبارنا، وبالوضع الصارم نفسه لـ SQL الذي يستخدمه خادم قواعد البيانات لدينا، فشل حفظ معرّف المستخدم 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، حتى تتسع معرّفات المحادثات أيضًا.
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() يعالج جميع التحديثات بالتسلسل: ينتهي أمر قبل أن يبدأ التالي. في اختبارنا أبقى البوت 1 اتصال مفتوح بعد الرد على 8 أوامر. 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): عمود المعرّف من النوع 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 ميغابايت من الذاكرة (المقيمة) بعد الرد على 8 أوامر، مع فتح مجمّعه، في حاوية محدودة بـ 512 ميغابايت مثل أصغر باقة بوت لدينا. وفي اختبار الاستضافة السابق لدينا، قاس grammY وحده 60 ميغابايت (توقف ذلك البوت عند رمز مرفوض، قبل معالجة أي رسالة)، لذا فإن الفرق، نحو 8 ميغابايت، يغطي mysql2 والمجمّع والأوامر المعالجة. وبلغ تثبيت grammY وmysql2 باستخدام npm من الصفر ذروة 140 ميغابايت، بما في ذلك ذاكرة الملفات المؤقتة.
الأمر نفسه مع مكتبة أخرى
هل تحتاج إلى خادم لبوت Telegram الخاص بك مع قاعدة بيانات مضمنة؟ اطلع على باقات بوتات Telegram.