feat: support MONTH_ROLLING traffic strategy and remnawave 2.7.x

- added MONTH_ROLLING traffic reset strategy with subscription creation date support
- added migration 0020: add MONTH_ROLLING value to plan_traffic_limit_strategy enum
- updated get_traffic_reset_delta to accept optional subscription_created_at for MONTH_ROLLING
- passed subscription.created_at to get_traffic_reset_delta in menu getter and show_reason handler
- added MONTH_ROLLING translation in utils.ftl and messages.ftl
- bumped remnapy to 10e5c95 (2.7.x)
- fixed ram_used_percent: fallback to 0 if memory.active is None
- fixed delete_device return type cast to int
- bumped version to 0.7.4
This commit is contained in:
Ilay
2026-04-05 20:23:37 +05:00
parent ad3751a8eb
commit 6ccaf0ffb7
13 changed files with 109 additions and 35 deletions

View File

@@ -527,6 +527,7 @@ msg-user-sync-subscription =
[DAY] Каждый день
[WEEK] Каждую неделю
[MONTH] Каждый месяц
[MONTH] Каждый месяц (по дате создания)
*[OTHER] { $traffic_limit_strategy }
}
Тег: { $tag ->

View File

@@ -361,6 +361,7 @@ traffic-strategy = { $strategy_type ->
[DAY] Каждый день
[WEEK] Каждую неделю
[MONTH] Каждый месяц
[MONTH_ROLLING] Каждый месяц (по дате создания)
*[OTHER] { $strategy_type }
}

View File

@@ -10,7 +10,7 @@ dependencies = [
"greenlet>=3.2.4",
"uvicorn>=0.38.0",
"fastapi>=0.120.2",
"remnapy==2.6.3",
"remnapy==2.7.0",
#
"dishka~=1.7.2",
"adaptix==3.0.0b11",
@@ -33,7 +33,7 @@ dependencies = [
]
[tool.uv.sources]
remnapy = { git = "https://github.com/snoups/remnapy", rev = "f442fb6" }
remnapy = { git = "https://github.com/snoups/remnapy", rev = "10e5c95" }
[project.urls]
homepage = "https://t.me/@remna_shop"

View File

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

View File

@@ -296,7 +296,10 @@ class RemnaWebhookService:
is_trial=current_subscription.is_trial,
traffic_strategy=current_subscription.traffic_limit_strategy,
reset_time=i18n_format_expire_time(
get_traffic_reset_delta(current_subscription.traffic_limit_strategy)
get_traffic_reset_delta(
current_subscription.traffic_limit_strategy,
current_subscription.created_at,
)
),
)
)

View File

@@ -1,5 +1,6 @@
import time
from datetime import datetime, timedelta
from typing import Optional
from remnapy.enums.users import TrafficLimitStrategy
@@ -17,7 +18,10 @@ def get_uptime() -> int:
return uptime_seconds
def get_traffic_reset_delta(strategy: TrafficLimitStrategy) -> timedelta:
def get_traffic_reset_delta( # noqa: C901
strategy: TrafficLimitStrategy,
subscription_created_at: Optional[datetime] = None,
) -> timedelta:
now = datetime_now()
if strategy == TrafficLimitStrategy.NO_RESET:
@@ -46,4 +50,25 @@ def get_traffic_reset_delta(strategy: TrafficLimitStrategy) -> timedelta:
reset_at = datetime(year, month, 1, 0, 10, 0, tzinfo=TIMEZONE)
return reset_at - now
if strategy == TrafficLimitStrategy.MONTH_ROLLING:
if subscription_created_at is None:
raise ValueError("subscription_created_at is required for MONTH_ROLLING strategy")
reset_day = subscription_created_at.day
year = now.year
month = now.month
if now.day >= reset_day:
month += 1
if month == 13:
year += 1
month = 1
try:
reset_at = datetime(year, month, reset_day, 0, 10, 0, tzinfo=TIMEZONE)
except ValueError:
month += 1
if month == 13:
year += 1
month = 1
reset_at = datetime(year, month, 1, 0, 10, 0, tzinfo=TIMEZONE)
return reset_at - now
raise ValueError("Unsupported strategy")

View File

@@ -0,0 +1,39 @@
from typing import Sequence, Union
from alembic import op
revision: str = "0020"
down_revision: Union[str, None] = "0019"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute("ALTER TYPE plan_traffic_limit_strategy ADD VALUE IF NOT EXISTS 'MONTH_ROLLING'")
def downgrade() -> None:
op.execute("""
ALTER TABLE subscriptions
ALTER COLUMN traffic_limit_strategy TYPE text
USING traffic_limit_strategy::text
""")
op.execute("""
CREATE TYPE plan_traffic_limit_strategy_new AS ENUM (
SELECT enumlabel
FROM pg_enum
JOIN pg_type ON pg_enum.enumtypid = pg_type.oid
WHERE typname = 'plan_traffic_limit_strategy'
AND enumlabel <> 'MONTH_ROLLING'
)
""")
op.execute("DROP TYPE plan_traffic_limit_strategy")
op.execute("ALTER TYPE plan_traffic_limit_strategy_new RENAME TO plan_traffic_limit_strategy")
op.execute("""
ALTER TABLE subscriptions
ALTER COLUMN traffic_limit_strategy TYPE plan_traffic_limit_strategy
USING traffic_limit_strategy::text::plan_traffic_limit_strategy
""")

View File

@@ -36,31 +36,30 @@ class NotificationQueue:
batch = list(self._queue)
self._queue.clear()
total = len(batch)
logger.debug(f"Processing '{total}' queued notifications")
logger.debug(f"Processing '{len(batch)}' queued notifications")
for i, chunk in enumerate(chunked(batch, BATCH_SIZE_10), start=1):
chunk_start = asyncio.get_running_loop().time()
try:
results = await asyncio.gather(
*(sender(task) for task in chunk),
return_exceptions=True,
)
results = []
for task in chunk:
try:
result = await sender(task)
results.append(result)
except Exception as e:
results.append(e)
logger.error(
f"Notification task failed: {type(e).__name__}: {e}",
exc_info=e,
)
errors = sum(1 for r in results if isinstance(r, Exception))
elapsed = asyncio.get_running_loop().time() - chunk_start
errors = sum(1 for r in results if isinstance(r, Exception))
logger.debug(
f"Chunk '{i}': {len(results) - errors} success, "
f"{errors} errors in {elapsed:.2f}s"
)
elapsed = asyncio.get_running_loop().time() - chunk_start
logger.debug(
f"Chunk '{i}' processed: "
f"'{len(chunk) - errors}' success, '{errors}' errors "
f"in {elapsed:.2f}s"
)
wait_time = BATCH_DELAY - elapsed
if wait_time > 0:
await asyncio.sleep(wait_time)
except Exception as e:
logger.error(f"Failed to process notification chunk: '{e}'")
wait_time = BATCH_DELAY - elapsed
if wait_time > 0:
await asyncio.sleep(wait_time)

View File

@@ -139,7 +139,7 @@ class RemnawaveImpl(Remnawave):
logger.debug(f"RemnaUser '{user_uuid}' not found in panel")
return None
return response.total
return int(response.total)
async def reset_traffic(self, uuid: UUID) -> Optional[UserResponseDto]:
try:

View File

@@ -27,7 +27,7 @@ async def system_getter(
"ram_used": i18n_format_bytes_to_unit(result.memory.active),
"ram_total": i18n_format_bytes_to_unit(result.memory.total),
"ram_used_percent": percent(
part=result.memory.active,
part=result.memory.active or 0,
whole=result.memory.total,
),
"uptime": i18n_format_seconds(result.uptime),
@@ -145,7 +145,7 @@ async def inbounds_getter(
result = await remnawave_sdk.inbounds.get_all_inbounds()
inbounds = []
for inbound in result.inbounds: # type: ignore[attr-defined]
for inbound in result.inbounds:
inbounds.append(
i18n.get(
"msg-remnawave-inbound-details",

View File

@@ -81,7 +81,10 @@ async def menu_getter(
"device_limit": i18n_format_device_limit(subscription.device_limit),
"expire_time": i18n_format_expire_time(subscription.expire_at),
"reset_time": i18n_format_expire_time(
get_traffic_reset_delta(subscription.traffic_limit_strategy)
get_traffic_reset_delta(
subscription.traffic_limit_strategy,
subscription.created_at,
)
),
"connectable": subscription.is_active,
"has_device_limit": (

View File

@@ -174,7 +174,10 @@ async def show_reason(
"is_trial": subscription.is_trial,
"traffic_strategy": subscription.traffic_limit_strategy,
"reset_time": i18n_format_expire_time(
get_traffic_reset_delta(subscription.traffic_limit_strategy)
get_traffic_reset_delta(
subscription.traffic_limit_strategy,
subscription.created_at,
)
),
}
else:

6
uv.lock generated
View File

@@ -1038,8 +1038,8 @@ wheels = [
[[package]]
name = "remnapy"
version = "2.6.3.post1"
source = { git = "https://github.com/snoups/remnapy?rev=f442fb6#f442fb6c42637a1eb4a9522e1ad65cc3323102cf" }
version = "2.7.1.dev1+g10e5c95b0"
source = { git = "https://github.com/snoups/remnapy?rev=10e5c95#10e5c95b00fe51f3d538bb91dd38d87b1ccfca5d" }
dependencies = [
{ name = "cryptography" },
{ name = "httpx" },
@@ -1105,7 +1105,7 @@ requires-dist = [
{ name = "pydantic-settings", specifier = "~=2.11.0" },
{ name = "qrcode", extras = ["pil"], specifier = ">=8.2" },
{ name = "redis", specifier = "~=7.0.0" },
{ name = "remnapy", git = "https://github.com/snoups/remnapy?rev=f442fb6" },
{ name = "remnapy", git = "https://github.com/snoups/remnapy?rev=10e5c95" },
{ name = "sqlalchemy", extras = ["mypy"], specifier = ">=2.0.0" },
{ name = "taskiq", extras = ["orjson"], specifier = "~=0.12.1" },
{ name = "taskiq-redis", specifier = "~=1.2.1" },