كيفية بناء بوت ديسكورد لتحدي 100 يوم من البرمجة باستخدام TypeScript وMongoDB

دقائق القراءة: 9
واجهة مقال تقني حول بناء بوت ديسكورد لتحدي 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 في الخادم المستهدف.

شاشة إعداد OAuth2 في ديسكورد مع تحديد النطاق والصلاحيات المناسبة للبوت

إعداد مشروع 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];

مثال على رسالة مدمجة من بوت ديسكورد تعرض تحديث تحدي 100 يوم من البرمجة

بناء أمر عرض التقدم الحالي

أنشئ الملف 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();
  },
};

ثم أضف الأمر إلى القائمة المركزية للأوامر.

رد بوت ديسكورد يعرض تقدم المستخدم الحالي في تحدي 100 يوم من البرمجة

بناء أمر تعديل منشور سابق

إذا ارتكب المستخدم خطأً في تحديثه السابق، فمن المفيد توفير أمر يسمح بتعديل الرسالة المضمنة التي أرسلها البوت.

أنشئ الملف 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 ثم انسخ معرف الرسالة المطلوب تعديلها.

تفعيل وضع المطور في ديسكورد للحصول على معرفات الرسائلنسخ معرف رسالة من ديسكورد لاستخدامه في تعديل منشور البوتمثال على تعديل رسالة مدمجة في بوت ديسكورد بعد استخدام أمر edit

بناء أمر المساعدة لعرض جميع الأوامر

أوامر المساعدة ضرورية لتحسين تجربة المستخدم، خاصة عندما يضم البوت أكثر من أمر.

أنشئ الملف 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 للتخزين المرن يمنحك توازناً ممتازاً بين الأمان والمرونة في مشاريع البوتات الحديثة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *