fix: broadcast close button, webhook response, FreeKassa nonce and misc fixes

- added get_close_notification_button helper and CLOSE_BUTTON_ID constant
- added close button as a selectable option in broadcast button picker
- renamed get_goto_buttons to get_broadcast_buttons
- fixed webhook endpoint: moved gateway.build_webhook_response out of finally block
- re-enabled YooKassa webhook verification
- fixed FreeKassa nonce: switched to time.time_ns() for strict monotonicity
- fixed FreeKassa currency: use .upper() instead of .value
- fixed system stats getter: use stats.memory.used instead of active, cpu.cores instead of physical_cores
- fixed node traffic_limit: pass None instead of 0 to i18n_format_bytes_to_unit
- added as_payload to UserDevicesUpdatedEvent and SubscriptionRevokedEvent with user keyboard
- added session.close() in UnitOfWork __aexit__
- added python-multipart dependency
- added DOCS constant, updated update keyboard links to docs instead of GitHub README
- bumped version 0.7.5
This commit is contained in:
Ilay
2026-04-09 08:50:45 +05:00
parent 01eb1bd6d2
commit 9664d7e199
15 changed files with 90 additions and 56 deletions

View File

@@ -42,7 +42,7 @@ btn-requirement =
btn-menu =
.trial = 🎁 ПОПРОБОВАТЬ БЕСПЛАТНО
.connect = 🚀 Подключиться
.devices = 📱 Мои устройства
.devices = 📱 Устройства
.subscription = 💳 Подписка
.invite = 👥 Пригласить
.support = 🆘 Поддержка

View File

@@ -687,10 +687,6 @@ msg-remnawave-main =
[one] ядро
[few] ядра
*[more] ядер
} { $cpu_threads } { $cpu_threads ->
[one] поток
[few] потока
*[more] потоков
}
• <b>ОЗУ</b>: { $ram_used } / { $ram_total } ({ $ram_used_percent }%)
• <b>Аптайм</b>: { $uptime }

View File

@@ -10,6 +10,7 @@ dependencies = [
"greenlet>=3.2.4",
"uvicorn>=0.38.0",
"fastapi>=0.120.2",
"python-multipart>=0.0.22",
"remnapy==2.7.0",
#
"dishka~=1.7.2",

View File

@@ -1 +1 @@
__version__ = "0.7.4"
__version__ = "0.7.5"

View File

@@ -283,6 +283,17 @@ class UserDevicesUpdatedEvent(UserEvent):
os_version: Optional[str]
user_agent: Optional[str]
def as_payload(self) -> "MessagePayloadDto":
from src.telegram.keyboards import get_user_keyboard # noqa: PLC0415
return MessagePayloadDto(
i18n_key=self.event_key,
i18n_kwargs={**asdict(self)},
reply_markup=get_user_keyboard(self.telegram_id),
disable_default_markup=False,
delete_after=None,
)
@dataclass(frozen=True, kw_only=True)
class UserDeviceAddedEvent(UserDevicesUpdatedEvent):
@@ -442,6 +453,17 @@ class SubscriptionRevokedEvent(UserEvent):
device_limit: Any
expire_time: Any
def as_payload(self) -> "MessagePayloadDto":
from src.telegram.keyboards import get_user_keyboard # noqa: PLC0415
return MessagePayloadDto(
i18n_key=self.event_key,
i18n_kwargs={**asdict(self)},
reply_markup=get_user_keyboard(self.telegram_id),
disable_default_markup=False,
delete_after=None,
)
@property
def event_key(self) -> str:
return "event-subscription.revoked"

View File

@@ -37,7 +37,7 @@ from src.core.enums import Locale, Role
from src.core.types import AnyKeyboard
from src.infrastructure.services import NotificationQueue
from src.infrastructure.services.event_bus import on_event
from src.telegram.states import Notification
from src.telegram.keyboards import get_close_notification_button
class NotificationService(Notifier):
@@ -292,10 +292,11 @@ class NotificationService(Notifier):
locale: Locale,
chat_id: int,
) -> Optional[AnyKeyboard]:
close_keyboard = self._get_default_keyboard(get_close_notification_button())
if reply_markup is None:
if not disable_default_markup and delete_after is None:
close_button = self._get_close_notification_button(locale=locale)
return self._get_default_keyboard(close_button)
return self._translate_keyboard_text(close_keyboard, locale)
return None
translated_markup = self._translate_keyboard_text(reply_markup, locale)
@@ -305,8 +306,8 @@ class NotificationService(Notifier):
if isinstance(translated_markup, InlineKeyboardMarkup):
builder = InlineKeyboardBuilder.from_markup(translated_markup)
builder.row(self._get_close_notification_button(locale))
return builder.as_markup()
builder.row(get_close_notification_button())
return self._translate_keyboard_text(builder.as_markup(), locale)
logger.warning(
f"Unsupported reply_markup type '{type(reply_markup).__name__}' "
@@ -314,10 +315,6 @@ class NotificationService(Notifier):
)
return translated_markup
def _get_close_notification_button(self, locale: Locale) -> InlineKeyboardButton:
text = self._get_translated_text(locale, "btn-common.notification-close")
return InlineKeyboardButton(text=text, callback_data=Notification.CLOSE.state)
def _get_default_keyboard(self, button: InlineKeyboardButton) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder([[button]])
return builder.as_markup()

View File

@@ -19,6 +19,7 @@ REMNAWAVE_MIN_VERSION: Final[Version] = Version("2.7.0")
REMNAWAVE_MAX_VERSION: Final[Version] = Version("2.8.0")
REPOSITORY: Final[str] = "https://github.com/snoups/remnashop"
DOCS: Final[str] = "https://remnashop.mintlify.app"
T_ME: Final[str] = "https://t.me/"
API_V1: Final[str] = "/api/v1"
BOT_WEBHOOK_PATH: Final[str] = "/telegram"

View File

@@ -23,6 +23,7 @@ class UnitOfWorkImpl(UnitOfWork):
) -> None:
if exc_type:
await self.rollback()
await self.session.close()
async def commit(self) -> None:
await self.session.commit()

View File

@@ -96,13 +96,13 @@ class FreeKassaGateway(BasePaymentGateway):
async def _create_payment_payload(self, amount: str, order_id: str) -> dict[str, Any]:
data: dict[str, Any] = {
"shopId": self.data.settings.shop_id, # type: ignore[union-attr]
"nonce": int(time.time() * 1000), # must be strictly increasing
"nonce": time.time_ns(), # must be strictly increasing
"paymentId": order_id,
"i": self.data.settings.payment_system_id, # type: ignore[union-attr]
"email": self.data.settings.customer_email, # type: ignore[union-attr]
"ip": self.data.settings.customer_ip, # type: ignore[union-attr]
"amount": str(amount),
"currency": self.data.currency.value,
"currency": self.data.currency.upper(),
"success_url": await self._get_bot_redirect_url(),
"failure_url": await self._get_bot_redirect_url(),
"notification_url": self.config.get_webhook(self.data.type),

View File

@@ -107,8 +107,8 @@ class YookassaGateway(BasePaymentGateway):
async def handle_webhook(self, request: Request) -> tuple[UUID, TransactionStatus]:
logger.debug("Received YooKassa webhook request")
# if not self._verify_webhook(request):
# raise PermissionError("Webhook verification failed")
if not self._verify_webhook(request):
raise PermissionError("Webhook verification failed")
webhook_data = await self._get_webhook_data(request)
payment_object: dict = webhook_data.get("object", {})

View File

@@ -9,9 +9,9 @@ from aiogram_dialog.widgets.style import Style
from aiogram_dialog.widgets.text import Format
from magic_filter import F
from src.core.constants import GOTO_PREFIX, PAYMENT_PREFIX, REPOSITORY, T_ME
from src.core.constants import DOCS, GOTO_PREFIX, PAYMENT_PREFIX, REPOSITORY, T_ME
from src.core.enums import ButtonType, PurchaseType
from src.telegram.states import DashboardUser, MainMenu, Subscription
from src.telegram.states import DashboardUser, MainMenu, Notification, Subscription
from src.telegram.widgets import I18nFormat
CALLBACK_CHANNEL_CONFIRM: Final[str] = "channel_confirm"
@@ -89,7 +89,17 @@ back_main_menu_button = (
)
def get_goto_buttons(support_url: str, is_referral_enable: bool) -> list[InlineKeyboardButton]:
CLOSE_BUTTON_ID: Final[int] = -1
def get_close_notification_button() -> InlineKeyboardButton:
return InlineKeyboardButton(
text="btn-common.notification-close",
callback_data=Notification.CLOSE.state,
)
def get_broadcast_buttons(support_url: str, is_referral_enable: bool) -> list[InlineKeyboardButton]:
buttons = [
InlineKeyboardButton(
text="btn-goto.contact-support",
@@ -113,6 +123,8 @@ def get_goto_buttons(support_url: str, is_referral_enable: bool) -> list[InlineK
)
)
buttons.append(get_close_notification_button())
return buttons
@@ -199,12 +211,12 @@ def get_remnashop_update_keyboard() -> InlineKeyboardMarkup:
builder.row(
InlineKeyboardButton(
text="btn-remnashop-info.release-latest",
url=f"{REPOSITORY}/releases/latest",
url=DOCS,
style=ButtonStyle.PRIMARY,
),
InlineKeyboardButton(
text="btn-remnashop-info.how-upgrade",
url=f"{REPOSITORY}?tab=readme-ov-file#step-5--how-to-upgrade",
url=f"{DOCS}/docs/ru/install/update",
style=ButtonStyle.PRIMARY,
),
)

View File

@@ -10,7 +10,7 @@ from src.application.common.dao import BroadcastDao, PlanDao, SettingsDao
from src.application.dto import PlanDto
from src.application.services import BotService
from src.core.constants import DATETIME_FORMAT
from src.telegram.keyboards import get_goto_buttons
from src.telegram.keyboards import CLOSE_BUTTON_ID, get_broadcast_buttons
@inject
@@ -61,18 +61,17 @@ async def buttons_getter(
settings = await settings_dao.get()
if not buttons:
all_buttons = get_broadcast_buttons(
support_url=bot_service.get_support_url(text=i18n.get("message.help")),
is_referral_enable=settings.referral.enable,
)
buttons = [
{
"id": index,
"text": goto_button.text,
"selected": False,
"id": CLOSE_BUTTON_ID if index == len(all_buttons) - 1 else index,
"text": btn.text,
"selected": index == len(all_buttons) - 1,
}
for index, goto_button in enumerate(
get_goto_buttons(
support_url=bot_service.get_support_url(text=i18n.get("message.help")),
is_referral_enable=settings.referral.enable,
)
)
for index, btn in enumerate(all_buttons)
]
dialog_manager.dialog_data["buttons"] = buttons

View File

@@ -29,7 +29,7 @@ from src.application.use_cases.broadcast.queries.audience import (
)
from src.core.constants import USER_KEY
from src.core.enums import BroadcastAudience, MediaType
from src.telegram.keyboards import get_goto_buttons
from src.telegram.keyboards import CLOSE_BUTTON_ID, get_broadcast_buttons
from src.telegram.states import DashboardBroadcast
from src.telegram.utils import is_double_click
@@ -223,17 +223,22 @@ async def on_button_select(
settings = await settings_dao.get()
goto_buttons = get_goto_buttons(
all_buttons = get_broadcast_buttons(
support_url=bot_service.get_support_url(text=i18n.get("message.help")),
is_referral_enable=settings.referral.enable,
)
goto_buttons = all_buttons[:-1]
builder = InlineKeyboardBuilder()
for button in buttons:
if button.get("selected"):
builder.row(goto_buttons[int(button["id"])])
if selected_id == CLOSE_BUTTON_ID:
close_selected = next((b["selected"] for b in buttons if b["id"] == CLOSE_BUTTON_ID), True)
_update_payload(dialog_manager, retort, disable_default_markup=not close_selected)
else:
builder = InlineKeyboardBuilder()
for button in buttons:
if button.get("selected") and button["id"] != CLOSE_BUTTON_ID:
builder.row(goto_buttons[int(button["id"])])
_update_payload(dialog_manager, retort, reply_markup=builder.as_markup().model_dump())
_update_payload(dialog_manager, retort, reply_markup=builder.as_markup().model_dump())
logger.debug(f"{user.log} Updated payload keyboard: {buttons}")

View File

@@ -4,6 +4,7 @@ from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.common import ManagedScroll
from dishka import FromDishka
from dishka.integrations.aiogram_dialog import inject
from loguru import logger
from remnapy import RemnawaveSDK
from src.application.common import TranslatorRunner
@@ -17,20 +18,19 @@ async def system_getter(
remnawave_sdk: FromDishka[RemnawaveSDK],
**kwargs: Any,
) -> dict[str, Any]:
result = await remnawave_sdk.system.get_stats()
stats = await remnawave_sdk.system.get_stats()
metadata = await remnawave_sdk.system.get_metadata()
logger.success(stats)
return {
"version": metadata.version,
"cpu_cores": result.cpu.physical_cores,
"cpu_threads": result.cpu.cores,
"ram_used": i18n_format_bytes_to_unit(result.memory.active),
"ram_total": i18n_format_bytes_to_unit(result.memory.total),
"cpu_cores": stats.cpu.cores,
"ram_used": i18n_format_bytes_to_unit(stats.memory.used),
"ram_total": i18n_format_bytes_to_unit(stats.memory.total),
"ram_used_percent": percent(
part=result.memory.active or 0,
whole=result.memory.total,
part=stats.memory.used or 0,
whole=stats.memory.total,
),
"uptime": i18n_format_seconds(result.uptime),
"uptime": i18n_format_seconds(stats.uptime),
}
@@ -118,7 +118,9 @@ async def nodes_getter(
xray_uptime=i18n_format_seconds(node.xray_uptime),
users_online=node.users_online,
traffic_used=i18n_format_bytes_to_unit(node.traffic_used_bytes),
traffic_limit=i18n_format_bytes_to_unit(node.traffic_limit_bytes, round_up=True),
traffic_limit=i18n_format_bytes_to_unit(
node.traffic_limit_bytes or None, round_up=True
),
)
)

View File

@@ -43,14 +43,12 @@ async def payments_webhook(
payment_id, payment_status = await gateway.handle_webhook(request)
await handle_payment_transaction_task.kiq(payment_id, payment_status) # type: ignore[call-overload]
return Response(status_code=status.HTTP_200_OK)
except Exception as e:
logger.exception(f"Error processing webhook for '{gateway_type}': {e}")
error_event = ErrorEvent(**config.build.data, exception=e)
await event_publisher.publish(error_event)
finally:
if gateway is not None:
return await gateway.build_webhook_response(request)
return Response(status_code=status.HTTP_200_OK)
if gateway is not None:
return await gateway.build_webhook_response(request)
return Response(status_code=status.HTTP_200_OK)