Compare commits

...

18 Commits

Author SHA1 Message Date
Egor 83af139d01 Update README.md 2025-08-19 17:48:07 +03:00
Egor e76e297d22 Update keyboards.py 2025-08-17 05:32:45 +03:00
Egor b317368df2 Update admin_handlers.py 2025-08-17 05:32:30 +03:00
Egor 572170b1cc Update database.py 2025-08-17 05:32:14 +03:00
Egor 1adeb53a0d Update handlers.py 2025-08-17 05:31:58 +03:00
Egor 3e5811ec11 Update handlers.py 2025-08-17 05:25:04 +03:00
Egor e26a76c8cf Update handlers.py 2025-08-17 05:20:31 +03:00
Egor 1d5755e415 Update keyboards.py 2025-08-17 05:16:43 +03:00
Egor 32beec7515 Update database.py 2025-08-17 05:16:31 +03:00
Egor feb2250f37 Update admin_handlers.py 2025-08-17 05:16:06 +03:00
Egor da36db39c5 Update handlers.py 2025-08-17 05:15:50 +03:00
Egor fd0fa71f85 Update subscription_monitor.py 2025-08-17 04:41:07 +03:00
Egor 04250b38bb Update README.md 2025-08-17 00:47:11 +03:00
Egor deb6978572 Update README.md 2025-08-16 17:10:40 +03:00
Egor d2a26ead1e Update .env.example 2025-08-16 13:59:00 +03:00
Egor 9e40f406ee Update config.py 2025-08-16 13:57:26 +03:00
Egor 2cdfaf9356 Update subscription_monitor.py 2025-08-16 13:56:54 +03:00
Egor 66af399bc0 Update .env.example 2025-08-16 13:13:10 +03:00
4 changed files with 199 additions and 21 deletions
+6 -1
View File
@@ -28,12 +28,17 @@ TRIAL_NOTIFICATION_HOURS_AFTER=1 # Через сколько отсылать с
TRIAL_NOTIFICATION_HOURS_WINDOW=23 # Через сколько выслать повторно
# Monitor Service Settings (дополнительные настройки)
MONITOR_CHECK_INTERVAL=21600 # Промежуток проверики (3600 - будет раз в час проверять и слать уведомления)
MONITOR_ENABLED=true
MONITOR_CHECK_INTERVAL=3600 # Промежуток проверики (3600 - будет раз в час проверять и слать уведомления)
MONITOR_DAILY_CHECK_HOUR=10 # Ежедневный чек в 10 утра
MONITOR_WARNING_DAYS=2 # За сколько начинать до окончания подписки слать уведомления
DELETE_EXPIRED_TRIAL_DAYS=1 # Через сколько дней после истечения удалять триал подписки
DELETE_EXPIRED_REGULAR_DAYS=7 # Через сколько дней после истечения удалять обычные подписки
AUTO_DELETE_ENABLED=true # Включить автоматическое удаление при ежедневной проверке
# Предупреждения о скором истечении триала (ДО окончания)
TRIAL_WARNING_ENABLED=true
TRIAL_WARNING_HOURS_BEFORE=2 # Предупреждения за сколько до окончания триала выслать
TRIAL_WARNING_HOURS_WINDOW=6 # Окно для Предупреждения
LUCKY_GAME_ENABLED=true # Включить/выключить игру удачи
+11 -2
View File
@@ -20,6 +20,13 @@
---
## ⚠️ Важная пометка
Автор полностью переписывает бота **с нуля**.
Совсем скоро здесь появится **совершенно новый продукт** с улучшенной архитектурой, функциональностью и стабильностью.
Оставайтесь на связи 🚀
## ✨ Основные возможности
<table>
@@ -617,8 +624,10 @@ emergency-fix:
| Донатер | Сумма |
|---------|-------|
| Илья (@ispanec_nn) | $15 |
| @Legacyyy777 | 600 руб |
| 1) Илья (@ispanec_nn) | $15 |
| 2) @pilot_737800 | 1250 руб |
| 3) @Legacyyy777 | 600 руб |
| 4) @Legacyyy777 | 400 руб |
---
+6
View File
@@ -31,6 +31,9 @@ class Config:
TRIAL_NOTIFICATION_ENABLED: bool = True
TRIAL_NOTIFICATION_HOURS_AFTER: int = 2
TRIAL_NOTIFICATION_HOURS_WINDOW: int = 22
TRIAL_WARNING_ENABLED: bool = True
TRIAL_WARNING_HOURS_BEFORE: int = 2
TRIAL_WARNING_HOURS_WINDOW: int = 6
TRIBUTE_ENABLED: bool = False
TRIBUTE_API_KEY: str = ""
TRIBUTE_WEBHOOK_PORT: int = 8081
@@ -124,6 +127,9 @@ def load_config() -> Config:
TRIAL_NOTIFICATION_ENABLED=get_bool('TRIAL_NOTIFICATION_ENABLED', True),
TRIAL_NOTIFICATION_HOURS_AFTER=get_int('TRIAL_NOTIFICATION_HOURS_AFTER', 2),
TRIAL_NOTIFICATION_HOURS_WINDOW=get_int('TRIAL_NOTIFICATION_HOURS_WINDOW', 22),
TRIAL_WARNING_ENABLED=get_bool('TRIAL_WARNING_ENABLED', True),
TRIAL_WARNING_HOURS_BEFORE=get_int('TRIAL_WARNING_HOURS_BEFORE', 2),
TRIAL_WARNING_HOURS_WINDOW=get_int('TRIAL_WARNING_HOURS_WINDOW', 6),
LUCKY_GAME_ENABLED=get_bool('LUCKY_GAME_ENABLED', True),
LUCKY_GAME_REWARD=get_float('LUCKY_GAME_REWARD', 50.0),
LUCKY_GAME_NUMBERS=get_int('LUCKY_GAME_NUMBERS', 30),
+176 -18
View File
@@ -87,27 +87,33 @@ class SubscriptionMonitorService:
async def _monitor_loop(self):
logger.info("🔥 Starting monitor loop")
logger.info("⏰ Initial check in 10 seconds...")
await asyncio.sleep(10)
while self.is_running:
try:
logger.info("🔍 Running periodic subscription check...")
trial_warnings = await self._check_trial_warnings()
warnings_sent = await self._check_expiring_subscriptions()
trial_notifications = await self._check_expired_trial_subscriptions()
if warnings_sent > 0 or trial_notifications > 0:
logger.info(f"✅ Monitor check completed: {warnings_sent} warnings sent, {trial_notifications} trial notifications sent")
total_notifications = trial_warnings + warnings_sent + trial_notifications
if total_notifications > 0:
logger.info(f"✅ Monitor check completed: {trial_warnings} trial warnings, "
f"{warnings_sent} expiry warnings, {trial_notifications} trial notifications")
else:
logger.info("✅ Monitor check completed: no warnings needed")
logger.info("✅ Monitor check completed: no notifications needed")
self.last_check_time = datetime.now()
logger.debug(f"😴 Sleeping for {self.config.MONITOR_CHECK_INTERVAL} seconds...")
await asyncio.sleep(self.config.MONITOR_CHECK_INTERVAL)
except asyncio.CancelledError:
logger.info("Monitor loop cancelled")
break
@@ -213,27 +219,44 @@ class SubscriptionMonitorService:
if not self.bot:
logger.error("❌ Bot instance is None, cannot send trial notification")
return
from translations import t
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
try:
available_subscriptions = await self.db.get_all_subscriptions(exclude_trial=True, exclude_imported=True)
min_price = None
if available_subscriptions:
active_subs = [sub for sub in available_subscriptions if sub.is_active and sub.price > 0]
if active_subs:
min_price = min(sub.price for sub in active_subs)
except Exception as e:
logger.error(f"Error getting subscription prices: {e}")
min_price = None
message = t('trial_subscription_expired', user.language, name=subscription.name)
if min_price:
buy_button_text = f"🛒 Купить от {min_price:.0f}"
else:
buy_button_text = "🛒 Купить подписку"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=t('buy_subscription_btn', user.language),
text=buy_button_text,
callback_data="buy_subscription"
)],
[InlineKeyboardButton(
text=t('my_subscriptions_btn', user.language),
text="📋 Мои подписки",
callback_data="my_subscriptions"
)]
])
await self.bot.send_message(user.telegram_id, message, reply_markup=keyboard)
logger.info(f"✅ Trial expiry notification sent to user {user.telegram_id} for subscription '{subscription.name}'")
await self.bot.send_message(user.telegram_id, message, reply_markup=keyboard)
logger.info(f"✅ Trial expiry notification sent to user {user.telegram_id} for subscription '{subscription.name}'")
except Exception as e:
logger.error(f"❌ Error sending trial expiry notification to user {user.telegram_id}: {e}", exc_info=True)
raise
@@ -914,6 +937,141 @@ class SubscriptionMonitorService:
}
}
async def _check_trial_warnings(self) -> int:
try:
if not getattr(self.config, 'TRIAL_WARNING_ENABLED', True):
return 0
logger.info("⚠️ Checking for trials that will expire soon...")
warnings_sent = 0
now_utc = datetime.utcnow()
hours_before = getattr(self.config, 'TRIAL_WARNING_HOURS_BEFORE', 2)
hours_window = getattr(self.config, 'TRIAL_WARNING_HOURS_WINDOW', 6)
logger.info(f"🔧 Trial warning settings: hours_before={hours_before}, hours_window={hours_window}")
all_users = await self.db.get_all_users()
for user in all_users:
try:
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
for user_sub in user_subs:
try:
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription or not subscription.is_trial:
continue
if not user_sub.is_active:
continue
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
time_diff = expires_at_utc - now_utc
hours_until_expiry = time_diff.total_seconds() / 3600
logger.debug(f"🔍 Trial warning check for '{subscription.name}' (user {user.telegram_id}):")
logger.debug(f" Expires at: {expires_at_utc}")
logger.debug(f" Current time: {now_utc}")
logger.debug(f" Hours until expiry: {hours_until_expiry:.2f}")
logger.debug(f" Should warn: {hours_before - hours_window/2 <= hours_until_expiry <= hours_before + hours_window/2}")
warning_start = hours_before - hours_window / 2
warning_end = hours_before + hours_window / 2
if warning_start <= hours_until_expiry <= warning_end:
logger.info(f"⚠️ Sending trial warning to user {user.telegram_id}: "
f"trial '{subscription.name}' expires in {hours_until_expiry:.1f} hours")
try:
await self._send_trial_warning(user, subscription, hours_until_expiry)
warnings_sent += 1
logger.info(f"✅ Trial warning sent to user {user.telegram_id}")
except Exception as notification_error:
logger.error(f"❌ Failed to send trial warning: {notification_error}")
except Exception as sub_error:
logger.error(f"❌ Error checking trial warning for subscription {user_sub.id}: {sub_error}")
except Exception as user_error:
logger.error(f"❌ Error checking trial warnings for user {user.telegram_id}: {user_error}")
if warnings_sent > 0:
logger.info(f"⚠️ Trial warnings completed: {warnings_sent} warnings sent")
return warnings_sent
except Exception as e:
logger.error(f"❌ Critical error in trial warnings: {e}", exc_info=True)
return 0
async def _send_trial_warning(self, user, subscription, hours_left: float):
try:
if not self.bot:
logger.error("❌ Bot instance is None, cannot send trial warning")
return
from translations import t
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
try:
available_subscriptions = await self.db.get_all_subscriptions(exclude_trial=True, exclude_imported=True)
min_price = None
if available_subscriptions:
active_subs = [sub for sub in available_subscriptions if sub.is_active and sub.price > 0]
if active_subs:
min_price = min(sub.price for sub in active_subs)
except Exception as e:
logger.error(f"Error getting subscription prices: {e}")
min_price = None
hours_int = int(hours_left)
if hours_int <= 1:
time_text = "менее часа"
elif hours_int == 1:
time_text = "около часа"
else:
time_text = f"около {hours_int} часов"
message = (
f"⚠️ **Ваша триальная подписка скоро истечет!**\n\n"
f"📋 Подписка: **{subscription.name}**\n"
f"⏰ Осталось: **{time_text}**\n\n"
f"🛒 Приобретите полную подписку, чтобы продолжить пользоваться VPN без перерывов!"
)
if min_price:
buy_button_text = f"🛒 Купить от {min_price:.0f}"
else:
buy_button_text = "🛒 Купить подписку"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=buy_button_text,
callback_data="buy_subscription"
)],
[InlineKeyboardButton(
text="📋 Мои подписки",
callback_data="my_subscriptions"
)]
])
await self.bot.send_message(user.telegram_id, message, reply_markup=keyboard, parse_mode='Markdown')
logger.info(f"✅ Trial warning sent to user {user.telegram_id} for subscription '{subscription.name}' ({hours_left:.1f} hours left)")
except Exception as e:
logger.error(f"❌ Error sending trial warning to user {user.telegram_id}: {e}", exc_info=True)
raise
async def create_subscription_monitor(bot, db: Database, config, api: RemnaWaveAPI = None) -> SubscriptionMonitorService:
logger.info("🏭 Creating subscription monitor service...")
service = SubscriptionMonitorService(bot, db, config, api)