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)