My custom Telegram bot for personal use based on State arhitecture.
Project description
PR Telegram Bot
A modern Python framework for building Telegram bots with a state-based architecture. Built on top of pyTelegramBotAPI with async support, Pydantic models, and clean state management.
Features
- 🎯 State-based Architecture: Organize bot logic into clean, maintainable states
- 🔄 Async/Await Support: Built for modern asynchronous Python
- 📝 Type Safety: Full type hints with Pydantic models
- 🎨 Flexible Filters: Expressive filter system for message handling
- 🗄️ Database Abstraction: Simple interface for user data persistence
- 🌐 Global Handlers: Define handlers that work across all states
- 🔌 Easy Integration: Drop-in replacement for pyTelegramBotAPI
Installation
Using uv (recommended):
uv add pr-telegram-bot
Using pip:
pip install pr-telegram-bot
Quick Start
import asyncio
from pr_telegram_bot.client import PRTeleBotClient
from pr_telegram_bot.db_service import InMemoryUserDBService
from pr_telegram_bot.state import State, StateGroup, handler
from pr_telegram_bot.filters import F
from pr_telegram_bot.models import User
# Define a global state for commands available everywhere
class GlobalState(State):
is_global = True
@staticmethod
@handler(F.text == "/start")
async def send_start_message(bot, chat_id, update, user, data):
await bot.send_message(chat_id=chat_id, text="Welcome! 👋")
await bot.db_service.update_user(user_id=user.id, state="MainMenu")
# Create a state group
state_group = StateGroup()
# Define application states
@state_group.register
class MainMenu(State):
@staticmethod
@handler(F.content_type == "text")
async def handle_text(bot, chat_id, update, user, data):
await bot.send_message(chat_id=chat_id, text=f"You said: {update.text}")
# Initialize bot
db_service = InMemoryUserDBService()
bot = PRTeleBotClient(db_service=db_service, token="YOUR_BOT_TOKEN")
# Register states
bot.register_state(GlobalState)
bot.include(state_group)
# Start polling
asyncio.run(bot.polling())
Core Concepts
States
States represent different conversation contexts in your bot. Each state can have multiple handlers:
@state_group.register
class OrderProcess(State):
@staticmethod
@handler(F.text == "/cancel")
async def cancel_order(bot, chat_id, update, user, data):
await bot.send_message(chat_id, "Order cancelled")
await bot.db_service.update_user(user_id=user.id, state="MainMenu")
@staticmethod
@handler(F.content_type == "text")
async def process_input(bot, chat_id, update, user, data):
# Process order details
pass
Filters
Use the F object to create expressive filters:
# Text matching
@handler(F.text == "/help")
@handler(F.text.startswith("/"))
# Content type
@handler(F.content_type == "photo")
@handler(F.content_type == "document")
# Update type
@handler(F.update_type == "message")
@handler(F.update_type == "callback_query")
# Commands
@handler(F.command == "start")
# Callback data
@handler(F.callback_data == "button_clicked")
# Combine filters
@handler(F.content_type == "text" & F.text.startswith("/"))
User Model
The User model tracks user state and data:
class User(BaseModel):
id: int | None
first_name: str
last_name: str | None = None
username: str | None = None
language_code: str | None = None
photo_url: str | None = None
state: str = "Start" # Current state
data: dict[str, Any] = {} # Persistent data
Database Service
Implement your own database service or use the built-in in-memory one:
from pr_telegram_bot.db_service import UserDBService
class MongoUserDBService(UserDBService):
async def create_user(self, user: User) -> User:
# Your implementation
pass
async def get_user(self, user_id: int) -> User | None:
# Your implementation
pass
async def update_user(self, user_id: int, **update_data) -> User:
# Your implementation
pass
Global Handlers
Global handlers work across all states (useful for commands like /help, /cancel):
class GlobalState(State):
is_global = True
@staticmethod
@handler(F.text == "/help")
async def show_help(bot, chat_id, update, user, data):
await bot.send_message(chat_id, "Help message")
Advanced Usage
State Groups
Organize related states into groups:
# states/shopping.py
shopping_states = StateGroup()
@shopping_states.register
class BrowseCatalog(State):
# handlers...
@shopping_states.register
class ViewCart(State):
# handlers...
# main.py
from states.shopping import shopping_states
bot.include(shopping_states)
Handler Priority
Handlers are executed in the order they're defined. The first matching handler stops execution:
class MyState(State):
@staticmethod
@handler(F.text == "/special")
async def handle_special(bot, chat_id, update, user, data):
# Handles /special first
pass
@staticmethod
@handler(F.content_type == "text")
async def handle_any_text(bot, chat_id, update, user, data):
# Handles any other text
pass
State Transitions
Change user state to control conversation flow:
# Move to next state
await bot.db_service.update_user(
user_id=user.id,
state="NextState",
data={"key": "value"}
)
Project Structure Example
my_bot/
├── main.py
├── states/
│ ├── __init__.py
│ ├── global_handlers.py
│ ├── onboarding.py
│ └── main_menu.py
├── db_service.py
└── requirements.txt
Development
Requirements
- Python >= 3.13
- aiohttp >= 3.13.2
- pydantic >= 2.12.4
- pymongo >= 4.15.4
- pytelegrambotapi >= 4.29.1
Development Setup
# Clone repository
git clone https://github.com/yourusername/pr-telegram-bot.git
cd pr-telegram-bot
# Install with dev dependencies
uv sync --dev
# Run tests
uv run pytest
# Run linter
uv run ruff check
# Format code
uv run ruff format
Code Quality
The project uses:
- Ruff for linting and formatting
- MyPy for type checking
- Pytest for testing
- Pre-commit hooks for automated checks
API Reference
PRTeleBotClient
Main bot client class extending AsyncTeleBot.
Methods:
register_state(state_class)- Register a single stateinclude(state_group)- Include all states from a StateGroup
State
Base class for defining bot states.
Attributes:
is_global- Boolean flag for global handlers
StateGroup
Container for organizing related states.
Methods:
register(state_class)- Decorator to register a state
Filters (F)
Filter expressions for message matching:
F.text- Match message textF.command- Match bot commandsF.content_type- Match content typeF.update_type- Match update typeF.callback_data- Match callback query data
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Run pre-commit checks
- Submit a pull request
License
This project is licensed under the MIT License.
Author
Petro Romaniuk
- Email: pr@email.com
Changelog
v0.1.1
- Initial release
- State-based architecture
- Filter system
- Database abstraction
- Global handlers support
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pr_telegram_bot-0.1.3.tar.gz.
File metadata
- Download URL: pr_telegram_bot-0.1.3.tar.gz
- Upload date:
- Size: 7.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
599554b7e1ff72e443f4654fd865c947b05f8da8a85e6d0b008370d683e3903f
|
|
| MD5 |
e8b2fa2d9c3c0e5d0bd7a726d5fbd027
|
|
| BLAKE2b-256 |
878fb563cacbabc94c4673a761b958bd9ec88405e8d4313c44991d4515740b85
|
File details
Details for the file pr_telegram_bot-0.1.3-py3-none-any.whl.
File metadata
- Download URL: pr_telegram_bot-0.1.3-py3-none-any.whl
- Upload date:
- Size: 9.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8a167d1ffc640c97a0511c91b4e34bed16de9faa192cab9d53a2dbca9a56702
|
|
| MD5 |
b0c374bea7f646f6221c1378e6c1a82c
|
|
| BLAKE2b-256 |
2f6de3900e2e0070faeb1299e95a43b86bec7096727f9858c253da04bfb4e7d3
|