mirror of
https://github.com/snoups/remnashop.git
synced 2026-04-18 08:53:57 +00:00
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:
@@ -527,6 +527,7 @@ msg-user-sync-subscription =
|
||||
[DAY] Каждый день
|
||||
[WEEK] Каждую неделю
|
||||
[MONTH] Каждый месяц
|
||||
[MONTH] Каждый месяц (по дате создания)
|
||||
*[OTHER] { $traffic_limit_strategy }
|
||||
}
|
||||
• Тег: { $tag ->
|
||||
|
||||
@@ -361,6 +361,7 @@ traffic-strategy = { $strategy_type ->
|
||||
[DAY] Каждый день
|
||||
[WEEK] Каждую неделю
|
||||
[MONTH] Каждый месяц
|
||||
[MONTH_ROLLING] Каждый месяц (по дате создания)
|
||||
*[OTHER] { $strategy_type }
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.7.3"
|
||||
__version__ = "0.7.4"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
""")
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": (
|
||||
|
||||
@@ -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
6
uv.lock
generated
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user