Merge pull request #2612 from BEDOLAGA-DEV/dev

Dev
This commit is contained in:
Egor
2026-02-16 19:23:30 +03:00
committed by GitHub
10 changed files with 97 additions and 25 deletions

View File

@@ -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:

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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()

View File

@@ -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

View File

@@ -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',

View File

@@ -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
View File

@@ -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',