DEV Community

Cover image for Everything should be in order. Telegram bots are no exception
Maksym Zavalniuk
Maksym Zavalniuk

Posted on

Everything should be in order. Telegram bots are no exception

A quick trailer 🤗

The information may be difficult to understand at first, but be patient and read to the last chapter. There will be both the code and the explanation.

Why do we need templates in programming? 🤔

Hello everyone! I think many of you have come across file structure patterns in your projects. This is best seen in web development. For example, when creating apps on React, RubyOnRails or Django:

File structure

This is really convenient for several reasons. Your code is organized and everything is in its place (folders). Finding the function that calculates the cube root will be much easier in the folder called "math". If you work in a team, your partner will not wonder where everything is.

The same applies to Telegram bots. Previously, I wrote everything in one file, as it was in the last post (although the config was in a separate file). And everything was fine until my file got too big. Really, look how many lines there are in this file:

Huge file

439 LINES! At some point, it became difficult to follow, and I started looking for a solution.

Templates for bots really existed! 😮

I found out about it quite by accident. While browsing awesome-aiogram, I saw a paragraph - templates. After getting to know them a little, I rewrote my bot.

Latand vs Tishka17 ⚔️

These are the nicknames of two authors whose templates I used. Here are links to them: Latand/aiogram-bot-template and Tishka17/tgbot_template Now we will look at the difference, advantages and disadvantages.

Latand Tishka17
Registering handlers Here we have the __init__.py file where all the other handler files are imported. And inside them, we import the dispatcher object. In each of the files with handlers, we have a function that registers the handler. And then we call these functions in some main file when starting the bot
Starting the bot This is where we usually run the command python main.py. A different structure is used here, which allows you to run a bot like a cli.

Although they are similar in their own way, and Latand eventually also switched to Tishka's template, they have their flaws:

  • 👿 The most common error that occurs when using the Latand's template is ImportError(circular import). And to solve it, you have to do ugly things;

  • 👿 When you use Tishka's template, the code increases, namely the registration of handlers:

    register_admin(dp)
    register_user(dp)
    register_admin(dp)
    # and so on...
Enter fullscreen mode Exit fullscreen mode

I thought, "Why not make my own template?"

My own template 😎

Yes, I took the best of both previous templates and made my own. You can download it by this link or just clone it with git:

git clone https://github.com/mezgoodle/bot_template.git
Enter fullscreen mode Exit fullscreen mode

So, let's look at the code and understand how and where to write it. The only thing, I would ask you to ignore the files that are already there. They are just for better understanding.

First of all, we have the following file structure.

📦bot_template-main
┣ 📂tgbot
┃ ┣ 📂filters
┃ ┃ ┗ 📜__init__.py
┃ ┣ 📂handlers
┃ ┃ ┣ 📜errors.py
┃ ┃ ┗ 📜__init__.py
┃ ┣ 📂keyboards
┃ ┃ ┣ 📂inline
┃ ┃ ┃ ┗ 📜__init__.py
┃ ┃ ┣ 📂reply
┃ ┃ ┃ ┗ 📜__init__.py
┃ ┃ ┗ 📜__init__.py
┃ ┣ 📂middlewares
┃ ┃ ┣ 📜throttling.py
┃ ┃ ┗ 📜__init__.py
┃ ┣ 📂misc
┃ ┃ ┗ 📜__init__.py
┃ ┣ 📂models
┃ ┃ ┗ 📜__init__.py
┃ ┣ 📂services
┃ ┃ ┣ 📜admins_notify.py
┃ ┃ ┣ 📜setting_commands.py
┃ ┃ ┗ 📜__init__.py
┃ ┣ 📂states
┃ ┃ ┗ 📜__init__.py
┃ ┣ 📜config.py
┃ ┗ 📜__init__.py
┣ 📜.gitignore
┣ 📜bot.py
┣ 📜LICENSE
┣ 📜loader.py
┣ 📜README.md
┗ 📜requirements.txt

So, to begin with, we will consider two main files: bot.py and loader.py.

  • loader.py
from aiogram import Bot, Dispatcher
from aiogram.contrib.fsm_storage.memory import MemoryStorage

from tgbot.config import load_config

config = load_config()
storage = MemoryStorage()
bot = Bot(token=config.tg_bot.token, parse_mode='HTML')
dp = Dispatcher(bot, storage=storage)
bot['config'] = config
Enter fullscreen mode Exit fullscreen mode

Here we only initialize the objects of the bot and the dispatcher (as for storage, then in the next articles), and also set the config value through the key.

  • bot.py
import functools
import logging
import os

from aiogram import Dispatcher
from aiogram.utils.executor import start_polling, start_webhook

from tgbot.config import load_config
from tgbot.filters.admin import IsAdminFilter
from tgbot.middlewares.throttling import ThrottlingMiddleware
from tgbot.services.setting_commands import set_default_commands
from loader import dp

logger = logging.getLogger(__name__)


def register_all_middlewares(dispatcher: Dispatcher) -> None:
    logger.info('Registering middlewares')
    dispatcher.setup_middleware(ThrottlingMiddleware())


def register_all_filters(dispatcher: Dispatcher) -> None:
    logger.info('Registering filters')
    dispatcher.filters_factory.bind(IsAdminFilter)


def register_all_handlers(dispatcher: Dispatcher) -> None:
    from tgbot import handlers
    logger.info('Registering handlers')


async def register_all_commands(dispatcher: Dispatcher) -> None:
    logger.info('Registering commands')
    await set_default_commands(dispatcher.bot)


async def on_startup(dispatcher: Dispatcher, webhook_url: str = None) -> None:
    register_all_middlewares(dispatcher)
    register_all_filters(dispatcher)
    register_all_handlers(dispatcher)
    await register_all_commands(dispatcher)
    # Get current webhook status
    webhook = await dispatcher.bot.get_webhook_info()

    if webhook_url:
        await dispatcher.bot.set_webhook(webhook_url)
        logger.info('Webhook was set')
    elif webhook.url:
        await dispatcher.bot.delete_webhook()
        logger.info('Webhook was deleted')
    logger.info('Bot started')


async def on_shutdown(dispatcher: Dispatcher) -> None:
    await dispatcher.storage.close()
    await dispatcher.storage.wait_closed()
    logger.info('Bot shutdown')


if __name__ == '__main__':
    logging.basicConfig(
        level=logging.INFO,
        format=u'%(filename)s:%(lineno)d #%(levelname)-8s [%(asctime)s] - %(name)s - %(message)s',
    )
    config = load_config()

    # Webhook settings
    HEROKU_APP_NAME = os.getenv('HEROKU_APP_NAME')
    WEBHOOK_HOST = f'https://{HEROKU_APP_NAME}.herokuapp.com'
    WEBHOOK_PATH = f'/webhook/{config.tg_bot.token}'
    WEBHOOK_URL = f'{WEBHOOK_HOST}{WEBHOOK_PATH}'
    # Webserver settings
    WEBAPP_HOST = '0.0.0.0'
    WEBAPP_PORT = int(os.getenv('PORT', 5000))

    start_polling(
        dispatcher=dp,
        on_startup=on_startup,
        on_shutdown=on_shutdown,
        skip_updates=True,
    )

    # start_webhook(
    #     dispatcher=dp,
    #     on_startup=functools.partial(on_startup, webhook_url=WEBHOOK_URL),
    #     on_shutdown=on_shutdown,
    #     webhook_path=WEBHOOK_PATH,
    #     skip_updates=True,
    #     host=WEBAPP_HOST,
    #     port=WEBAPP_PORT
    # )
Enter fullscreen mode Exit fullscreen mode

This is the place where our entire bot "gathers". Here are also handlers, filters, and middleware (about all this, in the following articles). We have a function that is executed when the file is launched with the python bot.py command. Function, in turn, starts long polling for the bot (commented - starting the bot in the state of webhooks). Next, the on_startup function is executed, and everything else is in it. This approach allows us to monitor each stage and run functions independently.

Now let's move on to the tgbot module. There is one main file here - config.py.

import os
from dataclasses import dataclass


@dataclass
class DbConfig:
    host: str
    password: str
    user: str
    database: str


@dataclass
class TgBot:
    token: str


@dataclass
class Config:
    tg_bot: TgBot
    db: DbConfig


def load_config(path: str = None) -> Config:
    # load_dotenv(path)
    return Config(
        tg_bot=TgBot(
            token=os.getenv('BOT_TOKEN', 'token'),
        ),
        db=DbConfig(
            host=os.getenv('DB_HOST', 'localhost'),
            password=os.getenv('DB_PASSWORD', 'password'),
            user=os.getenv('DB_USER', 'user'),
            database=os.getenv('DB_NAME', 'database'),
        ),
    )
Enter fullscreen mode Exit fullscreen mode

I am using dataclasses here to store data.

Looking from top to bottom, there are several folders: filters, keyboards/reply, keyboards/inline, middlewares, misc, models, services, states. There is no point in talking about most of them now, as they will be separate articles. But, for example, the misk folder contains various functions that are not directly related to the bot's logic; services - has two files: admins_notify.py (for notifying the user that the bot has started) and setting_commands.py (for setting commands in the bot menu). We are most interested in the handlers folder.

For example, let's make the echo bot again. Create echo.py in the handlers folder with code:

from aiogram.types import Message

from loader import dp


@dp.message_handler()
async def echo(message: Message) -> Message:
    await message.answer(message.text)
Enter fullscreen mode Exit fullscreen mode

Next in the handlers/__init__.py, we need to do an import:

from . import echo
Enter fullscreen mode Exit fullscreen mode

The end 🏁

And that's all. We made an echo bot with a following file structure. In the next articles, we will complicate it by adding various interesting things to it. Since, in my opinion, this is a difficult topic, do not hesitate to ask me questions by mail or Telegram.

Thank you for reading! ❤️ ❤️ ❤️

Top comments (0)