fix: notification sent for non-deactivated subs + webhook race condition
Two fixes for channel subscription enforcement: 1. Middleware notification guard: the deactivation notification was sent even when deactivated_subs was empty (no subs actually deactivated). Also fixed len(active_subs) -> len(deactivated_subs) for multi-tariff notification text selection. 2. Webhook echo race condition: when a user quickly leaves and rejoins a channel, the delayed user.disabled webhook from RemnaWave could re-deactivate a subscription that was already reactivated. Fix: stamp last_webhook_update_at on reactivation (both channel_member handler and middleware), then guard _handle_user_disabled against re-deactivating recently-reactivated ACTIVE subscriptions using the existing is_recently_updated_by_webhook (60s window).
This commit is contained in:
@@ -78,6 +78,9 @@ async def on_user_joined_channel(event: ChatMemberUpdated, bot: Bot) -> None:
|
||||
|
||||
for subscription in disabled_subs:
|
||||
await reactivate_subscription(db, subscription)
|
||||
# Ставим штамп чтобы webhook user.disabled (echo от нашего disable)
|
||||
# не переотключил подписку при быстрой реподписке
|
||||
subscription.last_webhook_update_at = datetime.now(UTC)
|
||||
logger.info(
|
||||
'Subscriptions reactivated via channel event',
|
||||
telegram_id=user.id,
|
||||
|
||||
@@ -406,29 +406,30 @@ class ChannelCheckerMiddleware(BaseMiddleware):
|
||||
)
|
||||
|
||||
# Notify user about deactivation
|
||||
try:
|
||||
normalized = _normalize_channels(channels)
|
||||
texts = get_texts(user.language or DEFAULT_LANGUAGE)
|
||||
if settings.is_multi_tariff_enabled() and len(active_subs) > 1:
|
||||
notification_text = texts.t(
|
||||
'SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE_MULTI',
|
||||
'🚫 Ваши подписки приостановлены, так как вы отписались от обязательного канала.\n\n'
|
||||
'Подпишитесь на все каналы для восстановления доступа к VPN.',
|
||||
if deactivated_subs:
|
||||
try:
|
||||
normalized = _normalize_channels(channels)
|
||||
texts = get_texts(user.language or DEFAULT_LANGUAGE)
|
||||
if settings.is_multi_tariff_enabled() and len(deactivated_subs) > 1:
|
||||
notification_text = texts.t(
|
||||
'SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE_MULTI',
|
||||
'🚫 Ваши подписки приостановлены, так как вы отписались от обязательного канала.\n\n'
|
||||
'Подпишитесь на все каналы для восстановления доступа к VPN.',
|
||||
)
|
||||
else:
|
||||
notification_text = texts.t(
|
||||
'SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE',
|
||||
'🚫 Ваша подписка приостановлена, так как вы отписались от канала.\n\n'
|
||||
'Подпишитесь на канал снова, чтобы восстановить доступ к VPN.',
|
||||
)
|
||||
channel_kb = get_channel_sub_keyboard(normalized, language=user.language)
|
||||
await bot.send_message(telegram_id, notification_text, reply_markup=channel_kb)
|
||||
except Exception as notify_error:
|
||||
logger.error(
|
||||
'Failed to send deactivation notification to user',
|
||||
telegram_id=telegram_id,
|
||||
notify_error=notify_error,
|
||||
)
|
||||
else:
|
||||
notification_text = texts.t(
|
||||
'SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE',
|
||||
'🚫 Ваша подписка приостановлена, так как вы отписались от канала.\n\n'
|
||||
'Подпишитесь на канал снова, чтобы восстановить доступ к VPN.',
|
||||
)
|
||||
channel_kb = get_channel_sub_keyboard(normalized, language=user.language)
|
||||
await bot.send_message(telegram_id, notification_text, reply_markup=channel_kb)
|
||||
except Exception as notify_error:
|
||||
logger.error(
|
||||
'Failed to send deactivation notification to user',
|
||||
telegram_id=telegram_id,
|
||||
notify_error=notify_error,
|
||||
)
|
||||
await db.commit()
|
||||
except Exception as db_error:
|
||||
logger.error(
|
||||
@@ -465,6 +466,8 @@ class ChannelCheckerMiddleware(BaseMiddleware):
|
||||
|
||||
for subscription in disabled_subs:
|
||||
await reactivate_subscription(db, subscription)
|
||||
# Штамп для защиты от echo-webhook user.disabled
|
||||
subscription.last_webhook_update_at = datetime.now(UTC)
|
||||
sub_type = 'trial' if subscription.is_trial else 'paid'
|
||||
logger.info(
|
||||
'Subscription reactivated after channel subscribe',
|
||||
|
||||
@@ -27,6 +27,7 @@ from app.database.crud.subscription import (
|
||||
decrement_subscription_server_counts,
|
||||
expire_subscription,
|
||||
get_subscription_by_user_id,
|
||||
is_recently_updated_by_webhook,
|
||||
reactivate_subscription,
|
||||
update_subscription_usage,
|
||||
)
|
||||
@@ -706,6 +707,18 @@ class RemnaWaveWebhookService:
|
||||
await db.commit()
|
||||
return
|
||||
|
||||
# Защита от echo-webhook: если подписка была недавно реактивирована
|
||||
# (канал-реподписка ставит last_webhook_update_at), пропускаем
|
||||
if subscription.status == SubscriptionStatus.ACTIVE.value and is_recently_updated_by_webhook(subscription):
|
||||
logger.info(
|
||||
'Webhook user.disabled: подписка недавно реактивирована, пропуск echo-webhook',
|
||||
subscription_id=subscription.id,
|
||||
user_id=user.id,
|
||||
)
|
||||
self._stamp_webhook_update(subscription)
|
||||
await db.commit()
|
||||
return
|
||||
|
||||
self._stamp_webhook_update(subscription)
|
||||
if subscription.status != SubscriptionStatus.DISABLED.value:
|
||||
await deactivate_subscription(db, subscription)
|
||||
|
||||
Reference in New Issue
Block a user