Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c101cb3d3f | |||
| 63912af43b | |||
| d19263dd79 | |||
| f8c9c59a03 | |||
| 5595f7a954 | |||
| bac4527945 | |||
| 51da8bcb72 | |||
| 8aa6e7e245 | |||
| 5029b35d14 | |||
| 2412027d9b | |||
| ddff5219e5 | |||
| 346bc18b58 | |||
| c256ae3664 | |||
| 2b47358a9e | |||
| e0b041ce44 | |||
| 3f64dee927 | |||
| 67c5ebeabd | |||
| 84bc8e96cc | |||
| 2d30cec7c6 | |||
| 5f5f66178b | |||
| e9fadec0ac | |||
| 10008e51ff | |||
| 3098610d17 | |||
| 7a2566d320 | |||
| 01ba8ca53e | |||
| 2a0a2dccff | |||
| 66a6f32d4c | |||
| 02b3f61953 | |||
| dc08526255 | |||
| 2ca339610b | |||
| ffe65937c1 | |||
| 4b6f03eef5 |
@@ -23,6 +23,9 @@ TRIAL_DURATION_DAYS=3 # Дней триала
|
||||
TRIAL_TRAFFIC_GB=2 # Лимит трафика у триал подписки
|
||||
TRIAL_SQUAD_UUID= # UUID сквада из панели remnawave /dashboard/management/internal-squads
|
||||
TRIAL_PRICE=0.0 # Оставить 0!
|
||||
TRIAL_NOTIFICATION_ENABLED=true # Уведомление об истекшей триальной подписке
|
||||
TRIAL_NOTIFICATION_HOURS_AFTER=1 # Через сколько отсылать сообщение
|
||||
TRIAL_NOTIFICATION_HOURS_WINDOW=23 # Через сколько выслать повторно
|
||||
|
||||
# Monitor Service Settings (дополнительные настройки)
|
||||
MONITOR_CHECK_INTERVAL=21600 # Промежуток проверики (3600 - будет раз в час проверять и слать уведомления)
|
||||
@@ -32,6 +35,7 @@ DELETE_EXPIRED_TRIAL_DAYS=1 # Через сколько дней после и
|
||||
DELETE_EXPIRED_REGULAR_DAYS=7 # Через сколько дней после истечения удалять обычные подписки
|
||||
AUTO_DELETE_ENABLED=true # Включить автоматическое удаление при ежедневной проверке
|
||||
|
||||
|
||||
LUCKY_GAME_ENABLED=true # Включить/выключить игру удачи
|
||||
LUCKY_GAME_REWARD=50.0 # Размер награды за выигрыш в рублях
|
||||
LUCKY_GAME_NUMBERS=30 # Общее количество чисел для выбора (1-30)
|
||||
@@ -58,3 +62,10 @@ STARS_500_RATE=550
|
||||
STARS_750_RATE=800
|
||||
# 1000 звёзд
|
||||
STARS_1000_RATE=1000
|
||||
|
||||
|
||||
TRIBUTE_ENABLED=true # true/false включить/выключить пополнение баланса с помощью доната на Tribute
|
||||
TRIBUTE_API_KEY= # API ключ из настроек Tribute
|
||||
TRIBUTE_WEBHOOK_PORT=8081
|
||||
TRIBUTE_WEBHOOK_PATH=/tribute-webhook
|
||||
TRIBUTE_DONATE_LINK=https://t.me/tribute/app?startapp=XXXXXXXX # укажите ссылку на донат в Tribute
|
||||
|
||||
+126
-28
@@ -52,7 +52,6 @@ logger = logging.getLogger(__name__)
|
||||
admin_router = Router()
|
||||
|
||||
async def check_admin_access(callback: CallbackQuery, user: User) -> bool:
|
||||
"""Check if user has admin access"""
|
||||
if not user.is_admin:
|
||||
await callback.answer(t('not_admin', user.language))
|
||||
return False
|
||||
@@ -60,7 +59,6 @@ async def check_admin_access(callback: CallbackQuery, user: User) -> bool:
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_panel")
|
||||
async def admin_panel_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
"""Show admin panel"""
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
@@ -71,46 +69,63 @@ async def admin_panel_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_stats")
|
||||
async def admin_stats_callback(callback: CallbackQuery, user: User, db: Database, api: RemnaWaveAPI = None, **kwargs):
|
||||
"""Show statistics"""
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
db_stats = await db.get_stats()
|
||||
|
||||
system_stats = None
|
||||
nodes_stats = None
|
||||
# Статистика игры в удачу
|
||||
lucky_stats = await db.get_lucky_game_admin_stats()
|
||||
|
||||
text = "📊 Расширенная статистика системы\n\n"
|
||||
|
||||
text += "💾 База данных бота:\n"
|
||||
text += f"👥 Пользователей: {db_stats['total_users']}\n"
|
||||
text += f"📋 Подписок: {db_stats['total_subscriptions_non_trial']}\n"
|
||||
text += f"💰 Доходы: {db_stats['total_revenue']} руб.\n\n"
|
||||
|
||||
text += "🎰 Игра в удачу:\n"
|
||||
if lucky_stats['total_games'] > 0:
|
||||
text += f"🎲 Всего игр: {lucky_stats['total_games']}\n"
|
||||
text += f"🏆 Выигрышей: {lucky_stats['total_wins']} ({lucky_stats['win_rate']:.1f}%)\n"
|
||||
text += f"👥 Уникальных игроков: {lucky_stats['unique_players']}\n"
|
||||
text += f"💎 Выплачено наград: {lucky_stats['total_rewards']:.0f}₽\n"
|
||||
text += f"📈 Средняя награда: {lucky_stats['avg_reward']:.0f}₽\n\n"
|
||||
|
||||
if lucky_stats['games_today'] > 0:
|
||||
text += f"📅 За сегодня:\n"
|
||||
text += f" • Игр: {lucky_stats['games_today']}\n"
|
||||
text += f" • Выигрышей: {lucky_stats['wins_today']} ({lucky_stats['win_rate_today']:.1f}%)\n"
|
||||
text += f" • Выплачено: {lucky_stats['wins_today'] * lucky_stats['avg_reward']:.0f}₽\n\n"
|
||||
|
||||
if lucky_stats['last_game']:
|
||||
last_game_str = format_datetime(
|
||||
datetime.fromisoformat(lucky_stats['last_game']).replace(tzinfo=None),
|
||||
user.language
|
||||
)
|
||||
text += f"🕐 Последняя игра: {last_game_str}\n\n"
|
||||
else:
|
||||
text += "🎯 Игр еще не было\n\n"
|
||||
|
||||
if api:
|
||||
try:
|
||||
system_stats = await api.get_system_stats()
|
||||
nodes_stats = await api.get_nodes_statistics()
|
||||
if nodes_stats and 'data' in nodes_stats:
|
||||
nodes = nodes_stats['data']
|
||||
online_nodes = len([n for n in nodes if n.get('status') == 'online'])
|
||||
text += f"🖥 Ноды RemnaWave: {online_nodes}/{len(nodes)} онлайн\n"
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get RemnaWave stats: {e}")
|
||||
|
||||
text = t('stats_info', user.language,
|
||||
users=db_stats['total_users'],
|
||||
subscriptions=db_stats['total_subscriptions_non_trial'],
|
||||
revenue=db_stats['total_revenue']
|
||||
)
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🎰 Детали игры в удачу", callback_data="lucky_game_admin_details")],
|
||||
[InlineKeyboardButton(text="🖥 Подробная системная статистика", callback_data="admin_system")],
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_stats")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', user.language), callback_data="admin_panel")]
|
||||
])
|
||||
|
||||
if system_stats:
|
||||
text += "\n\n🖥 Системная статистика:"
|
||||
if 'data' in system_stats:
|
||||
data = system_stats['data']
|
||||
if 'bandwidth' in data:
|
||||
bandwidth = data['bandwidth']
|
||||
text += f"\n📊 Трафик: ↓{format_bytes(bandwidth.get('downlink', 0))} ↑{format_bytes(bandwidth.get('uplink', 0))}"
|
||||
|
||||
if nodes_stats and 'data' in nodes_stats:
|
||||
nodes = nodes_stats['data']
|
||||
online_nodes = len([n for n in nodes if n.get('status') == 'online'])
|
||||
text += f"\n🖥 Нод: {online_nodes}/{len(nodes)} онлайн"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=back_keyboard("admin_panel", user.language)
|
||||
)
|
||||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting statistics: {e}")
|
||||
@@ -8303,3 +8318,86 @@ async def edit_sub_autopay_callback(callback: CallbackQuery, user: User, **kwarg
|
||||
except Exception as e:
|
||||
logger.error(f"Error editing subscription autopay: {e}")
|
||||
await callback.answer("❌ Ошибка редактирования", show_alert=True)
|
||||
|
||||
@admin_router.callback_query(F.data == "lucky_game_admin_details")
|
||||
async def lucky_game_admin_details_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
lucky_stats = await db.get_lucky_game_admin_stats()
|
||||
top_players = await db.get_lucky_game_top_players(5)
|
||||
|
||||
text = "🎰 **Детальная статистика игры в удачу**\n\n"
|
||||
|
||||
if lucky_stats['total_games'] > 0:
|
||||
text += "📊 **Общая статистика:**\n"
|
||||
text += f"🎲 Всего игр сыграно: {lucky_stats['total_games']}\n"
|
||||
text += f"🏆 Всего выигрышей: {lucky_stats['total_wins']}\n"
|
||||
text += f"📈 Процент побед: {lucky_stats['win_rate']:.2f}%\n"
|
||||
text += f"👥 Уникальных игроков: {lucky_stats['unique_players']}\n"
|
||||
text += f"💎 Общая сумма выплат: {lucky_stats['total_rewards']:.0f}₽\n"
|
||||
text += f"💰 Средняя выплата: {lucky_stats['avg_reward']:.1f}₽\n\n"
|
||||
|
||||
text += "📅 **За сегодня:**\n"
|
||||
text += f"🎯 Игр: {lucky_stats['games_today']}\n"
|
||||
text += f"🎉 Выигрышей: {lucky_stats['wins_today']}\n"
|
||||
if lucky_stats['games_today'] > 0:
|
||||
text += f"📊 Процент побед: {lucky_stats['win_rate_today']:.1f}%\n"
|
||||
today_payouts = lucky_stats['wins_today'] * lucky_stats['avg_reward']
|
||||
text += f"💸 Выплачено сегодня: {today_payouts:.0f}₽\n"
|
||||
text += "\n"
|
||||
|
||||
if top_players:
|
||||
text += "🏆 **Топ-5 игроков:**\n"
|
||||
for i, player in enumerate(top_players, 1):
|
||||
name = player['first_name'] or player['username']
|
||||
text += f"{i}. {name} (ID: {player['user_id']})\n"
|
||||
text += f" 💰 Выиграл: {player['total_won']:.0f}₽\n"
|
||||
text += f" 🎯 Игр: {player['games_played']} | Побед: {player['wins']} ({player['win_rate']:.1f}%)\n"
|
||||
|
||||
if player['last_game']:
|
||||
last_game = format_datetime(
|
||||
datetime.fromisoformat(player['last_game']).replace(tzinfo=None),
|
||||
user.language
|
||||
)
|
||||
text += f" 🕐 Последняя игра: {last_game}\n"
|
||||
text += "\n"
|
||||
|
||||
if lucky_stats['first_game'] and lucky_stats['last_game']:
|
||||
first_game = format_datetime(
|
||||
datetime.fromisoformat(lucky_stats['first_game']).replace(tzinfo=None),
|
||||
user.language
|
||||
)
|
||||
last_game = format_datetime(
|
||||
datetime.fromisoformat(lucky_stats['last_game']).replace(tzinfo=None),
|
||||
user.language
|
||||
)
|
||||
text += f"🕐 **Временные рамки:**\n"
|
||||
text += f"🥇 Первая игра: {first_game}\n"
|
||||
text += f"🕐 Последняя игра: {last_game}\n\n"
|
||||
|
||||
else:
|
||||
text += "🎯 В игру в удачу еще никто не играл.\n\n"
|
||||
text += "Игроки смогут играть после активации функции в боте."
|
||||
|
||||
text += f"🕕 _Обновлено: {format_datetime(datetime.now(), user.language)}_"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="lucky_game_admin_details")],
|
||||
[InlineKeyboardButton(text="📊 Общая статистика", callback_data="admin_stats")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting lucky game admin details: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка получения детальной статистики игры",
|
||||
reply_markup=back_keyboard("admin_stats", user.language)
|
||||
)
|
||||
|
||||
+1
-1
@@ -278,7 +278,7 @@ class AutoPayService:
|
||||
async def get_service_status(self) -> dict:
|
||||
return {
|
||||
'is_running': self.is_running,
|
||||
'check_interval': 1800, # 30 минут
|
||||
'check_interval': 1800,
|
||||
'has_api': self.api is not None,
|
||||
'has_bot': self.bot is not None
|
||||
}
|
||||
|
||||
@@ -28,6 +28,15 @@ class Config:
|
||||
DELETE_EXPIRED_TRIAL_DAYS: int = 1
|
||||
DELETE_EXPIRED_REGULAR_DAYS: int = 7
|
||||
AUTO_DELETE_ENABLED: bool = False
|
||||
TRIAL_NOTIFICATION_ENABLED: bool = True
|
||||
TRIAL_NOTIFICATION_HOURS_AFTER: int = 2
|
||||
TRIAL_NOTIFICATION_HOURS_WINDOW: int = 22
|
||||
TRIBUTE_ENABLED: bool = False
|
||||
TRIBUTE_API_KEY: str = ""
|
||||
TRIBUTE_WEBHOOK_PORT: int = 8081
|
||||
TRIBUTE_WEBHOOK_PATH: str = "/tribute-webhook"
|
||||
TRIBUTE_DONATE_URL: str = ""
|
||||
TRIBUTE_DONATE_LINK: str = ""
|
||||
|
||||
LUCKY_GAME_ENABLED: bool = True
|
||||
LUCKY_GAME_REWARD: float = 50.0
|
||||
@@ -112,12 +121,21 @@ def load_config() -> Config:
|
||||
DELETE_EXPIRED_TRIAL_DAYS=get_int('DELETE_EXPIRED_TRIAL_DAYS', 1),
|
||||
DELETE_EXPIRED_REGULAR_DAYS=get_int('DELETE_EXPIRED_REGULAR_DAYS', 7),
|
||||
AUTO_DELETE_ENABLED=get_bool('AUTO_DELETE_ENABLED', False),
|
||||
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),
|
||||
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),
|
||||
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3),
|
||||
STARS_ENABLED=get_bool('STARS_ENABLED', True),
|
||||
STARS_RATES=parse_stars_rates()
|
||||
STARS_RATES=parse_stars_rates(),
|
||||
TRIBUTE_ENABLED=get_bool('TRIBUTE_ENABLED', False),
|
||||
TRIBUTE_API_KEY=os.getenv('TRIBUTE_API_KEY', ''),
|
||||
TRIBUTE_WEBHOOK_PORT=get_int('TRIBUTE_WEBHOOK_PORT', 8081),
|
||||
TRIBUTE_WEBHOOK_PATH=os.getenv('TRIBUTE_WEBHOOK_PATH', '/tribute-webhook'),
|
||||
TRIBUTE_DONATE_URL=os.getenv('TRIBUTE_DONATE_URL', ''),
|
||||
TRIBUTE_DONATE_LINK=os.getenv('TRIBUTE_DONATE_LINK', '')
|
||||
)
|
||||
|
||||
def debug_environment():
|
||||
|
||||
+98
@@ -2435,3 +2435,101 @@ class Database:
|
||||
'trial_subscriptions': 0,
|
||||
'imported_subscriptions': 0
|
||||
}
|
||||
|
||||
async def get_lucky_game_admin_stats(self) -> dict:
|
||||
try:
|
||||
query = """
|
||||
SELECT
|
||||
COUNT(*) as total_games,
|
||||
COUNT(CASE WHEN is_winner = 1 THEN 1 END) as total_wins,
|
||||
COUNT(DISTINCT user_id) as unique_players,
|
||||
SUM(reward_amount) as total_rewards,
|
||||
AVG(reward_amount) as avg_reward,
|
||||
COUNT(CASE WHEN DATE(played_at) = DATE('now') THEN 1 END) as games_today,
|
||||
COUNT(CASE WHEN DATE(played_at) = DATE('now') AND is_winner = 1 THEN 1 END) as wins_today,
|
||||
MAX(played_at) as last_game,
|
||||
MIN(played_at) as first_game
|
||||
FROM lucky_games
|
||||
"""
|
||||
|
||||
async with self.get_connection() as conn:
|
||||
async with conn.execute(query) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
|
||||
if row:
|
||||
stats = {
|
||||
'total_games': row[0] or 0,
|
||||
'total_wins': row[1] or 0,
|
||||
'unique_players': row[2] or 0,
|
||||
'total_rewards': row[3] or 0.0,
|
||||
'avg_reward': row[4] or 0.0,
|
||||
'games_today': row[5] or 0,
|
||||
'wins_today': row[6] or 0,
|
||||
'last_game': row[7],
|
||||
'first_game': row[8]
|
||||
}
|
||||
|
||||
if stats['total_games'] > 0:
|
||||
stats['win_rate'] = (stats['total_wins'] / stats['total_games']) * 100
|
||||
stats['win_rate_today'] = (stats['wins_today'] / stats['games_today']) * 100 if stats['games_today'] > 0 else 0
|
||||
else:
|
||||
stats['win_rate'] = 0
|
||||
stats['win_rate_today'] = 0
|
||||
|
||||
return stats
|
||||
|
||||
return {
|
||||
'total_games': 0, 'total_wins': 0, 'unique_players': 0,
|
||||
'total_rewards': 0.0, 'avg_reward': 0.0, 'games_today': 0,
|
||||
'wins_today': 0, 'win_rate': 0, 'win_rate_today': 0,
|
||||
'last_game': None, 'first_game': None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting lucky game admin stats: {e}")
|
||||
return {
|
||||
'total_games': 0, 'total_wins': 0, 'unique_players': 0,
|
||||
'total_rewards': 0.0, 'avg_reward': 0.0, 'games_today': 0,
|
||||
'wins_today': 0, 'win_rate': 0, 'win_rate_today': 0,
|
||||
'last_game': None, 'first_game': None
|
||||
}
|
||||
|
||||
async def get_lucky_game_top_players(self, limit: int = 5) -> List[dict]:
|
||||
try:
|
||||
query = """
|
||||
SELECT
|
||||
lg.user_id,
|
||||
u.username,
|
||||
u.first_name,
|
||||
COUNT(*) as games_played,
|
||||
COUNT(CASE WHEN lg.is_winner = 1 THEN 1 END) as wins,
|
||||
SUM(lg.reward_amount) as total_won,
|
||||
MAX(lg.played_at) as last_game
|
||||
FROM lucky_games lg
|
||||
LEFT JOIN users u ON lg.user_id = u.telegram_id
|
||||
GROUP BY lg.user_id
|
||||
ORDER BY total_won DESC, wins DESC
|
||||
LIMIT ?
|
||||
"""
|
||||
|
||||
async with self.get_connection() as conn:
|
||||
async with conn.execute(query, (limit,)) as cursor:
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
return [
|
||||
{
|
||||
'user_id': row[0],
|
||||
'username': row[1] or 'N/A',
|
||||
'first_name': row[2] or 'Unknown',
|
||||
'games_played': row[3],
|
||||
'wins': row[4],
|
||||
'total_won': row[5],
|
||||
'last_game': row[6],
|
||||
'win_rate': (row[4] / row[3]) * 100 if row[3] > 0 else 0
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting lucky game top players: {e}")
|
||||
return []
|
||||
|
||||
+164
-4
@@ -66,6 +66,7 @@ class BotStates(StatesGroup):
|
||||
waiting_rule_edit_title = State()
|
||||
waiting_rule_edit_content = State()
|
||||
waiting_rule_edit_order = State()
|
||||
waiting_tribute_amount = State()
|
||||
|
||||
|
||||
router = Router()
|
||||
@@ -428,16 +429,18 @@ async def topup_balance_callback(callback: CallbackQuery, **kwargs):
|
||||
return
|
||||
|
||||
stars_enabled = config and config.STARS_ENABLED and config.STARS_RATES
|
||||
tribute_enabled = config and config.TRIBUTE_ENABLED
|
||||
|
||||
text = t('topup_balance', user.language)
|
||||
text = "💰 Выберите способ пополнения баланса:"
|
||||
|
||||
if tribute_enabled:
|
||||
text += "\n\n💳 **Tribute** - карты, СБП, быстрые платежи"
|
||||
if stars_enabled:
|
||||
text += "\n\n⭐ **Новинка!** Теперь можно пополнять баланс через Telegram Stars!"
|
||||
text += "\n💎 Быстро, безопасно, без комиссий!"
|
||||
text += "\n⭐ **Telegram Stars** - быстро и безопасно"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=topup_keyboard(user.language),
|
||||
reply_markup=topup_keyboard(user.language, tribute_enabled),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
@@ -2018,3 +2021,160 @@ async def autopay_days_callback(callback: CallbackQuery, db: Database, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting autopay days: {e}")
|
||||
await callback.answer("❌ Ошибка операции")
|
||||
|
||||
@router.callback_query(F.data == "topup_tribute")
|
||||
async def topup_tribute_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
if not config or not config.TRIBUTE_ENABLED:
|
||||
await callback.answer("❌ Tribute платежи недоступны")
|
||||
return
|
||||
|
||||
text = (
|
||||
"💳 **Пополнение через Tribute**\n\n"
|
||||
"🔹 **Доступные способы оплаты:**\n"
|
||||
"• 💳 Банковские карты (Visa, MasterCard, МИР)\n"
|
||||
"• 📱 Система быстрых платежей (СБП)\n"
|
||||
"• 🍎 Apple Pay, Google Pay\n\n"
|
||||
"💰 **Выберите сумму пополнения:**"
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=tribute_amounts_keyboard(user.language),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("tribute_amount_"))
|
||||
async def tribute_amount_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
amount_str = callback.data.split("_")[-1]
|
||||
try:
|
||||
amount = int(amount_str)
|
||||
except ValueError:
|
||||
await callback.answer("❌ Неверная сумма")
|
||||
return
|
||||
|
||||
await create_tribute_payment(callback, user, amount, config)
|
||||
|
||||
@router.callback_query(F.data == "tribute_custom_amount")
|
||||
async def tribute_custom_amount_callback(callback: CallbackQuery, state: FSMContext, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
"💰 Введите сумму для пополнения (100-15000 рублей):",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="topup_tribute")]
|
||||
])
|
||||
)
|
||||
await state.set_state(BotStates.waiting_tribute_amount)
|
||||
|
||||
@router.message(StateFilter(BotStates.waiting_tribute_amount))
|
||||
async def handle_tribute_amount(message: Message, state: FSMContext, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await message.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
is_valid, amount = is_valid_amount(message.text)
|
||||
|
||||
if not is_valid or amount < 100 or amount > 15000:
|
||||
await message.answer(
|
||||
"❌ Неверная сумма. Введите число от 100 до 15000:",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="topup_tribute")]
|
||||
])
|
||||
)
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
class FakeCallback:
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
async def answer(self, text, show_alert=False):
|
||||
await self.message.answer(text)
|
||||
|
||||
fake_callback = FakeCallback(message)
|
||||
await create_tribute_payment(fake_callback, user, int(amount), config)
|
||||
|
||||
async def create_tribute_payment(callback, user, amount: int, config):
|
||||
try:
|
||||
tribute_donate_link = config.TRIBUTE_DONATE_LINK
|
||||
|
||||
text = (
|
||||
f"💳 **Пополнение через Tribute**\n\n"
|
||||
f"💰 Сумма: **{amount}₽**\n\n"
|
||||
f"📋 **Инструкция:**\n"
|
||||
f"1️⃣ Нажмите кнопку «Открыть Tribute»\n"
|
||||
f"2️⃣ Введите сумму: **{amount}₽**\n"
|
||||
f"3️⃣ Выберите способ оплаты (карта/СБП)\n"
|
||||
f"4️⃣ Завершите платеж\n\n"
|
||||
f"⏱️ После оплаты средства поступят на баланс автоматически в течение 1 минуты\n\n"
|
||||
f"💡 Ваш аккаунт привязан автоматически, никаких дополнительных данных вводить не нужно"
|
||||
)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💳 Открыть Tribute", url=tribute_donate_link)],
|
||||
[InlineKeyboardButton(text="🔄 Проверить платеж", callback_data=f"check_tribute_{amount}")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="topup_balance")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="Markdown")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating Tribute payment: {e}")
|
||||
await callback.answer("❌ Ошибка создания платежа")
|
||||
|
||||
@router.callback_query(F.data.startswith("check_tribute_"))
|
||||
async def check_tribute_payment_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
payments = await db.get_user_payments(user.telegram_id)
|
||||
recent_tribute_payments = [
|
||||
p for p in payments
|
||||
if p.payment_type == 'tribute' and p.status == 'completed'
|
||||
and (datetime.utcnow() - p.created_at).total_seconds() < 1800
|
||||
]
|
||||
|
||||
if recent_tribute_payments:
|
||||
await callback.answer("✅ Платеж найден! Средства зачислены на баланс", show_alert=True)
|
||||
await callback.message.edit_text(
|
||||
f"✅ **Платеж успешно обработан!**\n\n"
|
||||
f"💰 Зачислено: {recent_tribute_payments[0].amount}₽\n"
|
||||
f"💳 Текущий баланс: {user.balance}₽",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💰 Мой баланс", callback_data="balance")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
]),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
else:
|
||||
await callback.answer("⏳ Платеж еще не обработан. Попробуйте через минуту.", show_alert=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking Tribute payment: {e}")
|
||||
await callback.answer("❌ Ошибка проверки платежа")
|
||||
|
||||
+47
-14
@@ -53,13 +53,54 @@ def balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
# [InlineKeyboardButton(text="💳 " + t('topup_card', lang), callback_data="topup_card")],
|
||||
[InlineKeyboardButton(text="👨💼 " + t('topup_support', lang), callback_data="topup_support")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="balance")]
|
||||
def topup_keyboard(lang: str, tribute_enabled: bool = False) -> InlineKeyboardMarkup:
|
||||
keyboard = []
|
||||
|
||||
if tribute_enabled:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="💳 Tribute (Карта/СБП)" if lang == 'ru' else "💳 Tribute (Card/SBP)",
|
||||
callback_data="topup_tribute"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.extend([
|
||||
[InlineKeyboardButton(
|
||||
text="⭐ Telegram Stars" if lang == 'ru' else "⭐ Telegram Stars",
|
||||
callback_data="topup_stars"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text="💬 Связаться с поддержкой" if lang == 'ru' else "💬 Contact Support",
|
||||
callback_data="topup_support"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text="🔙 Назад" if lang == 'ru' else "🔙 Back",
|
||||
callback_data="balance"
|
||||
)]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
def tribute_amounts_keyboard(lang: str) -> InlineKeyboardMarkup:
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(text="💯 100₽", callback_data="tribute_amount_100"),
|
||||
InlineKeyboardButton(text="💰 300₽", callback_data="tribute_amount_300")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="💎 500₽", callback_data="tribute_amount_500"),
|
||||
InlineKeyboardButton(text="🎯 1000₽", callback_data="tribute_amount_1000")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="💸 2000₽", callback_data="tribute_amount_2000"),
|
||||
InlineKeyboardButton(text="🏆 5000₽", callback_data="tribute_amount_5000")
|
||||
],
|
||||
[InlineKeyboardButton(text="✏️ Своя сумма", callback_data="tribute_custom_amount")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="topup_balance")]
|
||||
]
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
def subscriptions_keyboard(subscriptions: List[dict], lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
@@ -713,14 +754,6 @@ def lucky_game_result_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
|
||||
def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="⭐ Telegram Stars", callback_data="topup_stars")],
|
||||
[InlineKeyboardButton(text="👨💼 " + t('topup_support', lang), callback_data="topup_support")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="balance")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def stars_topup_keyboard(stars_rates: Dict[int, float], lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ class BotApplication:
|
||||
self.dp = None
|
||||
self.monitor_service = None
|
||||
self.autopay_service = None
|
||||
self.webhook_server = None
|
||||
|
||||
async def _init_autopay_service(self):
|
||||
"""Инициализирует сервис автоплатежей"""
|
||||
try:
|
||||
logger.info("🔧 Initializing autopay service...")
|
||||
|
||||
@@ -111,6 +111,7 @@ class BotApplication:
|
||||
logger.info(f"Bot Username: {self.config.BOT_USERNAME}")
|
||||
|
||||
self.db = Database(self.config.DATABASE_URL)
|
||||
|
||||
await self._init_database()
|
||||
|
||||
self.api = RemnaWaveAPI(
|
||||
@@ -130,6 +131,17 @@ class BotApplication:
|
||||
await self._test_bot_token()
|
||||
|
||||
self._setup_dispatcher()
|
||||
|
||||
await self._init_webhook_server()
|
||||
|
||||
if self.config.TRIBUTE_ENABLED:
|
||||
logger.info("✅ Tribute платежи включены")
|
||||
if not self.config.TRIBUTE_API_KEY:
|
||||
logger.warning("⚠️ TRIBUTE_API_KEY не установлен!")
|
||||
if not self.config.TRIBUTE_DONATE_URL:
|
||||
logger.warning("⚠️ TRIBUTE_DONATE_URL не установлен!")
|
||||
else:
|
||||
logger.info("❌ Tribute платежи отключены")
|
||||
|
||||
await self._init_monitor_service()
|
||||
await self._init_autopay_service()
|
||||
@@ -151,18 +163,37 @@ class BotApplication:
|
||||
self.config.STARS_ENABLED = False
|
||||
else:
|
||||
logger.info("❌ Telegram Stars пополнение отключено")
|
||||
|
||||
async def _init_webhook_server(self):
|
||||
"""Инициализация webhook сервера для Tribute"""
|
||||
try:
|
||||
logger.info("🔧 Initializing webhook server...")
|
||||
|
||||
from webhook_server import WebhookServer
|
||||
self.webhook_server = WebhookServer(self.bot, self.db, self.config)
|
||||
|
||||
logger.info("🚀 Starting webhook server...")
|
||||
await self.webhook_server.start()
|
||||
|
||||
logger.info("✅ Webhook server started successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to initialize webhook server: {e}", exc_info=True)
|
||||
logger.warning("⚠️ Continuing without webhook server")
|
||||
self.webhook_server = None
|
||||
|
||||
async def _init_database(self):
|
||||
max_retries = 3
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
logger.info(f"🗄️ Database initialization attempt {attempt + 1}/{max_retries}")
|
||||
await self.db.init_db()
|
||||
logger.info("Database initialized successfully")
|
||||
logger.info("✅ Database initialized successfully with all migrations")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Database initialization attempt {attempt + 1} failed: {e}")
|
||||
logger.error(f"❌ Database initialization attempt {attempt + 1} failed: {e}")
|
||||
if attempt == max_retries - 1:
|
||||
logger.error("Failed to initialize database after all retries")
|
||||
logger.error("💥 Failed to initialize database after all retries")
|
||||
raise
|
||||
await asyncio.sleep(2)
|
||||
|
||||
@@ -198,7 +229,8 @@ class BotApplication:
|
||||
"config": self.config,
|
||||
"api": self.api,
|
||||
"db": self.db,
|
||||
"monitor_service": None
|
||||
"monitor_service": None,
|
||||
"autopay_service": None
|
||||
})
|
||||
|
||||
self.dp.message.middleware(LoggingMiddleware())
|
||||
@@ -286,6 +318,13 @@ class BotApplication:
|
||||
async def shutdown(self):
|
||||
logger.info("Shutting down bot...")
|
||||
|
||||
if self.webhook_server:
|
||||
try:
|
||||
await self.webhook_server.stop()
|
||||
logger.info("Webhook server stopped")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping webhook server: {e}")
|
||||
|
||||
if self.autopay_service:
|
||||
try:
|
||||
await self.autopay_service.stop()
|
||||
|
||||
+2
-2
@@ -20,8 +20,8 @@ class RemnaWaveAPI:
|
||||
'Authorization': f'Bearer {self.token}',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-Forwarded-Proto': 'https',
|
||||
'X-Forwarded-For': '127.0.0.1'
|
||||
'X-Forwarded-Proto': 'https', # ← ДОБАВЬ ЭТО
|
||||
'X-Forwarded-For': '127.0.0.1' # ← И ЭТО
|
||||
}
|
||||
timeout = aiohttp.ClientTimeout(total=30)
|
||||
self.session = aiohttp.ClientSession(
|
||||
|
||||
+127
-15
@@ -86,7 +86,7 @@ class SubscriptionMonitorService:
|
||||
logger.info("Subscription monitor service stopped")
|
||||
|
||||
async def _monitor_loop(self):
|
||||
logger.info("🔄 Starting monitor loop")
|
||||
logger.info("🔥 Starting monitor loop")
|
||||
|
||||
logger.info("⏰ Initial check in 10 seconds...")
|
||||
await asyncio.sleep(10)
|
||||
@@ -96,8 +96,10 @@ class SubscriptionMonitorService:
|
||||
logger.info("🔍 Running periodic subscription check...")
|
||||
warnings_sent = await self._check_expiring_subscriptions()
|
||||
|
||||
if warnings_sent > 0:
|
||||
logger.info(f"✅ Monitor check completed: {warnings_sent} warnings sent")
|
||||
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")
|
||||
else:
|
||||
logger.info("✅ Monitor check completed: no warnings needed")
|
||||
|
||||
@@ -147,7 +149,94 @@ class SubscriptionMonitorService:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in daily loop: {e}", exc_info=True)
|
||||
await asyncio.sleep(3600)
|
||||
await asyncio.sleep(3600)
|
||||
|
||||
async def _check_expired_trial_subscriptions(self) -> int:
|
||||
try:
|
||||
logger.info("🆓 Checking for expired trial subscriptions...")
|
||||
|
||||
notifications_sent = 0
|
||||
now_utc = datetime.utcnow()
|
||||
|
||||
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:
|
||||
continue
|
||||
|
||||
if not subscription.is_trial:
|
||||
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_since_expiry = -time_diff.total_seconds() / 3600
|
||||
|
||||
if 1 <= hours_since_expiry <= 24 and user_sub.is_active:
|
||||
logger.info(f"🆓 Sending trial expiry notification to user {user.telegram_id}: "
|
||||
f"trial '{subscription.name}' expired {hours_since_expiry:.1f} hours ago")
|
||||
|
||||
try:
|
||||
await self._send_trial_expiry_notification(user, subscription)
|
||||
notifications_sent += 1
|
||||
logger.info(f"✅ Trial expiry notification sent to user {user.telegram_id}")
|
||||
except Exception as notification_error:
|
||||
logger.error(f"❌ Failed to send trial notification to user {user.telegram_id}: {notification_error}")
|
||||
|
||||
except Exception as sub_error:
|
||||
logger.error(f"❌ Error checking trial subscription {user_sub.id}: {sub_error}")
|
||||
|
||||
except Exception as user_error:
|
||||
logger.error(f"❌ Error checking trial subscriptions for user {user.telegram_id}: {user_error}")
|
||||
|
||||
if notifications_sent > 0:
|
||||
logger.info(f"🆓 Trial expiry check completed: {notifications_sent} notifications sent")
|
||||
|
||||
return notifications_sent
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Critical error in check_expired_trial_subscriptions: {e}", exc_info=True)
|
||||
return 0
|
||||
|
||||
async def _send_trial_expiry_notification(self, user, subscription):
|
||||
try:
|
||||
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
|
||||
|
||||
message = t('trial_subscription_expired', user.language, name=subscription.name)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(
|
||||
text=t('buy_subscription_btn', user.language),
|
||||
callback_data="buy_subscription"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=t('my_subscriptions_btn', user.language),
|
||||
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}'")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error sending trial expiry notification to user {user.telegram_id}: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def delete_expired_trial_subscriptions(self, force: bool = False) -> Dict[str, Any]:
|
||||
try:
|
||||
@@ -388,11 +477,11 @@ class SubscriptionMonitorService:
|
||||
f"threshold={self.config.MONITOR_WARNING_DAYS}")
|
||||
|
||||
if subscription.is_trial:
|
||||
logger.debug(f"⏭️ Skipping trial subscription '{subscription.name}'")
|
||||
logger.debug(f"⭐️ Skipping trial subscription '{subscription.name}'")
|
||||
continue
|
||||
|
||||
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
|
||||
logger.debug(f"⏭️ Skipping imported subscription '{subscription.name}'")
|
||||
logger.debug(f"⭐️ Skipping imported subscription '{subscription.name}'")
|
||||
continue
|
||||
|
||||
should_warn = (
|
||||
@@ -517,9 +606,13 @@ class SubscriptionMonitorService:
|
||||
warnings_sent = await self._check_expiring_subscriptions()
|
||||
logger.info(f"📢 Sent {warnings_sent} expiry warnings")
|
||||
|
||||
logger.info("🔄 Deactivating expired subscriptions...")
|
||||
logger.info("🆓 Checking expired trial subscriptions...")
|
||||
trial_notifications = await self._check_expired_trial_subscriptions()
|
||||
logger.info(f"🆓 Sent {trial_notifications} trial expiry notifications")
|
||||
|
||||
logger.info("🔥 Deactivating expired subscriptions...")
|
||||
deactivated_count = await self.deactivate_expired_subscriptions()
|
||||
logger.info(f"🔄 Deactivated {deactivated_count} expired subscriptions")
|
||||
logger.info(f"🔥 Deactivated {deactivated_count} expired subscriptions")
|
||||
|
||||
deleted_trials = 0
|
||||
deleted_regular = 0
|
||||
@@ -538,7 +631,7 @@ class SubscriptionMonitorService:
|
||||
await self._send_final_expiry_notifications()
|
||||
logger.info("📩 Final notifications sent")
|
||||
|
||||
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Deactivated: {deactivated_count}, "
|
||||
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Trial notifications: {trial_notifications}, Deactivated: {deactivated_count}, "
|
||||
f"Deleted trials: {deleted_trials}, Deleted regular: {deleted_regular}")
|
||||
return deactivated_count
|
||||
|
||||
@@ -590,7 +683,7 @@ class SubscriptionMonitorService:
|
||||
user_data = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
|
||||
if user_data and user_data.get('uuid'):
|
||||
await self.api.update_user(user_data['uuid'], {'status': 'EXPIRED'})
|
||||
logger.debug(f"🔄 Also deactivated user {user_data['uuid']} in RemnaWave")
|
||||
logger.debug(f"🔥 Also deactivated user {user_data['uuid']} in RemnaWave")
|
||||
except Exception as api_error:
|
||||
logger.warning(f"⚠️ Could not deactivate user in RemnaWave: {api_error}")
|
||||
else:
|
||||
@@ -704,11 +797,30 @@ class SubscriptionMonitorService:
|
||||
days_left = int(hours_left / 24)
|
||||
|
||||
if subscription.is_trial:
|
||||
results.append({
|
||||
'success': True,
|
||||
'message': f'Trial subscription "{subscription.name}" skipped (no warnings for trials)',
|
||||
'error': None
|
||||
})
|
||||
hours_since_expiry = -hours_left
|
||||
if 1 <= hours_since_expiry <= 24 and user_sub.is_active:
|
||||
test_message = f"🧪 [ТЕСТОВОЕ УВЕДОМЛЕНИЕ]\n\n🆓 Ваша триальная подписка '{subscription.name}' истекла! Купите новый тариф чтобы продолжить использование VPN."
|
||||
|
||||
if self.bot:
|
||||
try:
|
||||
await self.bot.send_message(user_id, test_message)
|
||||
results.append({
|
||||
'success': True,
|
||||
'message': f'✅ Sent test trial expiry notification for "{subscription.name}" (expired {hours_since_expiry:.1f} hours ago)',
|
||||
'error': None
|
||||
})
|
||||
except Exception as send_error:
|
||||
results.append({
|
||||
'success': False,
|
||||
'message': f'❌ Failed to send test trial notification for "{subscription.name}"',
|
||||
'error': str(send_error)
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'success': True,
|
||||
'message': f'Trial subscription "{subscription.name}" - no notification needed (expired {hours_since_expiry:.1f} hours ago)',
|
||||
'error': None
|
||||
})
|
||||
continue
|
||||
|
||||
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
|
||||
|
||||
+7
-1
@@ -194,7 +194,10 @@ TRANSLATIONS = {
|
||||
'lucky_game_games_played': 'Игр сыграно: {count}',
|
||||
'lucky_game_wins': 'Выигрышей: {count}',
|
||||
'lucky_game_total_won': 'Всего выиграно: {amount}₽',
|
||||
'lucky_game_win_rate': 'Процент побед: {rate}%'
|
||||
'lucky_game_win_rate': 'Процент побед: {rate}%',
|
||||
'trial_subscription_expired': '🆓 Ваша триальная подписка "{name}" истекла!\n\n'
|
||||
'💡 Чтобы продолжить использование VPN, купите полный тариф.\n\n'
|
||||
'✨ Доступны различные планы подписки с выгодными ценами!'
|
||||
},
|
||||
|
||||
'en': {
|
||||
@@ -246,6 +249,9 @@ TRANSLATIONS = {
|
||||
'subscription_expires_day_after_tomorrow': '⏰ Your subscription \'{name}\' expires in {days} days!\n\nWe recommend renewing it in advance in \'My Subscriptions\'.\n\n💰 Check your balance - you might want to top it up.',
|
||||
'subscription_expires_in_days': '⏳ Your subscription \'{name}\' expires in {days} days!\n\nYou can renew it in \'My Subscriptions\'.\n\n💡 Early renewal extends from current expiration date.',
|
||||
'extend_subscription_btn': '🔄 Extend Subscription',
|
||||
'trial_subscription_expired': '🆓 Your trial subscription "{name}" has expired!\n\n'
|
||||
'💡 To continue using VPN, please purchase a full plan.\n\n'
|
||||
'✨ Various subscription plans with great prices are available!',
|
||||
'my_subscriptions_btn': '📋 My Subscriptions',
|
||||
'buy_new_subscription_btn': '🛒 Buy New Subscription',
|
||||
'restore_subscription_btn': '🔄 Restore Subscription',
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import logging
|
||||
import hmac
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from aiohttp import web
|
||||
from aiogram import Bot
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from config import Config
|
||||
from database import Database
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def convert_period_to_months(period: Optional[str]) -> int:
|
||||
"""Map Tribute subscription period strings to months."""
|
||||
if not period:
|
||||
return 1
|
||||
|
||||
mapping = {
|
||||
"monthly": 1,
|
||||
"quarterly": 3,
|
||||
"3-month": 3,
|
||||
"3months": 3,
|
||||
"3-months": 3,
|
||||
"q": 3,
|
||||
"halfyearly": 6,
|
||||
"yearly": 12,
|
||||
"annual": 12,
|
||||
"y": 12,
|
||||
}
|
||||
return mapping.get(period.lower(), 1)
|
||||
|
||||
|
||||
class TributeService:
|
||||
def __init__(
|
||||
self,
|
||||
bot: Bot,
|
||||
config: Config,
|
||||
db: Database,
|
||||
):
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
self.db = db
|
||||
|
||||
async def handle_webhook(self, raw_body: bytes, signature_header: Optional[str]) -> web.Response:
|
||||
def ok(data: Optional[dict] = None) -> web.Response:
|
||||
payload = {"status": "ok"}
|
||||
if data:
|
||||
payload.update(data)
|
||||
return web.json_response(payload, status=200)
|
||||
|
||||
def ignored(reason: str) -> web.Response:
|
||||
return web.json_response({"status": "ignored", "reason": reason}, status=200)
|
||||
|
||||
def bad_request(reason: str) -> web.Response:
|
||||
return web.json_response({"status": "error", "reason": reason}, status=400)
|
||||
|
||||
if hasattr(self.config, 'TRIBUTE_API_KEY') and self.config.TRIBUTE_API_KEY:
|
||||
if not signature_header:
|
||||
return web.json_response({"status": "error", "reason": "no_signature"}, status=403)
|
||||
expected_sig = hmac.new(self.config.TRIBUTE_API_KEY.encode(), raw_body,
|
||||
hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(expected_sig, signature_header):
|
||||
return web.json_response({"status": "error", "reason": "invalid_signature"}, status=403)
|
||||
|
||||
try:
|
||||
payload = json.loads(raw_body.decode())
|
||||
except Exception:
|
||||
return bad_request("invalid_json")
|
||||
|
||||
logging.info(
|
||||
"Tribute webhook data: %s",
|
||||
json.dumps(payload, ensure_ascii=False),
|
||||
)
|
||||
|
||||
event_name = payload.get("name")
|
||||
data = payload.get("payload", {})
|
||||
|
||||
user_id = data.get("telegram_user_id")
|
||||
if not user_id:
|
||||
return ignored("missing_telegram_user_id")
|
||||
|
||||
amount_value = data.get("amount", 0)
|
||||
currency = data.get("currency", "RUB").upper()
|
||||
amount_float = round(amount_value / 100.0, 2)
|
||||
|
||||
if event_name == "new_donation":
|
||||
await self._handle_new_donation(user_id, amount_float, currency, data)
|
||||
elif event_name == "cancelled_subscription":
|
||||
await self._handle_cancellation(user_id)
|
||||
|
||||
return ok({"event": event_name or "unknown"})
|
||||
|
||||
async def _handle_new_donation(self, user_id: int, amount: float, currency: str, data: dict):
|
||||
try:
|
||||
if not user_id:
|
||||
logger.warning(f"No telegram_user_id in webhook data")
|
||||
return
|
||||
|
||||
async with self.db.session_factory() as session:
|
||||
payment = await self.db.create_payment(
|
||||
user_id=int(user_id),
|
||||
amount=amount,
|
||||
payment_type='tribute',
|
||||
description=f'Пополнение через Tribute: {amount} {currency}',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
await self.db.add_balance(int(user_id), amount)
|
||||
|
||||
try:
|
||||
success_msg = (
|
||||
f"✅ **Платеж через Tribute получен!**\n\n"
|
||||
f"💰 Сумма: {amount} {currency}\n"
|
||||
f"🎉 Средства зачислены на баланс!\n\n"
|
||||
f"💳 Ваш текущий баланс можно посмотреть в главном меню."
|
||||
)
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💰 Мой баланс", callback_data="balance")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
int(user_id),
|
||||
success_msg,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send Tribute payment success message to user {user_id}: {e}")
|
||||
|
||||
await session.commit()
|
||||
logger.info(f"Successfully processed Tribute donation: {amount} {currency} for user {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling tribute donation: {e}")
|
||||
|
||||
async def _handle_cancellation(self, user_id: int):
|
||||
try:
|
||||
cancellation_msg = (
|
||||
"🚨 Ваш платеж Tribute был отменен.\n\n"
|
||||
"Если это произошло по ошибке, обратитесь в поддержку."
|
||||
)
|
||||
|
||||
await self.bot.send_message(
|
||||
int(user_id),
|
||||
cancellation_msg,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
logger.info(f"Tribute subscription cancelled for user {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling tribute cancellation for user {user_id}: {e}")
|
||||
|
||||
|
||||
async def tribute_webhook_route(request: web.Request):
|
||||
tribute_service: TributeService = request.app['tribute_service']
|
||||
raw_body = await request.read()
|
||||
signature_header = request.headers.get('trbt-signature')
|
||||
return await tribute_service.handle_webhook(raw_body, signature_header)
|
||||
@@ -0,0 +1,70 @@
|
||||
import logging
|
||||
from aiohttp import web, ClientSession
|
||||
from aiogram import Bot
|
||||
from database import Database
|
||||
from config import Config
|
||||
from tribute_service import TributeService, tribute_webhook_route
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WebhookServer:
|
||||
def __init__(self, bot: Bot, db: Database, config: Config):
|
||||
self.bot = bot
|
||||
self.db = db
|
||||
self.config = config
|
||||
self.app = None
|
||||
self.runner = None
|
||||
self.site = None
|
||||
|
||||
async def create_app(self):
|
||||
self.app = web.Application()
|
||||
|
||||
tribute_service = TributeService(self.bot, self.config, self.db)
|
||||
|
||||
self.app['tribute_service'] = tribute_service
|
||||
|
||||
self.app.router.add_post(self.config.TRIBUTE_WEBHOOK_PATH, tribute_webhook_route)
|
||||
|
||||
async def health_check(request):
|
||||
return web.json_response({"status": "ok", "service": "tribute-webhooks"})
|
||||
|
||||
self.app.router.add_get('/health', health_check)
|
||||
|
||||
logger.info(f"Webhook server configured with route: {self.config.TRIBUTE_WEBHOOK_PATH}")
|
||||
return self.app
|
||||
|
||||
async def start(self):
|
||||
try:
|
||||
if not self.app:
|
||||
await self.create_app()
|
||||
|
||||
self.runner = web.AppRunner(self.app)
|
||||
await self.runner.setup()
|
||||
|
||||
self.site = web.TCPSite(
|
||||
self.runner,
|
||||
host='0.0.0.0',
|
||||
port=self.config.TRIBUTE_WEBHOOK_PORT
|
||||
)
|
||||
|
||||
await self.site.start()
|
||||
|
||||
logger.info(f"✅ Webhook server started on port {self.config.TRIBUTE_WEBHOOK_PORT}")
|
||||
logger.info(f"🎯 Tribute webhook URL: http://your-server:{self.config.TRIBUTE_WEBHOOK_PORT}{self.config.TRIBUTE_WEBHOOK_PATH}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to start webhook server: {e}")
|
||||
raise
|
||||
|
||||
async def stop(self):
|
||||
try:
|
||||
if self.site:
|
||||
await self.site.stop()
|
||||
logger.info("Webhook site stopped")
|
||||
|
||||
if self.runner:
|
||||
await self.runner.cleanup()
|
||||
logger.info("Webhook runner cleaned up")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping webhook server: {e}")
|
||||
Reference in New Issue
Block a user