черный список + фиксы

This commit is contained in:
gy9vin
2025-12-11 10:56:14 +03:00
parent 80785f22b0
commit 81b3c7ed3f
10 changed files with 245 additions and 90 deletions

BIN
app/.DS_Store vendored Normal file

Binary file not shown.

BIN
app/handlers/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -25,6 +25,8 @@ async def show_blacklist_settings(
"""
Показывает настройки черного списка
"""
logger.info(f"Вызван обработчик show_blacklist_settings для пользователя {callback.from_user.id}")
is_enabled = blacklist_service.is_blacklist_check_enabled()
github_url = blacklist_service.get_blacklist_github_url()
blacklist_count = len(await blacklist_service.get_all_blacklisted_users())
@@ -324,7 +326,7 @@ async def process_blacklist_url(
await state.clear()
async def register_blacklist_handlers(dp):
def register_blacklist_handlers(dp):
"""
Регистрация обработчиков черного списка
"""
@@ -360,4 +362,4 @@ async def register_blacklist_handlers(dp):
dp.message.register(
process_blacklist_url,
lambda m: True # Фильтр будет внутри функции
)
)

View File

@@ -60,8 +60,17 @@ async def process_bulk_ban_list(
"""
Обработка списка Telegram ID и выполнение массовой блокировки
"""
if not message.text:
await message.answer(
"❌ Отправьте текстовое сообщение со списком Telegram ID",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")]
])
)
return
input_text = message.text.strip()
if not input_text:
await message.answer(
"❌ Введите корректный список Telegram ID",
@@ -152,7 +161,7 @@ async def process_bulk_ban_list(
await state.clear()
async def register_bulk_ban_handlers(dp):
def register_bulk_ban_handlers(dp):
"""
Регистрация обработчиков команд для массовой блокировки
"""
@@ -165,5 +174,5 @@ async def register_bulk_ban_handlers(dp):
# Обработчик текстового сообщения с ID для блокировки
dp.message.register(
process_bulk_ban_list,
lambda m: m.text and AdminStates.waiting_for_bulk_ban_list
)
AdminStates.waiting_for_bulk_ban_list
)

View File

@@ -7,6 +7,7 @@ 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.utils.decorators import error_handler
from app.states import BalanceStates
@@ -96,8 +97,26 @@ async def process_cryptobot_payment_amount(
amount_kopeks: int,
state: FSMContext
):
# Проверяем, находится ли пользователь в черном списке
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():
await message.answer("❌ Оплата криптовалютой временно недоступна")
return

View File

@@ -6,6 +6,7 @@ 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
@@ -68,6 +69,24 @@ async def process_stars_payment_amount(
amount_kopeks: int,
state: FSMContext
):
# Проверяем, находится ли пользователь в черном списке
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:

View File

@@ -10,6 +10,7 @@ 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.utils.decorators import error_handler
from app.states import BalanceStates
@@ -133,8 +134,26 @@ async def process_yookassa_payment_amount(
amount_kopeks: int,
state: FSMContext
):
# Проверяем, находится ли пользователь в черном списке
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():
await message.answer("❌ Оплата через YooKassa временно недоступна")
return
@@ -261,8 +280,26 @@ async def process_yookassa_sbp_payment_amount(
amount_kopeks: int,
state: FSMContext
):
# Проверяем, находится ли пользователь в черном списке
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:
await message.answer("❌ Оплата через СБП временно недоступна")
return

View File

@@ -7,6 +7,7 @@ from app.states import PromoCodeStates
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.promocode_service import PromoCodeService
from app.services.admin_notification_service import AdminNotificationService
from app.utils.decorators import error_handler
@@ -79,6 +80,24 @@ async def process_promocode(
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()

View File

@@ -51,6 +51,7 @@ from app.services.user_cart_service import user_cart_service
from app.localization.texts import get_texts
from app.services.admin_notification_service import AdminNotificationService
from app.services.remnawave_service import RemnaWaveConfigurationError, RemnaWaveService
from app.services.blacklist_service import blacklist_service
from app.services.subscription_checkout_service import (
clear_subscription_checkout_draft,
get_subscription_checkout_draft,
@@ -995,7 +996,7 @@ async def save_cart_and_redirect_to_topup(
'return_to_cart': True,
'user_id': db_user.id
}
await user_cart_service.save_user_cart(db_user.id, cart_data)
await callback.message.edit_text(
@@ -1020,7 +1021,7 @@ async def return_to_saved_cart(
):
# Получаем данные корзины из Redis
cart_data = await user_cart_service.get_user_cart(db_user.id)
if not cart_data:
await callback.answer("❌ Сохраненная корзина не найдена", show_alert=True)
return
@@ -1347,6 +1348,25 @@ async def confirm_extend_subscription(
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
from app.services.admin_notification_service import AdminNotificationService
days = int(callback.data.split('_')[2])
@@ -1528,7 +1548,7 @@ async def confirm_extend_subscription(
'description': f"Продление подписки на {days} дней",
'consume_promo_offer': bool(promo_component["discount"] > 0),
}
await user_cart_service.save_user_cart(db_user.id, cart_data)
await callback.message.edit_text(
@@ -1811,6 +1831,25 @@ async def confirm_purchase(
):
from app.services.admin_notification_service import AdminNotificationService
# Проверяем, находится ли пользователь в черном списке
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
data = await state.get_data()
texts = get_texts(db_user.language)
@@ -2101,7 +2140,7 @@ async def confirm_purchase(
'return_to_cart': True,
'user_id': db_user.id
}
await user_cart_service.save_user_cart(db_user.id, cart_data)
await callback.message.edit_text(
@@ -2210,31 +2249,25 @@ async def confirm_purchase(
if should_update_devices:
existing_subscription.device_limit = selected_devices
# Проверяем, что при обновлении существующей подписки есть хотя бы одна страна
selected_countries = data.get('countries', [])
selected_countries = data.get('countries')
if not selected_countries:
# В случае если подписка уже существовала, не разрешаем отключать все страны
# Если подписка новая, разрешаем, но обычно через UI пользователь должен выбрать хотя бы один сервер
if existing_subscription and existing_subscription.connected_squads is not None:
# Проверим, что в данных есть информация о том, что это обновление существующей подписки
# или что-то указывает, что не нужно отключать все страны
pass # Для простоты в этом случае просто проверим, что список стран не пустой
else:
# Для новой подписки разрешаем пустой список, если не является обновлением
pass
# Иногда после возврата к оформлению из сохраненной корзины список стран не передается.
# В таком случае повторно используем текущие подключенные страны подписки.
selected_countries = existing_subscription.connected_squads or []
if selected_countries:
data['countries'] = selected_countries # чтобы далее использовать фактический список стран
# Но для безопасности - если список стран пустой, проверим, что это разрешено
# иначе вернем ошибку
if not selected_countries:
texts = get_texts(db_user.language)
await callback.message.edit_text(
texts.t(
"COUNTRIES_MINIMUM_REQUIRED",
"❌ Нельзя отключить все страны. Должна быть подключена хотя бы одна страна."
),
reply_markup=get_back_keyboard(db_user.language)
)
await callback.answer()
return
if not selected_countries:
texts = get_texts(db_user.language)
await callback.message.edit_text(
texts.t(
"COUNTRIES_MINIMUM_REQUIRED",
"❌ Нельзя отключить все страны. Должна быть подключена хотя бы одна страна."
),
reply_markup=get_back_keyboard(db_user.language)
)
await callback.answer()
return
existing_subscription.connected_squads = selected_countries
@@ -2266,7 +2299,7 @@ async def confirm_purchase(
resolved_device_limit = default_device_limit
# Проверяем, что для новой подписки также есть хотя бы одна страна, если пользователь проходит через интерфейс стран
new_subscription_countries = data.get('countries', [])
new_subscription_countries = data.get('countries')
if not new_subscription_countries:
# Проверяем, была ли это покупка через интерфейс стран, и если да, то требуем хотя бы одну страну
# Если в данных явно указано, что это интерфейс стран, или есть другие признаки - требуем страну
@@ -2304,11 +2337,11 @@ async def confirm_purchase(
await add_user_to_servers(db, server_ids)
logger.info(f"Сохранены цены серверов за весь период: {server_prices}")
await db.refresh(db_user)
subscription_service = SubscriptionService()
if db_user.remnawave_uuid:
remnawave_user = await subscription_service.update_remnawave_user(
db,
@@ -2323,7 +2356,7 @@ async def confirm_purchase(
reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT,
reset_reason="покупка подписки",
)
if not remnawave_user:
logger.error(f"Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
remnawave_user = await subscription_service.create_remnawave_user(
@@ -2332,7 +2365,7 @@ async def confirm_purchase(
reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT,
reset_reason="покупка подписки (повторная попытка)",
)
transaction = await create_transaction(
db=db,
user_id=db_user.id,
@@ -2939,7 +2972,7 @@ def register_handlers(dp: Dispatcher):
show_device_connection_help,
F.data == "device_connection_help"
)
# Регистрируем обработчик для простой покупки
dp.callback_query.register(
handle_simple_subscription_purchase,
@@ -2954,12 +2987,31 @@ 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:
await callback.answer("❌ Простая покупка подписки временно недоступна", show_alert=True)
return
# Определяем ограничение по устройствам для текущего режима
simple_device_limit = resolve_simple_subscription_device_limit()
@@ -2989,10 +3041,10 @@ async def handle_simple_subscription_purchase(
"traffic_limit_gb": settings.SIMPLE_SUBSCRIPTION_TRAFFIC_GB,
"squad_uuid": settings.SIMPLE_SUBSCRIPTION_SQUAD_UUID
}
# Сохраняем параметры в состояние
await state.update_data(subscription_params=subscription_params)
# Проверяем баланс пользователя
user_balance_kopeks = getattr(db_user, "balance_kopeks", 0)
# Рассчитываем цену подписки
@@ -3017,7 +3069,7 @@ async def handle_simple_subscription_purchase(
if subscription_params["traffic_limit_gb"] == 0
else f"{subscription_params['traffic_limit_gb']} ГБ"
)
if user_balance_kopeks >= price_kopeks:
# Если баланс достаточный, предлагаем оплатить с баланса
simple_lines = [
@@ -3040,7 +3092,7 @@ async def handle_simple_subscription_purchase(
])
message_text = "\n".join(simple_lines)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="✅ Оплатить с баланса", callback_data="simple_subscription_pay_with_balance")],
[types.InlineKeyboardButton(text="💳 Другие способы оплаты", callback_data="simple_subscription_other_payment_methods")],
@@ -3068,19 +3120,19 @@ async def handle_simple_subscription_purchase(
])
message_text = "\n".join(simple_lines)
keyboard = _get_simple_subscription_payment_keyboard(db_user.language)
await callback.message.edit_text(
message_text,
reply_markup=keyboard,
parse_mode="HTML"
)
await state.set_state(SubscriptionStates.waiting_for_simple_subscription_payment_method)
await callback.answer()
async def _calculate_simple_subscription_price(
@@ -3105,14 +3157,14 @@ def _get_simple_subscription_payment_keyboard(language: str) -> types.InlineKeyb
"""Создает клавиатуру с методами оплаты для простой подписки."""
texts = get_texts(language)
keyboard = []
# Добавляем доступные методы оплаты
if settings.TELEGRAM_STARS_ENABLED:
keyboard.append([types.InlineKeyboardButton(
text="⭐ Telegram Stars",
callback_data="simple_subscription_stars"
)])
if settings.is_yookassa_enabled():
yookassa_methods = []
if settings.YOOKASSA_SBP_ENABLED:
@@ -3126,38 +3178,38 @@ def _get_simple_subscription_payment_keyboard(language: str) -> types.InlineKeyb
))
if yookassa_methods:
keyboard.append(yookassa_methods)
if settings.is_cryptobot_enabled():
keyboard.append([types.InlineKeyboardButton(
text="🪙 CryptoBot",
callback_data="simple_subscription_cryptobot"
)])
if settings.is_mulenpay_enabled():
mulenpay_name = settings.get_mulenpay_display_name()
keyboard.append([types.InlineKeyboardButton(
text=f"💳 {mulenpay_name}",
callback_data="simple_subscription_mulenpay"
)])
if settings.is_pal24_enabled():
keyboard.append([types.InlineKeyboardButton(
text="💳 PayPalych",
callback_data="simple_subscription_pal24"
)])
if settings.is_wata_enabled():
keyboard.append([types.InlineKeyboardButton(
text="💳 WATA",
callback_data="simple_subscription_wata"
)])
# Кнопка назад
keyboard.append([types.InlineKeyboardButton(
text=texts.BACK,
callback_data="subscription_purchase"
)])
return types.InlineKeyboardMarkup(inline_keyboard=keyboard)
@@ -3179,9 +3231,9 @@ async def _extend_existing_subscription(
from app.services.subscription_service import SubscriptionService
from app.utils.pricing_utils import calculate_months_from_days
from datetime import datetime, timedelta
texts = get_texts(db_user.language)
# Рассчитываем цену подписки
subscription_params = {
"period_days": period_days,
@@ -3205,7 +3257,7 @@ async def _extend_existing_subscription(
price_breakdown.get("servers_price", 0),
price_breakdown.get("total_discount", 0),
)
# Проверяем баланс пользователя
if db_user.balance_kopeks < price_kopeks:
missing_kopeks = price_kopeks - db_user.balance_kopeks
@@ -3223,7 +3275,7 @@ async def _extend_existing_subscription(
balance=texts.format_price(db_user.balance_kopeks),
missing=texts.format_price(missing_kopeks),
)
# Подготовим данные для сохранения в корзину
from app.services.user_cart_service import user_cart_service
cart_data = {
@@ -3241,9 +3293,9 @@ async def _extend_existing_subscription(
'squad_uuid': squad_uuid,
'consume_promo_offer': False,
}
await user_cart_service.save_user_cart(db_user.id, cart_data)
await callback.message.edit_text(
message_text,
reply_markup=get_insufficient_balance_keyboard(
@@ -3255,7 +3307,7 @@ async def _extend_existing_subscription(
)
await callback.answer()
return
# Списываем средства
success = await subtract_user_balance(
db,
@@ -3264,15 +3316,15 @@ async def _extend_existing_subscription(
f"Продление подписки на {period_days} дней",
consume_promo_offer=False, # Простая покупка не использует промо-скидки
)
if not success:
await callback.answer("⚠ Ошибка списания средств", show_alert=True)
return
# Обновляем параметры подписки
current_time = datetime.utcnow()
old_end_date = current_subscription.end_date
# Обновляем параметры в зависимости от типа текущей подписки
if current_subscription.is_trial:
# При продлении триальной подписки переводим её в обычную
@@ -3296,7 +3348,7 @@ async def _extend_existing_subscription(
if squad_uuid and squad_uuid not in current_subscription.connected_squads:
# Используем += для безопасного добавления в список SQLAlchemy
current_subscription.connected_squads = current_subscription.connected_squads + [squad_uuid]
# Продлеваем подписку
if current_subscription.end_date > current_time:
# Если подписка ещё активна, добавляем дни к текущей дате окончания
@@ -3304,15 +3356,15 @@ async def _extend_existing_subscription(
else:
# Если подписка уже истекла, начинаем от текущего времени
new_end_date = current_time + timedelta(days=period_days)
current_subscription.end_date = new_end_date
current_subscription.updated_at = current_time
# Сохраняем изменения
await db.commit()
await db.refresh(current_subscription)
await db.refresh(db_user)
# Обновляем пользователя в Remnawave
subscription_service = SubscriptionService()
try:
@@ -3328,7 +3380,7 @@ async def _extend_existing_subscription(
logger.error("⚠ ОШИБКА ОБНОВЛЕНИЯ REMNAWAVE")
except Exception as e:
logger.error(f"⚠ ИСКЛЮЧЕНИЕ ПРИ ОБНОВЛЕНИИ REMNAWAVE: {e}")
# Создаём транзакцию
transaction = await create_transaction(
db=db,
@@ -3337,7 +3389,7 @@ async def _extend_existing_subscription(
amount_kopeks=price_kopeks,
description=f"Продление подписки на {period_days} дней"
)
# Отправляем уведомление админу
try:
notification_service = AdminNotificationService(callback.bot)
@@ -3353,7 +3405,7 @@ async def _extend_existing_subscription(
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о продлении: {e}")
# Отправляем сообщение пользователю
success_message = (
"✅ Подписка успешно продлена!\n\n"
@@ -3361,15 +3413,15 @@ async def _extend_existing_subscription(
f"Действует до: {format_local_datetime(new_end_date, '%d.%m.%Y %H:%M')}\n\n"
f"💰 Списано: {texts.format_price(price_kopeks)}"
)
# Если это была триальная подписка, добавляем информацию о преобразовании
if current_subscription.is_trial:
success_message += "\n🎯 Триальная подписка преобразована в платную"
await callback.message.edit_text(
success_message,
reply_markup=get_back_keyboard(db_user.language)
)
logger.info(f"✅ Пользователь {db_user.telegram_id} продлил подписку на {period_days} дней за {price_kopeks / 100}")
await callback.answer()

View File

@@ -11,7 +11,7 @@ services:
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- bot_network
- remnawave-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-remnawave_user} -d ${POSTGRES_DB:-remnawave_bot}"]
interval: 30s
@@ -27,7 +27,7 @@ services:
volumes:
- redis_data:/data
networks:
- bot_network
- remnawave-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
@@ -74,7 +74,7 @@ services:
ports:
- "${WEB_API_PORT:-8080}:8080"
networks:
- bot_network
- remnawave-network
healthcheck:
test: ["CMD-SHELL", "python -c \"import requests, os; requests.get('http://localhost:8080/health', headers={'X-API-Key': os.environ.get('WEB_API_DEFAULT_TOKEN')}, timeout=5) or exit(1)\""]
interval: 60s
@@ -89,9 +89,7 @@ volumes:
driver: local
networks:
bot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1
remnawave-network:
name: remnawave-network
driver: bridge
external: true