mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-28 07:11:37 +00:00
@@ -269,7 +269,7 @@ async def update_ticket_settings(
|
||||
if request.sla_reminder_cooldown_minutes is not None:
|
||||
settings.SUPPORT_TICKET_SLA_REMINDER_COOLDOWN_MINUTES = request.sla_reminder_cooldown_minutes
|
||||
if request.support_system_mode is not None:
|
||||
settings.SUPPORT_SYSTEM_MODE = request.support_system_mode.strip().lower()
|
||||
SupportSettingsService.set_system_mode(request.support_system_mode.strip().lower())
|
||||
|
||||
# Update cabinet notification settings
|
||||
if request.cabinet_user_notifications_enabled is not None:
|
||||
|
||||
@@ -86,7 +86,7 @@ async def get_user_by_id(db: AsyncSession, user_id: int) -> User | None:
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
.options(
|
||||
selectinload(User.subscription),
|
||||
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
||||
selectinload(User.referrer),
|
||||
selectinload(User.promo_group),
|
||||
@@ -106,7 +106,7 @@ async def get_user_by_telegram_id(db: AsyncSession, telegram_id: int) -> User |
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
.options(
|
||||
selectinload(User.subscription),
|
||||
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
||||
selectinload(User.referrer),
|
||||
selectinload(User.promo_group),
|
||||
@@ -131,7 +131,7 @@ async def get_user_by_username(db: AsyncSession, username: str) -> User | None:
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
.options(
|
||||
selectinload(User.subscription),
|
||||
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
||||
selectinload(User.referrer),
|
||||
selectinload(User.promo_group),
|
||||
@@ -152,7 +152,7 @@ async def get_user_by_referral_code(db: AsyncSession, referral_code: str) -> Use
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
.options(
|
||||
selectinload(User.subscription),
|
||||
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||
selectinload(User.promo_group),
|
||||
selectinload(User.referrer),
|
||||
)
|
||||
@@ -171,7 +171,7 @@ async def get_user_by_remnawave_uuid(db: AsyncSession, remnawave_uuid: str) -> U
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
.options(
|
||||
selectinload(User.subscription),
|
||||
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||
selectinload(User.promo_group),
|
||||
selectinload(User.referrer),
|
||||
)
|
||||
@@ -719,7 +719,7 @@ async def get_users_list(
|
||||
order_by_purchase_count: bool = False,
|
||||
) -> list[User]:
|
||||
query = select(User).options(
|
||||
selectinload(User.subscription),
|
||||
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||
selectinload(User.promo_group),
|
||||
selectinload(User.referrer),
|
||||
)
|
||||
@@ -879,7 +879,7 @@ async def get_referrals(db: AsyncSession, user_id: int) -> list[User]:
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
.options(
|
||||
selectinload(User.subscription),
|
||||
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
||||
selectinload(User.referrer),
|
||||
selectinload(User.promo_group),
|
||||
@@ -904,7 +904,7 @@ async def get_users_for_promo_segment(db: AsyncSession, segment: str) -> list[Us
|
||||
base_query = (
|
||||
select(User)
|
||||
.options(
|
||||
selectinload(User.subscription),
|
||||
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||
selectinload(User.promo_group),
|
||||
selectinload(User.referrer),
|
||||
)
|
||||
@@ -966,7 +966,7 @@ async def get_inactive_users(db: AsyncSession, months: int = 3) -> list[User]:
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
.options(
|
||||
selectinload(User.subscription),
|
||||
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
||||
selectinload(User.referrer),
|
||||
selectinload(User.promo_group),
|
||||
@@ -1050,7 +1050,7 @@ async def get_users_with_active_subscriptions(db: AsyncSession) -> list[User]:
|
||||
Subscription.end_date > current_time,
|
||||
)
|
||||
)
|
||||
.options(selectinload(User.subscription))
|
||||
.options(selectinload(User.subscription).selectinload(Subscription.tariff))
|
||||
)
|
||||
|
||||
return result.scalars().unique().all()
|
||||
|
||||
@@ -3628,9 +3628,8 @@ async def set_user_devices_button(callback: types.CallbackQuery, db_user: User,
|
||||
await callback.answer()
|
||||
|
||||
logger.info(
|
||||
'Админ модем для пользователя', telegram_id=db_user.telegram_id, action_text=action_text, user_id=user_id
|
||||
'Админ изменил устройства для пользователя', telegram_id=db_user.telegram_id, devices=devices, user_id=user_id
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
|
||||
@@ -1547,7 +1547,7 @@ async def handle_extend_subscription(callback: types.CallbackQuery, db_user: Use
|
||||
# Проверяем, суточный ли тариф — у суточных нет period_prices, продление через resume
|
||||
from app.database.crud.tariff import get_tariff_by_id
|
||||
|
||||
tariff = getattr(subscription, 'tariff', None) or await get_tariff_by_id(db, subscription.tariff_id)
|
||||
tariff = await get_tariff_by_id(db, subscription.tariff_id)
|
||||
if tariff and getattr(tariff, 'is_daily', False):
|
||||
# Суточный тариф: перенаправляем на страницу подписки (там кнопка «Возобновить»)
|
||||
await show_subscription_info(callback, db_user, db)
|
||||
|
||||
@@ -48,6 +48,14 @@ def _clean_logger_name(logger: Any, method_name: str, event_dict: dict[str, Any]
|
||||
return event_dict
|
||||
|
||||
|
||||
def _prefix_logger_name(logger: Any, method_name: str, event_dict: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Move logger name before event text: [module.name] event text."""
|
||||
logger_name = event_dict.pop('logger', None)
|
||||
if logger_name:
|
||||
event_dict['event'] = f'[{logger_name}] {event_dict.get("event", "")}'
|
||||
return event_dict
|
||||
|
||||
|
||||
def setup_logging() -> tuple[logging.Formatter, logging.Formatter, Any]:
|
||||
"""Configure structlog and return formatters + notifier.
|
||||
|
||||
@@ -103,6 +111,7 @@ def setup_logging() -> tuple[logging.Formatter, logging.Formatter, Any]:
|
||||
foreign_pre_chain=shared_processors,
|
||||
processors=[
|
||||
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
||||
_prefix_logger_name,
|
||||
structlog.dev.ConsoleRenderer(
|
||||
colors=False,
|
||||
pad_event_to=0,
|
||||
@@ -118,6 +127,7 @@ def setup_logging() -> tuple[logging.Formatter, logging.Formatter, Any]:
|
||||
foreign_pre_chain=shared_processors,
|
||||
processors=[
|
||||
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
||||
_prefix_logger_name,
|
||||
structlog.dev.ConsoleRenderer(
|
||||
pad_event_to=0,
|
||||
pad_level=False,
|
||||
|
||||
@@ -57,7 +57,6 @@ from app.services.notification_delivery_service import (
|
||||
notification_delivery_service,
|
||||
)
|
||||
from app.services.notification_settings_service import NotificationSettingsService
|
||||
from app.services.payment_service import PaymentService
|
||||
from app.services.promo_offer_service import promo_offer_service
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
from app.utils.cache import cache
|
||||
@@ -83,7 +82,6 @@ class MonitoringService:
|
||||
def __init__(self, bot=None):
|
||||
self.is_running = False
|
||||
self.subscription_service = SubscriptionService()
|
||||
self.payment_service = PaymentService()
|
||||
self.bot = bot
|
||||
self._notified_users: set[str] = set()
|
||||
self._last_cleanup = datetime.utcnow()
|
||||
|
||||
@@ -62,6 +62,7 @@ class SupportSettingsService:
|
||||
return False
|
||||
cls._load()
|
||||
cls._data['system_mode'] = mode_clean
|
||||
settings.SUPPORT_SYSTEM_MODE = mode_clean
|
||||
return cls._save()
|
||||
|
||||
# Main menu visibility
|
||||
|
||||
@@ -1603,6 +1603,13 @@ class BotConfigurationService:
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error('Не удалось обновить сервис автосинхронизации RemnaWave', error=error)
|
||||
elif key == 'SUPPORT_SYSTEM_MODE':
|
||||
try:
|
||||
from app.services.support_settings_service import SupportSettingsService
|
||||
|
||||
SupportSettingsService.set_system_mode(str(value))
|
||||
except Exception as error:
|
||||
logger.error('Не удалось синхронизировать SupportSettingsService', error=error)
|
||||
elif key in {
|
||||
'REMNAWAVE_API_URL',
|
||||
'REMNAWAVE_API_KEY',
|
||||
|
||||
@@ -1,11 +1,58 @@
|
||||
import platform
|
||||
import time
|
||||
import unicodedata
|
||||
from collections.abc import Iterable, Sequence
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _char_width(ch: str) -> int:
|
||||
"""Return terminal display width of a single character."""
|
||||
cp = ord(ch)
|
||||
# Variation selector U+FE0F / U+FE0E — zero width (handled by caller)
|
||||
if cp in (0xFE0E, 0xFE0F, 0x200D):
|
||||
return 0
|
||||
# Combining marks — zero width
|
||||
if unicodedata.category(ch).startswith('M'):
|
||||
return 0
|
||||
# East Asian Wide / Fullwidth
|
||||
if unicodedata.east_asian_width(ch) in ('W', 'F'):
|
||||
return 2
|
||||
return 1
|
||||
|
||||
|
||||
def _display_width(text: str) -> int:
|
||||
"""Calculate terminal display width accounting for wide chars and emoji."""
|
||||
width = 0
|
||||
prev_base = 0
|
||||
for ch in text:
|
||||
cp = ord(ch)
|
||||
# U+FE0F emoji presentation selector — upgrades previous char to 2 cells
|
||||
if cp == 0xFE0F:
|
||||
if prev_base == 1:
|
||||
width += 1 # upgrade 1 → 2
|
||||
prev_base = 2
|
||||
continue
|
||||
cw = _char_width(ch)
|
||||
if cw > 0:
|
||||
prev_base = cw
|
||||
width += cw
|
||||
return width
|
||||
|
||||
|
||||
def _ljust(text: str, width: int) -> str:
|
||||
"""Left-justify text to given display width."""
|
||||
return text + ' ' * max(0, width - _display_width(text))
|
||||
|
||||
|
||||
def _center(text: str, width: int) -> str:
|
||||
"""Center text to given display width."""
|
||||
pad = max(0, width - _display_width(text))
|
||||
left = pad // 2
|
||||
return ' ' * left + text + ' ' * (pad - left)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StepRecord:
|
||||
title: str
|
||||
@@ -85,25 +132,25 @@ class StartupTimeline:
|
||||
subtitle_parts.append(f'{key}: {value}')
|
||||
subtitle_text = ' | '.join(subtitle_parts)
|
||||
|
||||
width = max(len(title_text), len(subtitle_text))
|
||||
width = max(_display_width(title_text), _display_width(subtitle_text))
|
||||
border = '╔' + '═' * (width + 2) + '╗'
|
||||
self.logger.info(border)
|
||||
self.logger.info('║ ' + title_text.ljust(width) + ' ║')
|
||||
self.logger.info('║ ' + subtitle_text.ljust(width) + ' ║')
|
||||
self.logger.info('║ ' + _ljust(title_text, width) + ' ║')
|
||||
self.logger.info('║ ' + _ljust(subtitle_text, width) + ' ║')
|
||||
self.logger.info('╚' + '═' * (width + 2) + '╝')
|
||||
|
||||
def log_section(self, title: str, lines: Iterable[str], icon: str = '📄') -> None:
|
||||
items = [f'{icon} {title}'] + [f'• {line}' for line in lines]
|
||||
width = max(len(item) for item in items)
|
||||
width = max(_display_width(item) for item in items)
|
||||
top = '┌ ' + '─' * width + ' ┐'
|
||||
middle = '├ ' + '─' * width + ' ┤'
|
||||
bottom = '└ ' + '─' * width + ' ┘'
|
||||
|
||||
self.logger.info(top)
|
||||
self.logger.info('│ ' + items[0].ljust(width) + ' │')
|
||||
self.logger.info('│ ' + _ljust(items[0], width) + ' │')
|
||||
self.logger.info(middle)
|
||||
for item in items[1:]:
|
||||
self.logger.info('│ ' + item.ljust(width) + ' │')
|
||||
self.logger.info('│ ' + _ljust(item, width) + ' │')
|
||||
self.logger.info(bottom)
|
||||
|
||||
def add_manual_step(
|
||||
@@ -169,15 +216,15 @@ class StartupTimeline:
|
||||
base += f' :: {step.message}'
|
||||
lines.append(base)
|
||||
|
||||
width = max(len(line) for line in lines)
|
||||
width = max(_display_width(line) for line in lines)
|
||||
border_top = '┏' + '━' * (width + 2) + '┓'
|
||||
border_mid = '┣' + '━' * (width + 2) + '┫'
|
||||
border_bottom = '┗' + '━' * (width + 2) + '┛'
|
||||
title = 'РЕЗЮМЕ ЗАПУСКА'
|
||||
|
||||
self.logger.info(border_top)
|
||||
self.logger.info('┃ ' + title.center(width) + ' ┃')
|
||||
self.logger.info('┃ ' + _center(title, width) + ' ┃')
|
||||
self.logger.info(border_mid)
|
||||
for line in lines:
|
||||
self.logger.info('┃ ' + line.ljust(width) + ' ┃')
|
||||
self.logger.info('┃ ' + _ljust(line, width) + ' ┃')
|
||||
self.logger.info(border_bottom)
|
||||
|
||||
10
main.py
10
main.py
@@ -119,6 +119,7 @@ async def main():
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, settings.LOG_LEVEL),
|
||||
handlers=log_handlers,
|
||||
force=True,
|
||||
)
|
||||
|
||||
# Регистрируем хэндлеры для управления при ротации
|
||||
@@ -137,6 +138,7 @@ async def main():
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, settings.LOG_LEVEL),
|
||||
handlers=log_handlers,
|
||||
force=True,
|
||||
)
|
||||
|
||||
# NOTE: TelegramNotifierProcessor and noisy logger suppression are
|
||||
@@ -684,8 +686,16 @@ async def main():
|
||||
webhook_lines.append(f'WATA: {_fmt(settings.WATA_WEBHOOK_PATH)}')
|
||||
if settings.is_heleket_enabled():
|
||||
webhook_lines.append(f'Heleket: {_fmt(settings.HELEKET_WEBHOOK_PATH)}')
|
||||
if settings.is_platega_enabled():
|
||||
webhook_lines.append(f'Platega: {_fmt(settings.PLATEGA_WEBHOOK_PATH)}')
|
||||
if settings.is_cloudpayments_enabled():
|
||||
webhook_lines.append(f'CloudPayments: {_fmt(settings.CLOUDPAYMENTS_WEBHOOK_PATH)}')
|
||||
if settings.is_freekassa_enabled():
|
||||
webhook_lines.append(f'Freekassa: {_fmt(settings.FREEKASSA_WEBHOOK_PATH)}')
|
||||
if settings.is_kassa_ai_enabled():
|
||||
webhook_lines.append(f'Kassa.ai: {_fmt(settings.KASSA_AI_WEBHOOK_PATH)}')
|
||||
if settings.is_remnawave_webhook_enabled():
|
||||
webhook_lines.append(f'RemnaWave: {_fmt(settings.REMNAWAVE_WEBHOOK_PATH)}')
|
||||
|
||||
timeline.log_section(
|
||||
'Активные webhook endpoints',
|
||||
|
||||
Reference in New Issue
Block a user