Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74544c9a9e | |||
| 3314bb7694 | |||
| c920bac33e | |||
| e6881ef617 | |||
| 94a7bb43f6 | |||
| 97841f0cbe | |||
| de392bd4de | |||
| f2a3e5f2fc | |||
| 20a2566fe6 | |||
| 96bd411839 | |||
| 773196fd84 | |||
| 26dc8b4963 | |||
| 06a9202c7c | |||
| 634786917d | |||
| 4ea839c30a | |||
| 10f6ea382b | |||
| 929414e31f | |||
| cd87a3e773 | |||
| 7a3168dcfd | |||
| 888f8fb01a | |||
| 6f06ec5aa0 | |||
| e10cb3f2bb | |||
| 2edd3cb5ab | |||
| d6d3dc83f5 |
@@ -198,11 +198,17 @@ LOGO_FILE=vpn_logo.png
|
||||
# guide - открывает гайд подключения (режим 1)
|
||||
# miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2)
|
||||
# miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3)
|
||||
# link - Открывает ссылку напрямую в браузере (режим 4)
|
||||
CONNECT_BUTTON_MODE=guide
|
||||
|
||||
# URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom)
|
||||
MINIAPP_CUSTOM_URL=
|
||||
|
||||
# Пропустить принятие правил использования бота
|
||||
SKIP_RULES_ACCEPT=false
|
||||
# Пропустить запрос реферального кода
|
||||
SKIP_REFERRAL_CODE=false
|
||||
|
||||
# ===== МОНИТОРИНГ И УВЕДОМЛЕНИЯ =====
|
||||
MONITORING_INTERVAL=60
|
||||
INACTIVE_USER_DELETE_MONTHS=3
|
||||
@@ -239,6 +245,12 @@ BACKUP_COMPRESSION=true
|
||||
BACKUP_INCLUDE_LOGS=false
|
||||
BACKUP_LOCATION=/app/data/backups
|
||||
|
||||
# Отправка бэкапов в телеграм
|
||||
BACKUP_SEND_ENABLED=true
|
||||
BACKUP_SEND_CHAT_ID=-100123456789 # Замени на ID твоего канала (-100) - ПРЕФИКС ЗАКРЫТОГО КАНАЛА!
|
||||
# ВСТАВИТЬ СВОЙ ID СРАЗУ ПОСЛЕ (-100) БЕЗ ПРОБЕЛОВ!
|
||||
BACKUP_SEND_TOPIC_ID=123 # Опционально: ID топика
|
||||
|
||||
# ===== ПРОВЕРКА ОБНОВЛЕНИЙ БОТА =====
|
||||
VERSION_CHECK_ENABLED=true
|
||||
VERSION_CHECK_REPO=fr1ngg/remnawave-bedolaga-telegram-bot
|
||||
|
||||
@@ -36,15 +36,15 @@ jobs:
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
|
||||
echo "🏷️ Собираем релизную версию: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/main ]]; then
|
||||
VERSION="v2.2.7-$(git rev-parse --short HEAD)"
|
||||
VERSION="v2.2.9-$(git rev-parse --short HEAD)"
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
|
||||
echo "🚀 Собираем версию из main: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
|
||||
VERSION="v2.2.7-dev-$(git rev-parse --short HEAD)"
|
||||
VERSION="v2.2.9-dev-$(git rev-parse --short HEAD)"
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:dev,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
|
||||
echo "🧪 Собираем dev версию: $VERSION"
|
||||
else
|
||||
VERSION="v2.2.7-pr-$(git rev-parse --short HEAD)"
|
||||
VERSION="v2.2.9-pr-$(git rev-parse --short HEAD)"
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:pr-$(git rev-parse --short HEAD)"
|
||||
echo "🔀 Собираем PR версию: $VERSION"
|
||||
fi
|
||||
|
||||
@@ -52,13 +52,13 @@ jobs:
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
echo "🏷️ Собираем релизную версию: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/main ]]; then
|
||||
VERSION="v2.2.7"
|
||||
VERSION="v2.2.9"
|
||||
echo "🚀 Собираем версию из main: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
|
||||
VERSION="v2.2.7-dev-$(git rev-parse --short HEAD)"
|
||||
VERSION="v2.2.9-dev-$(git rev-parse --short HEAD)"
|
||||
echo "🧪 Собираем dev версию: $VERSION"
|
||||
else
|
||||
VERSION="v2.2.7-pr-$(git rev-parse --short HEAD)"
|
||||
VERSION="v2.2.9-pr-$(git rev-parse --short HEAD)"
|
||||
echo "🔀 Собираем PR версию: $VERSION"
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
ARG VERSION="v2.2.7"
|
||||
ARG VERSION="v2.2.9"
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
|
||||
@@ -485,6 +485,7 @@ WEBHOOK_PATH=/webhook
|
||||
- Настраиваемый триал-период
|
||||
- Уведомления об истечении
|
||||
- Плавный переход на платную версию
|
||||
- Приветственное сообщение с предложением активации триала (С возможностью отключения)
|
||||
|
||||
💰 **Удобные платежи**
|
||||
- ⭐ Telegram Stars
|
||||
@@ -521,11 +522,12 @@ WEBHOOK_PATH=/webhook
|
||||
👥 **Управление пользователями**
|
||||
- 🔍 Поиск и редактирование профилей
|
||||
- 💰 Управление балансами
|
||||
- 📱 **НОВОЕ**: Изменение лимита устройств (1-X)
|
||||
- 📊 **НОВОЕ**: Настройка лимитов трафика (0-10000 ГБ)
|
||||
- 🌍 **НОВОЕ**: Мультивыбор серверов
|
||||
- 🔄 **НОВОЕ**: Сброс HWID устройств
|
||||
- 👥 **НОВОЕ**: Детальная реферальная статистика в профиле пользователя
|
||||
- 📱 Изменение лимита устройств (1-X)
|
||||
- 📊 Настройка лимитов трафика (0-10000 ГБ)
|
||||
- 🌍 Мультивыбор серверов
|
||||
- 🔄 Сброс HWID устройств
|
||||
- 👥 Детальная реферальная статистика в профиле пользователя
|
||||
- 🔄 **НОВОЕ**: Изминение типа подписки триал/платная
|
||||
- 🚫 Блокировка/разблокировка/удаление
|
||||
|
||||
🎫 **Промо-система**
|
||||
@@ -545,7 +547,7 @@ WEBHOOK_PATH=/webhook
|
||||
- 🔔 Автоуведомления о продлении
|
||||
- 💬 Система поддержки с HTML разметкой
|
||||
- 📝 Настройка правил сервиса
|
||||
- Настраиваемое приветственное сообщение с предложением активации триала
|
||||
- 🧪 Настраиваемое приветственное сообщение с предложением активации триала
|
||||
|
||||
📨 **Уведомления в закрытый канал**
|
||||
- 🎯 Активация триала
|
||||
@@ -556,6 +558,7 @@ WEBHOOK_PATH=/webhook
|
||||
- ♻️ Выход обновлений бота
|
||||
- 🚧 Потеря соелинения с апи Remnawave
|
||||
- 🗄️ **Бекапы/Восстановление бд**
|
||||
- 🗄️ Отправка бд файлов в отдельный чат/топики
|
||||
|
||||
🗄️ **Бекапы/Восстановление**
|
||||
- Ручной запуск бекапа
|
||||
@@ -568,60 +571,6 @@ WEBHOOK_PATH=/webhook
|
||||
|
||||
---
|
||||
|
||||
## 🆕 Недавние обновления (v2.2.1)
|
||||
|
||||
### 🛡️ **Защищенная реферальная система**
|
||||
- **Защита от абуза**: Бонусы выдаются только при реальном пополнении от 100₽
|
||||
- **Новая логика наград**: При первом пополнении ≥100₽ - новый пользователь и пригласивший получают по 100₽
|
||||
- **Комиссионная система**: Пригласивший получает 25% с последующих пополнений
|
||||
- **Автоматическая миграция**: Обновление статуса существующих пользователей с историей пополнений
|
||||
|
||||
### 🔔 **Умные уведомления**
|
||||
- **Реферальные уведомления**: Автоматические сообщения при переходе по ссылке, первом пополнении и получении комиссий
|
||||
- **Контроль повторов**: Защита от дублирования уведомлений и повторного получения бонусов
|
||||
- **Настраиваемые попытки**: Конфигурируемое количество попыток отправки уведомлений
|
||||
|
||||
### 📊 **Расширенная статистика рефералов**
|
||||
- **Детальная аналитика**: Количество приглашенных, активных рефералов, конверсия и доходы
|
||||
- **Список рефералов**: Просмотр всех приглашенных пользователей с пагинацией
|
||||
- **Топ-рефералы**: Самые прибыльные рефералы с индикацией активности
|
||||
- **Доходы по периодам**: Разбивка заработка по типам начислений
|
||||
|
||||
### 🔧 **Исправления админ-панели**
|
||||
- **Статистика**: Исправлена неработающая кнопка "Обновить" в статистике
|
||||
- **Промокоды**: Исправлена кнопка "Статистика" в разделе управления промокодами
|
||||
- **Навигация**: Синхронизированы callback_data между клавиатурами и обработчиками
|
||||
- **Реферальная статистика**: Добавлена детальная реферальная информация в профилях пользователей
|
||||
|
||||
### 📱 **Улучшения пользовательского интерфейса**
|
||||
- **User-friendly**: Улучшен интерфейс страницы подписки для лучшего UX
|
||||
- **Производительность**: Оптимизированы SQL-запросы для работы с реферальной статистикой
|
||||
- **Обработка ошибок**: Улучшена обработка ошибок при обновлении данных
|
||||
|
||||
---
|
||||
|
||||
## 🆕 Недавние обновления
|
||||
|
||||
### 🛠️ **Управление серверами**
|
||||
- **Мультивыбор серверов**: Админы могут назначать пользователям несколько серверов через удобный переключатель
|
||||
- **Умная логика отображения**: Показывает активные серверы + уже назначенные пользователю неактивные серверы
|
||||
- **Автоматический пропуск**: При наличии только одного сервера шаг выбора автоматически пропускается
|
||||
- **Синхронизация в реальном времени**: Все изменения серверов автоматически синхронизируются с Remnawave панелью
|
||||
|
||||
### 📱 **Управление устройствами**
|
||||
- **Изменение лимита устройств**: Админы могут менять количество разрешенных устройств (1-X)
|
||||
- **Быстрые кнопки**: Предустановленные значения 1, 2, 3, 5, 10 устройств
|
||||
- **Сброс HWID**: Функция сброса привязанных устройств пользователя
|
||||
- **Гибкая настройка**: Отдельные лимиты для триал и платных подписок
|
||||
|
||||
### 📊 **Управление трафиком**
|
||||
- **Настройка лимитов трафика**: Возможность установки лимита от 0 ГБ до 10000 ГБ
|
||||
- **Безлимитный режим**: Опция установки безлимитного трафика (0 ГБ)
|
||||
- **Быстрые настройки**: Предустановленные значения 50, 100, 500, 1000 ГБ
|
||||
- **Два режима продажи**: Фиксированный трафик или выбираемые пакеты
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Docker развертывание
|
||||
|
||||
### 📄 docker-compose.yml
|
||||
@@ -918,8 +867,6 @@ bedolaga_bot/
|
||||
|
||||
### 🏥 Health Checks
|
||||
- **Основной**: `http://localhost:8081/health`
|
||||
- **YooKassa**: `http://localhost:8082/health`
|
||||
- **Tribute**: `http://localhost:8081/tribute-webhook`
|
||||
|
||||
### 🔧 Полезные команды
|
||||
```bash
|
||||
|
||||
+20
-2
@@ -140,7 +140,9 @@ class Settings(BaseSettings):
|
||||
MINIAPP_CUSTOM_URL: str = ""
|
||||
ENABLE_LOGO_MODE: bool = True
|
||||
LOGO_FILE: str = "vpn_logo.png"
|
||||
|
||||
SKIP_RULES_ACCEPT: bool = False
|
||||
SKIP_REFERRAL_CODE: bool = False
|
||||
|
||||
DEFAULT_LANGUAGE: str = "ru"
|
||||
AVAILABLE_LANGUAGES: str = "ru,en"
|
||||
|
||||
@@ -166,6 +168,9 @@ class Settings(BaseSettings):
|
||||
BACKUP_COMPRESSION: bool = True
|
||||
BACKUP_INCLUDE_LOGS: bool = False
|
||||
BACKUP_LOCATION: str = "/app/data/backups"
|
||||
BACKUP_SEND_ENABLED: bool = False
|
||||
BACKUP_SEND_CHAT_ID: Optional[str] = None
|
||||
BACKUP_SEND_TOPIC_ID: Optional[int] = None
|
||||
|
||||
@field_validator('LOG_FILE', mode='before')
|
||||
@classmethod
|
||||
@@ -413,9 +418,22 @@ class Settings(BaseSettings):
|
||||
return None
|
||||
|
||||
def is_admin_notifications_enabled(self) -> bool:
|
||||
return (self.ADMIN_NOTIFICATIONS_ENABLED and
|
||||
return (self.ADMIN_NOTIFICATIONS_ENABLED and
|
||||
self.get_admin_notifications_chat_id() is not None)
|
||||
|
||||
def get_backup_send_chat_id(self) -> Optional[int]:
|
||||
if not self.BACKUP_SEND_CHAT_ID:
|
||||
return None
|
||||
|
||||
try:
|
||||
return int(self.BACKUP_SEND_CHAT_ID)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def is_backup_send_enabled(self) -> bool:
|
||||
return (self.BACKUP_SEND_ENABLED and
|
||||
self.get_backup_send_chat_id() is not None)
|
||||
|
||||
def get_referral_settings(self) -> Dict:
|
||||
return {
|
||||
"minimum_topup_kopeks": self.REFERRAL_MINIMUM_TOPUP_KOPEKS,
|
||||
|
||||
@@ -14,6 +14,7 @@ async def get_active_welcome_text(db: AsyncSession) -> Optional[str]:
|
||||
result = await db.execute(
|
||||
select(WelcomeText)
|
||||
.where(WelcomeText.is_active == True)
|
||||
.where(WelcomeText.is_enabled == True)
|
||||
.order_by(WelcomeText.updated_at.desc())
|
||||
)
|
||||
welcome_text = result.scalar_one_or_none()
|
||||
@@ -23,8 +24,72 @@ async def get_active_welcome_text(db: AsyncSession) -> Optional[str]:
|
||||
|
||||
return None
|
||||
|
||||
async def get_current_welcome_text_settings(db: AsyncSession) -> dict:
|
||||
result = await db.execute(
|
||||
select(WelcomeText)
|
||||
.where(WelcomeText.is_active == True)
|
||||
.order_by(WelcomeText.updated_at.desc())
|
||||
)
|
||||
welcome_text = result.scalar_one_or_none()
|
||||
|
||||
if welcome_text:
|
||||
return {
|
||||
'text': welcome_text.text_content,
|
||||
'is_enabled': welcome_text.is_enabled,
|
||||
'id': welcome_text.id
|
||||
}
|
||||
|
||||
return {
|
||||
'text': await get_current_welcome_text_or_default(),
|
||||
'is_enabled': True,
|
||||
'id': None
|
||||
}
|
||||
|
||||
async def toggle_welcome_text_status(db: AsyncSession, admin_id: int) -> bool:
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(WelcomeText)
|
||||
.where(WelcomeText.is_active == True)
|
||||
.order_by(WelcomeText.updated_at.desc())
|
||||
)
|
||||
welcome_text = result.scalar_one_or_none()
|
||||
|
||||
if welcome_text:
|
||||
welcome_text.is_enabled = not welcome_text.is_enabled
|
||||
welcome_text.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(welcome_text)
|
||||
|
||||
status = "включен" if welcome_text.is_enabled else "отключен"
|
||||
logger.info(f"Приветственный текст {status} администратором {admin_id}")
|
||||
return welcome_text.is_enabled
|
||||
else:
|
||||
default_text = await get_current_welcome_text_or_default()
|
||||
new_welcome_text = WelcomeText(
|
||||
text_content=default_text,
|
||||
is_active=True,
|
||||
is_enabled=True,
|
||||
created_by=admin_id
|
||||
)
|
||||
|
||||
db.add(new_welcome_text)
|
||||
await db.commit()
|
||||
await db.refresh(new_welcome_text)
|
||||
|
||||
logger.info(f"Создан и включен дефолтный приветственный текст администратором {admin_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при переключении статуса приветственного текста: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
async def set_welcome_text(db: AsyncSession, text_content: str, admin_id: int) -> bool:
|
||||
try:
|
||||
current_settings = await get_current_welcome_text_settings(db)
|
||||
current_enabled_status = current_settings.get('is_enabled', True)
|
||||
|
||||
await db.execute(
|
||||
update(WelcomeText).values(is_active=False)
|
||||
)
|
||||
@@ -32,6 +97,7 @@ async def set_welcome_text(db: AsyncSession, text_content: str, admin_id: int) -
|
||||
new_welcome_text = WelcomeText(
|
||||
text_content=text_content,
|
||||
is_active=True,
|
||||
is_enabled=current_enabled_status,
|
||||
created_by=admin_id
|
||||
)
|
||||
|
||||
@@ -54,7 +120,7 @@ async def get_current_welcome_text_or_default() -> str:
|
||||
f"✅ До 1 Гбит/с скорость "
|
||||
f"✅ Умный VPN — можно не отключать для большинства российских сервисов "
|
||||
f"✅ Современные протоколы — максимум защиты и анонимности "
|
||||
f"👉 Всего 99₽/мес за 1 устройство "
|
||||
f"💉 Всего 99₽/мес за 1 устройство "
|
||||
f"👇 Жмите кнопку и подключайтесь!"
|
||||
)
|
||||
|
||||
@@ -85,11 +151,10 @@ def replace_placeholders(text: str, user) -> str:
|
||||
return result
|
||||
|
||||
async def get_welcome_text_for_user(db: AsyncSession, user) -> str:
|
||||
"""Получает приветственный текст с заменой плейсхолдеров для конкретного пользователя"""
|
||||
welcome_text = await get_active_welcome_text(db)
|
||||
|
||||
if not welcome_text:
|
||||
welcome_text = await get_current_welcome_text_or_default()
|
||||
return None
|
||||
|
||||
if isinstance(user, str):
|
||||
class SimpleUser:
|
||||
|
||||
@@ -667,6 +667,7 @@ class WelcomeText(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
text_content = Column(Text, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_enabled = Column(Boolean, default=True)
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
@@ -248,11 +248,49 @@ async def create_user_messages_table():
|
||||
logger.error(f"Ошибка создания таблицы user_messages: {e}")
|
||||
return False
|
||||
|
||||
async def add_welcome_text_is_enabled_column():
|
||||
column_exists = await check_column_exists('welcome_texts', 'is_enabled')
|
||||
if column_exists:
|
||||
logger.info("Колонка is_enabled уже существует в таблице welcome_texts")
|
||||
return True
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
db_type = await get_database_type()
|
||||
|
||||
if db_type == 'sqlite':
|
||||
alter_sql = "ALTER TABLE welcome_texts ADD COLUMN is_enabled BOOLEAN DEFAULT 1 NOT NULL"
|
||||
elif db_type == 'postgresql':
|
||||
alter_sql = "ALTER TABLE welcome_texts ADD COLUMN is_enabled BOOLEAN DEFAULT TRUE NOT NULL"
|
||||
elif db_type == 'mysql':
|
||||
alter_sql = "ALTER TABLE welcome_texts ADD COLUMN is_enabled BOOLEAN DEFAULT TRUE NOT NULL"
|
||||
else:
|
||||
logger.error(f"Неподдерживаемый тип БД для добавления колонки: {db_type}")
|
||||
return False
|
||||
|
||||
await conn.execute(text(alter_sql))
|
||||
logger.info("✅ Поле is_enabled добавлено в таблицу welcome_texts")
|
||||
|
||||
if db_type == 'sqlite':
|
||||
update_sql = "UPDATE welcome_texts SET is_enabled = 1 WHERE is_enabled IS NULL"
|
||||
else:
|
||||
update_sql = "UPDATE welcome_texts SET is_enabled = TRUE WHERE is_enabled IS NULL"
|
||||
|
||||
result = await conn.execute(text(update_sql))
|
||||
updated_count = result.rowcount
|
||||
logger.info(f"Обновлено {updated_count} существующих записей welcome_texts")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при добавлении поля is_enabled: {e}")
|
||||
return False
|
||||
|
||||
async def create_welcome_texts_table():
|
||||
table_exists = await check_table_exists('welcome_texts')
|
||||
if table_exists:
|
||||
logger.info("Таблица welcome_texts уже существует")
|
||||
return True
|
||||
return await add_welcome_text_is_enabled_column()
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
@@ -264,6 +302,7 @@ async def create_welcome_texts_table():
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text_content TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
is_enabled BOOLEAN DEFAULT 1 NOT NULL,
|
||||
created_by INTEGER NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -271,6 +310,7 @@ async def create_welcome_texts_table():
|
||||
);
|
||||
|
||||
CREATE INDEX idx_welcome_texts_active ON welcome_texts(is_active);
|
||||
CREATE INDEX idx_welcome_texts_enabled ON welcome_texts(is_enabled);
|
||||
CREATE INDEX idx_welcome_texts_updated ON welcome_texts(updated_at);
|
||||
"""
|
||||
|
||||
@@ -280,6 +320,7 @@ async def create_welcome_texts_table():
|
||||
id SERIAL PRIMARY KEY,
|
||||
text_content TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_enabled BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
created_by INTEGER NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -287,6 +328,7 @@ async def create_welcome_texts_table():
|
||||
);
|
||||
|
||||
CREATE INDEX idx_welcome_texts_active ON welcome_texts(is_active);
|
||||
CREATE INDEX idx_welcome_texts_enabled ON welcome_texts(is_enabled);
|
||||
CREATE INDEX idx_welcome_texts_updated ON welcome_texts(updated_at);
|
||||
"""
|
||||
|
||||
@@ -296,6 +338,7 @@ async def create_welcome_texts_table():
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
text_content TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_enabled BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
created_by INT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
@@ -303,6 +346,7 @@ async def create_welcome_texts_table():
|
||||
);
|
||||
|
||||
CREATE INDEX idx_welcome_texts_active ON welcome_texts(is_active);
|
||||
CREATE INDEX idx_welcome_texts_enabled ON welcome_texts(is_enabled);
|
||||
CREATE INDEX idx_welcome_texts_updated ON welcome_texts(updated_at);
|
||||
"""
|
||||
else:
|
||||
@@ -310,7 +354,7 @@ async def create_welcome_texts_table():
|
||||
return False
|
||||
|
||||
await conn.execute(text(create_sql))
|
||||
logger.info("Таблица welcome_texts успешно создана")
|
||||
logger.info("✅ Таблица welcome_texts успешно создана с полем is_enabled")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -588,10 +632,10 @@ async def run_universal_migration():
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с таблицей user_messages")
|
||||
|
||||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ WELCOME_TEXTS ===")
|
||||
logger.info("=== СОЗДАНИЕ/ОБНОВЛЕНИЕ ТАБЛИЦЫ WELCOME_TEXTS ===")
|
||||
welcome_texts_created = await create_welcome_texts_table()
|
||||
if welcome_texts_created:
|
||||
logger.info("✅ Таблица welcome_texts готова")
|
||||
logger.info("✅ Таблица welcome_texts готова с полем is_enabled")
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с таблицей welcome_texts")
|
||||
|
||||
@@ -644,6 +688,7 @@ async def run_universal_migration():
|
||||
logger.info("✅ Реферальная система обновлена")
|
||||
logger.info("✅ CryptoBot таблица готова")
|
||||
logger.info("✅ Таблица конверсий подписок создана")
|
||||
logger.info("✅ Таблица welcome_texts с полем is_enabled готова")
|
||||
logger.info("✅ Дубликаты подписок исправлены")
|
||||
return True
|
||||
|
||||
@@ -660,6 +705,7 @@ async def check_migration_status():
|
||||
"cryptobot_table": False,
|
||||
"user_messages_table": False,
|
||||
"welcome_texts_table": False,
|
||||
"welcome_texts_is_enabled_column": False,
|
||||
"subscription_duplicates": False,
|
||||
"subscription_conversions_table": False
|
||||
}
|
||||
@@ -667,13 +713,12 @@ async def check_migration_status():
|
||||
status["has_made_first_topup_column"] = await check_column_exists('users', 'has_made_first_topup')
|
||||
|
||||
status["cryptobot_table"] = await check_table_exists('cryptobot_payments')
|
||||
|
||||
status["user_messages_table"] = await check_table_exists('user_messages')
|
||||
|
||||
status["welcome_texts_table"] = await check_table_exists('welcome_texts')
|
||||
|
||||
status["subscription_conversions_table"] = await check_table_exists('subscription_conversions')
|
||||
|
||||
status["welcome_texts_is_enabled_column"] = await check_column_exists('welcome_texts', 'is_enabled')
|
||||
|
||||
async with engine.begin() as conn:
|
||||
duplicates_check = await conn.execute(text("""
|
||||
SELECT COUNT(*) FROM (
|
||||
@@ -691,6 +736,7 @@ async def check_migration_status():
|
||||
"cryptobot_table": "Таблица CryptoBot payments",
|
||||
"user_messages_table": "Таблица пользовательских сообщений",
|
||||
"welcome_texts_table": "Таблица приветственных текстов",
|
||||
"welcome_texts_is_enabled_column": "Поле is_enabled в welcome_texts",
|
||||
"subscription_conversions_table": "Таблица конверсий подписок",
|
||||
"subscription_duplicates": "Отсутствие дубликатов подписок"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ from app.database.crud.welcome_text import (
|
||||
get_active_welcome_text,
|
||||
set_welcome_text,
|
||||
get_current_welcome_text_or_default,
|
||||
get_available_placeholders
|
||||
get_available_placeholders,
|
||||
get_current_welcome_text_settings,
|
||||
toggle_welcome_text_status
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -42,11 +44,41 @@ async def show_welcome_text_panel(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
settings = await get_current_welcome_text_settings(db)
|
||||
status_emoji = "🟢" if settings['is_enabled'] else "🔴"
|
||||
status_text = "включено" if settings['is_enabled'] else "отключено"
|
||||
|
||||
await callback.message.edit_text(
|
||||
"👋 Управление приветственным текстом\n\n"
|
||||
"Здесь вы можете изменить текст, который показывается новым пользователям после регистрации.\n\n"
|
||||
"💡 Доступные плейсхолдеры для автозамены:",
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language)
|
||||
f"👋 Управление приветственным текстом\n\n"
|
||||
f"{status_emoji} <b>Статус:</b> {status_text}\n\n"
|
||||
f"Здесь вы можете управлять текстом, который показывается новым пользователям после регистрации.\n\n"
|
||||
f"💡 Доступные плейсхолдеры для автозамены:",
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def toggle_welcome_text(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
new_status = await toggle_welcome_text_status(db, db_user.id)
|
||||
|
||||
status_emoji = "🟢" if new_status else "🔴"
|
||||
status_text = "включено" if new_status else "отключено"
|
||||
action_text = "включены" if new_status else "отключены"
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"👋 Управление приветственным текстом\n\n"
|
||||
f"{status_emoji} <b>Статус:</b> {status_text}\n\n"
|
||||
f"✅ Приветственные сообщения {action_text}!\n\n"
|
||||
f"Здесь вы можете управлять текстом, который показывается новым пользователям после регистрации.\n\n"
|
||||
f"💡 Доступные плейсхолдеры для автозамены:",
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language, new_status),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@@ -57,22 +89,27 @@ async def show_current_welcome_text(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
current_text = await get_active_welcome_text(db)
|
||||
settings = await get_current_welcome_text_settings(db)
|
||||
current_text = settings['text']
|
||||
is_enabled = settings['is_enabled']
|
||||
|
||||
if not current_text:
|
||||
current_text = await get_current_welcome_text_or_default()
|
||||
if not settings['id']:
|
||||
status = "📝 Используется стандартный текст:"
|
||||
else:
|
||||
status = "📝 Текущий приветственный текст:"
|
||||
|
||||
status_emoji = "🟢" if is_enabled else "🔴"
|
||||
status_text = "включено" if is_enabled else "отключено"
|
||||
|
||||
placeholders = get_available_placeholders()
|
||||
placeholders_text = "\n".join([f"• <code>{key}</code> - {desc}" for key, desc in placeholders.items()])
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"{status_emoji} <b>Статус:</b> {status_text}\n\n"
|
||||
f"{status}\n\n"
|
||||
f"<code>{current_text}</code>\n\n"
|
||||
f"💡 Доступные плейсхолдеры:\n{placeholders_text}",
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language),
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language, is_enabled),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
@@ -84,6 +121,7 @@ async def show_placeholders_help(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
settings = await get_current_welcome_text_settings(db)
|
||||
placeholders = get_available_placeholders()
|
||||
placeholders_text = "\n".join([f"• <code>{key}</code>\n {desc}" for key, desc in placeholders.items()])
|
||||
|
||||
@@ -99,7 +137,7 @@ async def show_placeholders_help(
|
||||
|
||||
await callback.message.edit_text(
|
||||
help_text,
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language),
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
@@ -111,11 +149,12 @@ async def show_formatting_help(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
settings = await get_current_welcome_text_settings(db)
|
||||
formatting_info = get_telegram_formatting_info()
|
||||
|
||||
await callback.message.edit_text(
|
||||
formatting_info,
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language),
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
@@ -128,10 +167,8 @@ async def start_edit_welcome_text(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
current_text = await get_active_welcome_text(db)
|
||||
|
||||
if not current_text:
|
||||
current_text = await get_current_welcome_text_or_default()
|
||||
settings = await get_current_welcome_text_settings(db)
|
||||
current_text = settings['text']
|
||||
|
||||
placeholders = get_available_placeholders()
|
||||
placeholders_text = "\n".join([f"• <code>{key}</code> - {desc}" for key, desc in placeholders.items()])
|
||||
@@ -169,21 +206,27 @@ async def process_welcome_text_edit(
|
||||
success = await set_welcome_text(db, new_text, db_user.id)
|
||||
|
||||
if success:
|
||||
settings = await get_current_welcome_text_settings(db)
|
||||
status_emoji = "🟢" if settings['is_enabled'] else "🔴"
|
||||
status_text = "включено" if settings['is_enabled'] else "отключено"
|
||||
|
||||
placeholders = get_available_placeholders()
|
||||
placeholders_text = "\n".join([f"• <code>{key}</code>" for key in placeholders.keys()])
|
||||
|
||||
await message.answer(
|
||||
f"✅ Приветственный текст успешно обновлен!\n\n"
|
||||
f"{status_emoji} <b>Статус:</b> {status_text}\n\n"
|
||||
f"Новый текст:\n"
|
||||
f"<code>{new_text}</code>\n\n"
|
||||
f"💡 Будут заменяться плейсхолдеры: {placeholders_text}",
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language),
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
settings = await get_current_welcome_text_settings(db)
|
||||
await message.answer(
|
||||
"❌ Ошибка при сохранении текста. Попробуйте еще раз.",
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language)
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled'])
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
@@ -199,18 +242,24 @@ async def reset_welcome_text(
|
||||
success = await set_welcome_text(db, default_text, db_user.id)
|
||||
|
||||
if success:
|
||||
settings = await get_current_welcome_text_settings(db)
|
||||
status_emoji = "🟢" if settings['is_enabled'] else "🔴"
|
||||
status_text = "включено" if settings['is_enabled'] else "отключено"
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"✅ Приветственный текст сброшен на стандартный!\n\n"
|
||||
f"{status_emoji} <b>Статус:</b> {status_text}\n\n"
|
||||
f"Стандартный текст:\n"
|
||||
f"<code>{default_text}</code>\n\n"
|
||||
f"💡 Плейсхолдер <code>{{user_name}}</code> будет заменяться на имя пользователя",
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language),
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
settings = await get_current_welcome_text_settings(db)
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка при сбросе текста. Попробуйте еще раз.",
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language)
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled'])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
@@ -232,13 +281,25 @@ async def show_preview_welcome_text(
|
||||
test_user = TestUser()
|
||||
preview_text = await get_welcome_text_for_user(db, test_user)
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"👁️ Предварительный просмотр\n\n"
|
||||
f"Как будет выглядеть текст для пользователя 'Иван' (@test_user):\n\n"
|
||||
f"<code>{preview_text}</code>",
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
settings = await get_current_welcome_text_settings(db)
|
||||
|
||||
if preview_text:
|
||||
await callback.message.edit_text(
|
||||
f"👁️ Предварительный просмотр\n\n"
|
||||
f"Как будет выглядеть текст для пользователя 'Иван' (@test_user):\n\n"
|
||||
f"<code>{preview_text}</code>",
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
f"👁️ Предварительный просмотр\n\n"
|
||||
f"🔴 Приветственные сообщения отключены.\n"
|
||||
f"Новые пользователи не будут получать приветственный текст после регистрации.",
|
||||
reply_markup=get_welcome_text_keyboard(db_user.language, settings['is_enabled']),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
def register_welcome_text_handlers(dp: Dispatcher):
|
||||
@@ -247,6 +308,11 @@ def register_welcome_text_handlers(dp: Dispatcher):
|
||||
F.data == "welcome_text_panel"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
toggle_welcome_text,
|
||||
F.data == "toggle_welcome_text"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_current_welcome_text,
|
||||
F.data == "show_welcome_text"
|
||||
|
||||
+128
-35
@@ -224,13 +224,36 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
|
||||
data['language'] = language
|
||||
await state.set_data(data)
|
||||
logger.info(f"💾 Установлен русский язык по умолчанию")
|
||||
|
||||
if settings.SKIP_RULES_ACCEPT:
|
||||
logger.info("⚙️ SKIP_RULES_ACCEPT включен - пропускаем принятие правил")
|
||||
if data.get('referral_code'):
|
||||
referrer = await get_user_by_referral_code(db, data['referral_code'])
|
||||
if referrer:
|
||||
data['referrer_id'] = referrer.id
|
||||
await state.set_data(data)
|
||||
logger.info(f"✅ Реферер найден: {referrer.id}")
|
||||
|
||||
if settings.SKIP_REFERRAL_CODE or data.get('referral_code'):
|
||||
await complete_registration(message, state, db)
|
||||
else:
|
||||
try:
|
||||
await message.answer(
|
||||
"У вас есть реферальный код? Введите его или нажмите 'Пропустить'",
|
||||
reply_markup=get_referral_code_keyboard(language)
|
||||
)
|
||||
await state.set_state(RegistrationStates.waiting_for_referral_code)
|
||||
logger.info("🔍 Ожидание ввода реферального кода")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при показе вопроса о реферальном коде: {e}")
|
||||
await complete_registration(message, state, db)
|
||||
return
|
||||
|
||||
await message.answer(
|
||||
texts.RULES_TEXT,
|
||||
reply_markup=get_rules_keyboard(language)
|
||||
)
|
||||
logger.info(f"📋 Правила отправлены")
|
||||
|
||||
|
||||
await state.set_state(RegistrationStates.waiting_for_rules_accept)
|
||||
current_state = await state.get_state()
|
||||
logger.info(f"📊 Установлено состояние: {current_state}")
|
||||
@@ -274,25 +297,29 @@ async def process_rules_accept(
|
||||
|
||||
if data.get('referral_code'):
|
||||
logger.info(f"🎫 Найден реферальный код из deep link: {data['referral_code']}")
|
||||
|
||||
|
||||
referrer = await get_user_by_referral_code(db, data['referral_code'])
|
||||
if referrer:
|
||||
data['referrer_id'] = referrer.id
|
||||
await state.set_data(data)
|
||||
logger.info(f"✅ Реферер найден: {referrer.id}")
|
||||
|
||||
|
||||
await complete_registration_from_callback(callback, state, db)
|
||||
else:
|
||||
try:
|
||||
await callback.message.answer(
|
||||
"У вас есть реферальный код? Введите его или нажмите 'Пропустить'",
|
||||
reply_markup=get_referral_code_keyboard(language)
|
||||
)
|
||||
await state.set_state(RegistrationStates.waiting_for_referral_code)
|
||||
logger.info(f"🔍 Ожидание ввода реферального кода")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при показе вопроса о реферальном коде: {e}")
|
||||
if settings.SKIP_REFERRAL_CODE:
|
||||
logger.info("⚙️ SKIP_REFERRAL_CODE включен - пропускаем запрос реферального кода")
|
||||
await complete_registration_from_callback(callback, state, db)
|
||||
else:
|
||||
try:
|
||||
await callback.message.answer(
|
||||
"У вас есть реферальный код? Введите его или нажмите 'Пропустить'",
|
||||
reply_markup=get_referral_code_keyboard(language)
|
||||
)
|
||||
await state.set_state(RegistrationStates.waiting_for_referral_code)
|
||||
logger.info(f"🔍 Ожидание ввода реферального кода")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при показе вопроса о реферальном коде: {e}")
|
||||
await complete_registration_from_callback(callback, state, db)
|
||||
|
||||
else:
|
||||
logger.info(f"❌ Правила отклонены пользователем {callback.from_user.id}")
|
||||
@@ -390,6 +417,8 @@ async def complete_registration_from_callback(
|
||||
):
|
||||
logger.info(f"🎯 COMPLETE: Завершение регистрации для пользователя {callback.from_user.id}")
|
||||
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
existing_user = await get_user_by_telegram_id(db, callback.from_user.id)
|
||||
|
||||
if existing_user and existing_user.status == UserStatus.ACTIVE.value:
|
||||
@@ -400,6 +429,8 @@ async def complete_registration_from_callback(
|
||||
if data.get('referral_code') and not existing_user.referred_by_id:
|
||||
await callback.message.answer("ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.")
|
||||
|
||||
await db.refresh(existing_user, ['subscription'])
|
||||
|
||||
has_active_subscription = existing_user.subscription is not None
|
||||
subscription_is_active = False
|
||||
|
||||
@@ -455,7 +486,7 @@ async def complete_registration_from_callback(
|
||||
existing_user.last_activity = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(existing_user)
|
||||
await db.refresh(existing_user, ['subscription'])
|
||||
|
||||
user = existing_user
|
||||
logger.info(f"✅ Пользователь {callback.from_user.id} восстановлен")
|
||||
@@ -475,6 +506,7 @@ async def complete_registration_from_callback(
|
||||
referred_by_id=referrer_id,
|
||||
referral_code=referral_code
|
||||
)
|
||||
await db.refresh(user, ['subscription'])
|
||||
else:
|
||||
logger.info(f"🔄 Обновляем существующего пользователя {callback.from_user.id}")
|
||||
existing_user.status = UserStatus.ACTIVE.value
|
||||
@@ -487,7 +519,7 @@ async def complete_registration_from_callback(
|
||||
existing_user.last_activity = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(existing_user)
|
||||
await db.refresh(existing_user, ['subscription'])
|
||||
user = existing_user
|
||||
|
||||
if referrer_id:
|
||||
@@ -500,20 +532,49 @@ async def complete_registration_from_callback(
|
||||
await state.clear()
|
||||
|
||||
from app.database.crud.welcome_text import get_welcome_text_for_user
|
||||
|
||||
user_name = callback.from_user.first_name or callback.from_user.username or "друг"
|
||||
offer_text = await get_welcome_text_for_user(db, user_name)
|
||||
offer_text = await get_welcome_text_for_user(db, callback.from_user)
|
||||
|
||||
try:
|
||||
await callback.message.answer(
|
||||
offer_text,
|
||||
reply_markup=get_post_registration_keyboard(),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке предложения триала: {e}")
|
||||
if offer_text:
|
||||
try:
|
||||
await callback.message.answer(
|
||||
offer_text,
|
||||
reply_markup=get_post_registration_keyboard(),
|
||||
)
|
||||
logger.info(f"✅ Приветственное сообщение отправлено пользователю {user.telegram_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке приветственного сообщения: {e}")
|
||||
else:
|
||||
logger.info(f"ℹ️ Приветственные сообщения отключены, показываем главное меню для пользователя {user.telegram_id}")
|
||||
|
||||
has_active_subscription = user.subscription is not None
|
||||
subscription_is_active = False
|
||||
|
||||
if user.subscription:
|
||||
subscription_is_active = user.subscription.is_active
|
||||
|
||||
menu_text = await get_main_menu_text(user, texts, db)
|
||||
|
||||
try:
|
||||
await callback.message.answer(
|
||||
menu_text,
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=user.language,
|
||||
is_admin=settings.is_admin(user.telegram_id),
|
||||
has_had_paid_subscription=user.has_had_paid_subscription,
|
||||
has_active_subscription=has_active_subscription,
|
||||
subscription_is_active=subscription_is_active,
|
||||
balance_kopeks=user.balance_kopeks
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
logger.info(f"✅ Главное меню показано пользователю {user.telegram_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при показе главного меню: {e}")
|
||||
await callback.message.answer(f"Добро пожаловать, {user.full_name}!")
|
||||
|
||||
logger.info(f"✅ Регистрация завершена для пользователя: {user.telegram_id}")
|
||||
|
||||
|
||||
async def complete_registration(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
@@ -531,6 +592,8 @@ async def complete_registration(
|
||||
if data.get('referral_code') and not existing_user.referred_by_id:
|
||||
await message.answer("ℹ️ Вы уже зарегистрированы в системе. Реферальная ссылка не может быть применена.")
|
||||
|
||||
await db.refresh(existing_user, ['subscription'])
|
||||
|
||||
has_active_subscription = existing_user.subscription is not None
|
||||
subscription_is_active = False
|
||||
|
||||
@@ -586,7 +649,7 @@ async def complete_registration(
|
||||
existing_user.last_activity = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(existing_user)
|
||||
await db.refresh(existing_user, ['subscription'])
|
||||
|
||||
user = existing_user
|
||||
logger.info(f"✅ Пользователь {message.from_user.id} восстановлен")
|
||||
@@ -606,6 +669,7 @@ async def complete_registration(
|
||||
referred_by_id=referrer_id,
|
||||
referral_code=referral_code
|
||||
)
|
||||
await db.refresh(user, ['subscription'])
|
||||
else:
|
||||
logger.info(f"🔄 Обновляем существующего пользователя {message.from_user.id}")
|
||||
existing_user.status = UserStatus.ACTIVE.value
|
||||
@@ -618,7 +682,7 @@ async def complete_registration(
|
||||
existing_user.last_activity = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(existing_user)
|
||||
await db.refresh(existing_user, ['subscription'])
|
||||
user = existing_user
|
||||
|
||||
if referrer_id:
|
||||
@@ -631,16 +695,45 @@ async def complete_registration(
|
||||
await state.clear()
|
||||
|
||||
from app.database.crud.welcome_text import get_welcome_text_for_user
|
||||
|
||||
offer_text = await get_welcome_text_for_user(db, message.from_user)
|
||||
|
||||
try:
|
||||
await message.answer(
|
||||
offer_text,
|
||||
reply_markup=get_post_registration_keyboard(),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке предложения триала: {e}")
|
||||
if offer_text:
|
||||
try:
|
||||
await message.answer(
|
||||
offer_text,
|
||||
reply_markup=get_post_registration_keyboard(),
|
||||
)
|
||||
logger.info(f"✅ Приветственное сообщение отправлено пользователю {user.telegram_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке приветственного сообщения: {e}")
|
||||
else:
|
||||
logger.info(f"ℹ️ Приветственные сообщения отключены, показываем главное меню для пользователя {user.telegram_id}")
|
||||
|
||||
has_active_subscription = user.subscription is not None
|
||||
subscription_is_active = False
|
||||
|
||||
if user.subscription:
|
||||
subscription_is_active = user.subscription.is_active
|
||||
|
||||
menu_text = await get_main_menu_text(user, texts, db)
|
||||
|
||||
try:
|
||||
await message.answer(
|
||||
menu_text,
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=user.language,
|
||||
is_admin=settings.is_admin(user.telegram_id),
|
||||
has_had_paid_subscription=user.has_had_paid_subscription,
|
||||
has_active_subscription=has_active_subscription,
|
||||
subscription_is_active=subscription_is_active,
|
||||
balance_kopeks=user.balance_kopeks
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
logger.info(f"✅ Главное меню показано пользователю {user.telegram_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при показе главного меню: {e}")
|
||||
await message.answer(f"Добро пожаловать, {user.full_name}!")
|
||||
|
||||
logger.info(f"✅ Регистрация завершена для пользователя: {user.telegram_id}")
|
||||
|
||||
|
||||
+17
-4
@@ -666,19 +666,32 @@ def get_sync_simplified_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
def get_welcome_text_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
|
||||
def get_welcome_text_keyboard(language: str = "ru", is_enabled: bool = True) -> InlineKeyboardMarkup:
|
||||
|
||||
toggle_text = "🔴 Отключить" if is_enabled else "🟢 Включить"
|
||||
toggle_callback = "toggle_welcome_text"
|
||||
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(text=toggle_text, callback_data=toggle_callback)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📝 Изменить текст", callback_data="edit_welcome_text"),
|
||||
InlineKeyboardButton(text="👁️ Показать текущий", callback_data="show_welcome_text")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="👁️ Предпросмотр", callback_data="preview_welcome_text"),
|
||||
InlineKeyboardButton(text="🔄 Сбросить", callback_data="reset_welcome_text")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🏷️ HTML форматирование", callback_data="show_formatting_help"),
|
||||
InlineKeyboardButton(text="💡 Плейсхолдеры", callback_data="show_placeholders_help")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🔄 Сбросить", callback_data="reset_welcome_text"),
|
||||
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
|
||||
]
|
||||
])
|
||||
]
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ def get_post_registration_keyboard() -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🚀 Подключиться бесплатно 🚀", callback_data="menu_trial"
|
||||
text="🚀 Подключиться бесплатно 🚀", callback_data="trial_activate"
|
||||
)
|
||||
],
|
||||
[InlineKeyboardButton(text="Пропустить ➡️", callback_data="back_to_menu")],
|
||||
|
||||
@@ -9,6 +9,7 @@ from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
from dataclasses import dataclass, asdict
|
||||
import aiofiles
|
||||
from aiogram.types import FSInputFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, text, inspect
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -197,6 +198,8 @@ class BackupService:
|
||||
await self._send_backup_notification(
|
||||
"success", message, str(backup_path)
|
||||
)
|
||||
|
||||
await self._send_backup_file_to_chat(str(backup_path))
|
||||
|
||||
return True, message, str(backup_path)
|
||||
|
||||
@@ -536,9 +539,9 @@ class BackupService:
|
||||
await asyncio.sleep(3600)
|
||||
|
||||
async def _send_backup_notification(
|
||||
self,
|
||||
event_type: str,
|
||||
message: str,
|
||||
self,
|
||||
event_type: str,
|
||||
message: str,
|
||||
file_path: str = None
|
||||
):
|
||||
try:
|
||||
@@ -570,5 +573,32 @@ class BackupService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о бекапе: {e}")
|
||||
|
||||
async def _send_backup_file_to_chat(self, file_path: str):
|
||||
try:
|
||||
if not settings.is_backup_send_enabled():
|
||||
return
|
||||
|
||||
chat_id = settings.get_backup_send_chat_id()
|
||||
if not chat_id:
|
||||
return
|
||||
|
||||
send_kwargs = {
|
||||
'chat_id': chat_id,
|
||||
'document': FSInputFile(file_path),
|
||||
'caption': (
|
||||
f"📦 <b>Резервная копия</b>\n\n"
|
||||
f"⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"
|
||||
),
|
||||
'parse_mode': 'HTML'
|
||||
}
|
||||
|
||||
if settings.BACKUP_SEND_TOPIC_ID:
|
||||
send_kwargs['message_thread_id'] = settings.BACKUP_SEND_TOPIC_ID
|
||||
|
||||
await self.bot.send_document(**send_kwargs)
|
||||
logger.info(f"Бекап отправлен в чат {chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки бекапа в чат: {e}")
|
||||
|
||||
|
||||
backup_service = BackupService()
|
||||
|
||||
Reference in New Issue
Block a user