كيفية بناء بوت ديسكورد لتحدي 100 يوم من البرمجة باستخدام TypeScript وMongoDB
يُعد تحدي 100 Days of Code من أكثر التحديات انتشاراً بين المبرمجين المبتدئين والمطورين الذين يسعون إلى تطوير مهاراتهم بشكل يومي ومنهجي. ونظراً لشعبيته الكبيرة داخل مجتمعات البرمجة، أصبح من المفيد إنشاء بوت على Discord يساعد الأعضاء على تسجيل تقدمهم ومتابعته بسهولة.
في هذا الدليل العملي، ستتعلم كيفية بناء بوت احترافي لتحدي 100 Days of Code باستخدام TypeScript وMongoDB، مع تنظيم المشروع بطريقة قابلة للتوسع والصيانة.
إنشاء تطبيق بوت على Discord
أول خطوة هي إعداد تطبيق البوت من خلال بوابة المطورين الخاصة بـ Discord. توجه إلى Discord Developer Portal، ثم سجّل الدخول إن لزم الأمر، وبعدها اختر Applications من القائمة الجانبية.

إذا كانت هذه أول مرة تنشئ فيها بوتاً، فلن ترى أي تطبيقات سابقة. اضغط على زر New Application، ثم اختر اسماً مناسباً للتطبيق وحدده كتطبيق شخصي.
بعد ذلك ستنتقل إلى صفحة إعدادات التطبيق، حيث يمكنك تغيير الاسم أو إضافة صورة رمزية. من الشريط الجانبي، اختر Bot ثم اضغط Add Bot. بهذه الخطوة سيتم إنشاء حساب بوت مرتبط بالتطبيق.

في هذه الصفحة ستحصل على رمز البوت token. احرص على إبقاء هذا الرمز سرياً تماماً، لأنه المفتاح الذي يسمح لتطبيقك بالاتصال بالبوت. لا تشاركه مع أي شخص.
لإضافة البوت إلى خادم، انتقل إلى قسم OAuth2 من الشريط الجانبي. ضمن أداة OAuth2 URL Generator، اترك قائمة Select Redirect URL فارغة، ثم فعّل نطاق bot. بعد ذلك ستظهر قائمة الصلاحيات، وحدد الصلاحيات التالية:
- إرسال الرسائل
Send Messages - إدارة الرسائل
Manage Messages - تضمين الروابط
Embed Links - قراءة سجل الرسائل
Read Message History - عرض القنوات
View Channels
سيتم إنشاء رابط تلقائياً، انسخه وافتحه في المتصفح لإتمام إضافة البوت إلى الخادم. تذكّر أنك تحتاج إلى صلاحية Manage Server في الخادم المستهدف.

إعداد مشروع TypeScript للبوت
إنشاء ملف package.json
أنشئ مجلداً جديداً للمشروع، ثم افتح الطرفية داخله ونفّذ الأمر npm init لإنشاء ملف package.json. يمكن الاكتفاء بالقيم الافتراضية في هذا الشرح.
سيكون الشكل الأولي للملف قريباً من التالي:
{
"name": "tutorial",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
بعد ذلك عدّل قيمة main من index.js إلى ./prod/index.js، واحذف السكربت test، ثم أضف السكربتين التاليين:
"build": "tsc",
"start": "node -r dotenv/config ./prod/index.js"
السكريبت build مسؤول عن تحويل كود TypeScript إلى JavaScript، بينما start يشغّل نقطة الدخول بعد تحميل متغيرات البيئة عبر الحزمة dotenv.
تثبيت الاعتماديات الأساسية
ثبّت الحزم التالية باستخدام npm install:
discord.js: للتعامل مع واجهةDiscord APIوالأحداث.dotenv: لتحميل القيم من ملف.env.mongoose: لتسهيل الاتصال بقاعدة بياناتMongoDBوتنظيم البيانات.
ثم ثبّت اعتماديات التطوير باستخدام npm install --save-dev:
typescript: لكتابة المشروع بلغةTypeScript.@types/node: لتوفير تعريفات الأنواع الخاصة ببيئةNode.js.
بعد التثبيت، سيبدو ملف package.json بالشكل التالي تقريباً:
{
"name": "tutorial",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc",
"start": "node -r dotenv/config ./prod/index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"discord.js": "^12.5.3",
"dotenv": "^10.0.0",
"mongoose": "^5.12.14"
},
"devDependencies": {
"@types/node": "^15.12.2",
"typescript": "^4.3.4"
}
}
إعداد ملف tsconfig.json
أنشئ ملف tsconfig.json في جذر المشروع، ثم استخدم الإعدادات التالية:
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"rootDir": "./src",
"outDir": "./prod",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
}
}
أهم خيارين هنا هما rootDir وoutDir، إذ يحددان مكان كود المصدر ومجلد الإخراج بعد الترجمة.
إعداد ملف .gitignore
إذا كنت تستخدم git، فمن الضروري منع رفع الملفات الحساسة وغير الضرورية إلى المستودع. أنشئ ملف .gitignore وأضف إليه:
/node_modules/
/prod/
.env
بهذا ستتجنب رفع الحزم المثبتة، والملفات المترجمة، ومتغيرات البيئة الحساسة.
إنشاء الاتصال الأولي مع بوت Discord
أنشئ مجلد src وداخله ملف index.ts. سنبدأ بدالة ذاتية التنفيذ من نوع IIFE حتى نتمكن من استخدام await على المستوى الأعلى:
(async () => {
})();
في أعلى الملف، استورد الصنف Client:
import { Client } from "discord.js";
ثم أنشئ كائناً جديداً للبوت وسجّل دخوله باستخدام الرمز السري:
import { Client } from "discord.js";
(async () => {
const BOT = new Client();
await BOT.login(process.env.BOT_TOKEN);
})();
أنشئ ملف .env في جذر المشروع وأضف السطر التالي:
BOT_TOKEN=""
ضع قيمة الرمز بين علامتي الاقتباس.
الاستماع إلى حدث الجاهزية
للتأكد من أن البوت متصل فعلاً، أضف مستمعاً للحدث ready:
BOT.on("ready", () => console.log("Connected to Discord!"));
بعد تنفيذ npm run build ثم npm start، من المفترض أن ترى رسالة الاتصال في الطرفية.
ربط المشروع بقاعدة بيانات MongoDB
يمكنك استخدام نسخة محلية من MongoDB أو الاعتماد على خدمة MongoDB Atlas. بعد الحصول على رابط الاتصال، أضفه إلى ملف .env بهذا الشكل:
MONGO_URI=""
استخدم اسم قاعدة بيانات مثل oneHundredDays.
أنشئ المجلد database ثم الملف connectDatabase.ts، وأضف الكود التالي:
import { connect } from "mongoose";
export const connectDatabase = async () => {
await connect(process.env.MONGO_URI as string);
console.log("Database Connected!");
};
بعد ذلك، استورده داخل ملف index.ts ونفّذه قبل تسجيل دخول البوت.
التحقق من متغيرات البيئة
من المشكلات الشائعة أن تكون قيم process.env غير معرّفة undefined. لهذا من الأفضل إنشاء دالة للتحقق منها مسبقاً.
أنشئ الملف src/utils/validateEnv.ts وأضف:
export const validateEnv = () => {
if (!process.env.BOT_TOKEN) {
console.warn("Missing Discord bot token.");
return false;
}
if (!process.env.MONGO_URI) {
console.warn("Missing MongoDB connection.");
return false;
}
return true;
};
ثم عد إلى index.ts واستخدم الدالة في بداية التنفيذ:
import { Client } from "discord.js";
import { connectDatabase } from "./database/connectDatabase";
import { validateEnv } from "./utils/validateEnv";
(async () => {
if (!validateEnv()) return;
const BOT = new Client();
BOT.on("ready", () => console.log("Connected to Discord!"));
await connectDatabase();
await BOT.login(process.env.BOT_TOKEN);
})();
التعامل مع الرسائل داخل Discord
لكي يستجيب البوت للرسائل، نحتاج إلى الاستماع لحدث message. أنشئ الملف src/events/onMessage.ts وأضف:
import { Message } from "discord.js";
export const onMessage = async (message: Message) => {
};
ثم اربط هذا الملف بحدث الرسائل في index.ts:
BOT.on("message", async (message) => await onMessage(message));
لاختبار الالتقاط، يمكنك مؤقتاً استخدام:
console.log(message.content);
تنظيم الأوامر بطريقة احترافية
إنشاء واجهة موحدة للأوامر
أنشئ الملف src/interfaces/CommandInt.ts:
import { Message } from "discord.js";
export interface CommandInt {
name: string;
description: string;
run: (message: Message) => Promise<void>;
}
هذه الواجهة تضمن أن جميع الأوامر تستخدم بنية موحدة، مما يسهل الصيانة والتوسع مستقبلاً.
إنشاء قائمة مركزية للأوامر
أنشئ الملف src/commands/_CommandList.ts:
import { CommandInt } from "../interfaces/CommandInt";
export const CommandList: CommandInt[] = [];
التحقق من البادئة وتنفيذ الأوامر
حدّث ملف onMessage.ts ليتجاهل رسائل البوتات، ويتحقق من بادئة الأوامر مثل !:
import { Message } from "discord.js";
import { CommandList } from "../commands/_CommandList";
export const onMessage = async (message: Message) => {
if (message.author.bot) {
return;
}
const prefix = "!";
if (!message.content.startsWith(prefix)) {
return;
}
for (const Command of CommandList) {
if (message.content.startsWith(prefix + Command.name)) {
await Command.run(message);
break;
}
}
};
إنشاء نموذج البيانات في MongoDB
لأن البوت سيتتبع تقدم المستخدمين، فنحن بحاجة إلى نموذج بيانات منظم. أنشئ الملف src/database/models/CamperModel.ts وأضف:
import { Document, model, Schema } from "mongoose";
export interface CamperInt extends Document {
discordId: string;
round: number;
day: number;
timestamp: number;
}
export const Camper = new Schema({
discordId: String,
round: Number,
day: Number,
timestamp: Number,
});
export default model<CamperInt>("camper", Camper);
هذا النموذج يخزن:
discordId: المعرف الفريد للمستخدم.round: رقم الجولة الحالية من التحدي.day: اليوم الحالي داخل الجولة.timestamp: وقت آخر تحديث.
بناء أمر إنشاء تحديث 100 يوم من البرمجة
أنشئ الملف src/commands/oneHundred.ts:
import { CommandInt } from "../interfaces/CommandInt";
import CamperModel from "../database/models/CamperModel";
import { MessageEmbed } from "discord.js";
export const oneHundred: CommandInt = {
name: "100",
description: "Creates a 100 Days of Code update",
run: async (message) => {
const { author, channel, content } = message;
const text = content.split(" ").slice(1).join(" ");
let targetCamperData = await CamperModel.findOne({ discordId: author.id });
if (!targetCamperData) {
targetCamperData = await CamperModel.create({
discordId: author.id,
round: 1,
day: 0,
timestamp: Date.now(),
});
}
targetCamperData.day++;
if (targetCamperData.day > 100) {
targetCamperData.day = 1;
targetCamperData.round++;
}
targetCamperData.timestamp = Date.now();
await targetCamperData.save();
const oneHundredEmbed = new MessageEmbed();
oneHundredEmbed.setTitle("100 Days of Code");
oneHundredEmbed.setDescription(text);
oneHundredEmbed.setAuthor(
author.username + "#" + author.discriminator,
author.displayAvatarURL()
);
oneHundredEmbed.addField("Round", targetCamperData.round, true);
oneHundredEmbed.addField("Day", targetCamperData.day, true);
oneHundredEmbed.setFooter(
"Day completed: " + new Date(targetCamperData.timestamp).toLocaleDateString()
);
await channel.send(oneHundredEmbed);
await message.delete();
},
};
بعد ذلك أضف الأمر إلى ملف _CommandList.ts:
import { CommandInt } from "../interfaces/CommandInt";
import { oneHundred } from "./oneHundred";
export const CommandList: CommandInt[] = [oneHundred];

بناء أمر عرض التقدم الحالي
أنشئ الملف src/commands/view.ts:
import { CommandInt } from "../interfaces/CommandInt";
import CamperModel from "../database/models/CamperModel";
import { MessageEmbed } from "discord.js";
export const view: CommandInt = {
name: "view",
description: "Views your 100 Days of Code progress.",
run: async (message) => {
const { author, channel } = message;
const targetCamperData = await CamperModel.findOne({
discordId: author.id,
});
if (!targetCamperData) {
await channel.send("You have not started the challenge yet.");
return;
}
const camperEmbed = new MessageEmbed();
camperEmbed.setTitle("My 100DoC Progress");
camperEmbed.setDescription(
`Here is my 100 Days of Code progress. I last reported an update on ${new Date(
targetCamperData.timestamp
).toLocaleDateString()}.`
);
camperEmbed.addField("Round", targetCamperData.round, true);
camperEmbed.addField("Day", targetCamperData.day, true);
camperEmbed.setAuthor(
author.username + "#" + author.discriminator,
author.displayAvatarURL()
);
await channel.send(camperEmbed);
await message.delete();
},
};
ثم أضف الأمر إلى القائمة المركزية للأوامر.

بناء أمر تعديل منشور سابق
إذا ارتكب المستخدم خطأً في تحديثه السابق، فمن المفيد توفير أمر يسمح بتعديل الرسالة المضمنة التي أرسلها البوت.
أنشئ الملف src/commands/edit.ts:
import { CommandInt } from "../interfaces/CommandInt";
export const edit: CommandInt = {
name: "edit",
description: "Edits a previous 100 Days of Code post.",
run: async (message) => {
const { author, channel, content } = message;
const [, targetId, ...text] = content.split(" ");
const targetMessage = await channel.messages.fetch(targetId);
if (!targetMessage) {
await channel.send("That does not appear to be a valid message ID.");
return;
}
const targetEmbed = targetMessage.embeds[0];
if (targetEmbed.author?.name !== author.username + "#" + author.discriminator) {
await channel.send(
"This does not appear to be your 100 Days of Code post. You cannot edit it."
);
return;
}
targetEmbed.setDescription(text.join(" "));
await targetMessage.edit(targetEmbed);
await message.delete();
},
};
لاختبار هذا الأمر، فعّل Developer Mode في تطبيق Discord ثم انسخ معرف الرسالة المطلوب تعديلها.



بناء أمر المساعدة لعرض جميع الأوامر
أوامر المساعدة ضرورية لتحسين تجربة المستخدم، خاصة عندما يضم البوت أكثر من أمر.
أنشئ الملف src/commands/help.ts:
import { CommandInt } from "../interfaces/CommandInt";
import { MessageEmbed } from "discord.js";
import { CommandList } from "./_CommandList";
export const help: CommandInt = {
name: "help",
description: "Returns information on the bot's available commands.",
run: async (message) => {
const helpEmbed = new MessageEmbed();
helpEmbed.setTitle("Available Commands!");
helpEmbed.setDescription("These are the available commands for this bot.");
helpEmbed.addField(
"Commands:",
CommandList.map((el) => `\`!${el.name}\`: ${el.description}`).join("\n")
);
await message.channel.send(helpEmbed);
},
};
ثم حدّث ملف _CommandList.ts ليشمل جميع الأوامر:
import { CommandInt } from "../interfaces/CommandInt";
import { oneHundred } from "./oneHundred";
import { view } from "./view";
import { edit } from "./edit";
import { help } from "./help";
export const CommandList: CommandInt[] = [oneHundred, view, edit, help];

أفضل ممارسات تقنية لتحسين المشروع
نصائح مهمة قبل النشر
- احفظ القيم الحساسة مثل
BOT_TOKENوMONGO_URIداخل.envفقط. - احرص على فصل المنطق البرمجي إلى ملفات مستقلة مثل
eventsوcommandsوdatabase. - اختبر كل أمر بشكل منفصل قبل دمجه في المشروع النهائي.
- استخدم
TypeScriptبصرامة عبر خيارstrictلتقليل الأخطاء المنطقية. - فكر لاحقاً في إضافة تسجيل أخطاء مركزي ونظام صلاحيات للأوامر.
لماذا هذا الهيكل مناسب للتوسع؟
هذا الأسلوب في البناء يجعل البوت قابلاً لإضافة أوامر جديدة بسهولة، مثل أوامر الإحصاءات، أو لوحات الصدارة، أو تذكيرات يومية، أو حتى تكاملات مع خدمات خارجية. كما أن استخدام mongoose مع نماذج واضحة يساعد على تجنب فساد البيانات ويسهّل التطوير الجماعي.
الخلاصة التقنية
بناء بوت Discord باستخدام TypeScript وMongoDB ليس مجرد تجربة تعليمية ممتعة، بل هو مشروع عملي يجمع بين التعامل مع الأحداث، وإدارة قواعد البيانات، وتنظيم الكود وفق أسس احترافية. إذا طبّقت هذا الهيكل كما ورد في الشرح، فستحصل على بوت منظم، قابل للتطوير، ومناسب لإنشاء مزايا إضافية مستقبلاً دون تعقيد كبير. ومن الناحية التقنية، فإن الجمع بين TypeScript للتحقق من الأنواع وMongoDB للتخزين المرن يمنحك توازناً ممتازاً بين الأمان والمرونة في مشاريع البوتات الحديثة.