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:
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:
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...
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
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
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 # )
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'), ), )
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)
Next in the handlers/__init__.py
, we need to do an import:
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! ❤️ ❤️ ❤️
Top comments (0)