Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68be5cbe4b | |||
| 3315c83e0b | |||
| ca9b6f4063 | |||
| cc490b616f | |||
| 988f9eb4eb | |||
| c41eeb2c8b | |||
| 881963ca7b | |||
| db6733083c | |||
| 77851b090a | |||
| e0ada505aa | |||
| 3179f33b67 | |||
| 9bbd72b65f | |||
| 68ace1358b | |||
| f7934fd0fe | |||
| 15cbbc04ce | |||
| d4260106cb | |||
| 0a46bd18db | |||
| 0ccbad4077 | |||
| 013ce09846 | |||
| 736ec95572 | |||
| bddf36261d | |||
| cbd74049dd | |||
| 84f3db4548 | |||
| c07ccf0daf | |||
| 3ff4906214 | |||
| 1bc5133c95 | |||
| dcbad389b3 | |||
| dfea057a25 | |||
| 3a6bf029d1 | |||
| 667041558e | |||
| 01e51f2dfd | |||
| 483971d12f | |||
| fad93dab53 | |||
| ff13b7685b | |||
| 4281701957 | |||
| a0ff5dbcd4 | |||
| ed9ac80e68 | |||
| e9662b9b42 | |||
| ba3fc35a14 | |||
| 18471f0162 | |||
| 71073fd0e5 | |||
| 2e1ee6bb28 | |||
| 239f14acaf | |||
| e87a62c180 | |||
| cb275fc4b6 | |||
| 4042dee884 | |||
| 2d3dc8e024 | |||
| 530759fc81 | |||
| 0f29850d7f | |||
| e060c39790 | |||
| 612d7ad529 | |||
| a310e88905 | |||
| bda959e8d6 | |||
| bd9d12c185 | |||
| adf509dc81 | |||
| 22f5fc43dd | |||
| 49b10b2d4a | |||
| 2f218b7567 | |||
| 1bb0894894 | |||
| 39da15fec2 | |||
| 9ff66b9650 | |||
| c5353b46b1 | |||
| 25aa214ffa | |||
| 28279b7ec2 | |||
| 126170bee7 | |||
| 8ce7935d49 | |||
| f756c9a7f9 | |||
| a59905b98f | |||
| f0d74185a1 | |||
| 06937e026f | |||
| 65acaf7058 | |||
| 7fc574daf3 | |||
| a93eb8d2d1 | |||
| 9fbd15f700 | |||
| cec96d866c | |||
| 80031306a4 | |||
| 9783781bfb | |||
| aba7302833 | |||
| 61ce10c52e |
@@ -108,7 +108,6 @@ TELEGRAM_STARS_RATE_RUB=1.3
|
||||
# Tribute (https://tribute.app)
|
||||
TRIBUTE_ENABLED=false
|
||||
TRIBUTE_API_KEY=
|
||||
TRIBUTE_WEBHOOK_SECRET=your_webhook_secret
|
||||
TRIBUTE_DONATE_LINK=
|
||||
TRIBUTE_WEBHOOK_PATH=/tribute-webhook
|
||||
TRIBUTE_WEBHOOK_PORT=8081
|
||||
|
||||
@@ -1,43 +1,29 @@
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first for better caching
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Create non-root user with specific UID/GID for consistency
|
||||
RUN groupadd -g 1000 app && \
|
||||
useradd -u 1000 -g 1000 -m -s /bin/bash app
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create necessary directories and set permissions
|
||||
RUN mkdir -p logs data && \
|
||||
chown -R app:app /app && \
|
||||
chmod -R 755 /app && \
|
||||
chmod -R 777 logs
|
||||
chown -R app:app /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER app
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONPATH=/app
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Expose webhook ports
|
||||
EXPOSE 8081
|
||||
EXPOSE 8082
|
||||
EXPOSE 8081 8082
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8081/health || exit 1
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "main.py"]
|
||||
|
||||
@@ -316,7 +316,6 @@ TELEGRAM_STARS_RATE_RUB=1.3
|
||||
# Tribute (https://tribute.app)
|
||||
TRIBUTE_ENABLED=false
|
||||
TRIBUTE_API_KEY=
|
||||
TRIBUTE_WEBHOOK_SECRET=your_webhook_secret
|
||||
TRIBUTE_DONATE_LINK=
|
||||
TRIBUTE_WEBHOOK_PATH=/tribute-webhook
|
||||
TRIBUTE_WEBHOOK_PORT=8081
|
||||
@@ -607,12 +606,6 @@ services:
|
||||
- "${YOOKASSA_WEBHOOK_PORT:-8082}:8082"
|
||||
networks:
|
||||
- bot_network
|
||||
user: "1000:1000"
|
||||
command: >
|
||||
bash -c "
|
||||
mkdir -p /app/logs /app/data &&
|
||||
python main.py
|
||||
"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -786,6 +779,7 @@ bedolaga_bot/
|
||||
│ │ ├── 🎁 promocode_service.py # Промокоды
|
||||
│ │ ├── 🚧 maintenance_service.py # Промокоды
|
||||
│ │ ├── 👥 referral_service.py # Рефералы
|
||||
│ │ ├── 💬 admin_notification_service.py # Уведомления для администраторов в чаты
|
||||
│ │ ├── 🔍 monitoring_service.py # Мониторинг
|
||||
│ │ ├── 🎖️ tribute_service.py # Tribute платежи
|
||||
│ │ ├── 💳 yookassa_service.py # YooKassa платежи
|
||||
@@ -1048,13 +1042,20 @@ REMNAWAVE_SECRET_KEY=XXXXXXX:DDDDDDDD
|
||||
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td><strong>@kr33njee</strong></td>
|
||||
<td>₽800</td>
|
||||
<td>За поддержку и доверие</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td><strong>@SmartSystemCompany</strong></td>
|
||||
<td>₽600</td>
|
||||
<td>За поддержку и доверие</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td>8</td>
|
||||
<td><strong>@PhiLin58</strong></td>
|
||||
<td>₽300</td>
|
||||
<td>За участие в развитии</td>
|
||||
|
||||
@@ -23,7 +23,8 @@ from app.handlers.admin import (
|
||||
monitoring as admin_monitoring, referrals as admin_referrals,
|
||||
rules as admin_rules, remnawave as admin_remnawave,
|
||||
statistics as admin_statistics, servers as admin_servers,
|
||||
maintenance as admin_maintenance
|
||||
maintenance as admin_maintenance,
|
||||
user_messages as admin_user_messages
|
||||
)
|
||||
from app.handlers.stars_payments import register_stars_handlers
|
||||
|
||||
@@ -97,6 +98,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
admin_remnawave.register_handlers(dp)
|
||||
admin_statistics.register_handlers(dp)
|
||||
admin_maintenance.register_handlers(dp)
|
||||
admin_user_messages.register_handlers(dp)
|
||||
|
||||
common.register_handlers(dp)
|
||||
|
||||
|
||||
@@ -93,7 +93,6 @@ class Settings(BaseSettings):
|
||||
|
||||
TRIBUTE_ENABLED: bool = False
|
||||
TRIBUTE_API_KEY: Optional[str] = None
|
||||
TRIBUTE_WEBHOOK_SECRET: Optional[str] = None
|
||||
TRIBUTE_DONATE_LINK: Optional[str] = None
|
||||
TRIBUTE_WEBHOOK_PATH: str = "/tribute-webhook"
|
||||
TRIBUTE_WEBHOOK_PORT: int = 8081
|
||||
|
||||
@@ -277,12 +277,12 @@ async def get_revenue_by_period(
|
||||
|
||||
return [{"date": row.date, "amount_kopeks": row.amount} for row in result]
|
||||
|
||||
|
||||
async def find_tribute_transactions_by_payment_id(
|
||||
db: AsyncSession,
|
||||
payment_id: str,
|
||||
user_telegram_id: Optional[int] = None
|
||||
) -> List[Transaction]:
|
||||
"""Найти все Tribute транзакции по payment_id"""
|
||||
|
||||
query = select(Transaction).options(selectinload(Transaction.user))
|
||||
|
||||
@@ -313,18 +313,27 @@ async def check_tribute_payment_duplicate(
|
||||
amount_kopeks: int,
|
||||
user_telegram_id: int
|
||||
) -> Optional[Transaction]:
|
||||
cutoff_time = datetime.utcnow() - timedelta(hours=24)
|
||||
|
||||
transactions = await find_tribute_transactions_by_payment_id(
|
||||
db, payment_id, user_telegram_id
|
||||
)
|
||||
exact_external_id = f"donation_{payment_id}"
|
||||
|
||||
for transaction in transactions:
|
||||
if (transaction.amount_kopeks == amount_kopeks and
|
||||
transaction.is_completed and
|
||||
transaction.user.telegram_id == user_telegram_id):
|
||||
return transaction
|
||||
query = select(Transaction).options(selectinload(Transaction.user)).where(
|
||||
and_(
|
||||
Transaction.payment_method == PaymentMethod.TRIBUTE.value,
|
||||
Transaction.external_id == exact_external_id,
|
||||
Transaction.amount_kopeks == amount_kopeks,
|
||||
Transaction.is_completed == True,
|
||||
Transaction.created_at >= cutoff_time
|
||||
)
|
||||
).join(User).where(User.telegram_id == user_telegram_id)
|
||||
|
||||
return None
|
||||
result = await db.execute(query)
|
||||
transaction = result.scalar_one_or_none()
|
||||
|
||||
if transaction:
|
||||
logger.info(f"🔍 Найден дубликат платежа в течение 24ч: {transaction.id}")
|
||||
|
||||
return transaction
|
||||
|
||||
|
||||
async def create_unique_tribute_transaction(
|
||||
@@ -334,7 +343,6 @@ async def create_unique_tribute_transaction(
|
||||
amount_kopeks: int,
|
||||
description: str
|
||||
) -> Transaction:
|
||||
"""Создать уникальную Tribute транзакцию с защитой от дубликатов"""
|
||||
|
||||
external_id = f"donation_{payment_id}"
|
||||
|
||||
@@ -356,16 +364,3 @@ async def create_unique_tribute_transaction(
|
||||
external_id=external_id,
|
||||
is_completed=True
|
||||
)
|
||||
|
||||
async def get_transaction_by_id(
|
||||
db: AsyncSession,
|
||||
transaction_id: int
|
||||
) -> Optional[Transaction]:
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Transaction).where(Transaction.id == transaction_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения транзакции {transaction_id}: {e}")
|
||||
return None
|
||||
|
||||
@@ -48,7 +48,6 @@ async def get_user_by_telegram_id(db: AsyncSession, telegram_id: int) -> Optiona
|
||||
|
||||
|
||||
async def get_user_by_referral_code(db: AsyncSession, referral_code: str) -> Optional[User]:
|
||||
|
||||
result = await db.execute(
|
||||
select(User).where(User.referral_code == referral_code)
|
||||
)
|
||||
@@ -92,7 +91,8 @@ async def create_user(
|
||||
referred_by_id=referred_by_id,
|
||||
referral_code=referral_code,
|
||||
balance_kopeks=0,
|
||||
has_had_paid_subscription=False
|
||||
has_had_paid_subscription=False,
|
||||
has_made_first_topup=False
|
||||
)
|
||||
|
||||
db.add(user)
|
||||
@@ -149,18 +149,6 @@ async def add_user_balance(
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
topup_keywords = ["пополнение", "stars", "yookassa", "topup"]
|
||||
exclude_keywords = ["комиссия", "бонус", "реферальн", "выплата", "вознаграждение"]
|
||||
|
||||
has_topup_keywords = any(word in description.lower() for word in topup_keywords)
|
||||
has_exclude_keywords = any(word in description.lower() for word in exclude_keywords)
|
||||
|
||||
if has_topup_keywords and not has_exclude_keywords:
|
||||
try:
|
||||
from app.services.referral_service import process_referral_topup
|
||||
await process_referral_topup(db, user.id, amount_kopeks, bot)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки реферального пополнения: {e}")
|
||||
|
||||
logger.info(f"💰 Баланс пользователя {user.telegram_id} изменен: {old_balance} → {user.balance_kopeks} (изменение: +{amount_kopeks})")
|
||||
return True
|
||||
@@ -169,7 +157,8 @@ async def add_user_balance(
|
||||
logger.error(f"Ошибка изменения баланса пользователя {user.id}: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
|
||||
async def add_user_balance_by_id(
|
||||
db: AsyncSession,
|
||||
telegram_id: int,
|
||||
@@ -187,11 +176,7 @@ async def add_user_balance_by_id(
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка пополнения баланса пользователя {telegram_id}: {e}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка пополнения баланса пользователя {user_id}: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
async def subtract_user_balance(
|
||||
db: AsyncSession,
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import logging
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import select, func, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import UserMessage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_user_message(
|
||||
db: AsyncSession,
|
||||
message_text: str,
|
||||
created_by: int,
|
||||
is_active: bool = True,
|
||||
sort_order: int = 0
|
||||
) -> UserMessage:
|
||||
message = UserMessage(
|
||||
message_text=message_text,
|
||||
is_active=is_active,
|
||||
sort_order=sort_order,
|
||||
created_by=created_by
|
||||
)
|
||||
|
||||
db.add(message)
|
||||
await db.commit()
|
||||
await db.refresh(message)
|
||||
|
||||
logger.info(f"✅ Создано сообщение ID {message.id} пользователем {created_by}")
|
||||
return message
|
||||
|
||||
|
||||
async def get_user_message_by_id(db: AsyncSession, message_id: int) -> Optional[UserMessage]:
|
||||
result = await db.execute(
|
||||
select(UserMessage).where(UserMessage.id == message_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_active_user_messages(db: AsyncSession) -> List[UserMessage]:
|
||||
result = await db.execute(
|
||||
select(UserMessage)
|
||||
.where(UserMessage.is_active == True)
|
||||
.order_by(UserMessage.sort_order.asc(), UserMessage.created_at.desc())
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def get_random_active_message(db: AsyncSession) -> Optional[str]:
|
||||
active_messages = await get_active_user_messages(db)
|
||||
|
||||
if not active_messages:
|
||||
return None
|
||||
|
||||
random_message = random.choice(active_messages)
|
||||
return random_message.message_text
|
||||
|
||||
|
||||
async def get_all_user_messages(
|
||||
db: AsyncSession,
|
||||
offset: int = 0,
|
||||
limit: int = 50
|
||||
) -> List[UserMessage]:
|
||||
result = await db.execute(
|
||||
select(UserMessage)
|
||||
.order_by(UserMessage.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def get_user_messages_count(db: AsyncSession) -> int:
|
||||
result = await db.execute(select(func.count(UserMessage.id)))
|
||||
return result.scalar()
|
||||
|
||||
|
||||
async def update_user_message(
|
||||
db: AsyncSession,
|
||||
message_id: int,
|
||||
message_text: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
sort_order: Optional[int] = None
|
||||
) -> Optional[UserMessage]:
|
||||
message = await get_user_message_by_id(db, message_id)
|
||||
|
||||
if not message:
|
||||
return None
|
||||
|
||||
if message_text is not None:
|
||||
message.message_text = message_text
|
||||
|
||||
if is_active is not None:
|
||||
message.is_active = is_active
|
||||
|
||||
if sort_order is not None:
|
||||
message.sort_order = sort_order
|
||||
|
||||
message.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(message)
|
||||
|
||||
logger.info(f"📝 Обновлено сообщение ID {message_id}")
|
||||
return message
|
||||
|
||||
|
||||
async def toggle_user_message_status(
|
||||
db: AsyncSession,
|
||||
message_id: int
|
||||
) -> Optional[UserMessage]:
|
||||
message = await get_user_message_by_id(db, message_id)
|
||||
|
||||
if not message:
|
||||
return None
|
||||
|
||||
message.is_active = not message.is_active
|
||||
message.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(message)
|
||||
|
||||
status_text = "активировано" if message.is_active else "деактивировано"
|
||||
logger.info(f"🔄 Сообщение ID {message_id} {status_text}")
|
||||
|
||||
return message
|
||||
|
||||
|
||||
async def delete_user_message(db: AsyncSession, message_id: int) -> bool:
|
||||
message = await get_user_message_by_id(db, message_id)
|
||||
|
||||
if not message:
|
||||
return False
|
||||
|
||||
await db.delete(message)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"🗑️ Удалено сообщение ID {message_id}")
|
||||
return True
|
||||
|
||||
|
||||
async def get_user_messages_stats(db: AsyncSession) -> dict:
|
||||
total_result = await db.execute(select(func.count(UserMessage.id)))
|
||||
total_messages = total_result.scalar()
|
||||
|
||||
active_result = await db.execute(
|
||||
select(func.count(UserMessage.id)).where(UserMessage.is_active == True)
|
||||
)
|
||||
active_messages = active_result.scalar()
|
||||
|
||||
return {
|
||||
"total_messages": total_messages,
|
||||
"active_messages": active_messages,
|
||||
"inactive_messages": total_messages - active_messages
|
||||
}
|
||||
@@ -582,3 +582,24 @@ class SubscriptionServer(Base):
|
||||
|
||||
subscription = relationship("Subscription", backref="subscription_servers")
|
||||
server_squad = relationship("ServerSquad", backref="subscription_servers")
|
||||
|
||||
class UserMessage(Base):
|
||||
__tablename__ = "user_messages"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
message_text = Column(Text, nullable=False)
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
sort_order = Column(Integer, default=0)
|
||||
|
||||
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())
|
||||
|
||||
creator = relationship("User", backref="created_messages")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UserMessage(id={self.id}, active={self.is_active}, text='{self.message_text[:50]}...')>"
|
||||
|
||||
@@ -123,7 +123,7 @@ class RemnaWaveAPI:
|
||||
async def __aenter__(self):
|
||||
conn_type = self._detect_connection_type()
|
||||
|
||||
logger.info(f"🔗 Подключение к RemnaWave: {self.base_url} (тип: {conn_type})")
|
||||
logger.info(f"🔗 Подключение к Remnawave: {self.base_url} (тип: {conn_type})")
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.api_key}',
|
||||
|
||||
@@ -10,10 +10,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TributeService:
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = settings.TRIBUTE_API_KEY
|
||||
self.webhook_secret = settings.TRIBUTE_WEBHOOK_SECRET
|
||||
self.donate_link = settings.TRIBUTE_DONATE_LINK
|
||||
|
||||
async def create_payment_link(
|
||||
@@ -38,27 +37,27 @@ class TributeService:
|
||||
return None
|
||||
|
||||
def verify_webhook_signature(self, payload: str, signature: str) -> bool:
|
||||
|
||||
if not self.webhook_secret:
|
||||
logger.warning("Webhook secret не настроен, пропускаем проверку")
|
||||
return True
|
||||
|
||||
|
||||
if not self.api_key:
|
||||
logger.warning("API key не настроен, пропускаем проверку")
|
||||
return True
|
||||
|
||||
try:
|
||||
expected_signature = hmac.new(
|
||||
self.webhook_secret.encode(),
|
||||
self.api_key.encode(),
|
||||
payload.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
|
||||
is_valid = hmac.compare_digest(signature, expected_signature)
|
||||
|
||||
|
||||
if is_valid:
|
||||
logger.info("✅ Подпись Tribute webhook проверена успешно")
|
||||
else:
|
||||
logger.error("❌ Неверная подпись Tribute webhook")
|
||||
|
||||
|
||||
return is_valid
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки подписи webhook: {e}")
|
||||
return False
|
||||
|
||||
@@ -79,7 +79,7 @@ class WebhookServer:
|
||||
headers={
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, X-Tribute-Signature',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, trbt-signature',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -111,10 +111,17 @@ class WebhookServer:
|
||||
status=400
|
||||
)
|
||||
|
||||
signature = request.headers.get('X-Tribute-Signature')
|
||||
signature = request.headers.get('trbt-signature')
|
||||
logger.info(f"🔐 Signature: {signature}")
|
||||
|
||||
if signature and settings.TRIBUTE_WEBHOOK_SECRET:
|
||||
|
||||
if not signature:
|
||||
logger.error("❌ Отсутствует заголовок подписи Tribute webhook")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "missing_signature"},
|
||||
status=401
|
||||
)
|
||||
|
||||
if settings.TRIBUTE_API_KEY:
|
||||
from app.external.tribute import TributeService as TributeAPI
|
||||
tribute_api = TributeAPI()
|
||||
if not tribute_api.verify_webhook_signature(payload, signature):
|
||||
@@ -123,8 +130,8 @@ class WebhookServer:
|
||||
{"status": "error", "reason": "invalid_signature"},
|
||||
status=401
|
||||
)
|
||||
|
||||
result = await self.tribute_service.process_webhook(payload, signature)
|
||||
|
||||
result = await self.tribute_service.process_webhook(payload)
|
||||
|
||||
if result:
|
||||
logger.info(f"✅ Tribute webhook обработан успешно: {result}")
|
||||
|
||||
@@ -16,6 +16,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class MaintenanceStates(StatesGroup):
|
||||
waiting_for_reason = State()
|
||||
waiting_for_notification_message = State()
|
||||
|
||||
|
||||
@admin_required
|
||||
@@ -30,6 +31,14 @@ async def show_maintenance_panel(
|
||||
|
||||
status_info = maintenance_service.get_status_info()
|
||||
|
||||
try:
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
rw_service = RemnaWaveService()
|
||||
panel_status = await rw_service.get_panel_status_summary()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения статуса панели: {e}")
|
||||
panel_status = {"description": "❓ Не удалось проверить", "has_issues": True}
|
||||
|
||||
status_emoji = "🔧" if status_info["is_active"] else "✅"
|
||||
status_text = "Включен" if status_info["is_active"] else "Выключен"
|
||||
|
||||
@@ -55,14 +64,19 @@ async def show_maintenance_panel(
|
||||
if status_info["consecutive_failures"] > 0:
|
||||
failures_info = f"\n⚠️ <b>Неудачных проверок подряд:</b> {status_info['consecutive_failures']}"
|
||||
|
||||
panel_info = f"\n🌐 <b>Панель Remnawave:</b> {panel_status['description']}"
|
||||
if panel_status.get("response_time"):
|
||||
panel_info += f"\n⚡ <b>Время отклика:</b> {panel_status['response_time']}с"
|
||||
|
||||
message_text = f"""
|
||||
🔧 <b>Режим технических работ</b>
|
||||
🔧 <b>Управление техническими работами</b>
|
||||
|
||||
{status_emoji} <b>Статус:</b> {status_text}
|
||||
{api_emoji} <b>API RemnaWave:</b> {api_text}
|
||||
{status_emoji} <b>Режим техработ:</b> {status_text}
|
||||
{api_emoji} <b>API Remnawave:</b> {api_text}
|
||||
{monitoring_emoji} <b>Мониторинг:</b> {monitoring_text}
|
||||
⏱️ <b>Интервал проверки:</b> {status_info['check_interval']}с
|
||||
🤖 <b>Автовключение:</b> {'Включено' if status_info['auto_enable_configured'] else 'Отключено'}
|
||||
{panel_info}
|
||||
{enabled_info}
|
||||
{last_check_info}
|
||||
{failures_info}
|
||||
@@ -72,7 +86,12 @@ async def show_maintenance_panel(
|
||||
|
||||
await callback.message.edit_text(
|
||||
message_text,
|
||||
reply_markup=get_maintenance_keyboard(db_user.language, status_info["is_active"], status_info["monitoring_active"])
|
||||
reply_markup=get_maintenance_keyboard(
|
||||
db_user.language,
|
||||
status_info["is_active"],
|
||||
status_info["monitoring_active"],
|
||||
panel_status.get("has_issues", False)
|
||||
)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@@ -186,6 +205,166 @@ async def force_api_check(
|
||||
await show_maintenance_panel(callback, db_user, db, None)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def check_panel_status(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
await callback.answer("Проверка статуса панели...", show_alert=False)
|
||||
|
||||
try:
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
rw_service = RemnaWaveService()
|
||||
|
||||
status_data = await rw_service.check_panel_health()
|
||||
|
||||
status_text = {
|
||||
"online": "🟢 Панель работает нормально",
|
||||
"offline": "🔴 Панель недоступна",
|
||||
"degraded": "🟡 Панель работает со сбоями"
|
||||
}.get(status_data["status"], "❓ Статус неизвестен")
|
||||
|
||||
message_parts = [
|
||||
f"🌐 <b>Статус панели Remnawave</b>\n",
|
||||
f"{status_text}",
|
||||
f"⚡ Время отклика: {status_data.get('response_time', 0)}с",
|
||||
f"👥 Пользователей онлайн: {status_data.get('users_online', 0)}",
|
||||
f"🖥️ Нод онлайн: {status_data.get('nodes_online', 0)}/{status_data.get('total_nodes', 0)}"
|
||||
]
|
||||
|
||||
if status_data.get("api_error"):
|
||||
message_parts.append(f"❌ Ошибка: {status_data['api_error'][:100]}")
|
||||
|
||||
message = "\n".join(message_parts)
|
||||
|
||||
await callback.message.answer(message, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
await callback.message.answer(f"❌ Ошибка проверки статуса: {str(e)}")
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def send_manual_notification(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
state: FSMContext
|
||||
):
|
||||
await state.set_state(MaintenanceStates.waiting_for_notification_message)
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(text="🟢 Онлайн", callback_data="manual_notify_online"),
|
||||
types.InlineKeyboardButton(text="🔴 Офлайн", callback_data="manual_notify_offline")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="🟡 Проблемы", callback_data="manual_notify_degraded"),
|
||||
types.InlineKeyboardButton(text="🔧 Обслуживание", callback_data="manual_notify_maintenance")
|
||||
],
|
||||
[types.InlineKeyboardButton(text="❌ Отмена", callback_data="maintenance_panel")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
"📢 <b>Ручная отправка уведомления</b>\n\nВыберите статус для уведомления:",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def handle_manual_notification(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
state: FSMContext
|
||||
):
|
||||
status_map = {
|
||||
"manual_notify_online": "online",
|
||||
"manual_notify_offline": "offline",
|
||||
"manual_notify_degraded": "degraded",
|
||||
"manual_notify_maintenance": "maintenance"
|
||||
}
|
||||
|
||||
status = status_map.get(callback.data)
|
||||
if not status:
|
||||
await callback.answer("Неизвестный статус")
|
||||
return
|
||||
|
||||
await state.update_data(notification_status=status)
|
||||
|
||||
status_names = {
|
||||
"online": "🟢 Онлайн",
|
||||
"offline": "🔴 Офлайн",
|
||||
"degraded": "🟡 Проблемы",
|
||||
"maintenance": "🔧 Обслуживание"
|
||||
}
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"📢 <b>Отправка уведомления: {status_names[status]}</b>\n\n"
|
||||
f"Введите сообщение для уведомления или отправьте /skip для отправки без дополнительного текста:",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="❌ Отмена", callback_data="maintenance_panel")]
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_notification_message(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
state: FSMContext
|
||||
):
|
||||
current_state = await state.get_state()
|
||||
|
||||
if current_state != MaintenanceStates.waiting_for_notification_message:
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
status = data.get("notification_status")
|
||||
|
||||
if not status:
|
||||
await message.answer("Ошибка: статус не выбран")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
notification_message = ""
|
||||
if message.text and message.text != "/skip":
|
||||
notification_message = message.text[:300]
|
||||
|
||||
try:
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
rw_service = RemnaWaveService()
|
||||
|
||||
success = await rw_service.send_manual_status_notification(
|
||||
message.bot,
|
||||
status,
|
||||
notification_message
|
||||
)
|
||||
|
||||
if success:
|
||||
await message.answer("✅ Уведомление отправлено")
|
||||
else:
|
||||
await message.answer("❌ Ошибка отправки уведомления")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки ручного уведомления: {e}")
|
||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"Вернуться к панели техработ:",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="🔧 Панель техработ", callback_data="maintenance_panel")]
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def back_to_admin_panel(
|
||||
@@ -224,6 +403,21 @@ def register_handlers(dp: Dispatcher):
|
||||
F.data == "maintenance_check_api"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
check_panel_status,
|
||||
F.data == "maintenance_check_panel"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
send_manual_notification,
|
||||
F.data == "maintenance_manual_notify"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_manual_notification,
|
||||
F.data.startswith("manual_notify_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
back_to_admin_panel,
|
||||
F.data == "admin_panel"
|
||||
@@ -233,3 +427,8 @@ def register_handlers(dp: Dispatcher):
|
||||
process_maintenance_reason,
|
||||
MaintenanceStates.waiting_for_reason
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_notification_message,
|
||||
MaintenanceStates.waiting_for_notification_message
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ async def show_remnawave_menu(
|
||||
status_emoji = "✅" if connection_test["status"] == "connected" else "❌"
|
||||
|
||||
text = f"""
|
||||
🖥️ <b>Управление RemnaWave</b>
|
||||
🖥️ <b>Управление Remnawave</b>
|
||||
|
||||
📡 <b>Соединение:</b> {status_emoji} {connection_test["message"]}
|
||||
🌐 <b>URL:</b> <code>{settings.REMNAWAVE_API_URL}</code>
|
||||
@@ -116,7 +116,7 @@ async def show_system_stats(
|
||||
return f" (🔺 {difference_str})"
|
||||
|
||||
text = f"""
|
||||
📊 <b>Детальная статистика RemnaWave</b>
|
||||
📊 <b>Детальная статистика Remnawave</b>
|
||||
|
||||
🖥️ <b>Сервер:</b>
|
||||
- CPU: {server_info.get('cpu_cores', 0)} ядер ({server_info.get('cpu_physical_cores', 0)} физ.)
|
||||
@@ -238,7 +238,7 @@ async def show_traffic_stats(
|
||||
return f" 🔺 {diff_str}"
|
||||
|
||||
text = f"""
|
||||
📊 <b>Статистика трафика RemnaWave</b>
|
||||
📊 <b>Статистика трафика Remnawave</b>
|
||||
|
||||
⚡ <b>Реалтайм данные:</b>
|
||||
- Скачивание: {format_bytes(total_realtime_download)}
|
||||
@@ -1247,7 +1247,7 @@ async def finish_squad_creation(
|
||||
f"Сквад готов к использованию!",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="📋 Список сквадов", callback_data="admin_rw_squads")],
|
||||
[types.InlineKeyboardButton(text="⬅️ К панели RemnaWave", callback_data="admin_remnawave")]
|
||||
[types.InlineKeyboardButton(text="⬅️ К панели Remnawave", callback_data="admin_remnawave")]
|
||||
])
|
||||
)
|
||||
await callback.answer("✅ Сквад создан!")
|
||||
@@ -1320,7 +1320,7 @@ async def show_sync_options(
|
||||
db: AsyncSession
|
||||
):
|
||||
text = """
|
||||
🔄 <b>Синхронизация с RemnaWave</b>
|
||||
🔄 <b>Синхронизация с Remnawave</b>
|
||||
|
||||
🔄 <b>Полная синхронизация выполняет:</b>
|
||||
• Создание новых пользователей из панели в боте
|
||||
@@ -1360,7 +1360,7 @@ async def sync_all_users(
|
||||
🔄 <b>Выполняется полная синхронизация...</b>
|
||||
|
||||
📋 Этапы:
|
||||
• Загрузка ВСЕХ пользователей из панели RemnaWave
|
||||
• Загрузка ВСЕХ пользователей из панели Remnawave
|
||||
• Создание новых пользователей в боте
|
||||
• Обновление существующих пользователей
|
||||
• Деактивация подписок отсутствующих пользователей
|
||||
@@ -1401,7 +1401,7 @@ async def sync_all_users(
|
||||
|
||||
🗑️ <b>Деактивированные подписки:</b>
|
||||
Деактивированы подписки пользователей, которые
|
||||
отсутствуют в панели RemnaWave.
|
||||
отсутствуют в панели Remnawave.
|
||||
💰 Балансы пользователей сохранены.
|
||||
"""
|
||||
|
||||
@@ -1546,7 +1546,7 @@ async def validate_subscriptions(
|
||||
if stats['fixed'] > 0:
|
||||
text += "\n✅ <b>Исправленные проблемы:</b>\n"
|
||||
text += "• Статусы просроченных подписок\n"
|
||||
text += "• Отсутствующие данные RemnaWave\n"
|
||||
text += "• Отсутствующие данные Remnawave\n"
|
||||
text += "• Некорректные лимиты трафика\n"
|
||||
text += "• Настройки устройств\n"
|
||||
|
||||
@@ -1600,7 +1600,7 @@ async def cleanup_subscriptions(
|
||||
if stats['deactivated'] > 0:
|
||||
text += f"\n🗑️ <b>Деактивированные подписки:</b>\n"
|
||||
text += f"Отключены подписки пользователей, которые\n"
|
||||
text += f"отсутствуют в панели RemnaWave.\n"
|
||||
text += f"отсутствуют в панели Remnawave.\n"
|
||||
else:
|
||||
text += f"\n✅ Все подписки актуальны!\nНеактуальных подписок не найдено."
|
||||
|
||||
@@ -1664,8 +1664,8 @@ async def force_cleanup_all_orphaned_users(
|
||||
• Удалены использования промокодов
|
||||
• Сброшены балансы к нулю
|
||||
• Удалены подключенные серверы
|
||||
• Сброшены HWID устройства в RemnaWave
|
||||
• Очищены RemnaWave UUID
|
||||
• Сброшены HWID устройства в Remnawave
|
||||
• Очищены Remnawave UUID
|
||||
"""
|
||||
else:
|
||||
text += f"\n✅ Неактуальных подписок не найдено!\nВсе пользователи синхронизированы с панелью."
|
||||
@@ -1703,8 +1703,8 @@ async def confirm_force_cleanup(
|
||||
• ВСЕ использования промокодов
|
||||
• ВСЕ подключенные серверы подписок
|
||||
• ВСЕ балансы (сброс к нулю)
|
||||
• ВСЕ HWID устройства в RemnaWave
|
||||
• ВСЕ RemnaWave UUID и ссылки
|
||||
• ВСЕ HWID устройства в Remnawave
|
||||
• ВСЕ Remnawave UUID и ссылки
|
||||
|
||||
⚡ <b>Это действие НЕОБРАТИМО!</b>
|
||||
|
||||
@@ -1813,7 +1813,7 @@ async def sync_users(
|
||||
if sync_type == "all_users" and 'deleted' in stats and stats['deleted'] > 0:
|
||||
text += f"\n🗑️ <b>Удаленные подписки:</b>\n"
|
||||
text += f"Деактивированы подписки пользователей,\n"
|
||||
text += f"которые отсутствуют в панели RemnaWave."
|
||||
text += f"которые отсутствуют в панели Remnawave."
|
||||
|
||||
text += f"\n\n💡 <b>Рекомендации:</b>\n"
|
||||
if sync_type == "all_users":
|
||||
|
||||
@@ -147,7 +147,7 @@ async def sync_servers_with_remnawave(
|
||||
):
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🔄 Синхронизация с RemnaWave...\n\nПодождите, это может занять время.",
|
||||
"🔄 Синхронизация с Remnawave...\n\nПодождите, это может занять время.",
|
||||
reply_markup=None
|
||||
)
|
||||
|
||||
@@ -157,7 +157,7 @@ async def sync_servers_with_remnawave(
|
||||
|
||||
if not squads:
|
||||
await callback.message.edit_text(
|
||||
"❌ Не удалось получить данные о сквадах из RemnaWave.\n\nПроверьте настройки API.",
|
||||
"❌ Не удалось получить данные о сквадах из Remnawave.\n\nПроверьте настройки API.",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_servers")]
|
||||
])
|
||||
|
||||
@@ -0,0 +1,584 @@
|
||||
import logging
|
||||
from aiogram import Dispatcher, types, F
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.filters import StateFilter
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.crud.user_message import (
|
||||
create_user_message, get_all_user_messages, get_user_message_by_id,
|
||||
update_user_message, delete_user_message, toggle_user_message_status,
|
||||
get_user_messages_stats
|
||||
)
|
||||
from app.database.models import User
|
||||
from app.keyboards.admin import get_admin_main_keyboard
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
from app.localization.texts import get_texts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserMessageStates(StatesGroup):
|
||||
waiting_for_message_text = State()
|
||||
waiting_for_edit_text = State()
|
||||
|
||||
|
||||
def get_user_messages_keyboard(language: str = "ru"):
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="📝 Добавить сообщение",
|
||||
callback_data="add_user_message"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="📋 Список сообщений",
|
||||
callback_data="list_user_messages:0"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="📊 Статистика",
|
||||
callback_data="user_messages_stats"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🔙 Назад в админку",
|
||||
callback_data="admin_panel"
|
||||
)
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
def get_message_actions_keyboard(message_id: int, is_active: bool, language: str = "ru"):
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
status_text = "🔴 Деактивировать" if is_active else "🟢 Активировать"
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="✏️ Редактировать",
|
||||
callback_data=f"edit_user_message:{message_id}"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=status_text,
|
||||
callback_data=f"toggle_user_message:{message_id}"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🗑️ Удалить",
|
||||
callback_data=f"delete_user_message:{message_id}"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🔙 К списку",
|
||||
callback_data="list_user_messages:0"
|
||||
)
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_user_messages_panel(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
text = (
|
||||
"📢 <b>Управление сообщениями в главном меню</b>\n\n"
|
||||
"Здесь вы можете добавлять сообщения, которые будут показываться пользователям "
|
||||
"в главном меню между информацией о подписке и кнопками действий.\n\n"
|
||||
"• Сообщения поддерживают HTML теги\n"
|
||||
"• Можно создать несколько сообщений\n"
|
||||
"• Активные сообщения показываются случайно\n"
|
||||
"• Неактивные сообщения не показываются"
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=get_user_messages_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def add_user_message_start(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
from app.utils.validators import get_html_help_text
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"📝 <b>Добавление нового сообщения</b>\n\n"
|
||||
f"Введите текст сообщения, которое будет показываться в главном меню.\n\n"
|
||||
f"{get_html_help_text()}\n\n"
|
||||
f"Отправьте /cancel для отмены.",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await state.set_state(UserMessageStates.waiting_for_message_text)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_new_message_text(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
if message.text == "/cancel":
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
"❌ Добавление сообщения отменено.",
|
||||
reply_markup=get_user_messages_keyboard(db_user.language)
|
||||
)
|
||||
return
|
||||
|
||||
message_text = message.text.strip()
|
||||
|
||||
if len(message_text) > 4000:
|
||||
await message.answer(
|
||||
"❌ Сообщение слишком длинное. Максимум 4000 символов.\n"
|
||||
"Попробуйте еще раз или отправьте /cancel для отмены."
|
||||
)
|
||||
return
|
||||
|
||||
from app.utils.validators import validate_html_tags, get_html_help_text
|
||||
|
||||
is_valid, error_msg = validate_html_tags(message_text)
|
||||
if not is_valid:
|
||||
await message.answer(
|
||||
f"❌ Ошибка в HTML разметке: {error_msg}\n\n"
|
||||
f"Исправьте ошибку и попробуйте еще раз, или отправьте /cancel для отмены.",
|
||||
parse_mode=None
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
new_message = await create_user_message(
|
||||
db=db,
|
||||
message_text=message_text,
|
||||
created_by=db_user.id,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
f"✅ <b>Сообщение добавлено!</b>\n\n"
|
||||
f"<b>ID:</b> {new_message.id}\n"
|
||||
f"<b>Статус:</b> {'🟢 Активно' if new_message.is_active else '🔴 Неактивно'}\n"
|
||||
f"<b>Создано:</b> {new_message.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
|
||||
f"<b>Предварительный просмотр:</b>\n"
|
||||
f"<blockquote>{message_text}</blockquote>",
|
||||
reply_markup=get_user_messages_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания сообщения: {e}")
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
"❌ Произошла ошибка при создании сообщения. Попробуйте еще раз.",
|
||||
reply_markup=get_user_messages_keyboard(db_user.language)
|
||||
)
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def list_user_messages(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
page = 0
|
||||
if ":" in callback.data:
|
||||
try:
|
||||
page = int(callback.data.split(":")[1])
|
||||
except (ValueError, IndexError):
|
||||
page = 0
|
||||
|
||||
limit = 5
|
||||
offset = page * limit
|
||||
|
||||
messages = await get_all_user_messages(db, offset=offset, limit=limit)
|
||||
|
||||
if not messages:
|
||||
await callback.message.edit_text(
|
||||
"📋 <b>Список сообщений</b>\n\n"
|
||||
"Сообщений пока нет. Добавьте первое сообщение!",
|
||||
reply_markup=get_user_messages_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
text = "📋 <b>Список сообщений</b>\n\n"
|
||||
|
||||
for msg in messages:
|
||||
status_emoji = "🟢" if msg.is_active else "🔴"
|
||||
preview = msg.message_text[:100] + "..." if len(msg.message_text) > 100 else msg.message_text
|
||||
preview = preview.replace('<', '<').replace('>', '>')
|
||||
|
||||
text += (
|
||||
f"{status_emoji} <b>ID {msg.id}</b>\n"
|
||||
f"{preview}\n"
|
||||
f"📅 {msg.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
|
||||
)
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
keyboard = []
|
||||
|
||||
for msg in messages:
|
||||
status_emoji = "🟢" if msg.is_active else "🔴"
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"{status_emoji} ID {msg.id}",
|
||||
callback_data=f"view_user_message:{msg.id}"
|
||||
)
|
||||
])
|
||||
|
||||
nav_buttons = []
|
||||
if page > 0:
|
||||
nav_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️ Назад",
|
||||
callback_data=f"list_user_messages:{page-1}"
|
||||
)
|
||||
)
|
||||
|
||||
nav_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text="➕ Добавить",
|
||||
callback_data="add_user_message"
|
||||
)
|
||||
)
|
||||
|
||||
if len(messages) == limit:
|
||||
nav_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text="Вперед ➡️",
|
||||
callback_data=f"list_user_messages:{page+1}"
|
||||
)
|
||||
)
|
||||
|
||||
if nav_buttons:
|
||||
keyboard.append(nav_buttons)
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="🔙 Назад",
|
||||
callback_data="user_messages_panel"
|
||||
)
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def view_user_message(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
try:
|
||||
message_id = int(callback.data.split(":")[1])
|
||||
except (ValueError, IndexError):
|
||||
await callback.answer("❌ Неверный ID сообщения", show_alert=True)
|
||||
return
|
||||
|
||||
message = await get_user_message_by_id(db, message_id)
|
||||
|
||||
if not message:
|
||||
await callback.answer("❌ Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
status_text = "🟢 Активно" if message.is_active else "🔴 Неактивно"
|
||||
|
||||
text = (
|
||||
f"📋 <b>Сообщение ID {message.id}</b>\n\n"
|
||||
f"<b>Статус:</b> {status_text}\n"
|
||||
f"<b>Создано:</b> {message.created_at.strftime('%d.%m.%Y %H:%M')}\n"
|
||||
f"<b>Обновлено:</b> {message.updated_at.strftime('%d.%m.%Y %H:%M')}\n\n"
|
||||
f"<b>Содержимое:</b>\n"
|
||||
f"<blockquote>{message.message_text}</blockquote>"
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=get_message_actions_keyboard(
|
||||
message_id, message.is_active, db_user.language
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def toggle_message_status(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
try:
|
||||
message_id = int(callback.data.split(":")[1])
|
||||
except (ValueError, IndexError):
|
||||
await callback.answer("❌ Неверный ID сообщения", show_alert=True)
|
||||
return
|
||||
|
||||
message = await toggle_user_message_status(db, message_id)
|
||||
|
||||
if not message:
|
||||
await callback.answer("❌ Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
status_text = "активировано" if message.is_active else "деактивировано"
|
||||
await callback.answer(f"✅ Сообщение {status_text}")
|
||||
|
||||
await view_user_message(callback, db_user, db)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def delete_message_confirm(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
"""Подтвердить удаление сообщения"""
|
||||
try:
|
||||
message_id = int(callback.data.split(":")[1])
|
||||
except (ValueError, IndexError):
|
||||
await callback.answer("❌ Неверный ID сообщения", show_alert=True)
|
||||
return
|
||||
|
||||
success = await delete_user_message(db, message_id)
|
||||
|
||||
if success:
|
||||
await callback.answer("✅ Сообщение удалено")
|
||||
await list_user_messages(
|
||||
types.CallbackQuery(
|
||||
id=callback.id,
|
||||
from_user=callback.from_user,
|
||||
chat_instance=callback.chat_instance,
|
||||
data="list_user_messages:0",
|
||||
message=callback.message
|
||||
),
|
||||
db_user,
|
||||
db
|
||||
)
|
||||
else:
|
||||
await callback.answer("❌ Ошибка удаления сообщения", show_alert=True)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_messages_stats(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
stats = await get_user_messages_stats(db)
|
||||
|
||||
text = (
|
||||
"📊 <b>Статистика сообщений</b>\n\n"
|
||||
f"📝 Всего сообщений: <b>{stats['total_messages']}</b>\n"
|
||||
f"🟢 Активных: <b>{stats['active_messages']}</b>\n"
|
||||
f"🔴 Неактивных: <b>{stats['inactive_messages']}</b>\n\n"
|
||||
"Активные сообщения показываются пользователям случайным образом "
|
||||
"в главном меню между информацией о подписке и кнопками действий."
|
||||
)
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🔙 Назад",
|
||||
callback_data="user_messages_panel"
|
||||
)
|
||||
]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def edit_user_message_start(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
try:
|
||||
message_id = int(callback.data.split(":")[1])
|
||||
except (ValueError, IndexError):
|
||||
await callback.answer("❌ Неверный ID сообщения", show_alert=True)
|
||||
return
|
||||
|
||||
message = await get_user_message_by_id(db, message_id)
|
||||
|
||||
if not message:
|
||||
await callback.answer("❌ Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"✏️ <b>Редактирование сообщения ID {message.id}</b>\n\n"
|
||||
f"<b>Текущий текст:</b>\n"
|
||||
f"<blockquote>{message.message_text}</blockquote>\n\n"
|
||||
f"Введите новый текст сообщения или отправьте /cancel для отмены:",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await state.set_data({"editing_message_id": message_id})
|
||||
await state.set_state(UserMessageStates.waiting_for_edit_text)
|
||||
await callback.answer()
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_edit_message_text(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
if message.text == "/cancel":
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
"❌ Редактирование отменено.",
|
||||
reply_markup=get_user_messages_keyboard(db_user.language)
|
||||
)
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
message_id = data.get("editing_message_id")
|
||||
|
||||
if not message_id:
|
||||
await state.clear()
|
||||
await message.answer("❌ Ошибка: ID сообщения не найден")
|
||||
return
|
||||
|
||||
new_text = message.text.strip()
|
||||
|
||||
if len(new_text) > 4000:
|
||||
await message.answer(
|
||||
"❌ Сообщение слишком длинное. Максимум 4000 символов.\n"
|
||||
"Попробуйте еще раз или отправьте /cancel для отмены."
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
updated_message = await update_user_message(
|
||||
db=db,
|
||||
message_id=message_id,
|
||||
message_text=new_text
|
||||
)
|
||||
|
||||
if updated_message:
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
f"✅ <b>Сообщение обновлено!</b>\n\n"
|
||||
f"<b>ID:</b> {updated_message.id}\n"
|
||||
f"<b>Обновлено:</b> {updated_message.updated_at.strftime('%d.%m.%Y %H:%M')}\n\n"
|
||||
f"<b>Новый текст:</b>\n"
|
||||
f"<blockquote>{new_text}</blockquote>",
|
||||
reply_markup=get_user_messages_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
"❌ Сообщение не найдено или ошибка обновления.",
|
||||
reply_markup=get_user_messages_keyboard(db_user.language)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления сообщения: {e}")
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
"❌ Произошла ошибка при обновлении сообщения.",
|
||||
reply_markup=get_user_messages_keyboard(db_user.language)
|
||||
)
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
|
||||
dp.callback_query.register(
|
||||
show_user_messages_panel,
|
||||
F.data == "user_messages_panel"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
add_user_message_start,
|
||||
F.data == "add_user_message"
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_new_message_text,
|
||||
StateFilter(UserMessageStates.waiting_for_message_text)
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
edit_user_message_start,
|
||||
F.data.startswith("edit_user_message:")
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_edit_message_text,
|
||||
StateFilter(UserMessageStates.waiting_for_edit_text)
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
list_user_messages,
|
||||
F.data.startswith("list_user_messages")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
view_user_message,
|
||||
F.data.startswith("view_user_message:")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
toggle_message_status,
|
||||
F.data.startswith("toggle_user_message:")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
delete_message_confirm,
|
||||
F.data.startswith("delete_user_message:")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_messages_stats,
|
||||
F.data == "user_messages_stats"
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from aiogram import Dispatcher, types, F
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -292,6 +293,12 @@ async def show_user_subscription(
|
||||
text="📊 Трафик",
|
||||
callback_data=f"admin_sub_traffic_{user_id}"
|
||||
)
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="🔄 Тип подписки",
|
||||
callback_data=f"admin_sub_change_type_{user_id}"
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
@@ -2206,6 +2213,133 @@ async def cleanup_inactive_users(
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def change_subscription_type(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
user_id = int(callback.data.split('_')[-1])
|
||||
|
||||
user_service = UserService()
|
||||
profile = await user_service.get_user_profile(db, user_id)
|
||||
|
||||
if not profile or not profile["subscription"]:
|
||||
await callback.answer("❌ Пользователь или подписка не найдены", show_alert=True)
|
||||
return
|
||||
|
||||
subscription = profile["subscription"]
|
||||
current_type = "🎁 Триал" if subscription.is_trial else "💎 Платная"
|
||||
|
||||
text = f"🔄 <b>Смена типа подписки</b>\n\n"
|
||||
text += f"👤 {profile['user'].full_name}\n"
|
||||
text += f"📱 Текущий тип: {current_type}\n\n"
|
||||
text += f"Выберите новый тип подписки:"
|
||||
|
||||
keyboard = []
|
||||
|
||||
if subscription.is_trial:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="💎 Сделать платной",
|
||||
callback_data=f"admin_sub_type_paid_{user_id}"
|
||||
)
|
||||
])
|
||||
else:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="🎁 Сделать триальной",
|
||||
callback_data=f"admin_sub_type_trial_{user_id}"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⬅️ Назад",
|
||||
callback_data=f"admin_user_subscription_{user_id}"
|
||||
)
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def change_subscription_type_confirm(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
parts = callback.data.split('_')
|
||||
new_type = parts[-2] # 'paid' или 'trial'
|
||||
user_id = int(parts[-1])
|
||||
|
||||
success = await _change_subscription_type(db, user_id, new_type, db_user.id)
|
||||
|
||||
if success:
|
||||
type_text = "платной" if new_type == "paid" else "триальной"
|
||||
await callback.message.edit_text(
|
||||
f"✅ Тип подписки успешно изменен на {type_text}",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка изменения типа подписки",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📱 К подписке", callback_data=f"admin_user_subscription_{user_id}")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
async def _change_subscription_type(db: AsyncSession, user_id: int, new_type: str, admin_id: int) -> bool:
|
||||
try:
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user_id)
|
||||
if not subscription:
|
||||
logger.error(f"Подписка не найдена для пользователя {user_id}")
|
||||
return False
|
||||
|
||||
new_is_trial = (new_type == "trial")
|
||||
|
||||
if subscription.is_trial == new_is_trial:
|
||||
logger.info(f"Тип подписки уже установлен корректно для пользователя {user_id}")
|
||||
return True
|
||||
|
||||
old_type = "триальной" if subscription.is_trial else "платной"
|
||||
new_type_text = "триальной" if new_is_trial else "платной"
|
||||
|
||||
subscription.is_trial = new_is_trial
|
||||
subscription.updated_at = datetime.utcnow()
|
||||
|
||||
if not new_is_trial and subscription.is_trial:
|
||||
user = await get_user_by_id(db, user_id)
|
||||
if user:
|
||||
user.has_had_paid_subscription = True
|
||||
|
||||
await db.commit()
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.update_remnawave_user(db, subscription)
|
||||
|
||||
logger.info(f"Админ {admin_id} изменил тип подписки пользователя {user_id}: {old_type} -> {new_type_text}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка изменения типа подписки: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
|
||||
@@ -2429,3 +2563,13 @@ def register_handlers(dp: Dispatcher):
|
||||
reset_user_devices,
|
||||
F.data.startswith("admin_user_reset_devices_confirm_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
change_subscription_type,
|
||||
F.data.startswith("admin_sub_change_type_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
change_subscription_type_confirm,
|
||||
F.data.startswith("admin_sub_type_")
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ from app.keyboards.inline import get_main_menu_keyboard
|
||||
from app.localization.texts import get_texts
|
||||
from app.database.models import User
|
||||
from app.utils.user_utils import mark_user_as_had_paid_subscription
|
||||
from app.database.crud.user_message import get_random_active_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,10 +32,7 @@ async def show_main_menu(
|
||||
if db_user.subscription:
|
||||
subscription_is_active = db_user.subscription.is_active
|
||||
|
||||
menu_text = texts.MAIN_MENU.format(
|
||||
user_name=db_user.full_name,
|
||||
subscription_status=_get_subscription_status(db_user, texts)
|
||||
)
|
||||
menu_text = await get_main_menu_text(db_user, texts, db)
|
||||
|
||||
await callback.message.edit_text(
|
||||
menu_text,
|
||||
@@ -45,7 +43,8 @@ async def show_main_menu(
|
||||
has_active_subscription=has_active_subscription,
|
||||
subscription_is_active=subscription_is_active,
|
||||
balance_kopeks=db_user.balance_kopeks
|
||||
)
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@@ -100,10 +99,31 @@ async def handle_back_to_menu(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
|
||||
await state.clear()
|
||||
|
||||
await show_main_menu(callback, db_user, db)
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
has_active_subscription = db_user.subscription is not None
|
||||
subscription_is_active = False
|
||||
|
||||
if db_user.subscription:
|
||||
subscription_is_active = db_user.subscription.is_active
|
||||
|
||||
menu_text = await get_main_menu_text(db_user, texts, db)
|
||||
|
||||
await callback.message.edit_text(
|
||||
menu_text,
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=db_user.language,
|
||||
is_admin=settings.is_admin(db_user.telegram_id),
|
||||
has_had_paid_subscription=db_user.has_had_paid_subscription,
|
||||
has_active_subscription=has_active_subscription,
|
||||
subscription_is_active=subscription_is_active,
|
||||
balance_kopeks=db_user.balance_kopeks
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
def _get_subscription_status(user: User, texts) -> str:
|
||||
@@ -136,6 +156,31 @@ def _get_subscription_status(user: User, texts) -> str:
|
||||
else:
|
||||
return f"💎 Активна\n⚠️ истекает сегодня!"
|
||||
|
||||
async def get_main_menu_text(user, texts, db: AsyncSession):
|
||||
|
||||
base_text = texts.MAIN_MENU.format(
|
||||
user_name=user.full_name,
|
||||
subscription_status=_get_subscription_status(user, texts)
|
||||
)
|
||||
|
||||
try:
|
||||
random_message = await get_random_active_message(db)
|
||||
if random_message:
|
||||
if "Выберите действие:" in base_text:
|
||||
parts = base_text.split("Выберите действие:")
|
||||
if len(parts) == 2:
|
||||
return f"{parts[0]}\n{random_message}\n\nВыберите действие:{parts[1]}"
|
||||
|
||||
if "Выберите действие:" in base_text:
|
||||
return base_text.replace("Выберите действие:", f"\n{random_message}\n\nВыберите действие:")
|
||||
else:
|
||||
return f"{base_text}\n\n{random_message}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения случайного сообщения: {e}")
|
||||
|
||||
return base_text
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.keyboards.inline import (
|
||||
from app.localization.texts import get_texts
|
||||
from app.services.referral_service import process_referral_registration
|
||||
from app.utils.user_utils import generate_unique_referral_code
|
||||
from app.database.crud.user_message import get_random_active_message
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -136,11 +137,10 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
|
||||
if user.subscription:
|
||||
subscription_is_active = user.subscription.is_active
|
||||
|
||||
menu_text = await get_main_menu_text(user, texts, db)
|
||||
|
||||
await message.answer(
|
||||
texts.MAIN_MENU.format(
|
||||
user_name=user.full_name,
|
||||
subscription_status=_get_subscription_status(user, texts)
|
||||
),
|
||||
menu_text,
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=user.language,
|
||||
is_admin=settings.is_admin(user.telegram_id),
|
||||
@@ -148,7 +148,8 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
|
||||
has_active_subscription=has_active_subscription,
|
||||
subscription_is_active=subscription_is_active,
|
||||
balance_kopeks=user.balance_kopeks
|
||||
)
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await state.clear()
|
||||
return
|
||||
@@ -405,30 +406,24 @@ async def complete_registration_from_callback(
|
||||
if existing_user.subscription:
|
||||
subscription_is_active = existing_user.subscription.is_active
|
||||
|
||||
user_name = existing_user.full_name
|
||||
user_telegram_id = existing_user.telegram_id
|
||||
user_language = existing_user.language
|
||||
has_had_paid_subscription = existing_user.has_had_paid_subscription
|
||||
balance_kopeks = existing_user.balance_kopeks
|
||||
menu_text = await get_main_menu_text(existing_user, texts, db)
|
||||
|
||||
try:
|
||||
await callback.message.answer(
|
||||
texts.MAIN_MENU.format(
|
||||
user_name=user_name,
|
||||
subscription_status=_get_subscription_status(existing_user, texts)
|
||||
),
|
||||
menu_text,
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=user_language,
|
||||
is_admin=settings.is_admin(user_telegram_id),
|
||||
has_had_paid_subscription=has_had_paid_subscription,
|
||||
language=existing_user.language,
|
||||
is_admin=settings.is_admin(existing_user.telegram_id),
|
||||
has_had_paid_subscription=existing_user.has_had_paid_subscription,
|
||||
has_active_subscription=has_active_subscription,
|
||||
subscription_is_active=subscription_is_active,
|
||||
balance_kopeks=balance_kopeks
|
||||
)
|
||||
balance_kopeks=existing_user.balance_kopeks
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при показе главного меню существующему пользователю: {e}")
|
||||
await callback.message.answer(f"Добро пожаловать, {user_name}!")
|
||||
await callback.message.answer(f"Добро пожаловать, {existing_user.full_name}!")
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
@@ -506,55 +501,50 @@ async def complete_registration_from_callback(
|
||||
|
||||
has_active_subscription = False
|
||||
subscription_is_active = False
|
||||
user_name = user.full_name
|
||||
balance_kopeks = user.balance_kopeks
|
||||
user_telegram_id = user.telegram_id
|
||||
user_language = user.language
|
||||
has_had_paid_subscription = user.has_had_paid_subscription
|
||||
|
||||
menu_text = await get_main_menu_text_simple(user.full_name, texts, db)
|
||||
|
||||
try:
|
||||
await callback.message.answer(
|
||||
texts.MAIN_MENU.format(
|
||||
user_name=user_name,
|
||||
subscription_status=_get_subscription_status_simple(texts)
|
||||
),
|
||||
menu_text,
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=user_language,
|
||||
is_admin=settings.is_admin(user_telegram_id),
|
||||
has_had_paid_subscription=has_had_paid_subscription,
|
||||
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=balance_kopeks
|
||||
)
|
||||
balance_kopeks=user.balance_kopeks
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
logger.info(f"✅ Главное меню отправлено для пользователя {user_telegram_id}")
|
||||
logger.info(f"✅ Главное меню отправлено для пользователя {user.telegram_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке главного меню: {e}")
|
||||
try:
|
||||
balance_rubles = balance_kopeks / 100
|
||||
balance_rubles = user.balance_kopeks / 100
|
||||
await callback.message.answer(
|
||||
f"Добро пожаловать, {user_name}!\n"
|
||||
f"Добро пожаловать, {user.full_name}!\n"
|
||||
f"Баланс: {balance_rubles:.2f} ₽\n"
|
||||
f"Подписка: Нет активной подписки",
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=user_language,
|
||||
is_admin=settings.is_admin(user_telegram_id),
|
||||
has_had_paid_subscription=has_had_paid_subscription,
|
||||
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=balance_kopeks
|
||||
balance_kopeks=user.balance_kopeks
|
||||
)
|
||||
)
|
||||
logger.info(f"✅ Fallback главное меню отправлено для пользователя {user_telegram_id}")
|
||||
logger.info(f"✅ Fallback главное меню отправлено для пользователя {user.telegram_id}")
|
||||
except Exception as fallback_error:
|
||||
logger.error(f"⛔ Критическая ошибка при отправке fallback меню: {fallback_error}")
|
||||
try:
|
||||
await callback.message.answer(f"Добро пожаловать, {user_name}! Регистрация завершена.")
|
||||
logger.info(f"✅ Простое приветствие отправлено для пользователя {user_telegram_id}")
|
||||
await callback.message.answer(f"Добро пожаловать, {user.full_name}! Регистрация завершена.")
|
||||
logger.info(f"✅ Простое приветствие отправлено для пользователя {user.telegram_id}")
|
||||
except Exception as final_error:
|
||||
logger.error(f"⛔ Критическая ошибка при отправке простого сообщения: {final_error}")
|
||||
|
||||
logger.info(f"✅ Регистрация завершена для пользователя: {user_telegram_id}")
|
||||
logger.info(f"✅ Регистрация завершена для пользователя: {user.telegram_id}")
|
||||
|
||||
async def complete_registration(
|
||||
message: types.Message,
|
||||
@@ -579,30 +569,24 @@ async def complete_registration(
|
||||
if existing_user.subscription:
|
||||
subscription_is_active = existing_user.subscription.is_active
|
||||
|
||||
user_name = existing_user.full_name
|
||||
user_telegram_id = existing_user.telegram_id
|
||||
user_language = existing_user.language
|
||||
has_had_paid_subscription = existing_user.has_had_paid_subscription
|
||||
balance_kopeks = existing_user.balance_kopeks
|
||||
menu_text = await get_main_menu_text(existing_user, texts, db)
|
||||
|
||||
try:
|
||||
await message.answer(
|
||||
texts.MAIN_MENU.format(
|
||||
user_name=user_name,
|
||||
subscription_status=_get_subscription_status(existing_user, texts)
|
||||
),
|
||||
menu_text,
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=user_language,
|
||||
is_admin=settings.is_admin(user_telegram_id),
|
||||
has_had_paid_subscription=has_had_paid_subscription,
|
||||
language=existing_user.language,
|
||||
is_admin=settings.is_admin(existing_user.telegram_id),
|
||||
has_had_paid_subscription=existing_user.has_had_paid_subscription,
|
||||
has_active_subscription=has_active_subscription,
|
||||
subscription_is_active=subscription_is_active,
|
||||
balance_kopeks=balance_kopeks
|
||||
)
|
||||
balance_kopeks=existing_user.balance_kopeks
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при показе главного меню существующему пользователю: {e}")
|
||||
await message.answer(f"Добро пожаловать, {user_name}!")
|
||||
await message.answer(f"Добро пожаловать, {existing_user.full_name}!")
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
@@ -680,55 +664,51 @@ async def complete_registration(
|
||||
|
||||
has_active_subscription = False
|
||||
subscription_is_active = False
|
||||
user_name = user.full_name
|
||||
balance_kopeks = user.balance_kopeks
|
||||
user_telegram_id = user.telegram_id
|
||||
user_language = user.language
|
||||
has_had_paid_subscription = user.has_had_paid_subscription
|
||||
|
||||
menu_text = await get_main_menu_text_simple(user.full_name, texts, db)
|
||||
|
||||
try:
|
||||
await message.answer(
|
||||
texts.MAIN_MENU.format(
|
||||
user_name=user_name,
|
||||
subscription_status=_get_subscription_status_simple(texts)
|
||||
),
|
||||
menu_text,
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=user_language,
|
||||
is_admin=settings.is_admin(user_telegram_id),
|
||||
has_had_paid_subscription=has_had_paid_subscription,
|
||||
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=balance_kopeks
|
||||
)
|
||||
balance_kopeks=user.balance_kopeks
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
logger.info(f"✅ Главное меню отправлено для пользователя {user_telegram_id}")
|
||||
logger.info(f"✅ Главное меню отправлено для пользователя {user.telegram_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке главного меню: {e}")
|
||||
try:
|
||||
balance_rubles = balance_kopeks / 100
|
||||
balance_rubles = user.balance_kopeks / 100
|
||||
await message.answer(
|
||||
f"Добро пожаловать, {user_name}!\n"
|
||||
f"Добро пожаловать, {user.full_name}!\n"
|
||||
f"Баланс: {balance_rubles:.2f} ₽\n"
|
||||
f"Подписка: Нет активной подписки",
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=user_language,
|
||||
is_admin=settings.is_admin(user_telegram_id),
|
||||
has_had_paid_subscription=has_had_paid_subscription,
|
||||
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=balance_kopeks
|
||||
)
|
||||
balance_kopeks=user.balance_kopeks
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
logger.info(f"✅ Fallback главное меню отправлено для пользователя {user_telegram_id}")
|
||||
logger.info(f"✅ Fallback главное меню отправлено для пользователя {user.telegram_id}")
|
||||
except Exception as fallback_error:
|
||||
logger.error(f"⛔ Критическая ошибка при отправке fallback меню: {fallback_error}")
|
||||
try:
|
||||
await message.answer(f"Добро пожаловать, {user_name}! Регистрация завершена.")
|
||||
logger.info(f"✅ Простое приветствие отправлено для пользователя {user_telegram_id}")
|
||||
await message.answer(f"Добро пожаловать, {user.full_name}! Регистрация завершена.")
|
||||
logger.info(f"✅ Простое приветствие отправлено для пользователя {user.telegram_id}")
|
||||
except:
|
||||
pass
|
||||
|
||||
logger.info(f"✅ Регистрация завершена для пользователя: {user_telegram_id}")
|
||||
logger.info(f"✅ Регистрация завершена для пользователя: {user.telegram_id}")
|
||||
|
||||
|
||||
def _get_subscription_status(user, texts):
|
||||
@@ -787,6 +767,55 @@ def get_referral_code_keyboard(language: str):
|
||||
)]
|
||||
])
|
||||
|
||||
async def get_main_menu_text(user, texts, db: AsyncSession):
|
||||
|
||||
base_text = texts.MAIN_MENU.format(
|
||||
user_name=user.full_name,
|
||||
subscription_status=_get_subscription_status(user, texts)
|
||||
)
|
||||
|
||||
try:
|
||||
random_message = await get_random_active_message(db)
|
||||
if random_message:
|
||||
if "Выберите действие:" in base_text:
|
||||
parts = base_text.split("Выберите действие:")
|
||||
if len(parts) == 2:
|
||||
return f"{parts[0]}\n{random_message}\n\nВыберите действие:{parts[1]}"
|
||||
|
||||
if "Выберите действие:" in base_text:
|
||||
return base_text.replace("Выберите действие:", f"\n{random_message}\n\nВыберите действие:")
|
||||
else:
|
||||
return f"{base_text}\n\n{random_message}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения случайного сообщения: {e}")
|
||||
|
||||
return base_text
|
||||
|
||||
async def get_main_menu_text_simple(user_name, texts, db: AsyncSession):
|
||||
|
||||
base_text = texts.MAIN_MENU.format(
|
||||
user_name=user_name,
|
||||
subscription_status=_get_subscription_status_simple(texts)
|
||||
)
|
||||
|
||||
try:
|
||||
random_message = await get_random_active_message(db)
|
||||
if random_message:
|
||||
if "Выберите действие:" in base_text:
|
||||
parts = base_text.split("Выберите действие:")
|
||||
if len(parts) == 2:
|
||||
return f"{parts[0]}\n{random_message}\n\nВыберите действие:{parts[1]}"
|
||||
|
||||
if "Выберите действие:" in base_text:
|
||||
return base_text.replace("Выберите действие:", f"\n{random_message}\n\nВыберите действие:")
|
||||
else:
|
||||
return f"{base_text}\n\n{random_message}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения случайного сообщения: {e}")
|
||||
|
||||
return base_text
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
|
||||
|
||||
@@ -376,7 +376,7 @@ async def activate_trial(
|
||||
|
||||
try:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_trial_activation_notification(db_user, subscription)
|
||||
await notification_service.send_trial_activation_notification(db, db_user, subscription)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о триале: {e}")
|
||||
|
||||
@@ -1200,7 +1200,7 @@ async def confirm_extend_subscription(
|
||||
try:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_subscription_extension_notification(
|
||||
db_user, subscription, transaction, days, old_end_date
|
||||
db, db_user, subscription, transaction, days, old_end_date
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о продлении: {e}")
|
||||
@@ -1908,7 +1908,7 @@ async def confirm_purchase(
|
||||
try:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_subscription_purchase_notification(
|
||||
db_user, subscription, transaction, data['period_days'], was_trial_conversion
|
||||
db, db_user, subscription, transaction, data['period_days'], was_trial_conversion
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о покупке: {e}")
|
||||
@@ -2994,15 +2994,16 @@ def get_reset_devices_confirm_keyboard(language: str = "ru") -> InlineKeyboardMa
|
||||
]
|
||||
])
|
||||
|
||||
async def send_trial_notification(callback: types.CallbackQuery, db_user: User, subscription: Subscription):
|
||||
async def send_trial_notification(callback: types.CallbackQuery, db: AsyncSession, db_user: User, subscription: Subscription):
|
||||
try:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_trial_activation_notification(db_user, subscription)
|
||||
await notification_service.send_trial_activation_notification(db, db_user, subscription)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о триале: {e}")
|
||||
|
||||
async def send_purchase_notification(
|
||||
callback: types.CallbackQuery,
|
||||
db: AsyncSession,
|
||||
db_user: User,
|
||||
subscription: Subscription,
|
||||
transaction_id: int,
|
||||
@@ -3011,20 +3012,19 @@ async def send_purchase_notification(
|
||||
):
|
||||
try:
|
||||
from app.database.crud.transaction import get_transaction_by_id
|
||||
from app.database.database import AsyncSessionLocal
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
transaction = await get_transaction_by_id(db, transaction_id)
|
||||
if transaction:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_subscription_purchase_notification(
|
||||
db_user, subscription, transaction, period_days, was_trial_conversion
|
||||
)
|
||||
transaction = await get_transaction_by_id(db, transaction_id)
|
||||
if transaction:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_subscription_purchase_notification(
|
||||
db, db_user, subscription, transaction, period_days, was_trial_conversion
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о покупке: {e}")
|
||||
|
||||
async def send_extension_notification(
|
||||
callback: types.CallbackQuery,
|
||||
db: AsyncSession,
|
||||
db_user: User,
|
||||
subscription: Subscription,
|
||||
transaction_id: int,
|
||||
@@ -3033,15 +3033,13 @@ async def send_extension_notification(
|
||||
):
|
||||
try:
|
||||
from app.database.crud.transaction import get_transaction_by_id
|
||||
from app.database.database import AsyncSessionLocal
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
transaction = await get_transaction_by_id(db, transaction_id)
|
||||
if transaction:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_subscription_extension_notification(
|
||||
db_user, subscription, transaction, extended_days, old_end_date
|
||||
)
|
||||
transaction = await get_transaction_by_id(db, transaction_id)
|
||||
if transaction:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_subscription_extension_notification(
|
||||
db, db_user, subscription, transaction, extended_days, old_end_date
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о продлении: {e}")
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
async def tribute_webhook(request):
|
||||
try:
|
||||
signature = request.headers.get('X-Signature', '')
|
||||
signature = request.headers.get('trbt-signature', '')
|
||||
payload = await request.text()
|
||||
|
||||
tribute_service = TributeService()
|
||||
|
||||
@@ -28,6 +28,9 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
InlineKeyboardButton(text=texts.ADMIN_STATISTICS, callback_data="admin_statistics"),
|
||||
InlineKeyboardButton(text="🔧 Техработы", callback_data="maintenance_panel")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📢 Сообщения в меню", callback_data="user_messages_panel")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
|
||||
]
|
||||
@@ -580,25 +583,72 @@ def get_admin_pagination_keyboard(
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
def get_maintenance_keyboard(language: str = "ru", is_active: bool = False, monitoring_active: bool = False) -> InlineKeyboardMarkup:
|
||||
def get_maintenance_keyboard(
|
||||
language: str,
|
||||
is_maintenance_active: bool,
|
||||
is_monitoring_active: bool,
|
||||
panel_has_issues: bool = False
|
||||
) -> InlineKeyboardMarkup:
|
||||
keyboard = []
|
||||
|
||||
if language == "en":
|
||||
toggle_text = "🔴 Disable maintenance" if is_active else "🔧 Enable maintenance"
|
||||
monitoring_text = "⏹️ Stop monitoring" if monitoring_active else "🔄 Start monitoring"
|
||||
check_api_text = "🔍 Check API"
|
||||
back_text = "⬅️ Back to admin"
|
||||
if is_maintenance_active:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="🟢 Выключить техработы",
|
||||
callback_data="maintenance_toggle"
|
||||
)
|
||||
])
|
||||
else:
|
||||
toggle_text = "🔴 Выключить техработы" if is_active else "🔧 Включить техработы"
|
||||
monitoring_text = "⏹️ Остановить мониторинг" if monitoring_active else "🔄 Запустить мониторинг"
|
||||
check_api_text = "🔍 Проверить API"
|
||||
back_text = "⬅️ Назад в админку"
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="🔧 Включить техработы",
|
||||
callback_data="maintenance_toggle"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard = [
|
||||
[InlineKeyboardButton(text=toggle_text, callback_data="maintenance_toggle")],
|
||||
[InlineKeyboardButton(text=monitoring_text, callback_data="maintenance_monitoring")],
|
||||
[InlineKeyboardButton(text=check_api_text, callback_data="maintenance_check_api")],
|
||||
[InlineKeyboardButton(text=back_text, callback_data="admin_panel")]
|
||||
]
|
||||
if is_monitoring_active:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⏹️ Остановить мониторинг",
|
||||
callback_data="maintenance_monitoring"
|
||||
)
|
||||
])
|
||||
else:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="▶️ Запустить мониторинг",
|
||||
callback_data="maintenance_monitoring"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="🔍 Проверить API",
|
||||
callback_data="maintenance_check_api"
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="🌐 Статус панели" + ("⚠️" if panel_has_issues else ""),
|
||||
callback_data="maintenance_check_panel"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="📢 Отправить уведомление",
|
||||
callback_data="maintenance_manual_notify"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="🔄 Обновить",
|
||||
callback_data="maintenance_panel"
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="◀️ Назад",
|
||||
callback_data="admin_panel"
|
||||
)
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
@@ -44,16 +44,18 @@ class AuthMiddleware(BaseMiddleware):
|
||||
current_state = await state.get_state()
|
||||
|
||||
registration_states = [
|
||||
RegistrationStates.waiting_for_rules_accept,
|
||||
RegistrationStates.waiting_for_referral_code
|
||||
RegistrationStates.waiting_for_rules_accept.state,
|
||||
RegistrationStates.waiting_for_referral_code.state
|
||||
]
|
||||
|
||||
|
||||
is_registration_process = (
|
||||
(isinstance(event, Message) and event.text and event.text.startswith('/start'))
|
||||
or (isinstance(event, CallbackQuery) and current_state and
|
||||
any(str(state) in str(current_state) for state in registration_states))
|
||||
or (isinstance(event, CallbackQuery) and event.data and
|
||||
(event.data in ['rules_accept', 'rules_decline', 'referral_skip']))
|
||||
or (current_state in registration_states)
|
||||
or (
|
||||
isinstance(event, CallbackQuery)
|
||||
and event.data
|
||||
and (event.data in ['rules_accept', 'rules_decline', 'referral_skip'])
|
||||
)
|
||||
)
|
||||
|
||||
if is_registration_process:
|
||||
@@ -93,16 +95,18 @@ class AuthMiddleware(BaseMiddleware):
|
||||
current_state = await state.get_state()
|
||||
|
||||
registration_states = [
|
||||
RegistrationStates.waiting_for_rules_accept,
|
||||
RegistrationStates.waiting_for_referral_code
|
||||
RegistrationStates.waiting_for_rules_accept.state,
|
||||
RegistrationStates.waiting_for_referral_code.state
|
||||
]
|
||||
|
||||
|
||||
is_start_or_registration = (
|
||||
(isinstance(event, Message) and event.text and event.text.startswith('/start'))
|
||||
or (isinstance(event, CallbackQuery) and current_state and
|
||||
any(str(state) in str(current_state) for state in registration_states))
|
||||
or (isinstance(event, CallbackQuery) and event.data and
|
||||
(event.data in ['rules_accept', 'rules_decline', 'referral_skip']))
|
||||
or (current_state in registration_states)
|
||||
or (
|
||||
isinstance(event, CallbackQuery)
|
||||
and event.data
|
||||
and (event.data in ['rules_accept', 'rules_decline', 'referral_skip'])
|
||||
)
|
||||
)
|
||||
|
||||
if is_start_or_registration:
|
||||
|
||||
@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import User, Subscription, Transaction
|
||||
from app.database.crud.user import get_user_by_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,8 +20,27 @@ class AdminNotificationService:
|
||||
self.topic_id = getattr(settings, 'ADMIN_NOTIFICATIONS_TOPIC_ID', None)
|
||||
self.enabled = getattr(settings, 'ADMIN_NOTIFICATIONS_ENABLED', False)
|
||||
|
||||
async def _get_referrer_info(self, db: AsyncSession, referred_by_id: Optional[int]) -> str:
|
||||
if not referred_by_id:
|
||||
return "Нет"
|
||||
|
||||
try:
|
||||
referrer = await get_user_by_id(db, referred_by_id)
|
||||
if not referrer:
|
||||
return f"ID {referred_by_id} (не найден)"
|
||||
|
||||
if referrer.username:
|
||||
return f"@{referrer.username} (ID: {referred_by_id})"
|
||||
else:
|
||||
return f"ID {referrer.telegram_id}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения данных рефера {referred_by_id}: {e}")
|
||||
return f"ID {referred_by_id}"
|
||||
|
||||
async def send_trial_activation_notification(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
subscription: Subscription
|
||||
) -> bool:
|
||||
@@ -29,6 +49,7 @@ class AdminNotificationService:
|
||||
|
||||
try:
|
||||
user_status = "🆕 Новый" if not user.has_had_paid_subscription else "🔄 Существующий"
|
||||
referrer_info = await self._get_referrer_info(db, user.referred_by_id)
|
||||
|
||||
message = f"""🎯 <b>АКТИВАЦИЯ ТРИАЛА</b>
|
||||
|
||||
@@ -44,7 +65,7 @@ class AdminNotificationService:
|
||||
🌐 Сервер: {subscription.connected_squads[0] if subscription.connected_squads else 'По умолчанию'}
|
||||
|
||||
📆 <b>Действует до:</b> {subscription.end_date.strftime('%d.%m.%Y %H:%M')}
|
||||
🔗 <b>Реферер:</b> {f'ID {user.referred_by_id}' if user.referred_by_id else 'Нет'}
|
||||
🔗 <b>Реферер:</b> {referrer_info}
|
||||
|
||||
⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
|
||||
|
||||
@@ -56,6 +77,7 @@ class AdminNotificationService:
|
||||
|
||||
async def send_subscription_purchase_notification(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
subscription: Subscription,
|
||||
transaction: Transaction,
|
||||
@@ -76,8 +98,8 @@ class AdminNotificationService:
|
||||
user_status = "🆕 Первая покупка"
|
||||
|
||||
servers_info = await self._get_servers_info(subscription.connected_squads)
|
||||
|
||||
payment_method = self._get_payment_method_display(transaction.payment_method)
|
||||
referrer_info = await self._get_referrer_info(db, user.referred_by_id)
|
||||
|
||||
message = f"""💎 <b>{event_type}</b>
|
||||
|
||||
@@ -99,7 +121,7 @@ class AdminNotificationService:
|
||||
|
||||
📆 <b>Действует до:</b> {subscription.end_date.strftime('%d.%m.%Y %H:%M')}
|
||||
💰 <b>Баланс после покупки:</b> {settings.format_price(user.balance_kopeks)}
|
||||
🔗 <b>Реферер:</b> {f'ID {user.referred_by_id}' if user.referred_by_id else 'Нет'}
|
||||
🔗 <b>Реферер:</b> {referrer_info}
|
||||
|
||||
⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
|
||||
|
||||
@@ -111,6 +133,7 @@ class AdminNotificationService:
|
||||
|
||||
async def send_balance_topup_notification(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
transaction: Transaction,
|
||||
old_balance: int
|
||||
@@ -120,10 +143,9 @@ class AdminNotificationService:
|
||||
|
||||
try:
|
||||
topup_status = "🆕 Первое пополнение" if not user.has_made_first_topup else "🔄 Пополнение"
|
||||
|
||||
payment_method = self._get_payment_method_display(transaction.payment_method)
|
||||
|
||||
balance_change = user.balance_kopeks - old_balance
|
||||
referrer_info = await self._get_referrer_info(db, user.referred_by_id)
|
||||
|
||||
message = f"""💰 <b>ПОПОЛНЕНИЕ БАЛАНСА</b>
|
||||
|
||||
@@ -142,7 +164,7 @@ class AdminNotificationService:
|
||||
📈 Стало: {settings.format_price(user.balance_kopeks)}
|
||||
➕ Изменение: +{settings.format_price(balance_change)}
|
||||
|
||||
🔗 <b>Реферер:</b> {f'ID {user.referred_by_id}' if user.referred_by_id else 'Нет'}
|
||||
🔗 <b>Реферер:</b> {referrer_info}
|
||||
📱 <b>Подписка:</b> {self._get_subscription_status(user)}
|
||||
|
||||
⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
|
||||
@@ -155,6 +177,7 @@ class AdminNotificationService:
|
||||
|
||||
async def send_subscription_extension_notification(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
subscription: Subscription,
|
||||
transaction: Transaction,
|
||||
@@ -166,7 +189,6 @@ class AdminNotificationService:
|
||||
|
||||
try:
|
||||
payment_method = self._get_payment_method_display(transaction.payment_method)
|
||||
|
||||
servers_info = await self._get_servers_info(subscription.connected_squads)
|
||||
|
||||
message = f"""⏰ <b>ПРОДЛЕНИЕ ПОДПИСКИ</b>
|
||||
@@ -275,3 +297,342 @@ class AdminNotificationService:
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось получить названия серверов: {e}")
|
||||
return f"{len(squad_uuids)} шт."
|
||||
|
||||
|
||||
async def send_maintenance_status_notification(
|
||||
self,
|
||||
event_type: str,
|
||||
status: str,
|
||||
details: Dict[str, Any] = None
|
||||
) -> bool:
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
details = details or {}
|
||||
|
||||
if event_type == "enable":
|
||||
if details.get("auto_enabled", False):
|
||||
icon = "⚠️"
|
||||
title = "АВТОМАТИЧЕСКОЕ ВКЛЮЧЕНИЕ ТЕХРАБОТ"
|
||||
alert_type = "warning"
|
||||
else:
|
||||
icon = "🔧"
|
||||
title = "ВКЛЮЧЕНИЕ ТЕХРАБОТ"
|
||||
alert_type = "info"
|
||||
|
||||
elif event_type == "disable":
|
||||
icon = "✅"
|
||||
title = "ОТКЛЮЧЕНИЕ ТЕХРАБОТ"
|
||||
alert_type = "success"
|
||||
|
||||
elif event_type == "api_status":
|
||||
if status == "online":
|
||||
icon = "🟢"
|
||||
title = "API REMNAWAVE ВОССТАНОВЛЕНО"
|
||||
alert_type = "success"
|
||||
else:
|
||||
icon = "🔴"
|
||||
title = "API REMNAWAVE НЕДОСТУПНО"
|
||||
alert_type = "error"
|
||||
|
||||
elif event_type == "monitoring":
|
||||
if status == "started":
|
||||
icon = "🔍"
|
||||
title = "МОНИТОРИНГ ЗАПУЩЕН"
|
||||
alert_type = "info"
|
||||
else:
|
||||
icon = "⏹️"
|
||||
title = "МОНИТОРИНГ ОСТАНОВЛЕН"
|
||||
alert_type = "info"
|
||||
else:
|
||||
icon = "ℹ️"
|
||||
title = "СИСТЕМА ТЕХРАБОТ"
|
||||
alert_type = "info"
|
||||
|
||||
message_parts = [f"{icon} <b>{title}</b>", ""]
|
||||
|
||||
if event_type == "enable":
|
||||
if details.get("reason"):
|
||||
message_parts.append(f"📋 <b>Причина:</b> {details['reason']}")
|
||||
|
||||
if details.get("enabled_at"):
|
||||
enabled_at = details["enabled_at"]
|
||||
if isinstance(enabled_at, str):
|
||||
from datetime import datetime
|
||||
enabled_at = datetime.fromisoformat(enabled_at)
|
||||
message_parts.append(f"🕐 <b>Время включения:</b> {enabled_at.strftime('%d.%m.%Y %H:%M:%S')}")
|
||||
|
||||
message_parts.append(f"🤖 <b>Автоматически:</b> {'Да' if details.get('auto_enabled', False) else 'Нет'}")
|
||||
message_parts.append("")
|
||||
message_parts.append("❗ Обычные пользователи временно не могут использовать бота.")
|
||||
|
||||
elif event_type == "disable":
|
||||
if details.get("disabled_at"):
|
||||
disabled_at = details["disabled_at"]
|
||||
if isinstance(disabled_at, str):
|
||||
from datetime import datetime
|
||||
disabled_at = datetime.fromisoformat(disabled_at)
|
||||
message_parts.append(f"🕐 <b>Время отключения:</b> {disabled_at.strftime('%d.%m.%Y %H:%M:%S')}")
|
||||
|
||||
if details.get("duration"):
|
||||
duration = details["duration"]
|
||||
if isinstance(duration, (int, float)):
|
||||
hours = int(duration // 3600)
|
||||
minutes = int((duration % 3600) // 60)
|
||||
if hours > 0:
|
||||
duration_str = f"{hours}ч {minutes}мин"
|
||||
else:
|
||||
duration_str = f"{minutes}мин"
|
||||
message_parts.append(f"⏱️ <b>Длительность:</b> {duration_str}")
|
||||
|
||||
message_parts.append(f"🤖 <b>Было автоматическим:</b> {'Да' if details.get('was_auto', False) else 'Нет'}")
|
||||
message_parts.append("")
|
||||
message_parts.append("✅ Сервис снова доступен для пользователей.")
|
||||
|
||||
elif event_type == "api_status":
|
||||
message_parts.append(f"🔗 <b>API URL:</b> {details.get('api_url', 'неизвестно')}")
|
||||
|
||||
if status == "online":
|
||||
if details.get("response_time"):
|
||||
message_parts.append(f"⚡ <b>Время отклика:</b> {details['response_time']} сек")
|
||||
|
||||
if details.get("consecutive_failures", 0) > 0:
|
||||
message_parts.append(f"🔄 <b>Неудачных попыток было:</b> {details['consecutive_failures']}")
|
||||
|
||||
message_parts.append("")
|
||||
message_parts.append("API снова отвечает на запросы.")
|
||||
|
||||
else:
|
||||
if details.get("consecutive_failures"):
|
||||
message_parts.append(f"🔄 <b>Попытка №:</b> {details['consecutive_failures']}")
|
||||
|
||||
if details.get("error"):
|
||||
error_msg = str(details["error"])[:100]
|
||||
message_parts.append(f"❌ <b>Ошибка:</b> {error_msg}")
|
||||
|
||||
message_parts.append("")
|
||||
message_parts.append("⚠️ Началась серия неудачных проверок API.")
|
||||
|
||||
elif event_type == "monitoring":
|
||||
if status == "started":
|
||||
if details.get("check_interval"):
|
||||
message_parts.append(f"🔄 <b>Интервал проверки:</b> {details['check_interval']} сек")
|
||||
|
||||
if details.get("auto_enable_configured") is not None:
|
||||
auto_enable = "Включено" if details["auto_enable_configured"] else "Отключено"
|
||||
message_parts.append(f"🤖 <b>Автовключение:</b> {auto_enable}")
|
||||
|
||||
if details.get("max_failures"):
|
||||
message_parts.append(f"🎯 <b>Порог ошибок:</b> {details['max_failures']}")
|
||||
|
||||
message_parts.append("")
|
||||
message_parts.append("Система будет следить за доступностью API.")
|
||||
|
||||
else:
|
||||
message_parts.append("Автоматический мониторинг API остановлен.")
|
||||
|
||||
from datetime import datetime
|
||||
message_parts.append("")
|
||||
message_parts.append(f"⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>")
|
||||
|
||||
message = "\n".join(message_parts)
|
||||
|
||||
return await self._send_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о техработах: {e}")
|
||||
return False
|
||||
|
||||
async def send_remnawave_panel_status_notification(
|
||||
self,
|
||||
status: str,
|
||||
details: Dict[str, Any] = None
|
||||
) -> bool:
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
details = details or {}
|
||||
|
||||
status_config = {
|
||||
"online": {"icon": "🟢", "title": "ПАНЕЛЬ REMNAWAVE ДОСТУПНА", "alert_type": "success"},
|
||||
"offline": {"icon": "🔴", "title": "ПАНЕЛЬ REMNAWAVE НЕДОСТУПНА", "alert_type": "error"},
|
||||
"degraded": {"icon": "🟡", "title": "ПАНЕЛЬ REMNAWAVE РАБОТАЕТ СО СБОЯМИ", "alert_type": "warning"},
|
||||
"maintenance": {"icon": "🔧", "title": "ПАНЕЛЬ REMNAWAVE НА ОБСЛУЖИВАНИИ", "alert_type": "info"}
|
||||
}
|
||||
|
||||
config = status_config.get(status, status_config["offline"])
|
||||
|
||||
message_parts = [
|
||||
f"{config['icon']} <b>{config['title']}</b>",
|
||||
""
|
||||
]
|
||||
|
||||
if details.get("api_url"):
|
||||
message_parts.append(f"🔗 <b>URL:</b> {details['api_url']}")
|
||||
|
||||
if details.get("response_time"):
|
||||
message_parts.append(f"⚡ <b>Время отклика:</b> {details['response_time']} сек")
|
||||
|
||||
if details.get("last_check"):
|
||||
last_check = details["last_check"]
|
||||
if isinstance(last_check, str):
|
||||
from datetime import datetime
|
||||
last_check = datetime.fromisoformat(last_check)
|
||||
message_parts.append(f"🕐 <b>Последняя проверка:</b> {last_check.strftime('%H:%M:%S')}")
|
||||
|
||||
if status == "online":
|
||||
if details.get("uptime"):
|
||||
message_parts.append(f"⏱️ <b>Время работы:</b> {details['uptime']}")
|
||||
|
||||
if details.get("users_online"):
|
||||
message_parts.append(f"👥 <b>Пользователей онлайн:</b> {details['users_online']}")
|
||||
|
||||
message_parts.append("")
|
||||
message_parts.append("✅ Все системы работают нормально.")
|
||||
|
||||
elif status == "offline":
|
||||
if details.get("error"):
|
||||
error_msg = str(details["error"])[:150]
|
||||
message_parts.append(f"❌ <b>Ошибка:</b> {error_msg}")
|
||||
|
||||
if details.get("consecutive_failures"):
|
||||
message_parts.append(f"🔄 <b>Неудачных попыток:</b> {details['consecutive_failures']}")
|
||||
|
||||
message_parts.append("")
|
||||
message_parts.append("⚠️ Панель недоступна. Проверьте соединение и статус сервера.")
|
||||
|
||||
elif status == "degraded":
|
||||
if details.get("issues"):
|
||||
issues = details["issues"]
|
||||
if isinstance(issues, list):
|
||||
message_parts.append("⚠️ <b>Обнаруженные проблемы:</b>")
|
||||
for issue in issues[:3]:
|
||||
message_parts.append(f" • {issue}")
|
||||
else:
|
||||
message_parts.append(f"⚠️ <b>Проблема:</b> {issues}")
|
||||
|
||||
message_parts.append("")
|
||||
message_parts.append("Панель работает, но возможны задержки или сбои.")
|
||||
|
||||
elif status == "maintenance":
|
||||
if details.get("maintenance_reason"):
|
||||
message_parts.append(f"🔧 <b>Причина:</b> {details['maintenance_reason']}")
|
||||
|
||||
if details.get("estimated_duration"):
|
||||
message_parts.append(f"⏰ <b>Ожидаемая длительность:</b> {details['estimated_duration']}")
|
||||
|
||||
message_parts.append("")
|
||||
message_parts.append("Панель временно недоступна для обслуживания.")
|
||||
|
||||
from datetime import datetime
|
||||
message_parts.append("")
|
||||
message_parts.append(f"⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>")
|
||||
|
||||
message = "\n".join(message_parts)
|
||||
|
||||
return await self._send_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о статусе панели Remnawave: {e}")
|
||||
return False
|
||||
|
||||
async def send_remnawave_panel_status_notification(
|
||||
self,
|
||||
status: str,
|
||||
details: Dict[str, Any] = None
|
||||
) -> bool:
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
details = details or {}
|
||||
|
||||
status_config = {
|
||||
"online": {"icon": "🟢", "title": "ПАНЕЛЬ REMNAWAVE ДОСТУПНА", "alert_type": "success"},
|
||||
"offline": {"icon": "🔴", "title": "ПАНЕЛЬ REMNAWAVE НЕДОСТУПНА", "alert_type": "error"},
|
||||
"degraded": {"icon": "🟡", "title": "ПАНЕЛЬ REMNAWAVE РАБОТАЕТ СО СБОЯМИ", "alert_type": "warning"},
|
||||
"maintenance": {"icon": "🔧", "title": "ПАНЕЛЬ REMNAWAVE НА ОБСЛУЖИВАНИИ", "alert_type": "info"}
|
||||
}
|
||||
|
||||
config = status_config.get(status, status_config["offline"])
|
||||
|
||||
message_parts = [
|
||||
f"{config['icon']} <b>{config['title']}</b>",
|
||||
""
|
||||
]
|
||||
|
||||
if details.get("api_url"):
|
||||
message_parts.append(f"🔗 <b>URL:</b> {details['api_url']}")
|
||||
|
||||
if details.get("response_time"):
|
||||
message_parts.append(f"⚡ <b>Время отклика:</b> {details['response_time']} сек")
|
||||
|
||||
if details.get("last_check"):
|
||||
last_check = details["last_check"]
|
||||
if isinstance(last_check, str):
|
||||
from datetime import datetime
|
||||
last_check = datetime.fromisoformat(last_check)
|
||||
message_parts.append(f"🕐 <b>Последняя проверка:</b> {last_check.strftime('%H:%M:%S')}")
|
||||
|
||||
if status == "online":
|
||||
if details.get("uptime"):
|
||||
message_parts.append(f"⏱️ <b>Время работы:</b> {details['uptime']}")
|
||||
|
||||
if details.get("users_online"):
|
||||
message_parts.append(f"👥 <b>Пользователей онлайн:</b> {details['users_online']}")
|
||||
|
||||
message_parts.append("")
|
||||
message_parts.append("✅ Все системы работают нормально.")
|
||||
|
||||
elif status == "offline":
|
||||
if details.get("error"):
|
||||
error_msg = str(details["error"])[:150]
|
||||
message_parts.append(f"❌ <b>Ошибка:</b> {error_msg}")
|
||||
|
||||
if details.get("consecutive_failures"):
|
||||
message_parts.append(f"🔄 <b>Неудачных попыток:</b> {details['consecutive_failures']}")
|
||||
|
||||
message_parts.append("")
|
||||
message_parts.append("⚠️ Панель недоступна. Проверьте соединение и статус сервера.")
|
||||
|
||||
elif status == "degraded":
|
||||
if details.get("issues"):
|
||||
issues = details["issues"]
|
||||
if isinstance(issues, list):
|
||||
message_parts.append("⚠️ <b>Обнаруженные проблемы:</b>")
|
||||
for issue in issues[:3]:
|
||||
message_parts.append(f" • {issue}")
|
||||
else:
|
||||
message_parts.append(f"⚠️ <b>Проблема:</b> {issues}")
|
||||
|
||||
message_parts.append("")
|
||||
message_parts.append("Панель работает, но возможны задержки или сбои.")
|
||||
|
||||
elif status == "maintenance":
|
||||
if details.get("maintenance_reason"):
|
||||
message_parts.append(f"🔧 <b>Причина:</b> {details['maintenance_reason']}")
|
||||
|
||||
if details.get("estimated_duration"):
|
||||
message_parts.append(f"⏰ <b>Ожидаемая длительность:</b> {details['estimated_duration']}")
|
||||
|
||||
if details.get("manual_message"):
|
||||
message_parts.append(f"💬 <b>Сообщение:</b> {details['manual_message']}")
|
||||
|
||||
message_parts.append("")
|
||||
message_parts.append("Панель временно недоступна для обслуживания.")
|
||||
|
||||
from datetime import datetime
|
||||
message_parts.append("")
|
||||
message_parts.append(f"⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>")
|
||||
|
||||
message = "\n".join(message_parts)
|
||||
|
||||
return await self._send_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о статусе панели Remnawave: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ class MaintenanceService:
|
||||
self._last_notification_sent = None
|
||||
|
||||
def set_bot(self, bot):
|
||||
"""Устанавливает ссылку на бота для отправки уведомлений"""
|
||||
self._bot = bot
|
||||
logger.info("Бот установлен для maintenance_service")
|
||||
|
||||
@@ -58,12 +57,49 @@ class MaintenanceService:
|
||||
else:
|
||||
return settings.get_maintenance_message()
|
||||
|
||||
async def _send_admin_notification(self, message: str, alert_type: str = "info"):
|
||||
if not self._bot:
|
||||
logger.warning("Бот не установлен, уведомления не могут быть отправлены")
|
||||
return False
|
||||
|
||||
try:
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
|
||||
notification_service = AdminNotificationService(self._bot)
|
||||
|
||||
if not notification_service._is_enabled():
|
||||
logger.debug("Уведомления администраторов отключены")
|
||||
return False
|
||||
|
||||
emoji_map = {
|
||||
"error": "🚨",
|
||||
"warning": "⚠️",
|
||||
"success": "✅",
|
||||
"info": "ℹ️"
|
||||
}
|
||||
emoji = emoji_map.get(alert_type, "ℹ️")
|
||||
|
||||
formatted_message = f"{emoji} <b>ТЕХНИЧЕСКИЕ РАБОТЫ</b>\n\n{message}\n\n⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"
|
||||
|
||||
return await notification_service._send_message(formatted_message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления через AdminNotificationService: {e}")
|
||||
return False
|
||||
|
||||
async def _notify_admins(self, message: str, alert_type: str = "info"):
|
||||
"""Отправка уведомлений администраторам"""
|
||||
if not self._bot:
|
||||
logger.warning("Бот не установлен, уведомления не могут быть отправлены")
|
||||
return
|
||||
|
||||
notification_sent = await self._send_admin_notification(message, alert_type)
|
||||
|
||||
if notification_sent:
|
||||
logger.info("Уведомление успешно отправлено через AdminNotificationService")
|
||||
return
|
||||
|
||||
logger.info("Отправляем уведомление напрямую администраторам")
|
||||
|
||||
cache_key = f"maintenance_notification_{alert_type}"
|
||||
if await cache.get(cache_key):
|
||||
return
|
||||
@@ -116,15 +152,13 @@ class MaintenanceService:
|
||||
|
||||
await self._save_status_to_cache()
|
||||
|
||||
notification_msg = f"""
|
||||
Режим технических работ ВКЛЮЧЕН
|
||||
notification_msg = f"""Режим технических работ ВКЛЮЧЕН
|
||||
|
||||
📋 <b>Причина:</b> {self._status.reason}
|
||||
🤖 <b>Автоматически:</b> {'Да' if auto else 'Нет'}
|
||||
🕐 <b>Время:</b> {self._status.enabled_at.strftime('%d.%m.%Y %H:%M:%S')}
|
||||
|
||||
Обычные пользователи временно не смогут использовать бота.
|
||||
"""
|
||||
Обычные пользователи временно не смогут использовать бота."""
|
||||
|
||||
await self._notify_admins(notification_msg, "warning" if auto else "info")
|
||||
|
||||
@@ -163,15 +197,13 @@ class MaintenanceService:
|
||||
else:
|
||||
duration_str = f"\n⏱️ <b>Длительность:</b> {minutes}мин"
|
||||
|
||||
notification_msg = f"""
|
||||
Режим технических работ ВЫКЛЮЧЕН
|
||||
notification_msg = f"""Режим технических работ ВЫКЛЮЧЕН
|
||||
|
||||
🤖 <b>Автоматически:</b> {'Да' if was_auto else 'Нет'}
|
||||
🕐 <b>Время:</b> {datetime.utcnow().strftime('%d.%m.%Y %H:%M:%S')}
|
||||
{duration_str}
|
||||
|
||||
Сервис снова доступен для пользователей.
|
||||
"""
|
||||
Сервис снова доступен для пользователей."""
|
||||
|
||||
await self._notify_admins(notification_msg, "success")
|
||||
|
||||
@@ -191,17 +223,15 @@ class MaintenanceService:
|
||||
await self._load_status_from_cache()
|
||||
|
||||
self._check_task = asyncio.create_task(self._monitoring_loop())
|
||||
logger.info(f"🔄 Запущен мониторинг API RemnaWave (интервал: {settings.get_maintenance_check_interval()}с)")
|
||||
logger.info(f"🔄 Запущен мониторинг API Remnawave (интервал: {settings.get_maintenance_check_interval()}с)")
|
||||
|
||||
await self._notify_admins(f"""
|
||||
Мониторинг технических работ запущен
|
||||
await self._notify_admins(f"""Мониторинг технических работ запущен
|
||||
|
||||
🔄 <b>Интервал проверки:</b> {settings.get_maintenance_check_interval()} секунд
|
||||
🤖 <b>Автовключение:</b> {'Включено' if settings.is_maintenance_auto_enable() else 'Отключено'}
|
||||
🎯 <b>Порог ошибок:</b> {self._max_consecutive_failures}
|
||||
|
||||
Система будет следить за доступностью API.
|
||||
""", "info")
|
||||
Система будет следить за доступностью API.""", "info")
|
||||
|
||||
return True
|
||||
|
||||
@@ -240,17 +270,14 @@ class MaintenanceService:
|
||||
is_connected = await test_api_connection(api)
|
||||
|
||||
if is_connected:
|
||||
# API восстановилось
|
||||
if not self._status.api_status:
|
||||
await self._notify_admins(f"""
|
||||
API RemnaWave восстановлено!
|
||||
await self._notify_admins(f"""API Remnawave восстановлено!
|
||||
|
||||
✅ <b>Статус:</b> Доступно
|
||||
🕐 <b>Время восстановления:</b> {self._status.last_check.strftime('%H:%M:%S')}
|
||||
🔄 <b>Неудачных попыток было:</b> {self._status.consecutive_failures}
|
||||
|
||||
API снова отвечает на запросы.
|
||||
""", "success")
|
||||
API снова отвечает на запросы.""", "success")
|
||||
|
||||
self._status.api_status = True
|
||||
self._status.consecutive_failures = 0
|
||||
@@ -266,15 +293,13 @@ API снова отвечает на запросы.
|
||||
self._status.consecutive_failures += 1
|
||||
|
||||
if was_available:
|
||||
await self._notify_admins(f"""
|
||||
API RemnaWave недоступно!
|
||||
await self._notify_admins(f"""API Remnawave недоступно!
|
||||
|
||||
❌ <b>Статус:</b> Недоступно
|
||||
🕐 <b>Время обнаружения:</b> {self._status.last_check.strftime('%H:%M:%S')}
|
||||
🔄 <b>Попытка:</b> {self._status.consecutive_failures}
|
||||
|
||||
Началась серия неудачных проверок API.
|
||||
""", "error")
|
||||
Началась серия неудачных проверок API.""", "error")
|
||||
|
||||
if (self._status.consecutive_failures >= self._max_consecutive_failures and
|
||||
not self._status.is_active and
|
||||
@@ -291,14 +316,12 @@ API RemnaWave недоступно!
|
||||
logger.error(f"Ошибка проверки API: {e}")
|
||||
|
||||
if self._status.api_status:
|
||||
await self._notify_admins(f"""
|
||||
Ошибка при проверке API RemnaWave
|
||||
await self._notify_admins(f"""Ошибка при проверке API Remnawave
|
||||
|
||||
❌ <b>Ошибка:</b> {str(e)}
|
||||
🕐 <b>Время:</b> {datetime.utcnow().strftime('%H:%M:%S')}
|
||||
|
||||
Не удалось выполнить проверку доступности API.
|
||||
""", "error")
|
||||
Не удалось выполнить проверку доступности API.""", "error")
|
||||
|
||||
self._status.api_status = False
|
||||
self._status.consecutive_failures += 1
|
||||
@@ -401,6 +424,33 @@ API RemnaWave недоступно!
|
||||
"checked_at": end_time,
|
||||
"consecutive_failures": self._status.consecutive_failures
|
||||
}
|
||||
|
||||
async def send_remnawave_status_notification(self, status: str, details: str = "") -> bool:
|
||||
try:
|
||||
status_emojis = {
|
||||
"online": "🟢",
|
||||
"offline": "🔴",
|
||||
"warning": "🟡",
|
||||
"error": "⚠️"
|
||||
}
|
||||
|
||||
emoji = status_emojis.get(status, "ℹ️")
|
||||
|
||||
message = f"""Статус панели Remnawave изменился
|
||||
|
||||
{emoji} <b>Статус:</b> {status.upper()}
|
||||
🔗 <b>URL:</b> {settings.REMNAWAVE_API_URL}
|
||||
{details}"""
|
||||
|
||||
alert_type = "error" if status in ["offline", "error"] else "info"
|
||||
await self._notify_admins(message, alert_type)
|
||||
|
||||
logger.info(f"Отправлено уведомление о статусе Remnawave: {status}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о статусе Remnawave: {e}")
|
||||
return False
|
||||
|
||||
|
||||
maintenance_service = MaintenanceService()
|
||||
|
||||
@@ -108,7 +108,7 @@ class PaymentService:
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
notification_service = AdminNotificationService(self.bot)
|
||||
await notification_service.send_balance_topup_notification(
|
||||
user, transaction, old_balance
|
||||
db, user, transaction, old_balance
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о пополнении Stars: {e}")
|
||||
@@ -297,7 +297,7 @@ class PaymentService:
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
notification_service = AdminNotificationService(self.bot)
|
||||
await notification_service.send_balance_topup_notification(
|
||||
user, transaction, old_balance
|
||||
db, user, transaction, old_balance
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о пополнении YooKassa: {e}")
|
||||
@@ -429,17 +429,17 @@ class PaymentService:
|
||||
signature: str
|
||||
) -> bool:
|
||||
|
||||
if not settings.TRIBUTE_WEBHOOK_SECRET:
|
||||
if not settings.TRIBUTE_API_KEY:
|
||||
return False
|
||||
|
||||
|
||||
try:
|
||||
message = str(data).encode()
|
||||
expected_signature = hmac.new(
|
||||
settings.TRIBUTE_WEBHOOK_SECRET.encode(),
|
||||
settings.TRIBUTE_API_KEY.encode(),
|
||||
message,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
|
||||
return hmac.compare_digest(signature, expected_signature)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -78,7 +78,7 @@ async def process_referral_registration(
|
||||
|
||||
async def process_referral_topup(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
user_id: int,
|
||||
topup_amount_kopeks: int,
|
||||
bot: Bot = None
|
||||
):
|
||||
@@ -105,12 +105,12 @@ async def process_referral_topup(
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(
|
||||
ReferralEarning.user_id == referrer.id,
|
||||
ReferralEarning.referral_id == user_id,
|
||||
ReferralEarning.referral_id == user.id,
|
||||
ReferralEarning.reason == "referral_registration_pending"
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
logger.info(f"🗑️ Удалена запись 'ожидание пополнения' для реферала {user_id}")
|
||||
logger.info(f"🗑️ Удалена запись 'ожидание пополнения' для реферала {user.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления записи ожидания: {e}")
|
||||
|
||||
@@ -120,7 +120,7 @@ async def process_referral_topup(
|
||||
f"Бонус за первое пополнение по реферальной программе",
|
||||
bot=bot
|
||||
)
|
||||
logger.info(f"💰 Реферал {user_id} получил бонус {settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS/100}₽")
|
||||
logger.info(f"💰 Реферал {user.id} получил бонус {settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS/100}₽")
|
||||
|
||||
if bot:
|
||||
bonus_notification = (
|
||||
@@ -141,7 +141,7 @@ async def process_referral_topup(
|
||||
await create_referral_earning(
|
||||
db=db,
|
||||
user_id=referrer.id,
|
||||
referral_id=user_id,
|
||||
referral_id=user.id,
|
||||
amount_kopeks=settings.REFERRAL_INVITER_BONUS_KOPEKS,
|
||||
reason="referral_first_topup"
|
||||
)
|
||||
@@ -170,7 +170,7 @@ async def process_referral_topup(
|
||||
await create_referral_earning(
|
||||
db=db,
|
||||
user_id=referrer.id,
|
||||
referral_id=user_id,
|
||||
referral_id=user.id,
|
||||
amount_kopeks=commission_amount,
|
||||
reason="referral_commission_topup"
|
||||
)
|
||||
@@ -230,7 +230,7 @@ async def process_referral_purchase(
|
||||
await create_referral_earning(
|
||||
db=db,
|
||||
user_id=referrer.id,
|
||||
referral_id=user_id,
|
||||
referral_id=user.id,
|
||||
amount_kopeks=commission_amount,
|
||||
reason="referral_commission",
|
||||
referral_transaction_id=transaction_id
|
||||
|
||||
@@ -200,7 +200,7 @@ class RemnaWaveService:
|
||||
return result
|
||||
|
||||
except RemnaWaveAPIError as e:
|
||||
logger.error(f"Ошибка RemnaWave API при получении статистики: {e}")
|
||||
logger.error(f"Ошибка Remnawave API при получении статистики: {e}")
|
||||
return {"error": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(f"Общая ошибка получения системной статистики: {e}")
|
||||
@@ -271,11 +271,11 @@ class RemnaWaveService:
|
||||
'traffic_limit_bytes': node.traffic_limit_bytes
|
||||
})
|
||||
|
||||
logger.info(f"✅ Получено {len(result)} нод из RemnaWave")
|
||||
logger.info(f"✅ Получено {len(result)} нод из Remnawave")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения нод из RemnaWave: {e}")
|
||||
logger.error(f"Ошибка получения нод из Remnawave: {e}")
|
||||
return []
|
||||
|
||||
async def test_connection(self) -> bool:
|
||||
@@ -283,11 +283,11 @@ class RemnaWaveService:
|
||||
try:
|
||||
async with self.api as api:
|
||||
stats = await api.get_system_stats()
|
||||
logger.info("✅ Соединение с RemnaWave API работает")
|
||||
logger.info("✅ Соединение с Remnawave API работает")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка соединения с RemnaWave API: {e}")
|
||||
logger.error(f"❌ Ошибка соединения с Remnawave API: {e}")
|
||||
return False
|
||||
|
||||
async def get_node_details(self, node_uuid: str) -> Optional[Dict[str, Any]]:
|
||||
@@ -378,11 +378,11 @@ class RemnaWaveService:
|
||||
'inbounds': squad.inbounds
|
||||
})
|
||||
|
||||
logger.info(f"✅ Получено {len(result)} сквадов из RemnaWave")
|
||||
logger.info(f"✅ Получено {len(result)} сквадов из Remnawave")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения сквадов из RemnaWave: {e}")
|
||||
logger.error(f"Ошибка получения сквадов из Remnawave: {e}")
|
||||
return []
|
||||
|
||||
async def create_squad(self, name: str, inbounds: List[str]) -> Optional[str]:
|
||||
@@ -1119,9 +1119,6 @@ class RemnaWaveService:
|
||||
return False
|
||||
|
||||
async def cleanup_orphaned_subscriptions(self, db: AsyncSession) -> Dict[str, int]:
|
||||
"""
|
||||
Усиленная версия очистки с полным удалением данных
|
||||
"""
|
||||
try:
|
||||
stats = {"deactivated": 0, "errors": 0, "checked": 0}
|
||||
|
||||
@@ -1286,10 +1283,10 @@ class RemnaWaveService:
|
||||
if rw_user:
|
||||
subscription.remnawave_short_uuid = rw_user.short_uuid
|
||||
subscription.subscription_url = rw_user.subscription_url
|
||||
logger.info(f"🔧 Восстановлены данные RemnaWave для {user.telegram_id}")
|
||||
logger.info(f"🔧 Восстановлены данные Remnawave для {user.telegram_id}")
|
||||
issues_fixed += 1
|
||||
except Exception as rw_error:
|
||||
logger.warning(f"⚠️ Не удалось получить данные RemnaWave для {user.telegram_id}: {rw_error}")
|
||||
logger.warning(f"⚠️ Не удалось получить данные Remnawave для {user.telegram_id}: {rw_error}")
|
||||
|
||||
if subscription.traffic_limit_gb < 0:
|
||||
subscription.traffic_limit_gb = 0
|
||||
@@ -1356,7 +1353,7 @@ class RemnaWaveService:
|
||||
recommendations["should_sync"] = True
|
||||
recommendations["sync_type"] = "all"
|
||||
recommendations["priority"] = "high"
|
||||
recommendations["reasons"].append(f"Найдено {users_without_uuid} пользователей без связи с RemnaWave")
|
||||
recommendations["reasons"].append(f"Найдено {users_without_uuid} пользователей без связи с Remnawave")
|
||||
recommendations["estimated_time"] = "3-5 минут"
|
||||
|
||||
if active_expired > 5:
|
||||
@@ -1382,3 +1379,211 @@ class RemnaWaveService:
|
||||
"priority": "medium",
|
||||
"estimated_time": "3-5 минут"
|
||||
}
|
||||
|
||||
async def monitor_panel_status(self, bot) -> Dict[str, Any]:
|
||||
try:
|
||||
from app.utils.cache import cache
|
||||
previous_status = await cache.get("remnawave_panel_status") or "unknown"
|
||||
|
||||
status_result = await self.check_panel_health()
|
||||
current_status = status_result.get("status", "offline")
|
||||
|
||||
if current_status != previous_status and previous_status != "unknown":
|
||||
await self._send_status_change_notification(
|
||||
bot,
|
||||
previous_status,
|
||||
current_status,
|
||||
status_result
|
||||
)
|
||||
|
||||
await cache.set("remnawave_panel_status", current_status, expire=300)
|
||||
|
||||
return status_result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка мониторинга статуса панели Remnawave: {e}")
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
|
||||
async def _send_status_change_notification(
|
||||
self,
|
||||
bot,
|
||||
old_status: str,
|
||||
new_status: str,
|
||||
status_data: Dict[str, Any]
|
||||
):
|
||||
try:
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
|
||||
notification_service = AdminNotificationService(bot)
|
||||
|
||||
details = {
|
||||
"api_url": status_data.get("api_url"),
|
||||
"response_time": status_data.get("response_time"),
|
||||
"last_check": status_data.get("last_check"),
|
||||
"users_online": status_data.get("users_online"),
|
||||
"nodes_online": status_data.get("nodes_online"),
|
||||
"total_nodes": status_data.get("total_nodes"),
|
||||
"old_status": old_status
|
||||
}
|
||||
|
||||
if new_status == "offline":
|
||||
details["error"] = status_data.get("api_error")
|
||||
elif new_status == "degraded":
|
||||
issues = []
|
||||
if status_data.get("response_time", 0) > 10:
|
||||
issues.append(f"Медленный отклик API ({status_data.get('response_time')}с)")
|
||||
if status_data.get("nodes_health") == "unhealthy":
|
||||
issues.append(f"Проблемы с нодами ({status_data.get('nodes_online')}/{status_data.get('total_nodes')} онлайн)")
|
||||
details["issues"] = issues
|
||||
|
||||
await notification_service.send_remnawave_panel_status_notification(
|
||||
new_status,
|
||||
details
|
||||
)
|
||||
|
||||
logger.info(f"Отправлено уведомление об изменении статуса панели: {old_status} -> {new_status}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об изменении статуса: {e}")
|
||||
|
||||
|
||||
|
||||
async def send_manual_status_notification(self, bot, status: str, message: str = ""):
|
||||
try:
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
|
||||
notification_service = AdminNotificationService(bot)
|
||||
|
||||
details = {
|
||||
"api_url": settings.REMNAWAVE_API_URL,
|
||||
"last_check": datetime.utcnow(),
|
||||
"manual_message": message
|
||||
}
|
||||
|
||||
if status == "maintenance":
|
||||
details["maintenance_reason"] = message or "Плановое обслуживание"
|
||||
|
||||
await notification_service.send_remnawave_panel_status_notification(status, details)
|
||||
|
||||
logger.info(f"Отправлено ручное уведомление о статусе панели: {status}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки ручного уведомления: {e}")
|
||||
return False
|
||||
|
||||
async def get_panel_status_summary(self) -> Dict[str, Any]:
|
||||
try:
|
||||
status_data = await self.check_panel_health()
|
||||
|
||||
status_descriptions = {
|
||||
"online": "🟢 Панель работает нормально",
|
||||
"offline": "🔴 Панель недоступна",
|
||||
"degraded": "🟡 Панель работает со сбоями",
|
||||
"maintenance": "🔧 Панель на обслуживании"
|
||||
}
|
||||
|
||||
status = status_data.get("status", "offline")
|
||||
|
||||
summary = {
|
||||
"status": status,
|
||||
"description": status_descriptions.get(status, "❓ Статус неизвестен"),
|
||||
"response_time": status_data.get("response_time", 0),
|
||||
"api_available": status_data.get("api_available", False),
|
||||
"nodes_status": f"{status_data.get('nodes_online', 0)}/{status_data.get('total_nodes', 0)} нод онлайн",
|
||||
"users_online": status_data.get("users_online", 0),
|
||||
"last_check": status_data.get("last_check"),
|
||||
"has_issues": status in ["offline", "degraded"]
|
||||
}
|
||||
|
||||
if status == "offline":
|
||||
summary["recommendation"] = "Проверьте подключение к серверу и работоспособность панели"
|
||||
elif status == "degraded":
|
||||
summary["recommendation"] = "Рекомендуется проверить состояние нод и производительность сервера"
|
||||
else:
|
||||
summary["recommendation"] = "Все системы работают нормально"
|
||||
|
||||
return summary
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения сводки статуса панели: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"description": "❌ Ошибка проверки статуса",
|
||||
"response_time": 0,
|
||||
"api_available": False,
|
||||
"nodes_status": "неизвестно",
|
||||
"users_online": 0,
|
||||
"last_check": datetime.utcnow(),
|
||||
"has_issues": True,
|
||||
"recommendation": "Обратитесь к системному администратору",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def check_panel_health(self) -> Dict[str, Any]:
|
||||
try:
|
||||
start_time = datetime.utcnow()
|
||||
|
||||
async with self.api as api:
|
||||
try:
|
||||
system_stats = await api.get_system_stats()
|
||||
api_available = True
|
||||
api_error = None
|
||||
except Exception as e:
|
||||
api_available = False
|
||||
api_error = str(e)
|
||||
system_stats = {}
|
||||
|
||||
try:
|
||||
nodes = await api.get_all_nodes()
|
||||
nodes_online = sum(1 for node in nodes if node.is_connected and node.is_node_online)
|
||||
total_nodes = len(nodes)
|
||||
nodes_health = "healthy" if nodes_online > 0 else "unhealthy"
|
||||
except Exception:
|
||||
nodes_online = 0
|
||||
total_nodes = 0
|
||||
nodes_health = "unknown"
|
||||
|
||||
end_time = datetime.utcnow()
|
||||
response_time = (end_time - start_time).total_seconds()
|
||||
|
||||
if not api_available:
|
||||
status = "offline"
|
||||
elif response_time > 10:
|
||||
status = "degraded"
|
||||
elif nodes_health == "unhealthy":
|
||||
status = "degraded"
|
||||
else:
|
||||
status = "online"
|
||||
|
||||
return {
|
||||
"status": status,
|
||||
"api_available": api_available,
|
||||
"api_error": api_error,
|
||||
"response_time": round(response_time, 2),
|
||||
"nodes_online": nodes_online,
|
||||
"total_nodes": total_nodes,
|
||||
"nodes_health": nodes_health,
|
||||
"users_online": system_stats.get('onlineStats', {}).get('onlineNow', 0),
|
||||
"total_users": system_stats.get('users', {}).get('totalUsers', 0),
|
||||
"last_check": end_time,
|
||||
"api_url": settings.REMNAWAVE_API_URL
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки здоровья панели: {e}")
|
||||
return {
|
||||
"status": "offline",
|
||||
"api_available": False,
|
||||
"api_error": str(e),
|
||||
"response_time": 0,
|
||||
"nodes_online": 0,
|
||||
"total_nodes": 0,
|
||||
"nodes_health": "unknown",
|
||||
"last_check": datetime.utcnow(),
|
||||
"api_url": settings.REMNAWAVE_API_URL
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import logging
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
@@ -56,15 +54,9 @@ class TributeService:
|
||||
|
||||
async def process_webhook(
|
||||
self,
|
||||
payload: str,
|
||||
signature: Optional[str] = None
|
||||
payload: str
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
if signature and settings.TRIBUTE_WEBHOOK_SECRET:
|
||||
if not self.tribute_api.verify_webhook_signature(payload, signature):
|
||||
logger.warning("Неверная подпись Tribute webhook")
|
||||
return {"status": "error", "reason": "invalid_signature"}
|
||||
|
||||
try:
|
||||
webhook_data = json.loads(payload)
|
||||
except json.JSONDecodeError:
|
||||
@@ -90,37 +82,38 @@ class TributeService:
|
||||
return {"status": "ok", "event": event_type}
|
||||
|
||||
async def _handle_successful_payment(self, payment_data: Dict[str, Any]):
|
||||
"""Обработка успешного платежа - ПОЛНОСТЬЮ ПЕРЕРАБОТАННАЯ ВЕРСИЯ"""
|
||||
|
||||
try:
|
||||
user_id = payment_data["user_id"]
|
||||
user_telegram_id = payment_data["user_id"]
|
||||
amount_kopeks = payment_data["amount_kopeks"]
|
||||
payment_id = payment_data["payment_id"]
|
||||
|
||||
logger.info(f"Обрабатываем успешный Tribute платеж: user_id={user_id}, amount={amount_kopeks}, payment_id={payment_id}")
|
||||
logger.info(f"Обрабатываем успешный Tribute платеж: user_telegram_id={user_telegram_id}, amount={amount_kopeks}, payment_id={payment_id}")
|
||||
|
||||
async for session in get_db():
|
||||
from app.database.crud.transaction import check_tribute_payment_duplicate, create_unique_tribute_transaction
|
||||
|
||||
duplicate_transaction = await check_tribute_payment_duplicate(
|
||||
session, payment_id, amount_kopeks, user_id
|
||||
)
|
||||
|
||||
if duplicate_transaction:
|
||||
logger.warning(f"Найден дубликат платежа:")
|
||||
logger.warning(f" Transaction ID: {duplicate_transaction.id}")
|
||||
logger.warning(f" Amount: {duplicate_transaction.amount_kopeks} коп")
|
||||
logger.warning(f" Created: {duplicate_transaction.created_at}")
|
||||
logger.warning(f"Платеж игнорирован")
|
||||
return
|
||||
|
||||
user = await get_user_by_telegram_id(session, user_id)
|
||||
user = await get_user_by_telegram_id(session, user_telegram_id)
|
||||
if not user:
|
||||
logger.error(f"Пользователь {user_id} не найден")
|
||||
logger.error(f"Пользователь {user_telegram_id} не найден")
|
||||
return
|
||||
|
||||
logger.info(f"Найден пользователь {user.telegram_id}, текущий баланс: {user.balance_kopeks} коп")
|
||||
|
||||
from app.database.crud.transaction import check_tribute_payment_duplicate
|
||||
|
||||
duplicate_transaction = await check_tribute_payment_duplicate(
|
||||
session, payment_id, amount_kopeks, user_telegram_id
|
||||
)
|
||||
|
||||
if duplicate_transaction:
|
||||
logger.warning(f"Найден дубликат платежа в течение 24ч:")
|
||||
logger.warning(f" Transaction ID: {duplicate_transaction.id}")
|
||||
logger.warning(f" Amount: {duplicate_transaction.amount_kopeks} коп")
|
||||
logger.warning(f" Created: {duplicate_transaction.created_at}")
|
||||
logger.warning(f" External ID: {duplicate_transaction.external_id}")
|
||||
logger.warning(f"Платеж игнорирован - это дубликат свежего платежа")
|
||||
return
|
||||
|
||||
from app.database.crud.transaction import create_unique_tribute_transaction
|
||||
|
||||
transaction = await create_unique_tribute_transaction(
|
||||
db=session,
|
||||
user_id=user.id,
|
||||
@@ -135,18 +128,38 @@ class TributeService:
|
||||
|
||||
await session.commit()
|
||||
|
||||
logger.info(f"Баланс пользователя {user_id} обновлен: {old_balance} -> {user.balance_kopeks} коп (+{amount_kopeks})")
|
||||
logger.info(f"✅ Баланс пользователя {user_telegram_id} обновлен: {old_balance} -> {user.balance_kopeks} коп (+{amount_kopeks})")
|
||||
logger.info(f"✅ Создана транзакция ID: {transaction.id}")
|
||||
|
||||
await self._send_success_notification(user_id, amount_kopeks)
|
||||
try:
|
||||
from app.services.referral_service import process_referral_topup
|
||||
await process_referral_topup(session, user.id, amount_kopeks, self.bot)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки реферального пополнения Tribute: {e}")
|
||||
|
||||
if not user.has_made_first_topup:
|
||||
user.has_made_first_topup = True
|
||||
logger.info(f"Отмечен первый топап для пользователя {user_telegram_id}")
|
||||
|
||||
logger.info(f"Успешно обработан Tribute платеж: {amount_kopeks/100}₽ для пользователя {user_id}")
|
||||
|
||||
try:
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
notification_service = AdminNotificationService(self.bot)
|
||||
await notification_service.send_balance_topup_notification(
|
||||
session, user, transaction, old_balance
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о Tribute пополнении: {e}")
|
||||
|
||||
await self._send_success_notification(user_telegram_id, amount_kopeks)
|
||||
|
||||
logger.info(f"🎉 Успешно обработан Tribute платеж: {amount_kopeks/100}₽ для пользователя {user_telegram_id}")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки успешного Tribute платежа: {e}", exc_info=True)
|
||||
logger.error(f"⌘ Ошибка обработки успешного Tribute платежа: {e}", exc_info=True)
|
||||
|
||||
async def _handle_failed_payment(self, payment_data: Dict[str, Any]):
|
||||
"""Обработка неудачного платежа"""
|
||||
|
||||
try:
|
||||
user_id = payment_data["user_id"]
|
||||
@@ -170,7 +183,6 @@ class TributeService:
|
||||
logger.error(f"Ошибка обработки неудачного Tribute платежа: {e}")
|
||||
|
||||
async def _handle_refund(self, refund_data: Dict[str, Any]):
|
||||
"""Обработка возврата"""
|
||||
|
||||
try:
|
||||
user_id = refund_data["user_id"]
|
||||
@@ -203,7 +215,6 @@ class TributeService:
|
||||
logger.error(f"Ошибка обработки возврата Tribute: {e}")
|
||||
|
||||
async def _send_success_notification(self, user_id: int, amount_kopeks: int):
|
||||
"""Отправка уведомления об успешном платеже"""
|
||||
|
||||
try:
|
||||
amount_rubles = amount_kopeks / 100
|
||||
@@ -232,11 +243,10 @@ class TributeService:
|
||||
logger.error(f"Ошибка отправки уведомления об успешном платеже: {e}")
|
||||
|
||||
async def _send_failure_notification(self, user_id: int):
|
||||
"""Отправка уведомления о неудачном платеже"""
|
||||
|
||||
try:
|
||||
text = (
|
||||
"❌ **Платеж не прошел**\n\n"
|
||||
"⌘ **Платеж не прошел**\n\n"
|
||||
"К сожалению, ваш платеж через Tribute был отклонен.\n\n"
|
||||
"Возможные причины:\n"
|
||||
"• Недостаточно средств на карте\n"
|
||||
@@ -261,7 +271,6 @@ class TributeService:
|
||||
logger.error(f"Ошибка отправки уведомления о неудачном платеже: {e}")
|
||||
|
||||
async def _send_refund_notification(self, user_id: int, amount_kopeks: int):
|
||||
"""Отправка уведомления о возврате"""
|
||||
|
||||
try:
|
||||
amount_rubles = amount_kopeks / 100
|
||||
@@ -296,7 +305,6 @@ class TributeService:
|
||||
amount_kopeks: int,
|
||||
description: str = "Принудительная обработка Tribute платежа"
|
||||
) -> bool:
|
||||
"""Принудительная обработка платежа (для отладки)"""
|
||||
|
||||
try:
|
||||
logger.info(f"🔧 ПРИНУДИТЕЛЬНАЯ ОБРАБОТКА: payment_id={payment_id}, user_id={user_id}, amount={amount_kopeks}")
|
||||
@@ -304,7 +312,7 @@ class TributeService:
|
||||
async for session in get_db():
|
||||
user = await get_user_by_telegram_id(session, user_id)
|
||||
if not user:
|
||||
logger.error(f"❌ Пользователь {user_id} не найден")
|
||||
logger.error(f"⌘ Пользователь {user_id} не найден")
|
||||
return False
|
||||
|
||||
external_id = f"force_donation_{payment_id}_{int(datetime.utcnow().timestamp())}"
|
||||
@@ -334,7 +342,7 @@ class TributeService:
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка принудительной обработки: {e}", exc_info=True)
|
||||
logger.error(f"⌘ Ошибка принудительной обработки: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
async def get_payment_status(self, payment_id: str) -> Optional[Dict[str, Any]]:
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import re
|
||||
from typing import Optional, Union
|
||||
from datetime import datetime
|
||||
import html
|
||||
|
||||
ALLOWED_HTML_TAGS = {
|
||||
'b', 'strong',
|
||||
'i', 'em',
|
||||
'u', 'ins',
|
||||
's', 'strike', 'del',
|
||||
'code',
|
||||
'pre',
|
||||
'a',
|
||||
'blockquote',
|
||||
'spoiler', 'tg-spoiler'
|
||||
}
|
||||
|
||||
def validate_email(email: str) -> bool:
|
||||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
@@ -105,13 +117,24 @@ def validate_subscription_period(days: Union[str, int]) -> Optional[int]:
|
||||
|
||||
|
||||
def sanitize_html(text: str) -> str:
|
||||
allowed_tags = ['b', 'strong', 'i', 'em', 'u', 'ins', 's', 'strike', 'del', 'code', 'pre']
|
||||
if not text:
|
||||
return text
|
||||
|
||||
for tag in allowed_tags:
|
||||
text = re.sub(f'<{tag}>', f'<{tag}>', text, flags=re.IGNORECASE)
|
||||
text = re.sub(f'</{tag}>', f'</{tag}>', text, flags=re.IGNORECASE)
|
||||
text = html.escape(text)
|
||||
|
||||
text = re.sub(r'<(?!/?(?:' + '|'.join(allowed_tags) + r')\b)[^>]*>', '', text)
|
||||
for tag in ALLOWED_HTML_TAGS:
|
||||
text = re.sub(
|
||||
f'<{tag}(>|\\s[^&]*>)',
|
||||
lambda m: m.group(0).replace('<', '<').replace('>', '>'),
|
||||
text,
|
||||
flags=re.IGNORECASE
|
||||
)
|
||||
text = re.sub(
|
||||
f'</{tag}>',
|
||||
f'</{tag}>',
|
||||
text,
|
||||
flags=re.IGNORECASE
|
||||
)
|
||||
|
||||
return text
|
||||
|
||||
@@ -134,4 +157,52 @@ def validate_referral_code(code: str) -> bool:
|
||||
user_id_part = code[3:]
|
||||
return user_id_part.isdigit()
|
||||
|
||||
return validate_promocode(code)
|
||||
return validate_promocode(code)
|
||||
|
||||
def validate_html_tags(text: str) -> tuple[bool, str]:
|
||||
if not text:
|
||||
return True, ""
|
||||
|
||||
tag_pattern = r'<(/?)([a-zA-Z][a-zA-Z0-9-]*)[^>]*>'
|
||||
tags = re.findall(tag_pattern, text)
|
||||
|
||||
for is_closing, tag_name in tags:
|
||||
tag_name_lower = tag_name.lower()
|
||||
|
||||
if tag_name_lower not in ALLOWED_HTML_TAGS:
|
||||
return False, f"Неподдерживаемый тег: <{tag_name}>"
|
||||
|
||||
tag_stack = []
|
||||
for is_closing, tag_name in tags:
|
||||
tag_name_lower = tag_name.lower()
|
||||
|
||||
if not is_closing:
|
||||
tag_stack.append(tag_name_lower)
|
||||
else:
|
||||
if not tag_stack:
|
||||
return False, f"Закрывающий тег без открывающего: </{tag_name}>"
|
||||
|
||||
last_tag = tag_stack.pop()
|
||||
if last_tag != tag_name_lower:
|
||||
return False, f"Неправильная вложенность тегов: ожидался </{last_tag}>, найден </{tag_name}>"
|
||||
|
||||
if tag_stack:
|
||||
return False, f"Незакрытый тег: <{tag_stack[-1]}>"
|
||||
|
||||
return True, ""
|
||||
|
||||
|
||||
def get_html_help_text() -> str:
|
||||
return """<b>Поддерживаемые HTML теги:</b>
|
||||
|
||||
- <code><b>жирный</b></code> или <code><strong>жирный</strong></code>
|
||||
- <code><i>курсив</i></code> или <code><em>курсив</em></code>
|
||||
- <code><u>подчеркнутый</u></code>
|
||||
- <code><s>зачеркнутый</s></code>
|
||||
- <code><code>моноширинный</code></code>
|
||||
- <code><pre>блок кода</pre></code>
|
||||
- <code><a href="url">ссылка</a></code>
|
||||
- <code><blockquote>цитата</blockquote></code>
|
||||
- <code><spoiler>спойлер</spoiler></code>
|
||||
|
||||
<b>Неподдерживаемые теги:</b> <br>, <p>, <div>, <span> и другие"""
|
||||
|
||||
@@ -35,7 +35,9 @@ services:
|
||||
retries: 3
|
||||
|
||||
bot:
|
||||
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
|
||||
build:
|
||||
context: . # директория где лежит Dockerfile
|
||||
dockerfile: Dockerfile
|
||||
container_name: remnawave_bot
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
@@ -58,12 +60,6 @@ services:
|
||||
- "${YOOKASSA_WEBHOOK_PORT:-8082}:8082"
|
||||
networks:
|
||||
- bot_network
|
||||
user: "1000:1000"
|
||||
command: >
|
||||
bash -c "
|
||||
mkdir -p /app/logs /app/data &&
|
||||
python main.py
|
||||
"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 594 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 273 KiB |
|
Before Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 463 KiB |
|
Before Width: | Height: | Size: 459 KiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 357 KiB |
|
Before Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 378 KiB |
|
Before Width: | Height: | Size: 393 KiB |
|
Before Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 240 KiB |