.NET Telegram Bot Framework

repository·master·Indexed 42 days ago

https://github.com/majmccloud/telegrambotframework

A context-based .NET addon for building Telegram bots using a structured, form-driven approach. It features a lifecycle-based form system (FormBase), session serialization, and flexible message loop implementations including FormBaseMessageLoop and MiddlewareBaseMessageLoop. The framework provides tools for managing inline buttons, reply keyboards, and a ProgressBar control, with a BotBaseBuilder for configuration and command synchronization.

Tokens
10.9K
Snippets
28
Records
36
Agent score
53%

What's inside Telegram Bot Framework

  1. Create a custom Form by subclassing FormBase

    master

    In this framework, user interaction is managed through Form objects. Every user is internally redirected to your designated StartForm (a subclass of FormBase in the TelegramBotBase.Base namespace) when they first interact with the bot.

    To build a functional form, you override lifecycle methods and subscribe to events:

    Lifecycle Methods

    • PreLoad(MessageResult message): Invoked during navigation to the form.
    • Load(MessageResult message): Invoked on every Message, Action, or Data received in this context.
    • Edited(MessageResult message): Invoked when a message is edited.
    • Action(MessageResult message): Invoked on button clicks.
    • SentData(DataResult data): Invoked when the user uploads data (Photo, Audio, Video, Contact, Location, Document).
    • Render(MessageResult message): Invoked to render the design or response.

    Events

    • Init: Invoked on initialization, before navigation.
    • Opened: Invoked after the form is opened.
    • Closed: Invoked after the form has been closed.

    Interacting with the User

    Use this.Device.Send("message") to respond to the user in the current chat. To move to a different form, instantiate the new form and call await this.NavigateTo(newForm).

    public class Start : FormBase
    {
        public Start()
        {
            Init += Start_Init;
            Opened += Start_Opened;
            Closed += Start_Closed;
        }
    
        private async Task Start_Init(object sender, Args.InitEventArgs e) { }
        private async Task Start_Opened(object sender, EventArgs e) { }
        private async Task Start_Closed(object sender, EventArgs e) { }
    
        public override async Task PreLoad(MessageResult message) { }
    
        public override async Task Load(MessageResult message)
        {
            // `Device` is a wrapper for current chat
            await this.Device.Send("Hello world!");
        }
    
        public override async Task Edited(MessageResult message) { }
        public override async Task Action(MessageResult message) { }
        public override async Task SentData(DataResult data) { }
        public override async Task Render(MessageResult message) { }
    
        // Navigation example
        public async Task GoToNext() 
        {
            var form = new TestForm();
            await this.NavigateTo(form);
        }
    }
  2. Choose a Message Loop implementation

    master

    Message loops define how incoming updates and messages are processed. You should choose an implementation based on your bot's complexity:

    • FormBaseMessageLoop: The standard choice for most bots. It handles messages, edits, and callback queries, automatically triggering lifecycle events like PreLoad, LoadControls, Load, Action, SentData, Edited, and Render.
    • MinimalMessageLoop: A lightweight loop that only calls the Load event. Use this for very simple bots.
    • MiddlewareBaseMessageLoop: A flexible, modular loop where you can chain custom processing steps (middlewares). Ideal for complex bots requiring authentication, logging, or custom routing.
    • FullMessageLoop: The most comprehensive loop that reacts to all update types and triggers all relevant events. Use this only when you need maximum control over every update type.
  3. Use the Action Manager for unhandled actions and notifications

    master
    The Action Manager (provided via TelegramBotBase.Extensions.ActionManager) provides a way to manage unhandled action calls. It also enables handling notifications with inline buttons that can be shown to users from outside the standard messageloop (e.g., notifying a user in a group about a new event).
  4. Manage group and channel interactions with SplitterForm and GroupForm

    master

    The framework provides specialized base classes to handle different Telegram chat types:

    SplitterForm

    Use SplitterForm to easily switch logic between Single-User chats and Group/Channel management. You can override specific methods to handle different chat contexts:

    • Open(MessageResult e): The fallback method if no other specific method is overridden.
    • OpenGroup(MessageResult e): Triggered for group chats.
    • OpenChannel(MessageResult e): Triggered for channels.
    • OpenSupergroup(MessageResult e): Triggered for supergroups.

    GroupForm

    Use GroupForm for managing groups and channels. It provides specialized event hooks within its Load method:

    • OnMemberChanges(MemberChangeEventArgs e): Triggered when members are added or leave.
    • OnGroupChanged(GroupChangedEventArgs e): Triggered when group metadata changes (e.g., title, photo, pinning, or migration).
    • OnMessage(MessageResult e): The default handler for standard messages.
    public class Start : SplitterForm
    {
        public override async Task<bool> Open(MessageResult e)
        {
            var st = new Menu();
            await this.NavigateTo(st);
            return true;
        }
    
        public override async Task<bool> OpenGroup(MessageResult e)
        {
            var st = new Groups.LinkReplaceTest();
            await this.NavigateTo(st);
            return true;
        }
    }
  5. Implement a custom Message Loop

    master

    To gain full control over update processing, implement the IMessageLoopFactory interface. You must define ConfigureUpdateTypes() to specify which updates to listen for and implement the MessageLoop method to handle the logic.

    Example Implementation:

    public class CustomMessageLoop : IMessageLoopFactory
    {
        public UpdateType[] ConfigureUpdateTypes()
        {
            return Update.AllTypes;
        }
    
        public async Task MessageLoop(BotBase bot, IDeviceSession session, UpdateResult ur, MessageResult mr)
        {
            var activeForm = session.ActiveForm;
    
            //Loading Event
            await activeForm.Load(mr);
        }
    }

    Registering the custom loop in the builder:

    var bot = BotBaseBuilder
            .Create()
            .WithAPIKey("{YOUR API KEY}")
            .CustomMessageLoop(new CustomMessageLoop())
            .WithStartForm<StartForm>()
            .Build();
    
    await bot.Start();
    public class CustomMessageLoop : IMessageLoopFactory
    {
        public UpdateType[] ConfigureUpdateTypes()
        {
            return Update.AllTypes;
        }
    
        public async Task MessageLoop(BotBase bot, IDeviceSession session, UpdateResult ur, MessageResult mr)
        {
            var activeForm = session.ActiveForm;
    
            //Loading Event
            await activeForm.Load(mr);
        }
    }
    
    var bot = BotBaseBuilder
            .Create()
            .WithAPIKey("{YOUR API KEY}")
            .CustomMessageLoop(new CustomMessageLoop())
            .WithStartForm<StartForm>()
            .Build();
    
    await bot.Start();
  6. Configure threading and performance models

    master

    You can control how the bot processes updates and messages using the BotBaseBuilder during setup. This allows you to balance simplicity and performance.

    Threading Options

    • .UseSingleThread(): Default. Processes all updates sequentially on a single thread. Recommended for simple bots, bots using non-thread-safe resources, or when strict message order is required.
    • .UseThreadPool(): Enables concurrent processing using the .NET thread pool. Improves throughput for high-traffic bots. Warning: Message order is no longer guaranteed, and handlers must be thread-safe.
    • .UseThreadPool(workerThreads, ioThreads): Allows fine-tuning the number of worker and I/O threads for specific concurrency requirements.

    Best Practices

    • Use async/await for all I/O-bound operations (network, database) to prevent blocking.
    • Avoid blocking calls like Thread.Sleep, .Result, or .Wait().
    • For CPU-bound work, offload to background tasks using Task.Run.
    • Start with .UseSingleThread() and switch to .UseThreadPool() only if performance issues arise.
    var bot = BotBaseBuilder
        .Create()
        .WithAPIKey("{YOUR API KEY}")
        .DefaultMessageLoop()
        .WithStartForm<StartForm>()
        .NoProxy()
        .DefaultCommands()
        .NoSerialization()
        .UseEnglish()
        .UseThreadPool() // or .UseSingleThread()
        .Build();
  7. Use Newtonsoft.Json for session serialization

    master

    If you need to recover sessions using older dependencies, use the TelegramBotBase.Extensions.Serializer.Legacy.NewtonsoftJson extension. This allows you to use Newtonsoft.Json for session serialization within the bot builder pipeline by calling .UseNewtonsoftJson().

    using TelegramBotBase.Extensions.Serializer.Legacy.NewtonsoftJson;
    
    var bot = BotBaseBuilder
                .Create()
                .WithAPIKey(APIKey)
                .DefaultMessageLoop()
                .WithStartForm<Start>()
                .NoProxy()
                .OnlyStart()
                .UseNewtonsoftJson()
                .UseEnglish()
                .Build();
    
    bot.Start();
  8. Explore framework examples

    master

    The repository contains several example projects in the Examples/ folder to demonstrate specific use cases:

    • SystemCommandsBot: Running system commands or managing processes with password authentication.
    • EFCoreBot: Using a minimal .NET console template with Entity Framework Core and Dependency Injection.
    • JoinHiderBot: Automatically deleting 'Join' and 'Leave' messages in groups.
    • AsyncFormUpdates: Updating forms asynchronously without user interaction using the InvokeMessageLoop method.
    • BotAndWebApplication: Running a Telegram bot alongside an existing web application.
    • InlineAndReplyCombination: Using InlineMarkup and ReplyMarkup simultaneously.
    • DependencyInjection: A full implementation example using Dependency Injection.
  9. Manage navigation stacks with NavigationController

    master

    While NavigateTo performs direct (cross) navigation, the NavigationController allows for linear, stack-based navigation (Push/Pop) similar to a mobile app's back button behavior.

    Usage Pattern

    1. Create a NavigationController instance, passing the current FormBase as the root.
    2. Replace the current form with the controller using NavigateTo(nc).
    3. Use PushAsync(newForm) to add a form to the stack.
    4. Use PopAsync() to go back to the previous form.

    Note: ForceCleanupOnLastPop is enabled by default. When the stack returns to a size of 1, the controller is automatically replaced by the root form provided during construction.

    // Initialize controller with current form as root
    var nc = new NavigationController(this);
    
    // Replace current form with the controller
    await this.NavigateTo(nc);
    
    // Push a new form onto the stack
    await nc.PushAsync(new FormBase());
    
    // Go back one form
    await nc.PopAsync();
  10. Quick start with TelegramBotBase

    master

    To get started with the framework, create a new .NET console project and use the BotBaseBuilder to configure and launch your bot.

    Key steps include:

    1. Configure the Bot: Use BotBaseBuilder.Create() to set your API key, define a default message loop, and specify a StartForm (a subclass of FormBase).
    2. Register Commands: Use CustomCommands to define bot commands and call await bot.UploadBotCommands() to sync them with Telegram's BotFather.
    3. Start the Bot: Call await bot.Start() to begin processing messages.

    Security Note: Never store your API key as plain text in your project sources. Use User Secrets, environment variables, or an appsettings.json excluded from source control.

    var bot = BotBaseBuilder
        .Create()
        .WithAPIKey("{YOUR API KEY}")
        .DefaultMessageLoop()
        .WithStartForm<StartForm>()
        .NoProxy()
        .CustomCommands(a =>
        {
            a.Start("Starts the bot");
        })
        .NoSerialization()
        .UseEnglish()
        .UseSingleThread()
        .Build();
    
    // Upload bot commands to BotFather
    await bot.UploadBotCommands();
    
    // Start your Bot
    await bot.Start();
  11. Implement Buttons and Callback Data

    master

    To create interactive buttons, override the Render(MessageResult message) method in your form. Use ButtonForm and ButtonBase to construct rows of buttons. To pass data back when a button is clicked, use CallbackData(string key, string value).Serialize() as the button's data.

    When a button is pressed, the Action(MessageResult message) method is triggered. You can retrieve the data using message.GetData<CallbackData>(). Always call await message.ConfirmAction() to acknowledge the interaction, and set message.Handled = true if you have processed the input to prevent further propagation.

    public class ButtonTestForm : AutoCleanForm
    {
        public override async Task Action(MessageResult message)
        {
            var call = message.GetData<CallbackData>();
            await message.ConfirmAction();
    
            if (call == null) return;
            message.Handled = true;
    
            switch (call.Value)
            {
                case "button1":
                    await this.Device.Send("Button 1 pressed");
                    break;
            }
        }
    
        public override async Task Render(MessageResult message)
        {
            ButtonForm btn = new ButtonForm();
            // Pass serialized CallbackData to the button
            btn.AddButtonRow(new ButtonBase("Button 1", new CallbackData("a", "button1").Serialize()));
            await this.Device.Send("Click a button", btn);
        }
    }