Compare commits

...

17 Commits

Author SHA1 Message Date
c0mrade 8afe613451 Merge pull request #2541 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.5.0
2026-02-06 23:44:40 +03:00
github-actions[bot] 8de9c6e532 chore(main): release 3.5.0 2026-02-06 20:42:00 +00:00
Egor b69fcbde11 Merge pull request #2540 from BEDOLAGA-DEV/dev
Release 3.4.1
2026-02-06 23:33:38 +03:00
Fringg 44d6b6b266 chore: bump version to 3.4.1 2026-02-06 23:31:46 +03:00
c0mrade 4234769e92 revert: remove signature pop from HMAC validation
Telegram includes signature in the hash computation, so removing it
from the data-check-string breaks HMAC validation for all users.
2026-02-06 22:27:57 +03:00
c0mrade c2cabbee09 fix: restore unquote for user data parsing in telegram auth
parse_qsl does not fully decode nested URL-encoded JSON in the user
field, so unquote() is still needed before json.loads().
2026-02-06 22:13:32 +03:00
c0mrade 067b1b6716 chore: remove unused unquote import 2026-02-06 21:55:45 +03:00
c0mrade 5b64046137 fix: exclude signature field from Telegram initData HMAC validation
Telegram Bot API 8.0+ adds a `signature` field to WebApp initData.
Per the official spec, both `hash` and `signature` must be excluded
from the data-check-string before HMAC verification. Without this,
users with newer Telegram clients get a hash mismatch and 401.

Also remove redundant `unquote()` in telegram_auth.py — `parse_qsl`
already URL-decodes values, so the extra decode could corrupt user
data containing percent-like sequences.
2026-02-06 21:51:38 +03:00
c0mrade 085a61721a Merge pull request #2538 from BEDOLAGA-DEV/feat/tariff-sorting-dnd
feat: tariff reorder API endpoint
2026-02-06 17:45:27 +03:00
Fringg 4c2e11e64b feat: add tariff reorder API endpoint
Add PUT /cabinet/admin/tariffs/order endpoint for drag-and-drop
tariff sorting in admin cabinet. Move db.commit() from CRUD to
route level for consistency.
2026-02-06 17:42:01 +03:00
c0mrade 7c5f35b1cf Merge pull request #2539 from BEDOLAGA-DEV/feat/remnawave-original-config-format
Feat/remnawave original config format
2026-02-06 17:35:13 +03:00
Egor 561708b777 Merge pull request #2537 from BEDOLAGA-DEV/fix/blacklist-middleware
fix: enforce blacklist via middleware
2026-02-06 15:54:01 +03:00
Fringg 806a959662 style: format blacklist middleware 2026-02-06 15:52:19 +03:00
Fringg 966a599c2c fix: enforce blacklist via middleware instead of per-handler checks
Add BlacklistMiddleware for aiogram that blocks all message/callback/pre_checkout
from blacklisted users globally. Add blacklist check to cabinet API dependency.
Fix case-insensitive username matching. Remove 10 redundant manual checks from handlers.
2026-02-06 15:48:21 +03:00
c0mrade 0ed98c39b6 fix: improve button URL resolution and pass uiConfig to frontend
- Add {{HAPP_CRYPT3_LINK}} template support in _resolve_button_url
- Only resolve templates for subscriptionLink and copyButton, not external
- Always send subscriptionUrl and subscriptionCryptoLink (hideLink is display-only flag)
- Pass uiConfig from RemnaWave config for block renderer selection
2026-02-05 20:08:47 +03:00
c0mrade 095bc00b33 feat: pass platform-level fields from RemnaWave config to frontend
Preserve svgIconKey, displayName and other platform-level fields
instead of only forwarding apps array. Build platformNames from
RemnaWave displayName with English-only fallback.
2026-02-05 14:27:46 +03:00
c0mrade 43762ce8f4 feat: serve original RemnaWave config from app-config endpoint
- Return original blocks/svgLibrary instead of converting to steps
- Enrich apps with deepLink and buttons with resolvedUrl
- Add _resolve_button_url helper for template substitution
- Keep legacy file-based format as fallback
2026-02-05 08:29:57 +03:00
17 changed files with 240 additions and 223 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "3.4.0"
".": "3.5.0"
}
+25
View File
@@ -0,0 +1,25 @@
# Changelog
## [3.5.0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.4.0...v3.5.0) (2026-02-06)
### Features
* add tariff reorder API endpoint ([4c2e11e](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/4c2e11e64bed41592f5a12061dcca74ce43e0806))
* pass platform-level fields from RemnaWave config to frontend ([095bc00](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/095bc00b33d7082558a8b7252906db2850dce9da))
* serve original RemnaWave config from app-config endpoint ([43762ce](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/43762ce8f4fa7142a1ca62a92b97a027dab2564d))
* tariff reorder API endpoint ([085a617](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/085a61721a8175b3f4fd744614c446d73346f2b7))
### Bug Fixes
* enforce blacklist via middleware ([561708b](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/561708b7772ec5b84d6ee049aeba26dc70675583))
* enforce blacklist via middleware instead of per-handler checks ([966a599](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/966a599c2c778dce9eea3c61adf6067fb33119f6))
* exclude signature field from Telegram initData HMAC validation ([5b64046](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/5b6404613772610c595e55bde1249cdf6ec3269d))
* improve button URL resolution and pass uiConfig to frontend ([0ed98c3](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/0ed98c39b6c95911a38a26a32d0ffbcf9cfd7c80))
* restore unquote for user data parsing in telegram auth ([c2cabbe](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/c2cabbee097a41a95d16c34d43ab7e70d076c4dc))
### Reverts
* remove signature pop from HMAC validation ([4234769](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/4234769e92104a6c4f8f1d522e1fca25bc7b20d0))
+5
View File
@@ -61,6 +61,7 @@ from app.handlers.admin import (
)
from app.handlers.stars_payments import register_stars_handlers
from app.middlewares.auth import AuthMiddleware
from app.middlewares.blacklist import BlacklistMiddleware
from app.middlewares.button_stats import ButtonStatsMiddleware
from app.middlewares.display_name_restriction import DisplayNameRestrictionMiddleware
from app.middlewares.global_error import GlobalErrorMiddleware
@@ -119,6 +120,10 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
dp.callback_query.middleware(LoggingMiddleware())
dp.message.middleware(MaintenanceMiddleware())
dp.callback_query.middleware(MaintenanceMiddleware())
blacklist_middleware = BlacklistMiddleware()
dp.message.middleware(blacklist_middleware)
dp.callback_query.middleware(blacklist_middleware)
dp.pre_checkout_query.middleware(blacklist_middleware)
display_name_middleware = DisplayNameRestrictionMiddleware()
dp.message.middleware(display_name_middleware)
dp.callback_query.middleware(display_name_middleware)
+13
View File
@@ -12,6 +12,7 @@ from app.config import settings
from app.database.crud.user import get_user_by_id
from app.database.database import AsyncSessionLocal
from app.database.models import User
from app.services.blacklist_service import blacklist_service
from app.services.maintenance_service import maintenance_service
from .auth.jwt_handler import get_token_payload
@@ -104,6 +105,18 @@ async def get_current_cabinet_user(
detail='User account is not active',
)
# Check blacklist
if user.telegram_id is not None:
is_blacklisted, reason = await blacklist_service.is_user_blacklisted(user.telegram_id, user.username)
if is_blacklisted:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={
'code': 'blacklisted',
'message': reason or 'Доступ запрещен',
},
)
# Check maintenance mode (allow admins to pass)
if maintenance_service.is_maintenance_active():
# Проверяем админа по telegram_id ИЛИ email
+17
View File
@@ -14,6 +14,7 @@ from app.database.crud.tariff import (
get_tariff_by_id,
get_tariff_subscriptions_count,
load_period_prices_from_db,
reorder_tariffs,
set_tariff_promo_groups,
update_tariff,
)
@@ -29,6 +30,7 @@ from ..schemas.tariffs import (
TariffDetailResponse,
TariffListItem,
TariffListResponse,
TariffSortOrderRequest,
TariffStatsResponse,
TariffToggleResponse,
TariffTrialResponse,
@@ -157,6 +159,21 @@ async def get_available_servers(
]
@router.put('/order')
async def update_tariff_order(
request: TariffSortOrderRequest,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Update the display order of tariffs."""
await reorder_tariffs(db, request.tariff_ids)
await db.commit()
logger.info(f'Admin {admin.id} updated tariff order: {request.tariff_ids}')
return {'message': 'Tariff order updated successfully'}
@router.get('/{tariff_id}', response_model=TariffDetailResponse)
async def get_tariff(
tariff_id: int,
+121 -35
View File
@@ -2917,7 +2917,12 @@ def _convert_remnawave_config_to_cabinet(config: dict[str, Any]) -> dict[str, An
async def _load_app_config_async() -> dict[str, Any]:
"""Load app config from RemnaWave (if configured) or local file."""
"""Load app config from RemnaWave (if configured) or local file.
When config comes from RemnaWave, returns the original format with
``_isRemnawave`` flag so the caller can serve it as-is (enriched with
deep links) instead of converting to the legacy step-based format.
"""
remnawave_uuid = _get_remnawave_config_uuid()
if remnawave_uuid:
@@ -2927,22 +2932,9 @@ async def _load_app_config_async() -> dict[str, Any]:
config = await api.get_subscription_page_config(remnawave_uuid)
if config and config.config:
logger.debug(f'Loaded app config from RemnaWave: {remnawave_uuid}')
# Debug: log raw RemnaWave config structure
import json
logger.debug(
f'RemnaWave raw config: {json.dumps(config.config, ensure_ascii=False, indent=2)[:2000]}'
)
converted = _convert_remnawave_config_to_cabinet(config.config)
logger.debug(f'Converted config platforms: {list(converted.get("platforms", {}).keys())}')
# Log first app from each platform
for platform, apps in converted.get('platforms', {}).items():
if apps:
first_app = apps[0]
logger.debug(
f'Platform {platform} first app: name={first_app.get("name")}, urlScheme={first_app.get("urlScheme")}'
)
return converted
raw = dict(config.config)
raw['_isRemnawave'] = True
return raw
except Exception as e:
logger.warning(f'Failed to load RemnaWave config, falling back to file: {e}')
@@ -3329,6 +3321,29 @@ async def get_happ_downloads(
}
def _resolve_button_url(
url: str,
subscription_url: str | None,
subscription_crypto_link: str | None,
) -> str:
"""Resolve template variables in button URLs.
Matches remnawave/subscription-page frontend TemplateEngine:
- {{SUBSCRIPTION_LINK}} -> plain subscription URL
- {{HAPP_CRYPT3_LINK}} -> crypto link
- {{HAPP_CRYPT4_LINK}} -> crypto link
"""
if not url:
return url
result = url
if subscription_url:
result = result.replace('{{SUBSCRIPTION_LINK}}', subscription_url)
if subscription_crypto_link:
result = result.replace('{{HAPP_CRYPT3_LINK}}', subscription_crypto_link)
result = result.replace('{{HAPP_CRYPT4_LINK}}', subscription_crypto_link)
return result
@router.get('/app-config')
async def get_app_config(
user: User = Depends(get_current_cabinet_user),
@@ -3345,13 +3360,97 @@ async def get_app_config(
# Load config from RemnaWave (if configured) or local file
config = await _load_app_config_async()
platforms_raw = config.get('platforms', {})
is_remnawave = config.pop('_isRemnawave', False)
hide_link = settings.should_hide_subscription_link()
# Строим platformNames из displayName каждой платформы RemnaWave
platform_names: dict[str, Any] = {}
for pk, pd in config.get('platforms', {}).items():
if isinstance(pd, dict) and 'displayName' in pd:
platform_names[pk] = pd['displayName']
# Фоллбэк для платформ без displayName (en достаточно)
fallback_names = {
'ios': {'en': 'iPhone/iPad'},
'android': {'en': 'Android'},
'macos': {'en': 'macOS'},
'windows': {'en': 'Windows'},
'linux': {'en': 'Linux'},
'androidTV': {'en': 'Android TV'},
'appleTV': {'en': 'Apple TV'},
}
for k, v in fallback_names.items():
if k not in platform_names:
platform_names[k] = v
if is_remnawave:
# ── RemnaWave original format ──
# Serve original blocks/svgLibrary enriched with deep links and resolved URLs.
platforms: dict[str, Any] = {}
for platform_key, platform_data in config.get('platforms', {}).items():
if not isinstance(platform_data, dict):
continue
apps = platform_data.get('apps', [])
if not isinstance(apps, list):
continue
enriched_apps = []
for app in apps:
if not isinstance(app, dict):
continue
# Generate deep link
deep_link = None
if subscription_url or subscription_crypto_link:
deep_link = _create_deep_link(app, subscription_url, subscription_crypto_link)
app['deepLink'] = deep_link
# Resolve templates only for subscriptionLink and copyButton (not external)
for block in app.get('blocks', []):
if not isinstance(block, dict):
continue
for btn in block.get('buttons', []):
if not isinstance(btn, dict):
continue
btn_type = btn.get('type', '')
if btn_type in ('subscriptionLink', 'copyButton'):
url = btn.get('url', '') or btn.get('link', '')
if url and '{{' in url:
btn['resolvedUrl'] = _resolve_button_url(
url,
subscription_url,
subscription_crypto_link,
)
enriched_apps.append(app)
if enriched_apps:
# Сохраняем platform-level поля (svgIconKey, displayName и т.д.)
platform_output = {k: v for k, v in platform_data.items() if k != 'apps'}
platform_output['apps'] = enriched_apps
platforms[platform_key] = platform_output
return {
'isRemnawave': True,
'platforms': platforms,
'svgLibrary': config.get('svgLibrary', {}),
'baseTranslations': config.get('baseTranslations'),
'baseSettings': config.get('baseSettings'),
'uiConfig': config.get('uiConfig', {}),
'platformNames': platform_names,
'hasSubscription': bool(subscription_url or subscription_crypto_link),
'subscriptionUrl': subscription_url,
'subscriptionCryptoLink': subscription_crypto_link,
'hideLink': hide_link,
'branding': config.get('brandingSettings', {}),
}
# ── Legacy file-based format ──
platforms_raw = config.get('platforms', {})
if not isinstance(platforms_raw, dict):
platforms_raw = {}
# Build response with deep links
platforms = {}
platforms_legacy: dict[str, Any] = {}
for platform_key, apps in platforms_raw.items():
if not isinstance(apps, list):
continue
@@ -3379,23 +3478,10 @@ async def get_app_config(
platform_apps.append(app_data)
if platform_apps:
platforms[platform_key] = platform_apps
# Platform display names for UI
platform_names = {
'ios': {'ru': 'iPhone/iPad', 'en': 'iPhone/iPad'},
'android': {'ru': 'Android', 'en': 'Android'},
'macos': {'ru': 'macOS', 'en': 'macOS'},
'windows': {'ru': 'Windows', 'en': 'Windows'},
'linux': {'ru': 'Linux', 'en': 'Linux'},
'androidTV': {'ru': 'Android TV', 'en': 'Android TV'},
'appleTV': {'ru': 'Apple TV', 'en': 'Apple TV'},
}
hide_link = settings.should_hide_subscription_link()
platforms_legacy[platform_key] = platform_apps
return {
'platforms': platforms,
'platforms': platforms_legacy,
'platformNames': platform_names,
'hasSubscription': bool(subscription_url or subscription_crypto_link),
'subscriptionUrl': subscription_url if not hide_link else None,
+6
View File
@@ -194,6 +194,12 @@ class TariffUpdateRequest(BaseModel):
traffic_reset_mode: str | None = None # DAY, WEEK, MONTH, NO_RESET, None = глобальная настройка
class TariffSortOrderRequest(BaseModel):
"""Request to reorder tariffs."""
tariff_ids: list[int] = Field(..., min_length=1, description='Ordered list of tariff IDs')
class TariffToggleResponse(BaseModel):
"""Response after toggling tariff."""
-2
View File
@@ -486,8 +486,6 @@ async def reorder_tariffs(
for order, tariff_id in enumerate(tariff_order):
await db.execute(update(Tariff).where(Tariff.id == tariff_id).values(display_order=order))
await db.commit()
logger.info('Изменен порядок тарифов: %s', tariff_order)
-18
View File
@@ -8,7 +8,6 @@ from app.config import settings
from app.database.models import User
from app.keyboards.inline import get_back_keyboard
from app.localization.texts import get_texts
from app.services.blacklist_service import blacklist_service
from app.services.payment_service import PaymentService
from app.states import BalanceStates
from app.utils.decorators import error_handler
@@ -126,23 +125,6 @@ async def process_cryptobot_payment_amount(
await state.clear()
return
# Проверяем, находится ли пользователь в черном списке
is_blacklisted, blacklist_reason = await blacklist_service.is_user_blacklisted(
message.from_user.id, message.from_user.username
)
if is_blacklisted:
logger.warning(f'🚫 Пользователь {message.from_user.id} находится в черном списке: {blacklist_reason}')
try:
await message.answer(
f'🚫 Оплата невозможна\n\n'
f'Причина: {blacklist_reason}\n\n'
f'Если вы считаете, что это ошибка, обратитесь в поддержку.'
)
except Exception as e:
logger.error(f'Ошибка при отправке сообщения о блокировке: {e}')
return
texts = get_texts(db_user.language)
if not settings.is_cryptobot_enabled():
-18
View File
@@ -8,7 +8,6 @@ from app.database.models import User
from app.external.telegram_stars import TelegramStarsService
from app.keyboards.inline import get_back_keyboard
from app.localization.texts import get_texts
from app.services.blacklist_service import blacklist_service
from app.services.payment_service import PaymentService
from app.states import BalanceStates
from app.utils.decorators import error_handler
@@ -94,23 +93,6 @@ async def process_stars_payment_amount(message: types.Message, db_user: User, am
await state.clear()
return
# Проверяем, находится ли пользователь в черном списке
is_blacklisted, blacklist_reason = await blacklist_service.is_user_blacklisted(
message.from_user.id, message.from_user.username
)
if is_blacklisted:
logger.warning(f'🚫 Пользователь {message.from_user.id} находится в черном списке: {blacklist_reason}')
try:
await message.answer(
f'🚫 Оплата невозможна\n\n'
f'Причина: {blacklist_reason}\n\n'
f'Если вы считаете, что это ошибка, обратитесь в поддержку.'
)
except Exception as e:
logger.error(f'Ошибка при отправке сообщения о блокировке: {e}')
return
texts = get_texts(db_user.language)
if not settings.TELEGRAM_STARS_ENABLED:
-35
View File
@@ -10,7 +10,6 @@ from app.config import settings
from app.database.models import User
from app.keyboards.inline import get_back_keyboard
from app.localization.texts import get_texts
from app.services.blacklist_service import blacklist_service
from app.services.payment_service import PaymentService
from app.states import BalanceStates
from app.utils.decorators import error_handler
@@ -171,23 +170,6 @@ async def process_yookassa_payment_amount(
await state.clear()
return
# Проверяем, находится ли пользователь в черном списке
is_blacklisted, blacklist_reason = await blacklist_service.is_user_blacklisted(
message.from_user.id, message.from_user.username
)
if is_blacklisted:
logger.warning(f'🚫 Пользователь {message.from_user.id} находится в черном списке: {blacklist_reason}')
try:
await message.answer(
f'🚫 Оплата невозможна\n\n'
f'Причина: {blacklist_reason}\n\n'
f'Если вы считаете, что это ошибка, обратитесь в поддержку.'
)
except Exception as e:
logger.error(f'Ошибка при отправке сообщения о блокировке: {e}')
return
texts = get_texts(db_user.language)
if not settings.is_yookassa_enabled():
@@ -338,23 +320,6 @@ async def process_yookassa_sbp_payment_amount(
await state.clear()
return
# Проверяем, находится ли пользователь в черном списке
is_blacklisted, blacklist_reason = await blacklist_service.is_user_blacklisted(
message.from_user.id, message.from_user.username
)
if is_blacklisted:
logger.warning(f'🚫 Пользователь {message.from_user.id} находится в черном списке: {blacklist_reason}')
try:
await message.answer(
f'🚫 Оплата невозможна\n\n'
f'Причина: {blacklist_reason}\n\n'
f'Если вы считаете, что это ошибка, обратитесь в поддержку.'
)
except Exception as e:
logger.error(f'Ошибка при отправке сообщения о блокировке: {e}')
return
texts = get_texts(db_user.language)
if not settings.is_yookassa_enabled() or not settings.YOOKASSA_SBP_ENABLED:
-18
View File
@@ -9,7 +9,6 @@ from app.database.models import User
from app.keyboards.inline import get_back_keyboard
from app.localization.texts import get_texts
from app.services.admin_notification_service import AdminNotificationService
from app.services.blacklist_service import blacklist_service
from app.services.promocode_service import PromoCodeService
from app.states import PromoCodeStates
from app.utils.decorators import error_handler
@@ -71,23 +70,6 @@ async def activate_promocode_for_registration(db: AsyncSession, user_id: int, co
@error_handler
async def process_promocode(message: types.Message, db_user: User, state: FSMContext, db: AsyncSession):
# Проверяем, находится ли пользователь в черном списке
is_blacklisted, blacklist_reason = await blacklist_service.is_user_blacklisted(
message.from_user.id, message.from_user.username
)
if is_blacklisted:
logger.warning(f'🚫 Пользователь {message.from_user.id} находится в черном списке: {blacklist_reason}')
try:
await message.answer(
f'🚫 Активация промокода невозможна\n\n'
f'Причина: {blacklist_reason}\n\n'
f'Если вы считаете, что это ошибка, обратитесь в поддержку.'
)
except Exception as e:
logger.error(f'Ошибка при отправке сообщения о блокировке: {e}')
return
texts = get_texts(db_user.language)
code = message.text.strip()
-39
View File
@@ -35,7 +35,6 @@ from app.middlewares.channel_checker import (
get_pending_payload_from_redis,
)
from app.services.admin_notification_service import AdminNotificationService
from app.services.blacklist_service import blacklist_service
from app.services.campaign_service import AdvertisingCampaignService
from app.services.main_menu_button_service import MainMenuButtonService
from app.services.pinned_message_service import (
@@ -993,25 +992,6 @@ async def process_referral_code_skip(callback: types.CallbackQuery, state: FSMCo
async def complete_registration_from_callback(callback: types.CallbackQuery, state: FSMContext, db: AsyncSession):
logger.info(f'🎯 COMPLETE: Завершение регистрации для пользователя {callback.from_user.id}')
# Проверяем, находится ли пользователь в черном списке
is_blacklisted, blacklist_reason = await blacklist_service.is_user_blacklisted(
callback.from_user.id, callback.from_user.username
)
if is_blacklisted:
logger.warning(f'🚫 Пользователь {callback.from_user.id} находится в черном списке: {blacklist_reason}')
try:
await callback.message.answer(
f'🚫 Регистрация невозможна\n\n'
f'Причина: {blacklist_reason}\n\n'
f'Если вы считаете, что это ошибка, обратитесь в поддержку.'
)
except Exception as e:
logger.error(f'Ошибка при отправке сообщения о блокировке: {e}')
await state.clear()
return
existing_user = await get_user_by_telegram_id(db, callback.from_user.id)
if existing_user and existing_user.status == UserStatus.ACTIVE.value:
@@ -1262,25 +1242,6 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
async def complete_registration(message: types.Message, state: FSMContext, db: AsyncSession):
logger.info(f'🎯 COMPLETE: Завершение регистрации для пользователя {message.from_user.id}')
# Проверяем, находится ли пользователь в черном списке
is_blacklisted, blacklist_reason = await blacklist_service.is_user_blacklisted(
message.from_user.id, message.from_user.username
)
if is_blacklisted:
logger.warning(f'🚫 Пользователь {message.from_user.id} находится в черном списке: {blacklist_reason}')
try:
await message.answer(
f'🚫 Регистрация невозможна\n\n'
f'Причина: {blacklist_reason}\n\n'
f'Если вы считаете, что это ошибка, обратитесь в поддержку.'
)
except Exception as e:
logger.error(f'Ошибка при отправке сообщения о блокировке: {e}')
await state.clear()
return
existing_user = await get_user_by_telegram_id(db, message.from_user.id)
if existing_user and existing_user.status == UserStatus.ACTIVE.value:
-55
View File
@@ -39,7 +39,6 @@ from app.keyboards.inline import (
)
from app.localization.texts import get_texts
from app.services.admin_notification_service import AdminNotificationService
from app.services.blacklist_service import blacklist_service
from app.services.remnawave_service import RemnaWaveConfigurationError
from app.services.subscription_checkout_service import (
clear_subscription_checkout_draft,
@@ -1724,24 +1723,6 @@ async def handle_extend_subscription(callback: types.CallbackQuery, db_user: Use
async def confirm_extend_subscription(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
# Проверяем, находится ли пользователь в черном списке
is_blacklisted, blacklist_reason = await blacklist_service.is_user_blacklisted(
callback.from_user.id, callback.from_user.username
)
if is_blacklisted:
logger.warning(f'🚫 Пользователь {callback.from_user.id} находится в черном списке: {blacklist_reason}')
try:
await callback.answer(
f'🚫 Продление подписки невозможно\n\n'
f'Причина: {blacklist_reason}\n\n'
f'Если вы считаете, что это ошибка, обратитесь в поддержку.',
show_alert=True,
)
except Exception as e:
logger.error(f'Ошибка при отправке сообщения о блокировке: {e}')
return
days = int(callback.data.split('_')[2])
texts = get_texts(db_user.language)
@@ -2228,24 +2209,6 @@ async def devices_continue(callback: types.CallbackQuery, state: FSMContext, db_
async def confirm_purchase(callback: types.CallbackQuery, state: FSMContext, db_user: User, db: AsyncSession):
# Проверяем, находится ли пользователь в черном списке
is_blacklisted, blacklist_reason = await blacklist_service.is_user_blacklisted(
callback.from_user.id, callback.from_user.username
)
if is_blacklisted:
logger.warning(f'🚫 Пользователь {callback.from_user.id} находится в черном списке: {blacklist_reason}')
try:
await callback.answer(
f'🚫 Покупка подписки невозможна\n\n'
f'Причина: {blacklist_reason}\n\n'
f'Если вы считаете, что это ошибка, обратитесь в поддержку.',
show_alert=True,
)
except Exception as e:
logger.error(f'Ошибка при отправке сообщения о блокировке: {e}')
return
# Проверка ограничения на покупку/продление подписки
if getattr(db_user, 'restriction_subscription', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
@@ -4134,24 +4097,6 @@ async def handle_simple_subscription_purchase(
db: AsyncSession,
):
"""Обрабатывает простую покупку подписки."""
# Проверяем, находится ли пользователь в черном списке
is_blacklisted, blacklist_reason = await blacklist_service.is_user_blacklisted(
callback.from_user.id, callback.from_user.username
)
if is_blacklisted:
logger.warning(f'🚫 Пользователь {callback.from_user.id} находится в черном списке: {blacklist_reason}')
try:
await callback.answer(
f'🚫 Простая покупка подписки невозможна\n\n'
f'Причина: {blacklist_reason}\n\n'
f'Если вы считаете, что это ошибка, обратитесь в поддержку.',
show_alert=True,
)
except Exception as e:
logger.error(f'Ошибка при отправке сообщения о блокировке: {e}')
return
texts = get_texts(db_user.language)
if not settings.SIMPLE_SUBSCRIPTION_ENABLED:
+49
View File
@@ -0,0 +1,49 @@
import logging
from collections.abc import Awaitable, Callable
from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, PreCheckoutQuery, TelegramObject, User as TgUser
from app.services.blacklist_service import blacklist_service
logger = logging.getLogger(__name__)
class BlacklistMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
user: TgUser | None = None
if isinstance(event, (Message, CallbackQuery, PreCheckoutQuery)):
user = event.from_user
if not user or user.is_bot:
return await handler(event, data)
is_blacklisted, reason = await blacklist_service.is_user_blacklisted(user.id, user.username)
if not is_blacklisted:
return await handler(event, data)
logger.warning(f'🚫 Пользователь {user.id} (@{user.username}) из черного списка: {reason}')
block_text = (
f'🚫 Доступ запрещен\n\nПричина: {reason}\n\nЕсли вы считаете, что это ошибка, обратитесь в поддержку.'
)
try:
if isinstance(event, Message):
await event.answer(block_text)
elif isinstance(event, CallbackQuery):
await event.answer(block_text, show_alert=True)
elif isinstance(event, PreCheckoutQuery):
await event.answer(ok=False, error_message='Доступ запрещен')
except Exception as e:
logger.error(f'Ошибка отправки сообщения о блокировке пользователю {user.id}: {e}')
return None
+2 -1
View File
@@ -160,8 +160,9 @@ class BlacklistService:
# Проверяем по username, если он передан
if username:
username_lower = username.lower().lstrip('@')
for bl_id, bl_username, bl_reason in self.blacklist_data:
if bl_username and (bl_username == username or bl_username == f'@{username}'):
if bl_username and bl_username.lower().lstrip('@') == username_lower:
logger.info(
f'Пользователь {username} ({telegram_id}) найден в черном списке по username: {bl_reason}'
)
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = 'remnawave-bedolaga-telegram-bot'
version = '3.4.0'
version = "3.5.0"
description = 'Telegram bot for RemnaWave VPN service'
readme = 'README.md'
license = { text = 'MIT' }