Skip to content

Commit 9f24915

Browse files
author
Derssen
committed
refactor: complete codebase localization, PEP 8 cleanup, and documentation update
1 parent fc850de commit 9f24915

File tree

9 files changed

+323
-289
lines changed

9 files changed

+323
-289
lines changed

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Telegram Balance Monitoring Bot
2+
3+
A robust asynchronous Telegram bot designed to monitor financial balances across various API services (Zadarma, DIDWW) and manual subscription services (Wazzup, Streamtele, Callii).
4+
5+
The bot runs background scheduled tasks to check for low balances and notifies a specific administrative chat.
6+
7+
## Features
8+
9+
* **API Integration**: Real-time balance checking for Zadarma and DIDWW.
10+
* **Manual Tracking**: State management for services without APIs (Wazzup, Callii) via FSM (Finite State Machine).
11+
* **Recurring Payments**: Automatic tracking of monthly subscriptions and daily usage costs.
12+
* **Alerts**:
13+
* Low balance notifications.
14+
* Monthly payment reminders.
15+
* Daily top-up reminders for high-consumption services.
16+
* **Security**: Restricted access to a specific target chat ID.
17+
18+
## Tech Stack
19+
20+
* **Python 3.10+**
21+
* **aiogram 3.x**: Asynchronous framework for Telegram Bot API.
22+
* **SQLAlchemy + aiosqlite**: Asynchronous ORM for SQLite database.
23+
* **aiohttp**: Asynchronous HTTP client for API requests.
24+
25+
## Installation
26+
27+
1. **Clone the repository:**
28+
```bash
29+
git clone <repository-url>
30+
cd telegram-balance-bot
31+
```
32+
33+
2. **Create a virtual environment:**
34+
```bash
35+
python -m venv venv
36+
source venv/bin/activate # Windows: venv\Scripts\activate
37+
```
38+
39+
3. **Install dependencies:**
40+
```bash
41+
pip install -r requirements.txt
42+
```
43+
44+
4. **Configuration:**
45+
Create a `.env` file in the root directory (see `.env.example`).
46+
47+
5. **Run the bot:**
48+
```bash
49+
python main.py
50+
```
51+
52+
## Project Structure
53+
54+
* `main.py`: Entry point and scheduler loop.
55+
* `config.py`: Configuration and environment variable management.
56+
* `db/`: Database models and initialization.
57+
* `handlers/`: Telegram message and callback handlers.
58+
* `services/`: External API clients.
59+
* `scheduler/`: Background job logic.

config.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import os
22
from dotenv import load_dotenv
33

4-
# Загружаем переменные из .env
4+
# Load environment variables
55
load_dotenv()
66

7-
class Settings:
7+
class Config:
8+
"""Application configuration and constants."""
9+
810
# Telegram
911
BOT_TOKEN: str = os.getenv("BOT_TOKEN")
10-
TARGET_CHAT_ID: int = int(os.getenv("TARGET_CHAT_ID", -1)) # Важный ID чата
12+
TARGET_CHAT_ID: int = int(os.getenv("TARGET_CHAT_ID", -1))
1113

1214
# Database
1315
DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///balances.sqlite3")
@@ -18,15 +20,21 @@ class Settings:
1820
WAZZUP_TOKEN: str = os.getenv("WAZZUP_TOKEN")
1921
DIDWW_KEY: str = os.getenv("DIDWW_KEY")
2022

21-
# Constants
23+
# Financial Constants
2224
LOW_BALANCE_THRESHOLD: float = 10.0
2325
MIN_TOP_UP_AMOUNT: float = 5.0
26+
27+
# Service Costs & Fees
2428
CALLII_DAILY_COST: float = float(os.getenv("CALLII_DAILY_COST", 2.2))
2529
WAZZUP_DAILY_COST: float = float(os.getenv("WAZZUP_DAILY_COST", 400.0))
2630
STREAMTELE_MONTHLY_FEE: float = float(os.getenv("STREAMTELE_MONTHLY_FEE", 1500.0))
2731
WAZZUP_MONTHLY_FEE: float = float(os.getenv("WAZZUP_MONTHLY_FEE", 6000.0))
2832
DIDWW_MONTHLY_FEE: float = float(os.getenv("DIDWW_MONTHLY_FEE", 45.0))
33+
34+
# Service Info
2935
WAZZUP_PHONE: str = os.getenv("WAZZUP_PHONE", "+6281239838440")
36+
37+
# Currency Mapping
3038
SERVICE_CURRENCIES: dict = {
3139
'Zadarma': 'USD',
3240
'Wazzup24 Подписка': 'RUB',
@@ -35,19 +43,21 @@ class Settings:
3543
'Streamtele': 'UAH',
3644
'Callii': 'USD',
3745
}
46+
3847
CURRENCY_SIGNS: dict = {
3948
'USD': '$',
4049
'UAH': '₴',
4150
'RUB': '₽',
4251
}
43-
# Статус активности API-сервисов
52+
53+
# API Service Toggle
4454
API_SERVICE_STATUSES: dict = {
4555
'Zadarma': os.getenv("ZADARMA_ENABLED", "True").lower() in ('true', '1', 't'),
4656
'DIDWW': os.getenv("DIDWW_ENABLED", "True").lower() in ('true', '1', 't'),
4757
}
4858

49-
SETTINGS = Settings()
59+
SETTINGS = Config()
5060

51-
# Проверка, что критические переменные загружены
61+
# Validation
5262
if not SETTINGS.BOT_TOKEN or SETTINGS.TARGET_CHAT_ID == -1:
53-
raise ValueError("BOT_TOKEN or TARGET_CHAT_ID is not configured correctly.")
63+
raise ValueError("Critical configuration missing: BOT_TOKEN or TARGET_CHAT_ID.")

db/models.py

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,89 @@
1-
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, text
2-
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
3-
from sqlalchemy.orm import sessionmaker, declarative_base
41
from datetime import datetime
52
import pytz
3+
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, text
4+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, AsyncEngine
5+
from sqlalchemy.orm import sessionmaker, declarative_base
6+
from sqlalchemy.future import select
7+
68
from config import SETTINGS
79

8-
# Используем вашу таймзону
9-
TIMEZONE = pytz.timezone('Asia/Makassar')
10+
# Define Timezone
11+
TIMEZONE = pytz.timezone('Asia/Makassar')
1012

1113
Base = declarative_base()
1214

1315
class Service(Base):
16+
"""Database model representing a tracked service."""
1417
__tablename__ = 'services'
1518

1619
id = Column(Integer, primary_key=True)
1720
name = Column(String, unique=True, nullable=False)
1821

19-
# Мониторинг баланса
22+
# Balance Monitoring
2023
last_balance = Column(Float, default=0.0)
2124
low_balance_alert_sent = Column(Boolean, default=False)
2225

23-
# Новые поля (согласно твоему скриншоту)
26+
# Financial Configuration
2427
currency = Column(String, default="USD") # USD, RUB, UAH
25-
daily_cost = Column(Float, nullable=True) # Расход в день
26-
monthly_fee = Column(Float, nullable=True) # Ежемесячный платеж
28+
daily_cost = Column(Float, nullable=True) # Estimated daily cost
29+
monthly_fee = Column(Float, nullable=True) # Fixed monthly fee
2730

28-
# Даты оповещений
31+
# Alert Schedule
2932
next_alert_date = Column(DateTime, nullable=True)
30-
next_monthly_alert = Column(DateTime, nullable=True) # Для ежемесячных подписок
33+
next_monthly_alert = Column(DateTime, nullable=True)
3134

3235
def __repr__(self):
3336
return f"<Service(name='{self.name}', balance={self.last_balance})>"
3437

35-
# Инициализация
36-
async def init_db(database_url: str):
38+
async def init_db(database_url: str) -> sessionmaker:
39+
"""Initialize the database engine and session factory."""
3740
engine = create_async_engine(database_url, echo=False)
3841
async with engine.begin() as conn:
3942
await conn.run_sync(Base.metadata.create_all)
4043

41-
AsyncSessionLocal = sessionmaker(
44+
async_session = sessionmaker(
4245
autocommit=False,
4346
autoflush=False,
4447
bind=engine,
4548
class_=AsyncSession,
4649
expire_on_commit=False,
4750
)
48-
return AsyncSessionLocal
51+
return async_session
4952

50-
# Вспомогательная функция для добавления начальных данных
51-
async def initialize_services(SessionLocal):
52-
async with SessionLocal() as session:
53-
# --- simple sqlite migration to add new columns if missing ---
53+
async def initialize_services(session_factory) -> None:
54+
"""
55+
Populate the database with default services and perform schema migrations if necessary.
56+
"""
57+
async with session_factory() as session:
58+
# 1. Simple schema migration (add columns if missing)
5459
pragma_stmt = text("PRAGMA table_info(services)")
5560
result = await session.execute(pragma_stmt)
56-
columns = {row[1] for row in result.fetchall()}
61+
existing_columns = {row[1] for row in result.fetchall()}
5762

5863
alter_statements = []
59-
if 'currency' not in columns:
60-
alter_statements.append("ALTER TABLE services ADD COLUMN currency VARCHAR")
61-
if 'daily_cost' not in columns:
62-
alter_statements.append("ALTER TABLE services ADD COLUMN daily_cost FLOAT")
63-
if 'monthly_fee' not in columns:
64-
alter_statements.append("ALTER TABLE services ADD COLUMN monthly_fee FLOAT")
65-
if 'next_monthly_alert' not in columns:
66-
alter_statements.append("ALTER TABLE services ADD COLUMN next_monthly_alert DATETIME")
64+
required_columns = ['currency', 'daily_cost', 'monthly_fee', 'next_monthly_alert']
65+
66+
for col in required_columns:
67+
if col not in existing_columns:
68+
col_type = 'DATETIME' if 'next' in col else ('FLOAT' if 'cost' in col or 'fee' in col else 'VARCHAR')
69+
alter_statements.append(f"ALTER TABLE services ADD COLUMN {col} {col_type}")
6770

6871
for stmt in alter_statements:
6972
try:
7073
await session.execute(text(stmt))
7174
except Exception:
72-
# ignore if already exists or other minor issues
73-
pass
75+
pass # Ignore errors if column exists
76+
7477
if alter_statements:
7578
await session.commit()
7679

77-
services_to_add = [
78-
# API сервисы
80+
# 2. Seed default data
81+
services_to_seed = [
7982
{
8083
'name': 'Zadarma',
8184
'last_balance': 0.0,
8285
'currency': SETTINGS.SERVICE_CURRENCIES.get('Zadarma', 'USD'),
8386
},
84-
# Wazzup разделён: подписка (ежемесячно) и баланс номера (ежедневный расход)
8587
{
8688
'name': 'Wazzup24 Подписка',
8789
'last_balance': 0.0,
@@ -103,14 +105,12 @@ async def initialize_services(SessionLocal):
103105
'monthly_fee': SETTINGS.DIDWW_MONTHLY_FEE,
104106
'next_monthly_alert': TIMEZONE.localize(datetime(2025, 12, 20, 10, 0)),
105107
},
106-
# Callii (Управляемый FSM)
107108
{
108109
'name': 'Callii',
109110
'next_alert_date': TIMEZONE.localize(datetime(2025, 12, 11, 10, 0)),
110111
'currency': SETTINGS.SERVICE_CURRENCIES.get('Callii', 'USD'),
111112
'daily_cost': SETTINGS.CALLII_DAILY_COST,
112113
},
113-
# Streamtele (Ежемесячное напоминание)
114114
{
115115
'name': 'Streamtele',
116116
'currency': SETTINGS.SERVICE_CURRENCIES.get('Streamtele', 'UAH'),
@@ -119,10 +119,7 @@ async def initialize_services(SessionLocal):
119119
},
120120
]
121121

122-
from sqlalchemy.future import select # Дополнительный импорт нужен для 'select'
123-
124-
for data in services_to_add:
125-
# ИСПРАВЛЕНО: Используем select и where для поиска по уникальному полю 'name'
122+
for data in services_to_seed:
126123
stmt = select(Service).filter(Service.name == data['name'])
127124
result = await session.execute(stmt)
128125
exists = result.scalar_one_or_none()

0 commit comments

Comments
 (0)