Everything should be in order. Telegram bots are no exception
Tags: python, telegram, tutorial, bots
Published: 2022-08-17 06:22:47

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ierykvp2bsnoxjt0tfz.png)

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](https://github.com/mezidia/Telegramia/blob/a919116ab2fcaa4a3c5738cab2f942b6449e46c6/bot.py):

![Huge file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o024irdjlweueuuaxykh.PNG)

**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](https://github.com/comictomcat/awesome-aiogram#templates), 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](https://github.com/Latand/aiogram-bot-template/tree/7c151a4077fa5bb33f9feaea454e60265d380995) and [Tishka17/tgbot_template](https://github.com/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:

```python
    register_admin(dp)
    register_user(dp)
    register_admin(dp)
    # and so on...
```

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](https://github.com/mezgoodle/bot_template) or just clone it with git:

```bash
git clone https://github.com/mezgoodle/bot_template.git
```

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`

```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
```

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`

```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
    # )
```

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`.

```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'),
        ),
    )
```

I am using [dataclasses](https://docs.python.org/3/library/dataclasses.html) 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:

```py
from aiogram.types import Message

from loader import dp


@dp.message_handler()
async def echo(message: Message) -> Message:
    await message.answer(message.text)
```

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

```py
from . import echo
```

## 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! ❤️ ❤️ ❤️