Compare commits

..

13 Commits

Author SHA1 Message Date
Egor 7628fb9f6e Merge pull request #2613 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.14.0
2026-02-16 19:26:11 +03:00
github-actions[bot] 4c48eadebc chore(main): release 3.14.0 2026-02-16 16:23:56 +00:00
Egor 6ea3860a2f Merge pull request #2612 from BEDOLAGA-DEV/dev
Dev
2026-02-16 19:23:30 +03:00
Fringg 1b8ef69a1b fix: NameError in set_user_devices_button — undefined action_text
Replaced undefined action_text with devices (the actual value being set).
Removed duplicate await callback.answer() call.
2026-02-16 19:09:52 +03:00
Fringg 9d710050ad feat: show all active webhook endpoints in startup log
Added missing webhook endpoints to the startup section:
Platega, CloudPayments, Kassa.ai, and RemnaWave webhook.
2026-02-16 19:08:49 +03:00
Fringg 491a7e1c42 fix: remove unused PaymentService from MonitoringService init
MonitoringService instantiated PaymentService() at module level during
import, triggering a debug log before structlog/logging were configured.
This caused [debug    ] with padded spaces (structlog default pad_level)
and appeared 7 seconds before the startup banner. The payment_service
attribute was never used in MonitoringService.
2026-02-16 19:02:57 +03:00
Fringg 7eb8d4e153 fix: force basicConfig to replace pre-existing handlers
logging.basicConfig() silently does nothing if the root logger already
has handlers. When import-time side effects trigger stdlib logging before
main() configures formatters, our ProcessorFormatter with pad_level=False
never gets applied — producing [debug    ] instead of [debug].
2026-02-16 18:49:39 +03:00
Fringg f63720467a refactor: improve log formatting — logger name prefix and table alignment
1. Add _prefix_logger_name processor that moves [module.name] before
   event text for consistent format: timestamp [level] [module] message
2. Fix startup summary table alignment by using display width calculation
   instead of len() — properly accounts for wide emoji and variation
   selectors that render as 2 terminal cells
2026-02-16 18:33:40 +03:00
Fringg 516be6e600 fix: sync support mode from cabinet admin to SupportSettingsService
Cabinet admin endpoint was setting settings.SUPPORT_SYSTEM_MODE directly
without updating SupportSettingsService JSON, causing bot to show stale
mode. Now routes through set_system_mode() which updates both stores.
2026-02-16 18:24:27 +03:00
Fringg 0807a9ff19 fix: sync SUPPORT_SYSTEM_MODE between SystemSettings and SupportSettings
When changing SUPPORT_SYSTEM_MODE via system settings admin panel, the
SupportSettingsService JSON cache was not updated, causing the old value
to take priority. Now both services stay in sync bidirectionally.
2026-02-16 18:22:44 +03:00
Fringg a93a32f3a7 fix: resolve MissingGreenlet error when accessing subscription.tariff
Add .selectinload(Subscription.tariff) chain to all User queries that
load subscriptions, preventing lazy loading of the tariff relationship
in async context. Also replace unsafe getattr(subscription, 'tariff')
with explicit async get_tariff_by_id() in handle_extend_subscription.
2026-02-16 17:54:43 +03:00
Egor 68de66f526 Merge pull request #2610 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.13.0
2026-02-16 10:12:33 +03:00
github-actions[bot] 15aba2b3db chore(main): release 3.13.0 2026-02-16 07:11:21 +00:00
18 changed files with 149 additions and 34 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0
+3 -3
View File
@@ -10,11 +10,11 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
- uses: astral-sh/setup-uv@v5
- uses: actions/setup-python@v6
- uses: actions/setup-python@v5
with:
python-version: '3.13'
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "3.12.1"
".": "3.14.0"
}
+43
View File
@@ -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
View File
@@ -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
+1 -1
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:
+10 -10
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()
+1 -2
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
+1 -1
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)
+10
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,
-2
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()
+1
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
+7
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',
+56 -9
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
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',
+1 -1
View File
@@ -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' }