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
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
||||
|
||||
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 VCS_REF
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
+10
-10
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = 'remnawave-bedolaga-telegram-bot'
|
||||
version = "3.12.1"
|
||||
version = "3.14.0"
|
||||
description = 'Telegram bot for RemnaWave VPN service'
|
||||
readme = 'README.md'
|
||||
license = { text = 'MIT' }
|
||||
|
||||
Reference in New Issue
Block a user