Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] e375252d65 ci(deps): bump the github-actions group across 1 directory with 3 updates
Bumps the github-actions group with 3 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [actions/setup-python](https://github.com/actions/setup-python).


Updates `actions/checkout` from 4 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

Updates `astral-sh/setup-uv` from 5 to 7
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v7)

Updates `actions/setup-python` from 5 to 6
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: astral-sh/setup-uv
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 07:12:26 +00:00
18 changed files with 34 additions and 149 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
+3 -3
View File
@@ -10,11 +10,11 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
- uses: astral-sh/setup-uv@v7
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
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@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "3.14.0"
".": "3.12.1"
}
-43
View File
@@ -1,48 +1,5 @@
# 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.14.0" # x-release-please-version
ARG VERSION="v3.12.1" # 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:
SupportSettingsService.set_system_mode(request.support_system_mode.strip().lower())
settings.SUPPORT_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(Subscription.tariff),
selectinload(User.subscription),
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(Subscription.tariff),
selectinload(User.subscription),
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(Subscription.tariff),
selectinload(User.subscription),
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(Subscription.tariff),
selectinload(User.subscription),
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(Subscription.tariff),
selectinload(User.subscription),
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(Subscription.tariff),
selectinload(User.subscription),
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(Subscription.tariff),
selectinload(User.subscription),
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(Subscription.tariff),
selectinload(User.subscription),
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(Subscription.tariff),
selectinload(User.subscription),
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).selectinload(Subscription.tariff))
.options(selectinload(User.subscription))
)
return result.scalars().unique().all()
+2 -1
View File
@@ -3628,8 +3628,9 @@ async def set_user_devices_button(callback: types.CallbackQuery, db_user: User,
await callback.answer()
logger.info(
'Админ изменил устройства для пользователя', telegram_id=db_user.telegram_id, devices=devices, user_id=user_id
'Админ модем для пользователя', telegram_id=db_user.telegram_id, action_text=action_text, 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 = await get_tariff_by_id(db, subscription.tariff_id)
tariff = getattr(subscription, 'tariff', None) or 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,14 +48,6 @@ 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.
@@ -111,7 +103,6 @@ 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,
@@ -127,7 +118,6 @@ 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,6 +57,7 @@ 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
@@ -82,6 +83,7 @@ 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,7 +62,6 @@ 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,13 +1603,6 @@ 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',
+9 -56
View File
@@ -1,58 +1,11 @@
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
@@ -132,25 +85,25 @@ class StartupTimeline:
subtitle_parts.append(f'{key}: {value}')
subtitle_text = ' | '.join(subtitle_parts)
width = max(_display_width(title_text), _display_width(subtitle_text))
width = max(len(title_text), len(subtitle_text))
border = '' + '' * (width + 2) + ''
self.logger.info(border)
self.logger.info('' + _ljust(title_text, width) + '')
self.logger.info('' + _ljust(subtitle_text, width) + '')
self.logger.info('' + title_text.ljust(width) + '')
self.logger.info('' + subtitle_text.ljust(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(_display_width(item) for item in items)
width = max(len(item) for item in items)
top = '' + '' * width + ''
middle = '' + '' * width + ''
bottom = '' + '' * width + ''
self.logger.info(top)
self.logger.info('' + _ljust(items[0], width) + '')
self.logger.info('' + items[0].ljust(width) + '')
self.logger.info(middle)
for item in items[1:]:
self.logger.info('' + _ljust(item, width) + '')
self.logger.info('' + item.ljust(width) + '')
self.logger.info(bottom)
def add_manual_step(
@@ -216,15 +169,15 @@ class StartupTimeline:
base += f' :: {step.message}'
lines.append(base)
width = max(_display_width(line) for line in lines)
width = max(len(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('' + _center(title, width) + '')
self.logger.info('' + title.center(width) + '')
self.logger.info(border_mid)
for line in lines:
self.logger.info('' + _ljust(line, width) + '')
self.logger.info('' + line.ljust(width) + '')
self.logger.info(border_bottom)
-10
View File
@@ -119,7 +119,6 @@ async def main():
logging.basicConfig(
level=getattr(logging, settings.LOG_LEVEL),
handlers=log_handlers,
force=True,
)
# Регистрируем хэндлеры для управления при ротации
@@ -138,7 +137,6 @@ async def main():
logging.basicConfig(
level=getattr(logging, settings.LOG_LEVEL),
handlers=log_handlers,
force=True,
)
# NOTE: TelegramNotifierProcessor and noisy logger suppression are
@@ -686,16 +684,8 @@ 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.14.0"
version = "3.12.1"
description = 'Telegram bot for RemnaWave VPN service'
readme = 'README.md'
license = { text = 'MIT' }