Merge pull request #2079 from BEDOLAGA-DEV/revert-2078-lw4oi2-bedolaga/restore-reverse-sync-button-in-remnawave

Revert "Align RemnaWave sync with new status and expiration rules"
This commit is contained in:
Egor
2025-11-28 02:52:57 +03:00
committed by GitHub
8 changed files with 87 additions and 274 deletions
-54
View File
@@ -2285,9 +2285,6 @@ async def show_sync_options(
"• При полной синхронизации подписки пользователей, отсутствующих в панели, будут деактивированы\n"
"• Рекомендуется делать полную синхронизацию ежедневно\n"
"• Баланс пользователей НЕ удаляется\n\n"
"⬆️ <b>Обратная синхронизация:</b>\n"
"• Отправляет активных пользователей из бота в панель\n"
"• Используйте при сбоях панели или для восстановления данных\n\n"
+ "\n".join(status_lines)
)
@@ -2298,12 +2295,6 @@ async def show_sync_options(
callback_data="sync_all_users",
)
],
[
types.InlineKeyboardButton(
text="⬆️ Синхронизация в панель",
callback_data="sync_to_panel",
)
],
[
types.InlineKeyboardButton(
text="⚙️ Настройки автосинхронизации",
@@ -2663,50 +2654,6 @@ async def sync_all_users(
)
await callback.answer()
@admin_required
@error_handler
async def sync_users_to_panel(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
):
await callback.message.edit_text(
"⬆️ Выполняется синхронизация данных бота в панель Remnawave...\n\n"
"Это может занять несколько минут.",
reply_markup=None,
)
remnawave_service = RemnaWaveService()
stats = await remnawave_service.sync_users_to_panel(db)
if stats["errors"] == 0:
status_emoji = ""
status_text = "успешно завершена"
else:
status_emoji = "⚠️" if (stats["created"] + stats["updated"]) > 0 else ""
status_text = "завершена с предупреждениями" if status_emoji == "⚠️" else "завершена с ошибками"
text = (
f"{status_emoji} <b>Синхронизация в панель {status_text}</b>\n\n"
"📊 <b>Результаты:</b>\n"
f"• 🆕 Создано: {stats['created']}\n"
f"• 🔄 Обновлено: {stats['updated']}\n"
f"• ❌ Ошибок: {stats['errors']}"
)
keyboard = [
[types.InlineKeyboardButton(text="🔄 Повторить", callback_data="sync_to_panel")],
[types.InlineKeyboardButton(text="🔄 Полная синхронизация", callback_data="sync_all_users")],
[types.InlineKeyboardButton(text="⬅️ К синхронизации", callback_data="admin_rw_sync")],
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
)
await callback.answer()
@admin_required
@error_handler
async def show_sync_recommendations(
@@ -3179,7 +3126,6 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(cancel_auto_sync_schedule, F.data == "remnawave_auto_sync_cancel")
dp.callback_query.register(run_auto_sync_now, F.data == "remnawave_auto_sync_run")
dp.callback_query.register(sync_all_users, F.data == "sync_all_users")
dp.callback_query.register(sync_users_to_panel, F.data == "sync_to_panel")
dp.callback_query.register(show_squad_migration_menu, F.data == "admin_rw_migration")
dp.callback_query.register(paginate_migration_source, F.data.startswith("admin_migration_source_page_"))
dp.callback_query.register(handle_migration_source_selection, F.data.startswith("admin_migration_source_"))
+4 -22
View File
@@ -4595,19 +4595,10 @@ async def admin_buy_subscription_execute(
if target_user.remnawave_uuid:
async with remnawave_service.get_api_client() as api:
expire_at = remnawave_service.ensure_future_expire_at(
subscription.end_date
)
status = (
UserStatus.ACTIVE
if subscription.is_active
else UserStatus.DISABLED
)
update_kwargs = dict(
uuid=target_user.remnawave_uuid,
status=status,
expire_at=expire_at,
status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED,
expire_at=subscription.end_date,
traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
description=settings.format_remnawave_user_description(
@@ -4629,19 +4620,10 @@ async def admin_buy_subscription_execute(
telegram_id=target_user.telegram_id,
)
async with remnawave_service.get_api_client() as api:
expire_at = remnawave_service.ensure_future_expire_at(
subscription.end_date
)
status = (
UserStatus.ACTIVE
if subscription.is_active
else UserStatus.DISABLED
)
create_kwargs = dict(
username=username,
expire_at=expire_at,
status=status,
expire_at=subscription.end_date,
status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED,
traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
telegram_id=target_user.telegram_id,
-6
View File
@@ -1118,12 +1118,6 @@ def get_sync_options_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
callback_data="sync_all_users"
)
],
[
InlineKeyboardButton(
text=_t(texts, "ADMIN_SYNC_TO_PANEL", "⬆️ Синхронизация в панель"),
callback_data="sync_to_panel"
)
],
[
InlineKeyboardButton(
text=_t(texts, "ADMIN_SYNC_ONLY_NEW", "🆕 Только новые"),
-1
View File
@@ -688,7 +688,6 @@
"ADMIN_SUPPORT_SUBMENU_TITLE": "🛟 **Поддержка**\n\n",
"ADMIN_SUPPORT_TICKETS": "🎫 Тикеты поддержки",
"ADMIN_SYNC_BACK": "⬅️ К синхронизации",
"ADMIN_SYNC_TO_PANEL": "⬆️ Синхронизация в панель",
"ADMIN_SYNC_CLEANUP": "🧹 Очистка",
"ADMIN_SYNC_CONFIRM": "✅ Подтвердить",
"ADMIN_SYNC_FULL": "🔄 Полная синхронизация",
-1
View File
@@ -687,7 +687,6 @@
"ADMIN_SUPPORT_SUBMENU_TITLE": "🛟 **Підтримка**\n\n",
"ADMIN_SUPPORT_TICKETS": "🎫 Тікети підтримки",
"ADMIN_SYNC_BACK": "⬅️ До синхронізації",
"ADMIN_SYNC_TO_PANEL": "⬆️ Синхронізація в панель",
"ADMIN_SYNC_CLEANUP": "🧹 Очищення",
"ADMIN_SYNC_CONFIRM": "✅ Підтвердити",
"ADMIN_SYNC_FULL": "🔄 Повна синхронізація",
+6 -16
View File
@@ -281,15 +281,11 @@ class MonitoringService:
return None
current_time = datetime.utcnow()
is_active = (
subscription.status == SubscriptionStatus.ACTIVE.value
and subscription.end_date > current_time
)
is_active = (subscription.status == SubscriptionStatus.ACTIVE.value and
subscription.end_date > current_time)
if (
subscription.status == SubscriptionStatus.ACTIVE.value
and subscription.end_date <= current_time
):
if (subscription.status == SubscriptionStatus.ACTIVE.value and
subscription.end_date <= current_time):
subscription.status = SubscriptionStatus.EXPIRED.value
await db.commit()
is_active = False
@@ -305,16 +301,10 @@ class MonitoringService:
async with self.subscription_service.get_api_client() as api:
hwid_limit = resolve_hwid_device_limit_for_payload(subscription)
expire_at = self.subscription_service.ensure_future_expire_at(
subscription.end_date
)
status = UserStatus.ACTIVE if is_active else UserStatus.DISABLED
update_kwargs = dict(
uuid=user.remnawave_uuid,
status=status,
expire_at=expire_at,
status=UserStatus.ACTIVE if is_active else UserStatus.EXPIRED,
expire_at=subscription.end_date,
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
description=settings.format_remnawave_user_description(
+65 -118
View File
@@ -220,27 +220,6 @@ class RemnaWaveService:
"""Возвращает текущее время в UTC без привязки к часовому поясу."""
return datetime.now(self._utc_timezone).replace(tzinfo=None)
def ensure_future_expire_at(self, expire_at: Optional[datetime]) -> datetime:
"""Приводит дату окончания подписки к будущему значению для API панели."""
safe_now = self._now_utc()
if expire_at is None:
adjusted = safe_now + timedelta(days=30)
logger.debug("⚙️ Используем дефолтную дату окончания подписки: %s", adjusted)
return adjusted
if expire_at <= safe_now:
adjusted = safe_now + timedelta(minutes=1)
logger.debug(
"⚙️ Корректируем просроченную дату подписки %s%s для RemnaWave API",
expire_at,
adjusted,
)
return adjusted
return expire_at
def _parse_remnawave_date(self, date_str: str) -> datetime:
if not date_str:
return self._now_utc() + timedelta(days=30)
@@ -1636,109 +1615,77 @@ class RemnaWaveService:
async def sync_users_to_panel(self, db: AsyncSession) -> Dict[str, int]:
try:
stats = {"created": 0, "updated": 0, "errors": 0}
batch_size = 100
offset = 0
users = await get_users_list(db, offset=0, limit=10000)
async with self.get_api_client() as api:
while True:
users = await get_users_list(db, offset=offset, limit=batch_size)
if not users:
break
for user in users:
if not user.subscription:
continue
try:
subscription = user.subscription
hwid_limit = resolve_hwid_device_limit_for_payload(subscription)
expire_at = self.ensure_future_expire_at(
subscription.end_date
)
status = (
UserStatus.ACTIVE
if subscription.is_active
else UserStatus.DISABLED
)
if user.remnawave_uuid:
update_kwargs = dict(
uuid=user.remnawave_uuid,
status=status,
expire_at=expire_at,
traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=subscription.connected_squads,
)
if hwid_limit is not None:
update_kwargs['hwid_device_limit'] = hwid_limit
await api.update_user(**update_kwargs)
stats["updated"] += 1
else:
username = settings.format_remnawave_username(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id,
)
create_kwargs = dict(
username=username,
expire_at=expire_at,
status=status,
traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
telegram_id=user.telegram_id,
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=subscription.connected_squads,
)
if hwid_limit is not None:
create_kwargs['hwid_device_limit'] = hwid_limit
new_user = await api.create_user(**create_kwargs)
user.remnawave_uuid = new_user.uuid
subscription.remnawave_short_uuid = new_user.short_uuid
stats["created"] += 1
except Exception as e:
logger.error(f"Ошибка синхронизации пользователя {user.telegram_id} в панель: {e}")
stats["errors"] += 1
for user in users:
if not user.subscription:
continue
try:
await db.commit()
except Exception as commit_error:
logger.error(
"Ошибка фиксации транзакции при синхронизации в панель: %s",
commit_error,
)
await db.rollback()
stats["errors"] += len(users)
subscription = user.subscription
hwid_limit = resolve_hwid_device_limit_for_payload(subscription)
if len(users) < batch_size:
break
if user.remnawave_uuid:
update_kwargs = dict(
uuid=user.remnawave_uuid,
status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED,
expire_at=subscription.end_date,
traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=subscription.connected_squads,
)
offset += batch_size
if hwid_limit is not None:
update_kwargs['hwid_device_limit'] = hwid_limit
logger.info(
f"✅ Синхронизация в панель завершена: создано {stats['created']}, обновлено {stats['updated']}, ошибок {stats['errors']}"
)
await api.update_user(**update_kwargs)
stats["updated"] += 1
else:
username = settings.format_remnawave_username(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id,
)
create_kwargs = dict(
username=username,
expire_at=subscription.end_date,
status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED,
traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
telegram_id=user.telegram_id,
description=settings.format_remnawave_user_description(
full_name=user.full_name,
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=subscription.connected_squads,
)
if hwid_limit is not None:
create_kwargs['hwid_device_limit'] = hwid_limit
new_user = await api.create_user(**create_kwargs)
await update_user(db, user, remnawave_uuid=new_user.uuid)
subscription.remnawave_short_uuid = new_user.short_uuid
# Убираем немедленный коммит для пакетной обработки
# await db.commit()
stats["created"] += 1
except Exception as e:
logger.error(f"Ошибка синхронизации пользователя {user.telegram_id} в панель: {e}")
stats["errors"] += 1
logger.info(f"✅ Синхронизация в панель завершена: создано {stats['created']}, обновлено {stats['updated']}, ошибок {stats['errors']}")
return stats
except Exception as e:
+12 -56
View File
@@ -153,27 +153,6 @@ class SubscriptionService:
assert self.api is not None
async with self.api as api:
yield api
def ensure_future_expire_at(self, expire_at: Optional[datetime]) -> datetime:
"""Гарантирует, что дата окончания передается в будущее значение для RemnaWave API."""
safe_now = datetime.utcnow()
if expire_at is None:
adjusted = safe_now + timedelta(days=30)
logger.debug("⚙️ Используем дефолтную дату окончания подписки: %s", adjusted)
return adjusted
if expire_at <= safe_now:
adjusted = safe_now + timedelta(minutes=1)
logger.debug(
"⚙️ Корректируем просроченную дату подписки %s%s для RemnaWave API",
expire_at,
adjusted,
)
return adjusted
return expire_at
async def create_remnawave_user(
self,
@@ -208,18 +187,10 @@ class SubscriptionService:
except Exception as hwid_error:
logger.warning(f"⚠️ Не удалось сбросить HWID: {hwid_error}")
expire_at = self.ensure_future_expire_at(subscription.end_date)
status = (
UserStatus.ACTIVE
if subscription.is_active
else UserStatus.DISABLED
)
update_kwargs = dict(
uuid=remnawave_user.uuid,
status=status,
expire_at=expire_at,
status=UserStatus.ACTIVE,
expire_at=subscription.end_date,
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
traffic_limit_strategy=get_traffic_reset_strategy(),
description=settings.format_remnawave_user_description(
@@ -250,18 +221,10 @@ class SubscriptionService:
username=user.username,
telegram_id=user.telegram_id,
)
expire_at = self.ensure_future_expire_at(subscription.end_date)
status = (
UserStatus.ACTIVE
if subscription.is_active
else UserStatus.DISABLED
)
create_kwargs = dict(
username=username,
expire_at=expire_at,
status=status,
expire_at=subscription.end_date,
status=UserStatus.ACTIVE,
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
traffic_limit_strategy=get_traffic_reset_strategy(),
telegram_id=user.telegram_id,
@@ -322,32 +285,25 @@ class SubscriptionService:
return None
current_time = datetime.utcnow()
is_actually_active = (
subscription.status == SubscriptionStatus.ACTIVE.value
and subscription.end_date > current_time
)
is_actually_active = (subscription.status == SubscriptionStatus.ACTIVE.value and
subscription.end_date > current_time)
if (
subscription.status == SubscriptionStatus.ACTIVE.value
and subscription.end_date <= current_time
):
if (subscription.status == SubscriptionStatus.ACTIVE.value and
subscription.end_date <= current_time):
subscription.status = SubscriptionStatus.EXPIRED.value
subscription.updated_at = current_time
await db.commit()
is_actually_active = False
logger.info(f"🔔 Статус подписки {subscription.id} автоматически изменен на 'expired'")
async with self.get_api_client() as api:
hwid_limit = resolve_hwid_device_limit_for_payload(subscription)
expire_at = self.ensure_future_expire_at(subscription.end_date)
status = UserStatus.ACTIVE if is_actually_active else UserStatus.DISABLED
update_kwargs = dict(
uuid=user.remnawave_uuid,
status=status,
expire_at=expire_at,
status=UserStatus.ACTIVE if is_actually_active else UserStatus.EXPIRED,
expire_at=subscription.end_date,
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
traffic_limit_strategy=get_traffic_reset_strategy(),
description=settings.format_remnawave_user_description(