مقالات › C#

ساخت ربات تلگرام با سی‌شارپ و .NET؛ از Polling تا Webhook

ساخت ربات تلگرام با C#: ساخت ربات در BotFather، کتابخانه‌ی Telegram.Bot، دستورات و دکمه‌های شیشه‌ای، وضعیت گفتگو و Webhook در ASP.NET Core.

ربات‌های تلگرام یکی از سریع‌ترین راه‌ها برای ارائه‌ی یک رابط کاربردی به کاربران هستند: بدون نیاز به نصب اپلیکیشن، با اعلان داخلی و یک API غنی شامل دکمه، فایل و پرداخت. کسب‌وکارها از آن‌ها برای اطلاع‌رسانی سفارش، پشتیبانی، ابزارهای داخلی و هشدارها استفاده می‌کنند. با C# و کتابخانه‌ی Telegram.Bot می‌توانید در یک بعدازظهر یک ربات مقاوم بسازید. این راهنما از اولین «سلام» تا رباتی مبتنی بر Webhook که در ASP.NET Core میزبانی می‌شود پیش می‌رود.

گام ۱: ساخت ربات با BotFather

  1. در تلگرام @BotFather را باز کنید و /newbot را بفرستید.
  2. یک نام نمایشی و یک نام کاربری که به bot ختم شود انتخاب کنید (مثلاً ShopHelperBot).
  3. BotFather یک توکن مثل 123456:ABC-DEF... به شما می‌دهد.

هشدار: توکن حکم رمز عبور را دارد و هر کسی آن را داشته باشد ربات شما را کنترل می‌کند. هرگز آن را Commit نکنید؛ در User Secrets، متغیر محیطی یا Secret Store نگه دارید. اگر لو رفت، با /revoke در BotFather باطلش کنید.

فهرست دستورات را هم تنظیم کنید (/setcommands) تا کاربر منو ببیند:

start - شروع ربات
orders - سفارش‌های اخیر من
help - راهنما و پشتیبانی

گام ۲: یک ربات ساده با Polling

dotnet new worker -n ShopBot
cd ShopBot
dotnet add package Telegram.Bot
dotnet user-secrets set "Bot:Token" "123456:ABC-DEF..."

Long Polling در یک حلقه از تلگرام به‌روزرسانی‌های جدید را می‌پرسد؛ برای توسعه عالی است و پشت NAT بدون آدرس عمومی هم کار می‌کند.

using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;

public class BotWorker(ITelegramBotClient bot, IServiceScopeFactory scopes, ILogger<BotWorker> logger)
    : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var me = await bot.GetMe(stoppingToken);
        logger.LogInformation("Bot @{Username} started", me.Username);

        await bot.ReceiveAsync(
            updateHandler: HandleUpdateAsync,
            errorHandler: HandleErrorAsync,
            receiverOptions: new ReceiverOptions { AllowedUpdates = [UpdateType.Message, UpdateType.CallbackQuery] },
            cancellationToken: stoppingToken);
    }

    private async Task HandleUpdateAsync(ITelegramBotClient _, Update update, CancellationToken ct)
    {
        using var scope = scopes.CreateScope();
        var handler = scope.ServiceProvider.GetRequiredService<UpdateHandler>();
        await handler.HandleAsync(update, ct);
    }

    private Task HandleErrorAsync(ITelegramBotClient _, Exception ex, HandleErrorSource source, CancellationToken ct)
    {
        logger.LogError(ex, "Telegram polling error ({Source})", source);
        return Task.CompletedTask;
    }
}
// Program.cs
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<ITelegramBotClient>(_ => new TelegramBotClient(builder.Configuration["Bot:Token"]!));
builder.Services.AddScoped<UpdateHandler>();
builder.Services.AddHostedService<BotWorker>();
builder.Build().Run();

دقت کنید که Handler برای هر به‌روزرسانی در یک Scope جدا ساخته می‌شود؛ پس می‌تواند از سرویس‌های Scoped مثل DbContext استفاده کند.

نکته: نام متدها در نسخه‌های اصلی مختلف Telegram.Bot فرق دارد (نسخه‌های قدیمی‌تر از GetMeAsync و SendTextMessageAsync استفاده می‌کنند). نسخه‌ای را که نصب می‌کنید بررسی کنید؛ مفاهیم یکسان‌اند.

گام ۳: مدیریت دستورات و پیام‌ها

public class UpdateHandler(ITelegramBotClient bot, IOrderService orders)
{
    public async Task HandleAsync(Update update, CancellationToken ct)
    {
        switch (update)
        {
            case { Message: { Text: { } text } message }:
                await HandleMessageAsync(message, text, ct);
                break;
            case { CallbackQuery: { } query }:
                await HandleCallbackAsync(query, ct);
                break;
        }
    }

    private async Task HandleMessageAsync(Message message, string text, CancellationToken ct)
    {
        var chatId = message.Chat.Id;
        var command = text.Split(' ', 2)[0].Split('@')[0].ToLowerInvariant();

        switch (command)
        {
            case "/start":
                await bot.SendMessage(chatId,
                    $"سلام {message.From?.FirstName}! 👋\nمی‌توانم سفارش‌هایت را نشان دهم و تو را به پشتیبانی وصل کنم.",
                    replyMarkup: MainMenu(), cancellationToken: ct);
                break;

            case "/orders":
                var list = await orders.GetRecentAsync(chatId, ct);
                var body = list.Count == 0
                    ? "هنوز سفارشی ثبت نکرده‌ای."
                    : string.Join("\n", list.Select(o => $"• <b>#{o.Number}</b> — {o.Status} — {o.Total:N0} تومان"));
                await bot.SendMessage(chatId, body, parseMode: ParseMode.Html, cancellationToken: ct);
                break;

            default:
                await bot.SendMessage(chatId, "متوجه نشدم. /help را امتحان کن.", cancellationToken: ct);
                break;
        }
    }
}

برای قالب‌بندی از ParseMode.Html استفاده کنید و متن ورودی کاربر را Escape کنید (WebUtility.HtmlEncode)؛ وگرنه یک < سرگردان پیام را خراب می‌کند.

گام ۴: دکمه‌های شیشه‌ای و Callback

دکمه‌های شیشه‌ای (Inline) زیر پیام ظاهر می‌شوند و با لمس، یک Callback Query می‌فرستند:

private static InlineKeyboardMarkup MainMenu() => new(new[]
{
    new[] { InlineKeyboardButton.WithCallbackData("📦 سفارش‌های من", "orders"),
            InlineKeyboardButton.WithCallbackData("💬 پشتیبانی", "support") },
    new[] { InlineKeyboardButton.WithUrl("🌐 وب‌سایت", "https://example.com") },
});

private async Task HandleCallbackAsync(CallbackQuery query, CancellationToken ct)
{
    await bot.AnswerCallbackQuery(query.Id, cancellationToken: ct);   // توقف چرخش لودینگ

    switch (query.Data)
    {
        case "orders":
            await HandleMessageAsync(query.Message!, "/orders", ct);
            break;
        case "support":
            await bot.SendMessage(query.Message!.Chat.Id, "سؤالت را بنویس تا تیم ما پاسخ دهد.", cancellationToken: ct);
            break;
    }
}

Callback Data به ۶۴ بایت محدود است؛ شناسه ذخیره کنید ("order:1542")، نه کل شیء. همیشه AnswerCallbackQuery را صدا بزنید، وگرنه دکمه برای کاربر مدام در حال چرخش می‌ماند. برای ناوبری منومانند هم می‌توانید پیام اصلی را با EditMessageText ویرایش کنید.

گام ۵: وضعیت گفتگو

جریان‌های چندمرحله‌ای («شماره‌ات را بفرست» و بعد «آدرست را بفرست») به وضعیت جداگانه برای هر چت نیاز دارند:

public enum Step { None, AwaitingPhone, AwaitingAddress }

public class ConversationStore(IMemoryCache cache)
{
    public Step Get(long chatId) => cache.TryGetValue(chatId, out Step s) ? s : Step.None;
    public void Set(long chatId, Step step) => cache.Set(chatId, step, TimeSpan.FromMinutes(30));
}

نگه‌داری در حافظه برای یک نمونه کافی است؛ برای ری‌استارت و چند نمونه، وضعیت را در دیتابیس یا Redis ذخیره کنید.

برای گرفتن امن شماره‌ی تلفن از دکمه‌ی درخواست مخاطب استفاده کنید؛ تلگرام شماره‌ی تأییدشده‌ی کاربر را می‌فرستد:

var keyboard = new ReplyKeyboardMarkup(KeyboardButton.WithRequestContact("📱 ارسال شماره‌ی من")) { ResizeKeyboard = true, OneTimeKeyboard = true };

گام ۶: Webhook با ASP.NET Core

در محیط تولید Webhook کارآمدتر است: تلگرام هر به‌روزرسانی را با POST به Endpoint ‏HTTPS شما می‌فرستد.

app.MapPost("/telegram/{secret}", async (
    string secret, Update update, UpdateHandler handler,
    IConfiguration config, CancellationToken ct) =>
{
    if (secret != config["Bot:WebhookSecret"]) return Results.NotFound();
    await handler.HandleAsync(update, ct);
    return Results.Ok();
});

Webhook را یک بار هنگام راه‌اندازی ثبت کنید:

await bot.SetWebhook(
    url: $"https://bot.example.com/telegram/{config["Bot:WebhookSecret"]}",
    secretToken: config["Bot:HeaderSecret"],     // تلگرام آن را در هدر X-Telegram-Bot-Api-Secret-Token می‌فرستد
    allowedUpdates: [UpdateType.Message, UpdateType.CallbackQuery],
    dropPendingUpdates: true);

الزامات و نکات:

  • یک آدرس HTTPS عمومی با گواهی معتبر (پورت‌های 443، 80، 88 یا 8443).
  • هدر X-Telegram-Bot-Api-Secret-Token را بررسی کنید تا فقط تلگرام بتواند Endpoint را صدا بزند.
  • سریع پاسخ دهید. اگر پردازش طول می‌کشد (گزارش، هوش مصنوعی)، کار را در صف بگذارید (یک Channel<Update> با Worker پس‌زمینه) و فوراً 200 برگردانید؛ وگرنه تلگرام دوباره تلاش می‌کند و ممکن است به‌روزرسانی دو بار پردازش شود.
  • نمی‌توان هم‌زمان از Polling و Webhook استفاده کرد؛ برای بازگشت به Polling، DeleteWebhook را صدا بزنید.

ارسال اعلان از برنامه

همین کلاینت از هر جای برنامه پیام می‌فرستد؛ مثلاً وقتی سفارش ارسال شد:

public class OrderShippedNotifier(ITelegramBotClient bot)
{
    public Task NotifyAsync(long chatId, string orderNumber, CancellationToken ct) =>
        bot.SendMessage(chatId, $"🚚 سفارش <b>#{orderNumber}</b> ارسال شد!", parseMode: ParseMode.Html, cancellationToken: ct);
}

ربات فقط به کاربرانی پیام می‌دهد که آن را Start کرده باشند؛ chatId هر کاربر را هنگام ارسال /start ذخیره کنید (و به حسابش متصل کنید؛ مثلاً با لینک یک‌بارمصرف t.me/ShopHelperBot?start=TOKEN).

محدودیت‌ها و پایداری

  • حدود ۳۰ پیام در ثانیه در مجموع و حدود یک پیام در ثانیه برای هر چت؛ ارسال انبوه به کنترل نرخ نیاز دارد. در خطای 429 به اندازه‌ی retry_after صبر کنید.
  • کاربر می‌تواند ربات را بلاک کند؛ خطای 403 Forbidden را مدیریت و کاربر را غیرفعال علامت بزنید.
  • شناسه‌ی هر به‌روزرسانی را لاگ کنید و موارد تکراری را Idempotent مدیریت کنید.
  • در مناطقی که تلگرام فیلتر است، سروری که ربات را میزبانی می‌کند باید دسترسی پایدار به api.telegram.org داشته باشد (مثلاً سرور خارج از کشور).

جمع‌بندی

ربات را با BotFather بسازید و توکن را محرمانه نگه دارید، از Telegram.Bot همراه Hosted Service و Handlerهای Scoped استفاده کنید، منوها را با دکمه‌های شیشه‌ای بسازید و همیشه به Callbackها پاسخ دهید، وضعیت گفتگو را برای جریان‌های چندمرحله‌ای ذخیره کنید و در تولید به Webhook امن با پاسخ سریع مهاجرت کنید. با این الگوها ربات به یک کانال قابل اعتماد برای اعلان، پشتیبانی و ابزارهای داخلی تبدیل می‌شود.

پرسش‌های متداول

Polling یا Webhook؟

Polling برای توسعه و ربات‌های داخلی ساده؛ Webhook برای تولید، چون آنی و کارآمد است و همراه وب‌سرور شما مقیاس می‌پذیرد.

آیا ربات می‌تواند فایل و عکس دریافت کند؟

بله. پیام‌ها شامل Photo، Document، Voice و غیره هستند و با GetFile و DownloadFile دانلود می‌شوند. ربات‌ها با API استاندارد فایل تا ۲۰ مگابایت را دانلود می‌کنند.

چطور کاربر تلگرام را به حساب کاربری سایتم متصل کنم؟

در سایت یک توکن یک‌بارمصرف بسازید، کاربر را به t.me/YourBot?start=TOKEN بفرستید و وقتی ربات /start TOKEN را دریافت کرد، chatId را همراه حساب متناظر ذخیره کنید.