aiogram-smart-messages
Smart message renderer for Aiogram - A powerful toolkit for building sophisticated Telegram bots with JSON-based message templates, dynamic keyboards, and robust error handling.
✨ Features
- 🎯 JSON-based message templates - Organize messages by namespace, role, and language
- ⌨️ Advanced keyboard builder - Create inline and reply keyboards with WebApp support
- 🔄 Smart message operations - Send, edit, reply with automatic method selection
- 📬 Notificator service - High-level API for sending notifications to users
- 🌍 Multi-language support - Built-in localization system
- 🎨 Context formatting - Dynamic message content with variable substitution
- 🎬 Animation/GIF support - Send and edit GIF animations via JSON templates
- 🛡️ Error handling - Comprehensive logging with customizable decorators
- 📦 Type-safe - Full type hints for better IDE support
- ⚡ Async-first - Built for modern async/await patterns
- 🔌 Middleware support - Easy integration with Aiogram dispatcher
📋 Requirements
- Python 3.10+
- aiogram 3.x
🚀 Installation
pip install aiogram-smart-messages
Or install from source:
git clone https://github.com/yourusername/aiogram-smart-messages.git
cd aiogram-smart-messages
pip install -e .
📚 Quick Start
1. Basic Setup with Middleware (Recommended)
from aiogram import Bot, Dispatcher
from aiogram.types import Message
from aiogram_smart_messages.middleware import MessageEngineMiddleware
from aiogram_smart_messages.renderer.bot_messages import SmartMessageRenderer
from aiogram_smart_messages.logger import get_logger
# Initialize bot and dispatcher
bot = Bot(token="YOUR_BOT_TOKEN")
dp = Dispatcher()
logger = get_logger("my_bot")
# Register middleware
dp.message.middleware(MessageEngineMiddleware(bot))
dp.callback_query.middleware(MessageEngineMiddleware(bot))
# Now msg_engine is automatically available in handlers
@dp.message(commands=["start"])
async def cmd_start(message: Message, msg_engine):
await SmartMessageRenderer.send(
engine=msg_engine,
source=message,
role="user",
namespace="main",
menu_file="welcome",
block_key="greeting",
lang="en",
context={"username": message.from_user.first_name}
)
2. Alternative: Manual Setup
from aiogram import Bot, Dispatcher
from aiogram.types import Message
from aiogram_smart_messages.message_engine import MessageEngine
from aiogram_smart_messages.renderer.bot_messages import SmartMessageRenderer
# Initialize bot and engine manually
bot = Bot(token="YOUR_BOT_TOKEN")
dp = Dispatcher()
engine = MessageEngine(bot)
@dp.message(commands=["start"])
async def cmd_start(message: Message):
await SmartMessageRenderer.send(
engine=engine,
source=message,
role="user",
namespace="main",
menu_file="welcome",
block_key="greeting",
lang="en",
context={"username": message.from_user.first_name}
)
3. Using NotificatorService
For sending notifications to users (e.g., from background tasks, admin panels, or scheduled jobs):
from aiogram import Bot
from aiogram_smart_messages import NotificatorService
bot = Bot(token="YOUR_BOT_TOKEN")
# Send notification to specific user
async def notify_user(user_id: int):
notificator = NotificatorService(user_id=user_id, bot=bot)
# Simple text notification
await notificator.notify_message(
text="Your order has been processed!"
)
# Or use smart message with JSON template
await notificator.notify_message(
smart_data={
"role": "user",
"namespace": "shop",
"menu_file": "orders",
"block_key": "order_complete",
"lang": "en",
"context": {"order_id": "12345"}
}
)
# Example: Batch notifications
async def notify_all_users(user_ids: list[int]):
for user_id in user_ids:
notificator = NotificatorService(user_id=user_id, bot=bot)
await notificator.notify_message(text="Important update!")
4. JSON Message Structure
Create message files in your project structure:
my_bot/
├── messages/
│ ├── user/
│ │ ├── en/
│ │ │ └── welcome.json
│ │ └── ru/
│ │ └── welcome.json
│ └── admin/
│ └── ...
Example welcome.json:
{
"greeting": {
"text": "Hello, {username}! Welcome to our bot.",
"buttons": [
[
{"type": "callback", "text": "Get Started", "data": "start"},
{"type": "url", "text": "Help", "data": "https://example.com/help"}
],
[
{"type": "webapp", "text": "Open App", "data": "https://app.example.com"}
]
]
},
"main_menu": {
"text": "Choose an option:",
"photo": "menu_photo.jpg",
"caption": "Main Menu",
"buttons": [
[
{"type": "callback", "text": "Settings ⚙️", "data": "settings"},
{"type": "callback", "text": "Profile 👤", "data": "profile"}
]
]
},
"confirm_action": {
"text": "Are you sure?",
"buttons": [
[
{"text": "✅ Yes", "style": "success"},
{"text": "❌ No", "style": "danger"}
],
[
{"text": "Learn more", "style": "primary", "icon_custom_emoji_id": "5368324170671202286"}
]
]
},
"welcome_animation": {
"animation": "welcome.gif",
"caption": "Welcome, {username}! 🎉",
"buttons": [
[
{"type": "callback", "text": "Get Started", "data": "start"}
]
]
}
}
5. Using Middleware in All Handlers
from aiogram.types import CallbackQuery
from aiogram_smart_messages.decorators import with_error_logging
# Message handler with middleware
@dp.message(commands=["profile"])
async def show_profile(message: Message, msg_engine):
user_data = await get_user_data(message.from_user.id)
await SmartMessageRenderer.send(
engine=msg_engine,
source=message,
role="user",
namespace="main",
menu_file="profile",
block_key="view",
lang="en",
context={
"username": user_data["name"],
"balance": user_data["balance"],
"level": user_data["level"]
}
)
# Callback handler with middleware
@dp.callback_query(lambda c: c.data == "settings")
async def show_settings(callback: CallbackQuery, msg_engine):
await SmartMessageRenderer.edit(
engine=msg_engine,
source=callback,
role="user",
namespace="main",
menu_file="settings",
block_key="main",
lang="en"
)
await callback.answer()
# Error handling with decorator
@with_error_logging(logger=logger, error_label="WELCOME_MESSAGE")
async def send_personalized_welcome(message: Message, msg_engine, user_data: dict):
await SmartMessageRenderer.send(
engine=msg_engine,
source=message,
role="user",
namespace="main",
menu_file="welcome",
block_key="greeting",
lang=user_data.get("language", "en"),
context={
"username": user_data["name"],
"balance": user_data["balance"]
}
)
6. Advanced Usage
Edit Messages
@dp.callback_query(lambda c: c.data == "settings")
async def show_settings(callback: CallbackQuery, msg_engine):
await SmartMessageRenderer.edit(
engine=msg_engine,
source=callback,
role="user",
namespace="main",
menu_file="settings",
block_key="main",
lang="en"
)
await callback.answer()
Smart Edit or Send
Automatically decides whether to edit or resend based on media content:
@dp.callback_query(lambda c: c.data.startswith("view_"))
async def view_item(callback: CallbackQuery, msg_engine):
item_id = callback.data.split("_")[1]
await SmartMessageRenderer.smart_edit_or_send(
engine=msg_engine,
source=callback,
role="user",
namespace="shop",
menu_file="catalog",
block_key="item_details",
lang="en",
context={"item_id": item_id}
)
Reply to Messages
@dp.message(lambda m: m.text and "help" in m.text.lower())
async def help_reply(message: Message, msg_engine):
await SmartMessageRenderer.reply(
engine=msg_engine,
source=message,
role="user",
namespace="main",
menu_file="help",
block_key="quick_help",
lang="en"
)
Send Documents
from aiogram.types import FSInputFile
@dp.callback_query(lambda c: c.data == "download_report")
async def send_report(callback: CallbackQuery, msg_engine):
doc = FSInputFile("reports/monthly_report.pdf")
await SmartMessageRenderer.send_document(
engine=msg_engine,
source=callback.message.chat.id,
document=doc,
caption="Your monthly report"
)
await callback.answer("Report sent!")
7. Building Keyboards Manually
from aiogram_smart_messages.builder import KeyboardBuilder
# Inline keyboard
inline_buttons = [
[
{"type": "callback", "text": "Option 1", "data": "opt1"},
{"type": "callback", "text": "Option 2", "data": "opt2"}
],
[
{"type": "url", "text": "Visit Website", "data": "https://example.com"}
]
]
inline_kb = KeyboardBuilder.build_inline_keyboard(inline_buttons)
# Reply keyboard
reply_buttons = [
[{"text": "Main Menu"}, {"text": "Settings"}],
[{"type": "webapp", "text": "Open App", "data": "https://app.example.com"}]
]
reply_kb = KeyboardBuilder.build_reply_keyboard(
reply_buttons,
resize_keyboard=True,
one_time_keyboard=True
)
# Reply keyboard with button styles and custom emoji (Telegram 2026+)
styled_buttons = [
[
{"text": "Confirm", "style": "success", "icon_custom_emoji_id": "5368324170671202286"},
{"text": "Cancel", "style": "danger"},
],
[
{"text": "More info", "style": "primary"},
],
]
styled_kb = KeyboardBuilder.build_reply_keyboard(styled_buttons)
8. Using Decorators
Error Logging Decorator
from aiogram_smart_messages.decorators import with_error_logging
@with_error_logging(
logger=logger,
fallback=None,
error_label="DATABASE_ERROR"
)
async def get_user_data(user_id: int):
# Your database logic here
data = await db.fetch_user(user_id)
return data
# With re-raise for critical operations
@with_error_logging(
logger=logger,
reraise=True,
error_label="CRITICAL_PAYMENT"
)
async def process_payment(amount: float):
# Critical operation that should fail loudly
await payment_gateway.charge(amount)
9. Logger Configuration
from aiogram_smart_messages.logger import get_logger
import logging
# Basic logger
logger = get_logger("my_bot")
# Custom configuration
logger = get_logger(
name="my_bot",
level=logging.DEBUG,
format_string="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# Use logger
logger.info("Bot started")
logger.warning("Rate limit approaching")
logger.error("Failed to send message")
logger.debug("Processing callback: %s", callback_data)
10. Working with Complex Context Objects
You can pass entire objects (like database models, dataclasses, or dictionaries) into context and access their attributes in JSON templates:
from dataclasses import dataclass
from datetime import datetime
@dataclass
class UserData:
username: str
balance: float
level: int
created_at: datetime
is_premium: bool
# Get user from database
user_data = await db.get_user(user_id) # Returns UserData object
# Pass entire object to context
await SmartMessageRenderer.send(
engine=msg_engine,
source=message,
role="user",
namespace="main",
menu_file="profile",
block_key="detailed_view",
lang="en",
context={
"user_data": user_data, # Pass entire object
"current_date": datetime.now()
}
)
Example JSON template (profile.json):
{
"detailed_view": {
"text": "👤 Profile: {user_data.username}\n💰 Balance: ${user_data.balance:.2f}\n⭐ Level: {user_data.level}\n📅 Member since: {user_data.created_at:%Y-%m-%d}\n{'✨ Premium' if user_data.is_premium else '🆓 Free'}",
"buttons": [
[
{"type": "callback", "text": "Top Up Balance", "data": "topup_{user_data.username}"},
{"type": "callback", "text": "Upgrade", "data": "upgrade_{user_data.level}"}
]
]
}
}
Supported context types:
- ✅ Dataclasses and Pydantic models
- ✅ Database ORM objects (SQLAlchemy, Tortoise ORM, etc.)
- ✅ Nested dictionaries
- ✅ Custom classes with
__dict__ - ✅ DateTime objects with formatting
Advanced formatting examples:
# Nested objects
context = {
"user": {
"profile": {
"name": "John",
"settings": {
"theme": "dark"
}
}
}
}
# In JSON: "Welcome, {user.profile.name}! Theme: {user.profile.settings.theme}"
# Lists and indexing
context = {
"items": ["Apple", "Banana", "Orange"]
}
# In JSON: "First item: {items[0]}"
# Number formatting
context = {
"price": 1234.5678
}
# In JSON: "Price: ${price:.2f}" → "Price: $1234.57"
# Date formatting
context = {
"date": datetime(2025, 1, 15)
}
# In JSON: "Date: {date:%B %d, %Y}" → "Date: January 15, 2025"
11. Advanced Features
Extra Keyboard Buttons
Add dynamic buttons to existing keyboard layouts:
extra_buttons = [
{"type": "callback", "text": "🔙 Back", "data": "back"}
]
await SmartMessageRenderer.send(
engine=msg_engine,
source=message,
role="user",
namespace="main",
menu_file="menu",
block_key="main",
lang="en",
extra_kb=extra_buttons,
extra_kb_position="bottom" # or "top"
)
Override Block Data
Send messages without loading from JSON:
custom_block = {
"text": "Dynamic message",
"buttons": [
[{"type": "callback", "text": "OK", "data": "ok"}]
]
}
await SmartMessageRenderer.send(
engine=msg_engine,
source=message,
role="user",
override_block=custom_block
)
Custom Keyboard Markup
Use your own pre-built keyboards:
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
custom_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Custom", callback_data="custom")]
])
await SmartMessageRenderer.send(
engine=msg_engine,
source=message,
role="user",
namespace="main",
menu_file="menu",
block_key="main",
lang="en",
custom_markup=custom_keyboard
)
🔌 Middleware
MessageEngineMiddleware
The library includes a convenient middleware that automatically injects MessageEngine into your handlers.
from aiogram_smart_messages.middleware import MessageEngineMiddleware
# Register for messages
dp.message.middleware(MessageEngineMiddleware(bot))
# Register for callback queries
dp.callback_query.middleware(MessageEngineMiddleware(bot))
# Now use msg_engine in handlers
@dp.message(commands=["start"])
async def cmd_start(message: Message, msg_engine):
await SmartMessageRenderer.send(
engine=msg_engine,
source=message,
# ... other parameters
)
Benefits:
- ✅ No need to create
MessageEnginemanually in each handler - ✅ Cleaner code organization
- ✅ Consistent engine instance across all handlers
- ✅ Easy to test and mock
🎯 Supported Button Types
Inline Keyboards
callback- Callback query buttonurl- URL button (opens link)webapp- Web App buttonswitch_inline_query- Inline query switch
Reply Keyboards
- Regular text button (default)
webapp- Web App buttonstyle— button color:"danger"(red),"success"(green),"primary"(blue)icon_custom_emoji_id— custom emoji shown before the button text
📁 Project Structure Example
my_bot/
├── main_bot/
│ ├── messages/
│ │ ├── user/
│ │ │ ├── en/
│ │ │ │ ├── welcome.json
│ │ │ │ ├── menu.json
│ │ │ │ └── settings.json
│ │ │ └── ru/
│ │ │ └── ... (same structure)
│ │ └── admin/
│ │ └── ... (admin messages)
│ ├── photos/
│ │ ├── user/
│ │ │ ├── en/
│ │ │ │ ├── menu_photo.jpg
│ │ │ │ ├── welcome.gif
│ │ │ │ └── promo_animation.gif
│ │ │ └── ru/
│ │ │ ├── menu_photo.jpg
│ │ │ └── welcome.gif
│ │ └── admin/
│ │ └── ...
├── main.py
└── requirements.txt
🔧 Configuration
The library uses the following configuration structure:
module- Top-level module name (default: "main_bot")namespace- Feature namespace (e.g., "main", "admin_panel", "shop")role- User role (e.g., "user", "admin", "common")lang- Language code (e.g., "en", "ru", "es")
File paths:
- Messages:
{module}/messages/{role}/{lang}/{menu_file}.json - Photos:
{module}/{namespace}/photos/{role}/{lang}/{photo_file}
🐛 Error Handling
The library provides comprehensive error handling with clear labels:
# Labels used in SmartMessageRenderer:
# - SEND_SMART_MESSAGE
# - EDIT_SMART_MESSAGE
# - REPLY_SMART_MESSAGE
# - SEND_DOCUMENT
# - LOAD_JSON
# - PARSE_TO_SMART
# Custom error handling
@with_error_logging(
logger=logger,
fallback={"status": "error"},
error_label="CUSTOM_OPERATION"
)
async def my_operation():
# Your code here
pass
📖 API Reference
SmartMessage
Container for message content.
Fields:
text— plain textcaption— caption for media messagesphoto— image (FSInputFile or file_id)animation— GIF/animation (FSInputFile or file_id) ← new in 0.4.0reply_markup— keyboard markup
SmartMessageRenderer
Main class for message rendering operations.
Methods:
load_json()- Load message block from JSON fileparse_to_smart()- Parse JSON block to SmartMessage objectsend()- Send a new messageedit()- Edit existing messagesmart_edit_or_send()- Smart decision: edit or resendreply()- Reply to a messagesend_document()- Send document file
MessageEngine
Core engine for message operations.
Methods:
send_smart_message()- Send SmartMessageedit_smart_message()- Edit SmartMessagereply_smart_message()- Reply with SmartMessagesend_document()- Send document
MessageEngineMiddleware
Middleware for automatic MessageEngine injection.
class MessageEngineMiddleware(BaseMiddleware):
def __init__(self, bot: Bot):
# Initializes MessageEngine with bot instance
KeyboardBuilder
Utility for building keyboards.
Methods:
build_inline_keyboard()- Build inline keyboardbuild_reply_keyboard()- Build reply keyboard
NotificatorService
High-level service for sending notifications to users.
class NotificatorService:
def __init__(self, user_id: int, bot: Bot):
# Initialize notificator for specific user
Methods:
notify_message()- Send text or smart message notificationnotify_document()- Send document notification
Usage Example:
from aiogram import Bot
from aiogram.types import FSInputFile
from aiogram_smart_messages import NotificatorService
# Initialize
bot = Bot(token="YOUR_BOT_TOKEN")
notificator = NotificatorService(user_id=123456789, bot=bot)
# Simple text notification
await notificator.notify_message(
text="Your order has been processed"
)
# Smart message notification with JSON template
await notificator.notify_message(
smart_data={
"role": "user",
"namespace": "shop",
"menu_file": "orders",
"block_key": "order_complete",
"lang": "en",
"context": {"order_id": "12345", "total": 99.99}
}
)
# Document notification
doc = FSInputFile("invoice.pdf")
await notificator.notify_document(
document=doc,
caption="Your invoice for order #12345"
)
Use cases:
- Background job notifications (order processing, payments, etc.)
- Scheduled notifications (reminders, alerts)
- Admin notifications to users
- Batch notifications to multiple users
Benefits:
- Clean, simple API for notifications
- Integrates seamlessly with SmartMessageRenderer
- Automatic error logging
- Type-safe with full type hints
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
📄 License
MIT License © 2025 Kriva
🔗 Links
💡 Tips
- Use middleware - Register
MessageEngineMiddlewarefor cleaner code - Organize messages by feature - Use namespaces to separate different bot sections
- Use context variables - Make messages dynamic and personalized
- Leverage smart_edit_or_send - Let the library decide the best update method
- Add error labels - Make debugging easier with descriptive error labels
- Cache static content - Messages without context are automatically cached
- Use type hints - Full type support for better IDE experience
- Use animations sparingly - GIFs grab attention but increase message size; prefer for onboarding or key moments
🎓 Examples
Check out more examples in the /examples directory:
- Basic bot setup with middleware
- Multi-language support
- E-commerce bot
- Admin panel
- WebApp integration
Made with ❤️ for the Aiogram community
Release files for aiogram-smart-messages 0.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aiogram_smart_messages-0.4.2.tar.gz | 29.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aiogram_smart_messages-0.4.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.5 kB
Release files / aiogram_smart_messages-0.4.2.tar.gz
| Download URL | aiogram_smart_messages-0.4.2.tar.gz |
|---|---|
| Size | 29.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
caa8a4acd9912b01242b6b4291e41dda6370266eb8bfcd65bf28d29768b3cc6c
|
|
BLAKE2b-256 checksum How to use checksums |
433be5caf4a38e6ed36c3b0713853cd1b3a5e99ec3c65d830dddcac8641124c3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.1
|
Release files / aiogram_smart_messages-0.4.2-py3-none-any.whl
| Download URL | aiogram_smart_messages-0.4.2-py3-none-any.whl |
|---|---|
| Size | 27.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
27c0838d2acdafa340e57c296d401d0ebfa0a1f9383cfed394239b502f32a9d8
|
|
BLAKE2b-256 checksum How to use checksums |
ddc2caa6c8fcaf28b1190b9e2665bcaba403101a0e734eca24485abb901eedae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.1
|