merge pull request #96 from snoups/dev

This commit is contained in:
Ilay
2026-04-10 17:30:31 +05:00
committed by GitHub
21 changed files with 108 additions and 61 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

@@ -5,7 +5,6 @@ from aiogram.client.default import DefaultBotProperties
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.enums import ParseMode
from aiogram_dialog import BgManagerFactory
from aiohttp_socks import ProxyConnector
from dishka import Provider, Scope, from_context, provide
from loguru import logger
@@ -23,10 +22,8 @@ class BotProvider(Provider):
session = None
if config.bot.proxy_url:
proxy = config.bot.proxy_url.get_secret_value()
logger.info("Using SOCKS5 proxy for Telegram")
connector = ProxyConnector.from_url(proxy)
session = AiohttpSession(connector=connector)
session = AiohttpSession(proxy=config.bot.proxy_url.get_secret_value())
async with Bot(
token=config.bot.token.get_secret_value(),

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

@@ -50,7 +50,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
if not await bot_service.is_inline_enabled():
logger.warning(
"Bot is not enabled for inline mode. "
"Please set BOT_INLINE_MODE to True for correct work of some features"
"Please enable Inline Mode in BotFather for correct work of some features"
)
states = await bot_service.get_bot_states()

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

@@ -118,6 +118,7 @@ async def duration_getter(
if not raw_plan:
logger.debug("PlanDto not found in dialog data")
await dialog_manager.start(state=Subscription.MAIN)
return {}
plan = retort.load(raw_plan, PlanDto)
settings = await settings_dao.get()
@@ -171,6 +172,7 @@ async def payment_method_getter(
if not raw_plan:
logger.error("PlanDto not found in dialog data")
await dialog_manager.start(state=Subscription.MAIN)
return {}
plan = retort.load(raw_plan, PlanDto)
gateways = await payment_gateway_dao.get_active()
@@ -228,6 +230,7 @@ async def confirm_getter(
if not raw_plan:
logger.debug("PlanDto not found in dialog data")
await dialog_manager.start(state=Subscription.MAIN)
return {}
plan = retort.load(raw_plan, PlanDto)
selected_duration = dialog_manager.dialog_data["selected_duration"]

View File

@@ -308,6 +308,7 @@ async def on_duration_select(
if not raw_plan:
logger.error("PlanDto not found in dialog data")
await dialog_manager.start(state=Subscription.MAIN)
return
plan = retort.load(raw_plan, PlanDto)
settings = await settings_dao.get()
@@ -390,6 +391,7 @@ async def on_payment_method_select(
if not raw_plan:
logger.error("PlanDto not found in dialog data")
await dialog_manager.start(state=Subscription.MAIN)
return
plan = retort.load(raw_plan, PlanDto)

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)

11
uv.lock generated
View File

@@ -983,6 +983,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.24"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/45/e23b5dc14ddb9918ae4a625379506b17b6f8fc56ca1d82db62462f59aea6/python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8", size = 37695, upload-time = "2026-04-05T20:49:13.829Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/73/89930efabd4da63cea44a3f438aeb753d600123570e6d6264e763617a9ce/python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950", size = 24420, upload-time = "2026-04-05T20:49:12.555Z" },
]
[[package]]
name = "python-socks"
version = "2.8.1"
@@ -1090,6 +1099,7 @@ dependencies = [
{ name = "loguru" },
{ name = "msgspec" },
{ name = "pydantic-settings" },
{ name = "python-multipart" },
{ name = "qrcode", extra = ["pil"] },
{ name = "redis" },
{ name = "remnapy" },
@@ -1127,6 +1137,7 @@ requires-dist = [
{ name = "loguru", specifier = "~=0.7.3" },
{ name = "msgspec", specifier = "~=0.19.0" },
{ name = "pydantic-settings", specifier = "~=2.11.0" },
{ name = "python-multipart", specifier = ">=0.0.22" },
{ name = "qrcode", extras = ["pil"], specifier = ">=8.2" },
{ name = "redis", specifier = "~=7.0.0" },
{ name = "remnapy", git = "https://github.com/snoups/remnapy?rev=b712d1d" },