Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7628fb9f6e | |||
| 4c48eadebc | |||
| 6ea3860a2f | |||
| 1b8ef69a1b | |||
| 9d710050ad | |||
| 491a7e1c42 | |||
| 7eb8d4e153 | |||
| f63720467a | |||
| 516be6e600 | |||
| 0807a9ff19 | |||
| a93a32f3a7 | |||
| 68de66f526 | |||
| 15aba2b3db |
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
".": "3.12.1"
|
".": "3.14.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,48 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [3.14.0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.13.0...v3.14.0) (2026-02-16)
|
||||||
|
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
* show all active webhook endpoints in startup log ([9d71005](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/9d710050ad40ba76a14aa6ace8e8a47f25cdde94))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* force basicConfig to replace pre-existing handlers ([7eb8d4e](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/7eb8d4e153bab640a5829f75bfa6f70df5763284))
|
||||||
|
* NameError in set_user_devices_button — undefined action_text ([1b8ef69](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/1b8ef69a1bbb7d8d86827cf7aaa4f05cbf480d75))
|
||||||
|
* remove unused PaymentService from MonitoringService init ([491a7e1](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/491a7e1c425a355e55b3020e2bcc7b96047bdf5e))
|
||||||
|
* resolve MissingGreenlet error when accessing subscription.tariff ([a93a32f](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/a93a32f3a7d1b259a2e24954ae5d2b7c966c5639))
|
||||||
|
* sync support mode from cabinet admin to SupportSettingsService ([516be6e](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/516be6e600a08ad700d83b793dc64b2ca07bdf44))
|
||||||
|
* sync SUPPORT_SYSTEM_MODE between SystemSettings and SupportSettings ([0807a9f](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/0807a9ff19d1eb4f1204f7cbeb1da1c1cfefe83a))
|
||||||
|
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
* improve log formatting — logger name prefix and table alignment ([f637204](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/f63720467a935bdaaa58bb34d588d65e46698f26))
|
||||||
|
|
||||||
|
## [3.13.0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.12.1...v3.13.0) (2026-02-16)
|
||||||
|
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
* colored console logs via structlog + rich + FORCE_COLOR ([bf64611](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/bf646112df02aa7aa7918d0513cb6968ceb7f378))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* limit Rich traceback output to prevent console flood ([11ef714](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/11ef714e0dde25a08711c0daeee943b6e71e20b7))
|
||||||
|
* resolve exc_info for admin notifications, clean log formatting ([11f8af0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/11f8af003fc60384abafa2b670b89d6ad3ac57a4))
|
||||||
|
* suppress startup log noise (~350 lines → ~30) ([8a6650e](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/8a6650e57cd8ea396d9b057a7753469947f38d29))
|
||||||
|
* traceback in Telegram notifications + reduce log padding ([909a403](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/909a4039c43b910761bd05c36e79c8e6773199db))
|
||||||
|
* use sync context manager for structlog bound_contextvars ([25e8c9f](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/25e8c9f8fc4d2c66d5a1407d3de5c7402dc596da))
|
||||||
|
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
* complete structlog migration with contextvars, kwargs, and logging hardening ([1f0fef1](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/1f0fef114bd979b2b0d2bd38dde6ce05e7bba07b))
|
||||||
|
|
||||||
## [3.12.1](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.12.0...v3.12.1) (2026-02-16)
|
## [3.12.1](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.12.0...v3.12.1) (2026-02-16)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
|||||||
|
|
||||||
FROM python:3.13-slim
|
FROM python:3.13-slim
|
||||||
|
|
||||||
ARG VERSION="v3.12.1" # x-release-please-version
|
ARG VERSION="v3.14.0" # x-release-please-version
|
||||||
ARG BUILD_DATE
|
ARG BUILD_DATE
|
||||||
ARG VCS_REF
|
ARG VCS_REF
|
||||||
|
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ async def update_ticket_settings(
|
|||||||
if request.sla_reminder_cooldown_minutes is not None:
|
if request.sla_reminder_cooldown_minutes is not None:
|
||||||
settings.SUPPORT_TICKET_SLA_REMINDER_COOLDOWN_MINUTES = request.sla_reminder_cooldown_minutes
|
settings.SUPPORT_TICKET_SLA_REMINDER_COOLDOWN_MINUTES = request.sla_reminder_cooldown_minutes
|
||||||
if request.support_system_mode is not None:
|
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
|
# Update cabinet notification settings
|
||||||
if request.cabinet_user_notifications_enabled is not None:
|
if request.cabinet_user_notifications_enabled is not None:
|
||||||
|
|||||||
+10
-10
@@ -86,7 +86,7 @@ async def get_user_by_id(db: AsyncSession, user_id: int) -> User | None:
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(User)
|
select(User)
|
||||||
.options(
|
.options(
|
||||||
selectinload(User.subscription),
|
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||||
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
||||||
selectinload(User.referrer),
|
selectinload(User.referrer),
|
||||||
selectinload(User.promo_group),
|
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(
|
result = await db.execute(
|
||||||
select(User)
|
select(User)
|
||||||
.options(
|
.options(
|
||||||
selectinload(User.subscription),
|
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||||
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
||||||
selectinload(User.referrer),
|
selectinload(User.referrer),
|
||||||
selectinload(User.promo_group),
|
selectinload(User.promo_group),
|
||||||
@@ -131,7 +131,7 @@ async def get_user_by_username(db: AsyncSession, username: str) -> User | None:
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(User)
|
select(User)
|
||||||
.options(
|
.options(
|
||||||
selectinload(User.subscription),
|
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||||
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
||||||
selectinload(User.referrer),
|
selectinload(User.referrer),
|
||||||
selectinload(User.promo_group),
|
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(
|
result = await db.execute(
|
||||||
select(User)
|
select(User)
|
||||||
.options(
|
.options(
|
||||||
selectinload(User.subscription),
|
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||||
selectinload(User.promo_group),
|
selectinload(User.promo_group),
|
||||||
selectinload(User.referrer),
|
selectinload(User.referrer),
|
||||||
)
|
)
|
||||||
@@ -171,7 +171,7 @@ async def get_user_by_remnawave_uuid(db: AsyncSession, remnawave_uuid: str) -> U
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(User)
|
select(User)
|
||||||
.options(
|
.options(
|
||||||
selectinload(User.subscription),
|
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||||
selectinload(User.promo_group),
|
selectinload(User.promo_group),
|
||||||
selectinload(User.referrer),
|
selectinload(User.referrer),
|
||||||
)
|
)
|
||||||
@@ -719,7 +719,7 @@ async def get_users_list(
|
|||||||
order_by_purchase_count: bool = False,
|
order_by_purchase_count: bool = False,
|
||||||
) -> list[User]:
|
) -> list[User]:
|
||||||
query = select(User).options(
|
query = select(User).options(
|
||||||
selectinload(User.subscription),
|
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||||
selectinload(User.promo_group),
|
selectinload(User.promo_group),
|
||||||
selectinload(User.referrer),
|
selectinload(User.referrer),
|
||||||
)
|
)
|
||||||
@@ -879,7 +879,7 @@ async def get_referrals(db: AsyncSession, user_id: int) -> list[User]:
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(User)
|
select(User)
|
||||||
.options(
|
.options(
|
||||||
selectinload(User.subscription),
|
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||||
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
||||||
selectinload(User.referrer),
|
selectinload(User.referrer),
|
||||||
selectinload(User.promo_group),
|
selectinload(User.promo_group),
|
||||||
@@ -904,7 +904,7 @@ async def get_users_for_promo_segment(db: AsyncSession, segment: str) -> list[Us
|
|||||||
base_query = (
|
base_query = (
|
||||||
select(User)
|
select(User)
|
||||||
.options(
|
.options(
|
||||||
selectinload(User.subscription),
|
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||||
selectinload(User.promo_group),
|
selectinload(User.promo_group),
|
||||||
selectinload(User.referrer),
|
selectinload(User.referrer),
|
||||||
)
|
)
|
||||||
@@ -966,7 +966,7 @@ async def get_inactive_users(db: AsyncSession, months: int = 3) -> list[User]:
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(User)
|
select(User)
|
||||||
.options(
|
.options(
|
||||||
selectinload(User.subscription),
|
selectinload(User.subscription).selectinload(Subscription.tariff),
|
||||||
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
|
||||||
selectinload(User.referrer),
|
selectinload(User.referrer),
|
||||||
selectinload(User.promo_group),
|
selectinload(User.promo_group),
|
||||||
@@ -1050,7 +1050,7 @@ async def get_users_with_active_subscriptions(db: AsyncSession) -> list[User]:
|
|||||||
Subscription.end_date > current_time,
|
Subscription.end_date > current_time,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.options(selectinload(User.subscription))
|
.options(selectinload(User.subscription).selectinload(Subscription.tariff))
|
||||||
)
|
)
|
||||||
|
|
||||||
return result.scalars().unique().all()
|
return result.scalars().unique().all()
|
||||||
|
|||||||
@@ -3628,9 +3628,8 @@ async def set_user_devices_button(callback: types.CallbackQuery, db_user: User,
|
|||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
logger.info(
|
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
|
@admin_required
|
||||||
|
|||||||
@@ -1547,7 +1547,7 @@ async def handle_extend_subscription(callback: types.CallbackQuery, db_user: Use
|
|||||||
# Проверяем, суточный ли тариф — у суточных нет period_prices, продление через resume
|
# Проверяем, суточный ли тариф — у суточных нет period_prices, продление через resume
|
||||||
from app.database.crud.tariff import get_tariff_by_id
|
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):
|
if tariff and getattr(tariff, 'is_daily', False):
|
||||||
# Суточный тариф: перенаправляем на страницу подписки (там кнопка «Возобновить»)
|
# Суточный тариф: перенаправляем на страницу подписки (там кнопка «Возобновить»)
|
||||||
await show_subscription_info(callback, db_user, db)
|
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
|
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]:
|
def setup_logging() -> tuple[logging.Formatter, logging.Formatter, Any]:
|
||||||
"""Configure structlog and return formatters + notifier.
|
"""Configure structlog and return formatters + notifier.
|
||||||
|
|
||||||
@@ -103,6 +111,7 @@ def setup_logging() -> tuple[logging.Formatter, logging.Formatter, Any]:
|
|||||||
foreign_pre_chain=shared_processors,
|
foreign_pre_chain=shared_processors,
|
||||||
processors=[
|
processors=[
|
||||||
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
||||||
|
_prefix_logger_name,
|
||||||
structlog.dev.ConsoleRenderer(
|
structlog.dev.ConsoleRenderer(
|
||||||
colors=False,
|
colors=False,
|
||||||
pad_event_to=0,
|
pad_event_to=0,
|
||||||
@@ -118,6 +127,7 @@ def setup_logging() -> tuple[logging.Formatter, logging.Formatter, Any]:
|
|||||||
foreign_pre_chain=shared_processors,
|
foreign_pre_chain=shared_processors,
|
||||||
processors=[
|
processors=[
|
||||||
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
||||||
|
_prefix_logger_name,
|
||||||
structlog.dev.ConsoleRenderer(
|
structlog.dev.ConsoleRenderer(
|
||||||
pad_event_to=0,
|
pad_event_to=0,
|
||||||
pad_level=False,
|
pad_level=False,
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ from app.services.notification_delivery_service import (
|
|||||||
notification_delivery_service,
|
notification_delivery_service,
|
||||||
)
|
)
|
||||||
from app.services.notification_settings_service import NotificationSettingsService
|
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.promo_offer_service import promo_offer_service
|
||||||
from app.services.subscription_service import SubscriptionService
|
from app.services.subscription_service import SubscriptionService
|
||||||
from app.utils.cache import cache
|
from app.utils.cache import cache
|
||||||
@@ -83,7 +82,6 @@ class MonitoringService:
|
|||||||
def __init__(self, bot=None):
|
def __init__(self, bot=None):
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.subscription_service = SubscriptionService()
|
self.subscription_service = SubscriptionService()
|
||||||
self.payment_service = PaymentService()
|
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self._notified_users: set[str] = set()
|
self._notified_users: set[str] = set()
|
||||||
self._last_cleanup = datetime.utcnow()
|
self._last_cleanup = datetime.utcnow()
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class SupportSettingsService:
|
|||||||
return False
|
return False
|
||||||
cls._load()
|
cls._load()
|
||||||
cls._data['system_mode'] = mode_clean
|
cls._data['system_mode'] = mode_clean
|
||||||
|
settings.SUPPORT_SYSTEM_MODE = mode_clean
|
||||||
return cls._save()
|
return cls._save()
|
||||||
|
|
||||||
# Main menu visibility
|
# Main menu visibility
|
||||||
|
|||||||
@@ -1603,6 +1603,13 @@ class BotConfigurationService:
|
|||||||
)
|
)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.error('Не удалось обновить сервис автосинхронизации RemnaWave', error=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 {
|
elif key in {
|
||||||
'REMNAWAVE_API_URL',
|
'REMNAWAVE_API_URL',
|
||||||
'REMNAWAVE_API_KEY',
|
'REMNAWAVE_API_KEY',
|
||||||
|
|||||||
@@ -1,11 +1,58 @@
|
|||||||
import platform
|
import platform
|
||||||
import time
|
import time
|
||||||
|
import unicodedata
|
||||||
from collections.abc import Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
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
|
@dataclass
|
||||||
class StepRecord:
|
class StepRecord:
|
||||||
title: str
|
title: str
|
||||||
@@ -85,25 +132,25 @@ class StartupTimeline:
|
|||||||
subtitle_parts.append(f'{key}: {value}')
|
subtitle_parts.append(f'{key}: {value}')
|
||||||
subtitle_text = ' | '.join(subtitle_parts)
|
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) + '╗'
|
border = '╔' + '═' * (width + 2) + '╗'
|
||||||
self.logger.info(border)
|
self.logger.info(border)
|
||||||
self.logger.info('║ ' + title_text.ljust(width) + ' ║')
|
self.logger.info('║ ' + _ljust(title_text, width) + ' ║')
|
||||||
self.logger.info('║ ' + subtitle_text.ljust(width) + ' ║')
|
self.logger.info('║ ' + _ljust(subtitle_text, width) + ' ║')
|
||||||
self.logger.info('╚' + '═' * (width + 2) + '╝')
|
self.logger.info('╚' + '═' * (width + 2) + '╝')
|
||||||
|
|
||||||
def log_section(self, title: str, lines: Iterable[str], icon: str = '📄') -> None:
|
def log_section(self, title: str, lines: Iterable[str], icon: str = '📄') -> None:
|
||||||
items = [f'{icon} {title}'] + [f'• {line}' for line in lines]
|
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 + ' ┐'
|
top = '┌ ' + '─' * width + ' ┐'
|
||||||
middle = '├ ' + '─' * width + ' ┤'
|
middle = '├ ' + '─' * width + ' ┤'
|
||||||
bottom = '└ ' + '─' * width + ' ┘'
|
bottom = '└ ' + '─' * width + ' ┘'
|
||||||
|
|
||||||
self.logger.info(top)
|
self.logger.info(top)
|
||||||
self.logger.info('│ ' + items[0].ljust(width) + ' │')
|
self.logger.info('│ ' + _ljust(items[0], width) + ' │')
|
||||||
self.logger.info(middle)
|
self.logger.info(middle)
|
||||||
for item in items[1:]:
|
for item in items[1:]:
|
||||||
self.logger.info('│ ' + item.ljust(width) + ' │')
|
self.logger.info('│ ' + _ljust(item, width) + ' │')
|
||||||
self.logger.info(bottom)
|
self.logger.info(bottom)
|
||||||
|
|
||||||
def add_manual_step(
|
def add_manual_step(
|
||||||
@@ -169,15 +216,15 @@ class StartupTimeline:
|
|||||||
base += f' :: {step.message}'
|
base += f' :: {step.message}'
|
||||||
lines.append(base)
|
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_top = '┏' + '━' * (width + 2) + '┓'
|
||||||
border_mid = '┣' + '━' * (width + 2) + '┫'
|
border_mid = '┣' + '━' * (width + 2) + '┫'
|
||||||
border_bottom = '┗' + '━' * (width + 2) + '┛'
|
border_bottom = '┗' + '━' * (width + 2) + '┛'
|
||||||
title = 'РЕЗЮМЕ ЗАПУСКА'
|
title = 'РЕЗЮМЕ ЗАПУСКА'
|
||||||
|
|
||||||
self.logger.info(border_top)
|
self.logger.info(border_top)
|
||||||
self.logger.info('┃ ' + title.center(width) + ' ┃')
|
self.logger.info('┃ ' + _center(title, width) + ' ┃')
|
||||||
self.logger.info(border_mid)
|
self.logger.info(border_mid)
|
||||||
for line in lines:
|
for line in lines:
|
||||||
self.logger.info('┃ ' + line.ljust(width) + ' ┃')
|
self.logger.info('┃ ' + _ljust(line, width) + ' ┃')
|
||||||
self.logger.info(border_bottom)
|
self.logger.info(border_bottom)
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ async def main():
|
|||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=getattr(logging, settings.LOG_LEVEL),
|
level=getattr(logging, settings.LOG_LEVEL),
|
||||||
handlers=log_handlers,
|
handlers=log_handlers,
|
||||||
|
force=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Регистрируем хэндлеры для управления при ротации
|
# Регистрируем хэндлеры для управления при ротации
|
||||||
@@ -137,6 +138,7 @@ async def main():
|
|||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=getattr(logging, settings.LOG_LEVEL),
|
level=getattr(logging, settings.LOG_LEVEL),
|
||||||
handlers=log_handlers,
|
handlers=log_handlers,
|
||||||
|
force=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# NOTE: TelegramNotifierProcessor and noisy logger suppression are
|
# NOTE: TelegramNotifierProcessor and noisy logger suppression are
|
||||||
@@ -684,8 +686,16 @@ async def main():
|
|||||||
webhook_lines.append(f'WATA: {_fmt(settings.WATA_WEBHOOK_PATH)}')
|
webhook_lines.append(f'WATA: {_fmt(settings.WATA_WEBHOOK_PATH)}')
|
||||||
if settings.is_heleket_enabled():
|
if settings.is_heleket_enabled():
|
||||||
webhook_lines.append(f'Heleket: {_fmt(settings.HELEKET_WEBHOOK_PATH)}')
|
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():
|
if settings.is_freekassa_enabled():
|
||||||
webhook_lines.append(f'Freekassa: {_fmt(settings.FREEKASSA_WEBHOOK_PATH)}')
|
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(
|
timeline.log_section(
|
||||||
'Активные webhook endpoints',
|
'Активные webhook endpoints',
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = 'remnawave-bedolaga-telegram-bot'
|
name = 'remnawave-bedolaga-telegram-bot'
|
||||||
version = "3.12.1"
|
version = "3.14.0"
|
||||||
description = 'Telegram bot for RemnaWave VPN service'
|
description = 'Telegram bot for RemnaWave VPN service'
|
||||||
readme = 'README.md'
|
readme = 'README.md'
|
||||||
license = { text = 'MIT' }
|
license = { text = 'MIT' }
|
||||||
|
|||||||
Reference in New Issue
Block a user