Compare commits

...

32 Commits

Author SHA1 Message Date
Egor c101cb3d3f Update README.md 2025-08-14 05:29:41 +03:00
Egor 63912af43b Update README.md 2025-08-14 05:26:12 +03:00
Egor d19263dd79 Update .env.example 2025-08-14 05:08:03 +03:00
Egor f8c9c59a03 Update database.py 2025-08-14 04:45:02 +03:00
Egor 5595f7a954 Update admin_handlers.py 2025-08-14 04:44:33 +03:00
Egor bac4527945 Update autopay_service.py 2025-08-14 04:44:09 +03:00
Egor 51da8bcb72 Update remnawave_api.py 2025-08-14 04:41:38 +03:00
Egor 8aa6e7e245 Update subscription_monitor.py 2025-08-14 04:40:29 +03:00
Egor 5029b35d14 Update subscription_monitor.py 2025-08-14 04:39:37 +03:00
Egor 2412027d9b Update handlers.py 2025-08-14 04:39:17 +03:00
Egor ddff5219e5 Create webhook_server.py 2025-08-14 04:38:58 +03:00
Egor 346bc18b58 Create tribute_service.py 2025-08-14 04:38:12 +03:00
Egor c256ae3664 Update keyboards.py 2025-08-14 04:37:33 +03:00
Egor 2b47358a9e Update config.py 2025-08-14 04:36:58 +03:00
Egor e0b041ce44 Update main.py 2025-08-14 04:36:41 +03:00
Egor 3f64dee927 Update database.py 2025-08-13 08:02:35 +03:00
Egor 67c5ebeabd Update admin_handlers.py 2025-08-13 08:02:18 +03:00
Egor 84bc8e96cc Update database.py 2025-08-13 07:58:18 +03:00
Egor 2d30cec7c6 Update lucky_game.py 2025-08-13 07:55:50 +03:00
Egor 5f5f66178b Update lucky_game.py 2025-08-13 07:50:17 +03:00
Egor e9fadec0ac Update database.py 2025-08-13 07:49:40 +03:00
Egor 10008e51ff Update admin_handlers.py 2025-08-13 07:39:20 +03:00
Egor 3098610d17 Update database.py 2025-08-13 07:31:29 +03:00
Egor 7a2566d320 Update admin_handlers.py 2025-08-13 07:31:09 +03:00
Egor 01ba8ca53e Update subscription_monitor.py 2025-08-13 07:09:12 +03:00
Egor 2a0a2dccff Update README.md 2025-08-13 06:55:22 +03:00
Egor 66a6f32d4c Update .env.example 2025-08-13 06:51:35 +03:00
Egor 02b3f61953 Update translations.py 2025-08-13 06:37:10 +03:00
Egor dc08526255 Update config.py 2025-08-13 06:36:52 +03:00
Egor 2ca339610b Update subscription_monitor.py 2025-08-13 06:36:32 +03:00
Egor ffe65937c1 Update README.md 2025-08-12 19:16:27 +03:00
Egor 4b6f03eef5 Update README.md 2025-08-12 19:15:02 +03:00
14 changed files with 1335 additions and 811 deletions
+11
View File
@@ -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
+454 -740
View File
File diff suppressed because it is too large Load Diff
+126 -28
View File
@@ -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
View File
@@ -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
}
+19 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 = []
+44 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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',
+165
View File
@@ -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)
+70
View File
@@ -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}")