mirror of
https://github.com/snoups/remnashop.git
synced 2026-04-13 06:44:34 +00:00
merge pull request #96 from snoups/dev
This commit is contained in:
@@ -42,7 +42,7 @@ btn-requirement =
|
||||
btn-menu =
|
||||
.trial = 🎁 ПОПРОБОВАТЬ БЕСПЛАТНО
|
||||
.connect = 🚀 Подключиться
|
||||
.devices = 📱 Мои устройства
|
||||
.devices = 📱 Устройства
|
||||
.subscription = 💳 Подписка
|
||||
.invite = 👥 Пригласить
|
||||
.support = 🆘 Поддержка
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.7.4"
|
||||
__version__ = "0.7.5"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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", {})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
11
uv.lock
generated
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user