Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83af139d01 | |||
| e76e297d22 | |||
| b317368df2 | |||
| 572170b1cc | |||
| 1adeb53a0d | |||
| 3e5811ec11 | |||
| e26a76c8cf | |||
| 1d5755e415 | |||
| 32beec7515 | |||
| feb2250f37 | |||
| da36db39c5 | |||
| fd0fa71f85 | |||
| 04250b38bb | |||
| deb6978572 | |||
| d2a26ead1e | |||
| 9e40f406ee | |||
| 2cdfaf9356 | |||
| 66af399bc0 |
+6
-1
@@ -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 # Включить/выключить игру удачи
|
||||
|
||||
@@ -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 руб |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user