diff --git a/api/depends.py b/api/depends.py
index b5dad6d7..a1873235 100644
--- a/api/depends.py
+++ b/api/depends.py
@@ -1,9 +1,10 @@
-from typing import AsyncGenerator
import hashlib
+from collections.abc import AsyncGenerator
+
from fastapi import Depends, HTTPException, Header, Query
-from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
from database import async_session_maker
from database.models import Admin
@@ -24,10 +25,8 @@ async def verify_admin_token(
session: AsyncSession = Depends(get_session),
) -> Admin:
hashed = hash_token(token)
- result = await session.execute(
- select(Admin).where(Admin.tg_id == admin_id, Admin.token == hashed)
- )
+ result = await session.execute(select(Admin).where(Admin.tg_id == admin_id, Admin.token == hashed))
admin = result.scalar_one_or_none()
if not admin:
raise HTTPException(status_code=401, detail="Unauthorized")
- return admin
\ No newline at end of file
+ return admin
diff --git a/api/routes/base_crud.py b/api/routes/base_crud.py
index d4a133e4..7b00563d 100644
--- a/api/routes/base_crud.py
+++ b/api/routes/base_crud.py
@@ -1,14 +1,15 @@
-from fastapi import APIRouter, Depends, HTTPException, Query, Path
-from sqlalchemy.ext.asyncio import AsyncSession
+from typing import Any, Type, Union
+
+from fastapi import APIRouter, Depends, HTTPException, Path, Query
from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.attributes import InstrumentedAttribute
-from typing import Type, Union, Any
from api.depends import get_session, verify_admin_token
from database.models import Admin
-def _cast_identifier_type(field: InstrumentedAttribute, value: Union[int, str]):
+def _cast_identifier_type(field: InstrumentedAttribute, value: int | str):
column_type = type(field.property.columns[0].type).__name__
if column_type in ("Integer", "BigInteger"):
return int(value)
@@ -17,18 +18,19 @@ def _cast_identifier_type(field: InstrumentedAttribute, value: Union[int, str]):
def generate_crud_router(
*,
- model: Type,
- schema_response: Type,
- schema_create: Type,
- schema_update: Type,
- identifier_field: str = "tg_id",
- parameter_name: str = "tg_id",
+ model: type,
+ schema_response: type,
+ schema_create: type,
+ schema_update: type,
+ identifier_field: str = "tg_id",
+ parameter_name: str = "tg_id",
extra_get_by_email: bool = False,
- enabled_methods: list[str] = ("get_all", "get_one", "get_by_email", "create", "update", "delete")
+ enabled_methods: list[str] = ("get_all", "get_one", "get_by_email", "create", "update", "delete"),
) -> APIRouter:
router = APIRouter()
if "get_all" in enabled_methods:
+
@router.get("/", response_model=list[schema_response])
async def get_all(
admin: Admin = Depends(verify_admin_token),
@@ -38,6 +40,7 @@ def generate_crud_router(
return result.scalars().all()
if "get_by_email" in enabled_methods and extra_get_by_email:
+
@router.get("/by_email", response_model=schema_response)
async def get_by_email(
email: str = Query(...),
@@ -51,9 +54,10 @@ def generate_crud_router(
return obj
if "get_one" in enabled_methods:
+
@router.get(f"/{{{parameter_name}}}", response_model=schema_response)
async def get_one(
- value: Union[int, str] = Path(..., alias=parameter_name),
+ value: int | str = Path(..., alias=parameter_name),
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
@@ -66,9 +70,10 @@ def generate_crud_router(
return obj
if "get_all_by_field" in enabled_methods:
+
@router.get(f"/all/{{{parameter_name}}}", response_model=list[schema_response])
async def get_all_by_field(
- value: Union[int, str] = Path(..., alias=parameter_name),
+ value: int | str = Path(..., alias=parameter_name),
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
@@ -81,9 +86,10 @@ def generate_crud_router(
return objs
if "create" in enabled_methods:
+
@router.post("/", response_model=schema_response)
async def create(
- payload: schema_create, # type: ignore
+ payload: schema_create, # type: ignore
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
@@ -97,10 +103,11 @@ def generate_crud_router(
return obj
if "update" in enabled_methods:
+
@router.patch(f"/{{{parameter_name}}}", response_model=schema_response)
async def update(
- payload: schema_update, # type: ignore
- value: Union[int, str] = Path(..., alias=parameter_name),
+ payload: schema_update, # type: ignore
+ value: int | str = Path(..., alias=parameter_name),
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
@@ -119,9 +126,10 @@ def generate_crud_router(
return obj
if "delete" in enabled_methods:
+
@router.delete(f"/{{{parameter_name}}}", response_model=dict)
async def delete(
- value: Union[int, str] = Path(..., alias=parameter_name),
+ value: int | str = Path(..., alias=parameter_name),
admin: Admin = Depends(verify_admin_token),
session: AsyncSession = Depends(get_session),
):
diff --git a/api/routes/coupons.py b/api/routes/coupons.py
index ffa3d3ac..993ac191 100644
--- a/api/routes/coupons.py
+++ b/api/routes/coupons.py
@@ -1,8 +1,10 @@
from fastapi import APIRouter
+
from api.routes.base_crud import generate_crud_router
from api.schemas import CouponBase, CouponResponse, CouponUpdate
from database.models import Coupon
+
router: APIRouter = generate_crud_router(
model=Coupon,
schema_response=CouponResponse,
@@ -10,5 +12,5 @@ router: APIRouter = generate_crud_router(
schema_update=CouponUpdate,
identifier_field="code",
parameter_name="code",
- enabled_methods=["get_all", "get_one", "create", "update", "delete"]
+ enabled_methods=["get_all", "get_one", "create", "update", "delete"],
)
diff --git a/api/routes/gifts.py b/api/routes/gifts.py
index a55e07db..3db47d77 100644
--- a/api/routes/gifts.py
+++ b/api/routes/gifts.py
@@ -1,11 +1,12 @@
-from fastapi import APIRouter, Depends, Path, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
+from fastapi import APIRouter, Depends, HTTPException, Path
from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+from api.depends import get_session, verify_admin_token
from api.routes.base_crud import generate_crud_router
from api.schemas import GiftBase, GiftResponse, GiftUpdate, GiftUsageResponse
-from database.models import Gift, GiftUsage, Admin
-from api.depends import get_session, verify_admin_token
+from database.models import Admin, Gift, GiftUsage
+
router = APIRouter()
@@ -17,7 +18,7 @@ gift_router = generate_crud_router(
schema_update=GiftUpdate,
identifier_field="gift_id",
parameter_name="gift_id",
- enabled_methods=["get_all", "get_one", "create", "update", "delete"]
+ enabled_methods=["get_all", "get_one", "create", "update", "delete"],
)
router.include_router(gift_router, prefix="", tags=["Gifts"])
@@ -41,7 +42,7 @@ gift_usage_router = generate_crud_router(
schema_create=None,
schema_update=None,
identifier_field="gift_id",
- enabled_methods=["get_all", "get_one", "delete"]
+ enabled_methods=["get_all", "get_one", "delete"],
)
router.include_router(gift_usage_router, prefix="/usages", tags=["GiftUsages"])
router.include_router(gift_usage_router, prefix="/usages", tags=["Gifts"])
diff --git a/api/routes/keys.py b/api/routes/keys.py
index 98d4be8e..790d3be4 100644
--- a/api/routes/keys.py
+++ b/api/routes/keys.py
@@ -1,16 +1,16 @@
-from fastapi import Depends, HTTPException, Path, Body, status
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select
from datetime import datetime
-from database.models import Key, Admin, Tariff
-from api.schemas.keys import KeyBase, KeyResponse, KeyUpdate, KeyCreateRequest
-from api.routes.base_crud import generate_crud_router
+from fastapi import Body, Depends, HTTPException, Path, status
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
from api.depends import get_session, verify_admin_token
-from handlers.keys.key_utils import delete_key_from_cluster
+from api.routes.base_crud import generate_crud_router
+from api.schemas.keys import KeyBase, KeyCreateRequest, KeyResponse, KeyUpdate
+from database.models import Admin, Key, Tariff
+from handlers.keys.key_utils import create_key_on_cluster, delete_key_from_cluster, renew_key_in_cluster
from logger import logger
-from handlers.keys.key_utils import renew_key_in_cluster, create_key_on_cluster
router = generate_crud_router(
model=Key,
@@ -19,7 +19,7 @@ router = generate_crud_router(
schema_update=KeyUpdate,
identifier_field="tg_id",
extra_get_by_email=True,
- enabled_methods=["get_all", "get_one", "get_by_email", "get_all_by_field"]
+ enabled_methods=["get_all", "get_one", "get_by_email", "get_all_by_field"],
)
@@ -58,16 +58,12 @@ async def get_router_keys_by_tg_id(
session: AsyncSession = Depends(get_session),
admin: Admin = Depends(verify_admin_token),
):
- tariffs_result = await session.execute(
- select(Tariff.id).where(Tariff.group_code == "routers")
- )
+ tariffs_result = await session.execute(select(Tariff.id).where(Tariff.group_code == "routers"))
tariff_ids = [row[0] for row in tariffs_result.all()]
if not tariff_ids:
return []
- keys_result = await session.execute(
- select(Key).where(Key.tg_id == tg_id, Key.tariff_id.in_(tariff_ids))
- )
+ keys_result = await session.execute(select(Key).where(Key.tg_id == tg_id, Key.tariff_id.in_(tariff_ids)))
keys = keys_result.scalars().all()
return keys
@@ -109,7 +105,7 @@ async def edit_key_by_email(
total_gb=getattr(db_key, "traffic_limit", None),
session=session,
hwid_device_limit=getattr(db_key, "device_limit", None),
- reset_traffic=True
+ reset_traffic=True,
)
await session.commit()
@@ -147,4 +143,4 @@ async def create_key_api(
except Exception as e:
logger.error(f"[API] Ошибка при создании ключа: {e}")
- raise HTTPException(status_code=500, detail="Ошибка при создании ключа")
\ No newline at end of file
+ raise HTTPException(status_code=500, detail="Ошибка при создании ключа")
diff --git a/api/routes/misc.py b/api/routes/misc.py
index 11e7bd0b..9b6295da 100644
--- a/api/routes/misc.py
+++ b/api/routes/misc.py
@@ -1,17 +1,29 @@
-from fastapi import APIRouter, Path, HTTPException, Depends
-from sqlalchemy.ext.asyncio import AsyncSession
+from fastapi import APIRouter, Depends, HTTPException, Path
from sqlalchemy import select
-from api.routes.base_crud import generate_crud_router
-from database.models import (
- Payment, Referral, Notification,
- ManualBan, TemporaryData, BlockedUser, TrackingSource, Admin
-)
+from sqlalchemy.ext.asyncio import AsyncSession
+
from api.depends import get_session, verify_admin_token
+from api.routes.base_crud import generate_crud_router
from api.schemas import (
- PaymentResponse, ReferralResponse, NotificationResponse,
- ManualBanResponse, TemporaryDataResponse, BlockedUserResponse,
- TrackingSourceResponse
+ BlockedUserResponse,
+ ManualBanResponse,
+ NotificationResponse,
+ PaymentResponse,
+ ReferralResponse,
+ TemporaryDataResponse,
+ TrackingSourceResponse,
)
+from database.models import (
+ Admin,
+ BlockedUser,
+ ManualBan,
+ Notification,
+ Payment,
+ Referral,
+ TemporaryData,
+ TrackingSource,
+)
+
router = APIRouter()
@@ -22,11 +34,11 @@ router.include_router(
schema_create=None,
schema_update=None,
identifier_field="id",
- enabled_methods=["get_all", "get_one", "delete"]
+ enabled_methods=["get_all", "get_one", "delete"],
),
prefix="/payments",
tags=["Payments"],
- dependencies=[Depends(verify_admin_token)]
+ dependencies=[Depends(verify_admin_token)],
)
@@ -50,11 +62,11 @@ router.include_router(
schema_create=None,
schema_update=None,
identifier_field="referred_tg_id",
- enabled_methods=["get_all", "get_one", "delete"]
+ enabled_methods=["get_all", "get_one", "delete"],
),
prefix="/referrals",
tags=["Referrals"],
- dependencies=[Depends(verify_admin_token)]
+ dependencies=[Depends(verify_admin_token)],
)
router.include_router(
@@ -64,11 +76,11 @@ router.include_router(
schema_create=None,
schema_update=None,
identifier_field="tg_id",
- enabled_methods=["get_all", "get_one", "delete"]
+ enabled_methods=["get_all", "get_one", "delete"],
),
prefix="/notifications",
tags=["Notifications"],
- dependencies=[Depends(verify_admin_token)]
+ dependencies=[Depends(verify_admin_token)],
)
@@ -79,11 +91,11 @@ router.include_router(
schema_create=None,
schema_update=None,
identifier_field="tg_id",
- enabled_methods=["get_all", "get_one", "delete"]
+ enabled_methods=["get_all", "get_one", "delete"],
),
prefix="/manual-bans",
tags=["Bans"],
- dependencies=[Depends(verify_admin_token)]
+ dependencies=[Depends(verify_admin_token)],
)
router.include_router(
@@ -93,11 +105,11 @@ router.include_router(
schema_create=None,
schema_update=None,
identifier_field="tg_id",
- enabled_methods=["get_all", "get_one", "delete"]
+ enabled_methods=["get_all", "get_one", "delete"],
),
prefix="/blocked-users",
tags=["Bans"],
- dependencies=[Depends(verify_admin_token)]
+ dependencies=[Depends(verify_admin_token)],
)
router.include_router(
@@ -107,11 +119,11 @@ router.include_router(
schema_create=None,
schema_update=None,
identifier_field="tg_id",
- enabled_methods=["get_all", "get_one", "delete"]
+ enabled_methods=["get_all", "get_one", "delete"],
),
prefix="/temporary-data",
tags=["TemporaryData"],
- dependencies=[Depends(verify_admin_token)]
+ dependencies=[Depends(verify_admin_token)],
)
router.include_router(
@@ -121,9 +133,9 @@ router.include_router(
schema_create=None,
schema_update=None,
identifier_field="id",
- enabled_methods=["get_all", "get_one", "delete"]
+ enabled_methods=["get_all", "get_one", "delete"],
),
prefix="/tracking-sources",
tags=["TrackingSources"],
- dependencies=[Depends(verify_admin_token)]
+ dependencies=[Depends(verify_admin_token)],
)
diff --git a/api/routes/referrals.py b/api/routes/referrals.py
index 5c69cc12..bfeb6f1d 100644
--- a/api/routes/referrals.py
+++ b/api/routes/referrals.py
@@ -1,10 +1,12 @@
from fastapi import Depends, HTTPException, Query
+from sqlalchemy import delete, select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from api.depends import get_session, verify_admin_token
from api.routes.base_crud import generate_crud_router
from api.schemas import ReferralResponse
-from database.models import Referral, Admin
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, delete
-from api.depends import get_session, verify_admin_token
+from database.models import Admin, Referral
+
router = generate_crud_router(
model=Referral,
@@ -12,7 +14,7 @@ router = generate_crud_router(
schema_create=None,
schema_update=None,
identifier_field="referrer_tg_id",
- enabled_methods=["get_all", "get_one", "get_all_by_field"]
+ enabled_methods=["get_all", "get_one", "get_all_by_field"],
)
@@ -21,17 +23,14 @@ async def delete_one_referral(
referrer_tg_id: int = Query(..., description="ID пригласившего"),
referred_tg_id: int = Query(..., description="ID приглашённого"),
admin: Admin = Depends(verify_admin_token),
- session: AsyncSession = Depends(get_session)
+ session: AsyncSession = Depends(get_session),
):
result = await session.execute(
- select(Referral).where(
- Referral.referrer_tg_id == referrer_tg_id,
- Referral.referred_tg_id == referred_tg_id
- )
+ select(Referral).where(Referral.referrer_tg_id == referrer_tg_id, Referral.referred_tg_id == referred_tg_id)
)
obj = result.scalar_one_or_none()
if not obj:
raise HTTPException(status_code=404, detail="Referral not found")
await session.delete(obj)
await session.commit()
- return {"status": "deleted_one"}
\ No newline at end of file
+ return {"status": "deleted_one"}
diff --git a/api/routes/servers.py b/api/routes/servers.py
index 355c7369..92649254 100644
--- a/api/routes/servers.py
+++ b/api/routes/servers.py
@@ -1,8 +1,10 @@
from fastapi import APIRouter
+
from api.routes.base_crud import generate_crud_router
from api.schemas import ServerBase, ServerResponse, ServerUpdate
from database.models import Server
+
router: APIRouter = generate_crud_router(
model=Server,
schema_response=ServerResponse,
@@ -10,5 +12,5 @@ router: APIRouter = generate_crud_router(
schema_update=ServerUpdate,
identifier_field="server_name",
parameter_name="server_name",
- enabled_methods=["get_all", "get_one", "create", "update", "delete"]
+ enabled_methods=["get_all", "get_one", "create", "update", "delete"],
)
diff --git a/api/routes/tariffs.py b/api/routes/tariffs.py
index a13ef87f..b0e3b73b 100644
--- a/api/routes/tariffs.py
+++ b/api/routes/tariffs.py
@@ -1,8 +1,10 @@
from fastapi import APIRouter
+
from api.routes.base_crud import generate_crud_router
from api.schemas import TariffBase, TariffResponse, TariffUpdate
from database.models import Tariff
+
router: APIRouter = generate_crud_router(
model=Tariff,
schema_response=TariffResponse,
@@ -10,5 +12,5 @@ router: APIRouter = generate_crud_router(
schema_update=TariffUpdate,
identifier_field="name",
parameter_name="name",
- enabled_methods=["get_all", "get_one", "create", "update", "delete"]
+ enabled_methods=["get_all", "get_one", "create", "update", "delete"],
)
diff --git a/api/routes/users.py b/api/routes/users.py
index 579c9fba..19ab005c 100644
--- a/api/routes/users.py
+++ b/api/routes/users.py
@@ -1,14 +1,16 @@
+import asyncio
+
from fastapi import Depends, HTTPException, Path
-from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from api.depends import get_session, verify_admin_token
from api.routes.base_crud import generate_crud_router
from api.schemas.users import UserBase, UserResponse, UserUpdate
-from database.models import User, Key
-from api.depends import get_session, verify_admin_token
+from database import delete_user_data, get_servers
+from database.models import Key, User
from handlers.keys.key_utils import delete_key_from_cluster
-from database import get_servers, delete_user_data
from logger import logger
-import asyncio
router = generate_crud_router(
@@ -17,7 +19,7 @@ router = generate_crud_router(
schema_create=UserBase,
schema_update=UserUpdate,
identifier_field="tg_id",
- enabled_methods=["get_all", "get_one", "get_by_email", "create", "update"]
+ enabled_methods=["get_all", "get_one", "get_by_email", "create", "update"],
)
@@ -28,9 +30,7 @@ async def delete_user(
session: AsyncSession = Depends(get_session),
):
try:
- result = await session.execute(
- select(Key.email, Key.client_id).where(Key.tg_id == tg_id)
- )
+ result = await session.execute(select(Key.email, Key.client_id).where(Key.tg_id == tg_id))
key_records = result.all()
async def delete_keys_from_servers():
diff --git a/api/schemas/__init__.py b/api/schemas/__init__.py
index df7ab282..41ae1020 100644
--- a/api/schemas/__init__.py
+++ b/api/schemas/__init__.py
@@ -1,8 +1,15 @@
-from .users import UserBase, UserResponse, UserUpdate
-from .keys import KeyResponse, KeyDetailsResponse
-from .tariffs import TariffResponse, TariffUpdate, TariffBase
-from .servers import ServerResponse, ServerBase, ServerUpdate
-from .coupons import CouponResponse, CouponUsageResponse, CouponBase, CouponUpdate
+from .coupons import CouponBase, CouponResponse, CouponUpdate, CouponUsageResponse
+from .gifts import GiftBase, GiftResponse, GiftUpdate, GiftUsageResponse
+from .keys import KeyDetailsResponse, KeyResponse
+from .misc import (
+ BlockedUserResponse,
+ ManualBanResponse,
+ NotificationResponse,
+ PaymentResponse,
+ TemporaryDataResponse,
+ TrackingSourceResponse,
+)
from .referrals import ReferralResponse
-from .gifts import GiftResponse, GiftUsageResponse, GiftUpdate, GiftBase
-from .misc import ManualBanResponse, TemporaryDataResponse, BlockedUserResponse, TrackingSourceResponse, PaymentResponse, NotificationResponse
+from .servers import ServerBase, ServerResponse, ServerUpdate
+from .tariffs import TariffBase, TariffResponse, TariffUpdate
+from .users import UserBase, UserResponse, UserUpdate
diff --git a/api/schemas/coupons.py b/api/schemas/coupons.py
index b81fb08f..f50bc943 100644
--- a/api/schemas/coupons.py
+++ b/api/schemas/coupons.py
@@ -1,15 +1,16 @@
-from pydantic import BaseModel, Field, model_validator
-from typing import Optional
from datetime import datetime
+from typing import Optional
+
+from pydantic import BaseModel, Field, model_validator
class CouponBase(BaseModel):
code: str
- amount: Optional[int] = Field(default=None)
+ amount: int | None = Field(default=None)
usage_limit: int
usage_count: int = 0
is_used: bool = False
- days: Optional[int] = Field(default=None)
+ days: int | None = Field(default=None)
@model_validator(mode="after")
def check_exactly_one_of_amount_or_days(self) -> "CouponBase":
@@ -31,12 +32,12 @@ class CouponResponse(CouponBase):
class CouponUpdate(BaseModel):
- code: Optional[str] = None
- amount: Optional[int] = None
- usage_limit: Optional[int] = None
- usage_count: Optional[int] = None
- is_used: Optional[bool] = None
- days: Optional[int] = Field(default=None)
+ code: str | None = None
+ amount: int | None = None
+ usage_limit: int | None = None
+ usage_count: int | None = None
+ is_used: bool | None = None
+ days: int | None = Field(default=None)
@model_validator(mode="after")
def validate_amount_or_days(self) -> "CouponUpdate":
@@ -53,4 +54,4 @@ class CouponUsageResponse(BaseModel):
used_at: datetime
class Config:
- from_attributes = True
\ No newline at end of file
+ from_attributes = True
diff --git a/api/schemas/gifts.py b/api/schemas/gifts.py
index f40906f8..e47ea6cc 100644
--- a/api/schemas/gifts.py
+++ b/api/schemas/gifts.py
@@ -1,18 +1,19 @@
-from pydantic import BaseModel
-from typing import Optional
from datetime import datetime
+from typing import Optional
+
+from pydantic import BaseModel
class GiftBase(BaseModel):
sender_tg_id: int
- recipient_tg_id: Optional[int] = None
- selected_months: Optional[int] = None
+ recipient_tg_id: int | None = None
+ selected_months: int | None = None
expiry_time: datetime
gift_link: str
is_used: bool = False
- is_unlimited: Optional[bool] = False
- max_usages: Optional[int] = None
- tariff_id: Optional[int] = None
+ is_unlimited: bool | None = False
+ max_usages: int | None = None
+ tariff_id: int | None = None
class GiftResponse(GiftBase):
@@ -33,14 +34,14 @@ class GiftUsageResponse(BaseModel):
class GiftUpdate(BaseModel):
- recipient_tg_id: Optional[int] = None
- selected_months: Optional[int] = None
- expiry_time: Optional[datetime] = None
- gift_link: Optional[str] = None
- is_used: Optional[bool] = None
- is_unlimited: Optional[bool] = None
- max_usages: Optional[int] = None
- tariff_id: Optional[int] = None
+ recipient_tg_id: int | None = None
+ selected_months: int | None = None
+ expiry_time: datetime | None = None
+ gift_link: str | None = None
+ is_used: bool | None = None
+ is_unlimited: bool | None = None
+ max_usages: int | None = None
+ tariff_id: int | None = None
class Config:
- from_attributes = True
\ No newline at end of file
+ from_attributes = True
diff --git a/api/schemas/keys.py b/api/schemas/keys.py
index c4d821f9..d6f35609 100644
--- a/api/schemas/keys.py
+++ b/api/schemas/keys.py
@@ -1,21 +1,22 @@
-from pydantic import BaseModel, Field
from typing import Optional
+from pydantic import BaseModel, Field
+
class KeyBase(BaseModel):
tg_id: int
client_id: str
- email: Optional[str] = None
- created_at: Optional[int] = None
+ email: str | None = None
+ created_at: int | None = None
expiry_time: int
- key: Optional[str] = None
- server_id: Optional[str] = None
- remnawave_link: Optional[str] = None
- tariff_id: Optional[int] = None
- is_frozen: Optional[bool] = False
- alias: Optional[str] = None
- notified: Optional[bool] = False
- notified_24h: Optional[bool] = False
+ key: str | None = None
+ server_id: str | None = None
+ remnawave_link: str | None = None
+ tariff_id: int | None = None
+ is_frozen: bool | None = False
+ alias: str | None = None
+ notified: bool | None = False
+ notified_24h: bool | None = False
class KeyResponse(KeyBase):
@@ -24,39 +25,39 @@ class KeyResponse(KeyBase):
class KeyDetailsResponse(BaseModel):
- key: Optional[str]
- remnawave_link: Optional[str]
- server_id: Optional[str]
- created_at: Optional[int]
- expiry_time: Optional[int]
+ key: str | None
+ remnawave_link: str | None
+ server_id: str | None
+ created_at: int | None
+ expiry_time: int | None
client_id: str
tg_id: int
- email: Optional[str]
+ email: str | None
is_frozen: bool
balance: float
- alias: Optional[str]
+ alias: str | None
expiry_date: str
days_left_message: str
- link: Optional[str]
- cluster_name: Optional[str]
- location_name: Optional[str]
- tariff_id: Optional[int]
+ link: str | None
+ cluster_name: str | None
+ location_name: str | None
+ tariff_id: int | None
class Config:
from_attributes = True
class KeyUpdate(BaseModel):
- email: Optional[str] = None
- expiry_time: Optional[int] = None
- key: Optional[str] = None
- server_id: Optional[str] = None
- remnawave_link: Optional[str] = None
- tariff_id: Optional[int] = None
- is_frozen: Optional[bool] = None
- alias: Optional[str] = None
- notified: Optional[bool] = None
- notified_24h: Optional[bool] = None
+ email: str | None = None
+ expiry_time: int | None = None
+ key: str | None = None
+ server_id: str | None = None
+ remnawave_link: str | None = None
+ tariff_id: int | None = None
+ is_frozen: bool | None = None
+ alias: str | None = None
+ notified: bool | None = None
+ notified_24h: bool | None = None
class Config:
from_attributes = True
@@ -68,10 +69,10 @@ class KeyCreateRequest(BaseModel):
tariff_id: int = Field(..., description="ID тарифа из базы данных")
client_id: str = Field(..., description="UUID клиента (уникальный)")
expiry_timestamp: int = Field(..., description="Срок окончания в миллисекундах")
-
- email: Optional[str] = Field(None, description="Условное имя подписки")
- alias: Optional[str] = Field(None, description="пользовательское имя")
- remnawave_link: Optional[str] = Field(None, description="Ссылка на подписку Remnawave")
- hwid_limit: Optional[int] = Field(None, description="Ограничение по HWID")
- traffic_limit_bytes: Optional[int] = Field(None, description="Ограничение трафика в байтах")
- is_trial: Optional[bool] = Field(False, description="Флаг триального ключа")
\ No newline at end of file
+
+ email: str | None = Field(None, description="Условное имя подписки")
+ alias: str | None = Field(None, description="пользовательское имя")
+ remnawave_link: str | None = Field(None, description="Ссылка на подписку Remnawave")
+ hwid_limit: int | None = Field(None, description="Ограничение по HWID")
+ traffic_limit_bytes: int | None = Field(None, description="Ограничение трафика в байтах")
+ is_trial: bool | None = Field(False, description="Флаг триального ключа")
diff --git a/api/schemas/misc.py b/api/schemas/misc.py
index 8c2e0a63..23995d20 100644
--- a/api/schemas/misc.py
+++ b/api/schemas/misc.py
@@ -1,6 +1,7 @@
-from pydantic import BaseModel
-from typing import Optional, Union, Literal
from datetime import datetime
+from typing import Literal, Optional, Union
+
+from pydantic import BaseModel
class PaymentBase(BaseModel):
@@ -38,14 +39,14 @@ class NotificationResponse(BaseModel):
class GiftBase(BaseModel):
sender_tg_id: int
- recipient_tg_id: Optional[int] = None
+ recipient_tg_id: int | None = None
selected_months: int
expiry_time: datetime
gift_link: str
is_used: bool = False
is_unlimited: bool = False
- max_usages: Optional[int] = None
- tariff_id: Optional[int] = None
+ max_usages: int | None = None
+ tariff_id: int | None = None
class GiftResponse(GiftBase):
@@ -70,7 +71,7 @@ class ManualBanResponse(BaseModel):
banned_at: datetime
reason: str
banned_by: int
- until: Optional[datetime] = None
+ until: datetime | None = None
class Config:
from_attributes = True
diff --git a/api/schemas/referrals.py b/api/schemas/referrals.py
index cf4660ac..39c60d42 100644
--- a/api/schemas/referrals.py
+++ b/api/schemas/referrals.py
@@ -7,4 +7,4 @@ class ReferralResponse(BaseModel):
reward_issued: bool = False
class Config:
- from_attributes = True
\ No newline at end of file
+ from_attributes = True
diff --git a/api/schemas/servers.py b/api/schemas/servers.py
index e4065208..5c8a5f36 100644
--- a/api/schemas/servers.py
+++ b/api/schemas/servers.py
@@ -1,16 +1,17 @@
-from pydantic import BaseModel
from typing import Optional
+from pydantic import BaseModel
+
class ServerBase(BaseModel):
cluster_name: str
server_name: str
api_url: str
- subscription_url: Optional[str] = None
+ subscription_url: str | None = None
inbound_id: str
panel_type: str
- max_keys: Optional[int] = None
- tariff_group: Optional[str] = ""
+ max_keys: int | None = None
+ tariff_group: str | None = ""
enabled: bool = True
@@ -22,15 +23,15 @@ class ServerResponse(ServerBase):
class ServerUpdate(BaseModel):
- cluster_name: Optional[str] = None
- server_name: Optional[str] = None
- api_url: Optional[str] = None
- subscription_url: Optional[str] = None
- inbound_id: Optional[str] = None
- panel_type: Optional[str] = None
- max_keys: Optional[int] = None
- tariff_group: Optional[str] = None
- enabled: Optional[bool] = None
+ cluster_name: str | None = None
+ server_name: str | None = None
+ api_url: str | None = None
+ subscription_url: str | None = None
+ inbound_id: str | None = None
+ panel_type: str | None = None
+ max_keys: int | None = None
+ tariff_group: str | None = None
+ enabled: bool | None = None
class Config:
- from_attributes = True
\ No newline at end of file
+ from_attributes = True
diff --git a/api/schemas/tariffs.py b/api/schemas/tariffs.py
index 24244971..519f9ec0 100644
--- a/api/schemas/tariffs.py
+++ b/api/schemas/tariffs.py
@@ -1,7 +1,7 @@
-from pydantic import BaseModel
-from typing import Optional
from datetime import datetime
+from typing import Optional
+from pydantic import BaseModel
class TariffBase(BaseModel):
@@ -9,30 +9,30 @@ class TariffBase(BaseModel):
group_code: str
duration_days: int
price_rub: int
- traffic_limit: Optional[int] = None
- device_limit: Optional[int] = None
+ traffic_limit: int | None = None
+ device_limit: int | None = None
is_active: bool = True
- subgroup_title: Optional[str] = None
+ subgroup_title: str | None = None
class TariffResponse(TariffBase):
id: int
- created_at: Optional[datetime] = None
- updated_at: Optional[datetime] = None
+ created_at: datetime | None = None
+ updated_at: datetime | None = None
class Config:
from_attributes = True
class TariffUpdate(BaseModel):
- name: Optional[str] = None
- group_code: Optional[str] = None
- duration_days: Optional[int] = None
- price_rub: Optional[int] = None
- traffic_limit: Optional[int] = None
- device_limit: Optional[int] = None
- is_active: Optional[bool] = None
- subgroup_title: Optional[str] = None
+ name: str | None = None
+ group_code: str | None = None
+ duration_days: int | None = None
+ price_rub: int | None = None
+ traffic_limit: int | None = None
+ device_limit: int | None = None
+ is_active: bool | None = None
+ subgroup_title: str | None = None
class Config:
- from_attributes = True
\ No newline at end of file
+ from_attributes = True
diff --git a/api/schemas/users.py b/api/schemas/users.py
index c3e29d66..659b550b 100644
--- a/api/schemas/users.py
+++ b/api/schemas/users.py
@@ -1,37 +1,38 @@
-from pydantic import BaseModel
-from typing import Optional
from datetime import datetime
+from typing import Optional
+
+from pydantic import BaseModel
class UserBase(BaseModel):
tg_id: int
- username: Optional[str] = None
- first_name: Optional[str] = None
- last_name: Optional[str] = None
- language_code: Optional[str] = None
- is_bot: Optional[bool] = False
+ username: str | None = None
+ first_name: str | None = None
+ last_name: str | None = None
+ language_code: str | None = None
+ is_bot: bool | None = False
balance: float = 0.0
trial: int = 0
- source_code: Optional[str] = None
+ source_code: str | None = None
class UserResponse(UserBase):
- created_at: Optional[datetime]
- updated_at: Optional[datetime]
+ created_at: datetime | None
+ updated_at: datetime | None
class Config:
from_attributes = True
class UserUpdate(BaseModel):
- username: Optional[str] = None
- first_name: Optional[str] = None
- last_name: Optional[str] = None
- language_code: Optional[str] = None
- is_bot: Optional[bool] = None
- balance: Optional[float] = None
- trial: Optional[int] = None
- source_code: Optional[str] = None
+ username: str | None = None
+ first_name: str | None = None
+ last_name: str | None = None
+ language_code: str | None = None
+ is_bot: bool | None = None
+ balance: float | None = None
+ trial: int | None = None
+ source_code: str | None = None
class Config:
from_attributes = True
diff --git a/backup.py b/backup.py
index 98b48b55..481c0953 100644
--- a/backup.py
+++ b/backup.py
@@ -1,16 +1,28 @@
import os
import subprocess
+
from datetime import datetime, timedelta
from pathlib import Path
import aiofiles
-from aiogram.types import BufferedInputFile
+
from aiogram import Bot
+from aiogram.types import BufferedInputFile
from bot import bot
from config import (
- ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT,
- BACKUP_SEND_MODE, BACKUP_CHANNEL_ID, BACKUP_CHANNEL_THREAD_ID, BACKUP_OTHER_BOT_TOKEN, BACKUP_CAPTION
+ ADMIN_ID,
+ BACKUP_CAPTION,
+ BACKUP_CHANNEL_ID,
+ BACKUP_CHANNEL_THREAD_ID,
+ BACKUP_OTHER_BOT_TOKEN,
+ BACKUP_SEND_MODE,
+ BACK_DIR,
+ DB_NAME,
+ DB_PASSWORD,
+ DB_USER,
+ PG_HOST,
+ PG_PORT,
)
from logger import logger
@@ -146,9 +158,7 @@ async def _send_backup_to_admins(backup_file_path: str) -> None:
async def send_default():
for admin_id in ADMIN_ID:
try:
- await bot.send_document(
- chat_id=admin_id, document=backup_input_file
- )
+ await bot.send_document(chat_id=admin_id, document=backup_input_file)
logger.info(f"Бэкап базы данных отправлен админу: {admin_id}")
except Exception as e:
logger.error(f"Не удалось отправить бэкап админу {admin_id}: {e}")
@@ -169,7 +179,7 @@ async def _send_backup_to_admins(backup_file_path: str) -> None:
logger.error("BACKUP_CHANNEL_ID не задан для режима 'channel', fallback на default")
await send_default()
return
- send_kwargs = dict(chat_id=channel_id, document=backup_input_file)
+ send_kwargs = {"chat_id": channel_id, "document": backup_input_file}
if thread_id:
send_kwargs["message_thread_id"] = int(thread_id)
if BACKUP_CAPTION:
@@ -190,7 +200,7 @@ async def _send_backup_to_admins(backup_file_path: str) -> None:
try:
for admin_id in ADMIN_ID:
try:
- send_kwargs = dict(chat_id=admin_id, document=backup_input_file)
+ send_kwargs = {"chat_id": admin_id, "document": backup_input_file}
if BACKUP_CAPTION:
send_kwargs["caption"] = BACKUP_CAPTION
await other_bot.send_document(**send_kwargs)
diff --git a/bot.py b/bot.py
index 58acf948..3ec52f71 100644
--- a/bot.py
+++ b/bot.py
@@ -1,5 +1,6 @@
-import traceback
import os
+import subprocess
+import traceback
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
@@ -9,12 +10,12 @@ from aiogram.filters import ExceptionTypeFilter
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import BufferedInputFile, ErrorEvent
from aiogram.utils.markdown import hbold
-import subprocess
from config import ADMIN_ID, API_TOKEN
from filters.private import IsPrivateFilter
from logger import logger
+
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
storage = MemoryStorage()
dp = Dispatcher(bot=bot, storage=storage)
@@ -32,32 +33,25 @@ def get_git_commit_number() -> str:
env["GIT_WORK_TREE"] = cwd
try:
- local_number = subprocess.check_output(
- ["git", "rev-list", "--count", "HEAD"],
- cwd=cwd,
- env=env
- ).decode().strip()
+ local_number = (
+ subprocess.check_output(["git", "rev-list", "--count", "HEAD"], cwd=cwd, env=env).decode().strip()
+ )
- local_hash = subprocess.check_output(
- ["git", "rev-parse", "HEAD"],
- cwd=cwd,
- env=env
- ).decode().strip()
+ local_hash = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=cwd, env=env).decode().strip()
try:
- branch = subprocess.check_output(
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
- cwd=cwd,
- env=env
- ).decode().strip()
+ branch = (
+ subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, env=env).decode().strip()
+ )
if branch == "HEAD":
- describe = subprocess.check_output(
- ["git", "describe", "--tags", "--exact-match"],
- cwd=cwd,
- env=env,
- stderr=subprocess.DEVNULL
- ).decode().strip()
+ describe = (
+ subprocess.check_output(
+ ["git", "describe", "--tags", "--exact-match"], cwd=cwd, env=env, stderr=subprocess.DEVNULL
+ )
+ .decode()
+ .strip()
+ )
if describe.startswith("v") or "release" in describe.lower():
branch = "main"
@@ -71,29 +65,25 @@ def get_git_commit_number() -> str:
try:
remote_commit = subprocess.check_output(
- ["git", "ls-remote", "origin", f"refs/heads/{branch}"],
- cwd=cwd,
- env=env
+ ["git", "ls-remote", "origin", f"refs/heads/{branch}"], cwd=cwd, env=env
).decode()
remote_hash = remote_commit.split()[0]
- remote_number = subprocess.check_output(
- ["git", "rev-list", "--count", remote_hash],
- cwd=cwd,
- env=env
- ).decode().strip()
+ remote_number = (
+ subprocess.check_output(["git", "rev-list", "--count", remote_hash], cwd=cwd, env=env).decode().strip()
+ )
if local_hash == remote_hash:
return "\n(Актуальная версия)"
return (
- f"\n(commit "
+ f'\n(commit '
f"#{local_number} / actual commit "
- f"#{remote_number})"
+ f'#{remote_number})'
)
except Exception:
- return f"\n(Требуется обновление через CLI, команда sudo solobot)"
+ return "\n(Требуется обновление через CLI, команда sudo solobot)"
version = f"v4.4-b030735{get_git_commit_number()}"
@@ -113,8 +103,7 @@ async def errors_handler(event: ErrorEvent, bot: Bot) -> bool:
error_message = str(event.exception)
if (
- "query is too old and response timeout expired or query ID is invalid"
- in error_message
+ "query is too old and response timeout expired or query ID is invalid" in error_message
or "message can't be deleted for everyone" in error_message
or "message to delete not found" in error_message
):
diff --git a/cli_launcher.py b/cli_launcher.py
index fdd42af2..6d5bd7c6 100755
--- a/cli_launcher.py
+++ b/cli_launcher.py
@@ -4,6 +4,7 @@ import subprocess
import sys
import requests
+
from rich.console import Console
from rich.prompt import Confirm, Prompt
from rich.table import Table
@@ -11,6 +12,7 @@ from rich.text import Text
from config import BOT_SERVICE
+
try:
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")
@@ -30,13 +32,9 @@ IS_ROOT_DIR = PROJECT_DIR == "/root"
if IS_ROOT_DIR:
console.print("[bold red]⛔ КРИТИЧЕСКАЯ ОШИБКА:[/bold red]")
- console.print(
- "[red]Обнаружена установка бота прямо в корневой папке (/root).[/red]"
- )
+ console.print("[red]Обнаружена установка бота прямо в корневой папке (/root).[/red]")
console.print("[red]Это крайне опасно и может привести к потере данных![/red]")
- console.print(
- "[yellow]Рекомендуется перенести бота в отдельную папку, например /root/solobot[/yellow]"
- )
+ console.print("[yellow]Рекомендуется перенести бота в отдельную папку, например /root/solobot[/yellow]")
console.print("[red]Обновление заблокировано в целях безопасности.[/red]")
sys.exit(1)
GITHUB_REPO = "https://github.com/Vladless/Solo_bot"
@@ -44,9 +42,7 @@ SERVICE_NAME = BOT_SERVICE
def is_service_exists(service_name):
- result = subprocess.run(
- ["systemctl", "list-unit-files", service_name], capture_output=True, text=True
- )
+ result = subprocess.run(["systemctl", "list-unit-files", service_name], capture_output=True, text=True)
return service_name in result.stdout
@@ -79,9 +75,7 @@ def fix_permissions():
try:
user = os.getenv("SUDO_USER") or os.getenv("USER")
if user:
- subprocess.run(
- ["sudo", "chown", "-R", f"{user}:{user}", PROJECT_DIR], check=True
- )
+ subprocess.run(["sudo", "chown", "-R", f"{user}:{user}", PROJECT_DIR], check=True)
subprocess.run(["sudo", "chmod", "-R", "u=rwX,go=rX", PROJECT_DIR], check=True)
@@ -147,9 +141,7 @@ def install_dependencies():
with console.status("[bold green]Устанавливаются зависимости...[/bold green]"):
try:
if not os.path.exists("venv"):
- console.print(
- "[yellow]⚠️ Виртуальное окружение не найдено. Создаю...[/yellow]"
- )
+ console.print("[yellow]⚠️ Виртуальное окружение не найдено. Создаю...[/yellow]")
subprocess.run("python3 -m venv venv", shell=True, check=True)
subprocess.run(
@@ -202,9 +194,7 @@ def update_from_beta():
remote_version = get_remote_version(branch="dev")
if local_version and remote_version:
- console.print(
- f"[cyan]🔢 Локальная версия: {local_version} | Последняя в dev: {remote_version}[/cyan]"
- )
+ console.print(f"[cyan]🔢 Локальная версия: {local_version} | Последняя в dev: {remote_version}[/cyan]")
if local_version == remote_version:
if not Confirm.ask("[yellow]❗ Версия актуальна. Обновить всё равно?[/yellow]"):
return
@@ -257,9 +247,7 @@ def update_from_beta():
def update_from_release():
update_cli_launcher()
- if not Confirm.ask(
- "[yellow]🔁 Подтвердите обновление Solobot до одного из последних релизов[/yellow]"
- ):
+ if not Confirm.ask("[yellow]🔁 Подтвердите обновление Solobot до одного из последних релизов[/yellow]"):
return
console.print("[red]⚠️ ВНИМАНИЕ! Папка бота будет полностью перезаписана![/red]")
@@ -275,9 +263,7 @@ def update_from_release():
install_rsync_if_needed()
try:
- response = requests.get(
- "https://api.github.com/repos/Vladless/Solo_bot/releases", timeout=10
- )
+ response = requests.get("https://api.github.com/repos/Vladless/Solo_bot/releases", timeout=10)
releases = response.json()[:3]
tag_choices = [r["tag_name"] for r in releases]
@@ -294,9 +280,7 @@ def update_from_release():
)
tag_name = tag_choices[int(selected) - 1]
- if not Confirm.ask(
- f"[yellow]🔁 Подтвердите установку релиза {tag_name}[/yellow]"
- ):
+ if not Confirm.ask(f"[yellow]🔁 Подтвердите установку релиза {tag_name}[/yellow]"):
return
console.print(f"[cyan]📥 Клонируем релиз {tag_name} во временную папку...[/cyan]")
@@ -336,9 +320,7 @@ def update_from_release():
def show_update_menu():
if IS_ROOT_DIR:
console.print("[red]⛔ Обновление невозможно: бот находится в /root[/red]")
- console.print(
- "[yellow]Перенесите бота в отдельную папку и повторите попытку[/yellow]"
- )
+ console.print("[yellow]Перенесите бота в отдельную папку и повторите попытку[/yellow]")
return
table = Table(title="Выберите способ обновления", title_style="bold green")
@@ -358,9 +340,7 @@ def show_update_menu():
def show_menu():
- table = Table(
- title="Solobot CLI v0.2.0", title_style="bold magenta", header_style="bold blue"
- )
+ table = Table(title="Solobot CLI v0.2.0", title_style="bold magenta", header_style="bold blue")
table.add_column("№", justify="center", style="cyan", no_wrap=True)
table.add_column("Операция", style="white")
table.add_row("1", "Запустить бота (systemd)")
@@ -383,9 +363,7 @@ def update_cli_launcher():
response = requests.get(url, timeout=10)
if response.status_code == 200:
- with open(
- os.path.join(PROJECT_DIR, "cli_launcher.py"), "w", encoding="utf-8"
- ) as f:
+ with open(os.path.join(PROJECT_DIR, "cli_launcher.py"), "w", encoding="utf-8") as f:
f.write(response.text)
console.print("[green]✅ CLI лаунчер успешно обновлён[/green]")
os.chmod(os.path.join(PROJECT_DIR, "cli_launcher.py"), 0o755)
@@ -411,39 +389,31 @@ def main():
else:
console.print(f"[red]❌ Служба {SERVICE_NAME} не найдена.[/red]")
elif choice == "2":
- if Confirm.ask(
- "[green]Вы действительно хотите запустить main.py вручную?[/green]"
- ):
+ if Confirm.ask("[green]Вы действительно хотите запустить main.py вручную?[/green]"):
subprocess.run(["venv/bin/python", "main.py"])
elif choice == "3":
if is_service_exists(SERVICE_NAME):
- if Confirm.ask(
- "[yellow]Вы действительно хотите перезапустить бота?[/yellow]"
- ):
+ if Confirm.ask("[yellow]Вы действительно хотите перезапустить бота?[/yellow]"):
subprocess.run(["sudo", "systemctl", "restart", SERVICE_NAME])
else:
console.print(f"[red]❌ Служба {SERVICE_NAME} не найдена.[/red]")
elif choice == "4":
if is_service_exists(SERVICE_NAME):
- if Confirm.ask(
- "[red]Вы уверены, что хотите остановить бота?[/red]"
- ):
+ if Confirm.ask("[red]Вы уверены, что хотите остановить бота?[/red]"):
subprocess.run(["sudo", "systemctl", "stop", SERVICE_NAME])
else:
console.print(f"[red]❌ Служба {SERVICE_NAME} не найдена.[/red]")
elif choice == "5":
if is_service_exists(SERVICE_NAME):
- subprocess.run(
- [
- "sudo",
- "journalctl",
- "-u",
- SERVICE_NAME,
- "-n",
- "80",
- "--no-pager",
- ]
- )
+ subprocess.run([
+ "sudo",
+ "journalctl",
+ "-u",
+ SERVICE_NAME,
+ "-n",
+ "80",
+ "--no-pager",
+ ])
else:
console.print(f"[red]❌ Служба {SERVICE_NAME} не найдена.[/red]")
elif choice == "6":
diff --git a/database/bans.py b/database/bans.py
index bc9cd0a6..876ab675 100644
--- a/database/bans.py
+++ b/database/bans.py
@@ -5,10 +5,6 @@ from database.models import BlockedUser
async def create_blocked_user(session: AsyncSession, tg_id: int):
- stmt = (
- insert(BlockedUser)
- .values(tg_id=tg_id)
- .on_conflict_do_nothing(index_elements=[BlockedUser.tg_id])
- )
+ stmt = insert(BlockedUser).values(tg_id=tg_id).on_conflict_do_nothing(index_elements=[BlockedUser.tg_id])
await session.execute(stmt)
await session.commit()
diff --git a/database/coupons.py b/database/coupons.py
index ee1e228e..5fa0693b 100644
--- a/database/coupons.py
+++ b/database/coupons.py
@@ -8,9 +8,7 @@ from database.models import Coupon, CouponUsage
from logger import logger
-async def create_coupon(
- session: AsyncSession, code: str, amount: int, usage_limit: int, days: int = None
-) -> bool:
+async def create_coupon(session: AsyncSession, code: str, amount: int, usage_limit: int, days: int = None) -> bool:
try:
exists = await session.scalar(select(Coupon.id).where(Coupon.code == code))
if exists:
@@ -42,9 +40,7 @@ async def get_coupon_by_code(session: AsyncSession, code: str) -> Coupon | None:
return result.scalar_one_or_none()
-async def get_all_coupons(
- session: AsyncSession, page: int = 1, per_page: int = 10
-) -> dict:
+async def get_all_coupons(session: AsyncSession, page: int = 1, per_page: int = 10) -> dict:
offset = (page - 1) * per_page
stmt = select(Coupon).order_by(Coupon.id.desc()).offset(offset).limit(per_page)
@@ -81,9 +77,7 @@ async def delete_coupon(session: AsyncSession, code: str) -> bool:
async def create_coupon_usage(session: AsyncSession, coupon_id: int, user_id: int):
try:
- stmt = insert(CouponUsage).values(
- coupon_id=coupon_id, user_id=user_id, used_at=datetime.utcnow()
- )
+ stmt = insert(CouponUsage).values(coupon_id=coupon_id, user_id=user_id, used_at=datetime.utcnow())
await session.execute(stmt)
await session.commit()
logger.info(f"✅ Купон {coupon_id} использован пользователем {user_id}")
@@ -92,12 +86,8 @@ async def create_coupon_usage(session: AsyncSession, coupon_id: int, user_id: in
await session.rollback()
-async def check_coupon_usage(
- session: AsyncSession, coupon_id: int, user_id: int
-) -> bool:
- stmt = select(CouponUsage).where(
- CouponUsage.coupon_id == coupon_id, CouponUsage.user_id == user_id
- )
+async def check_coupon_usage(session: AsyncSession, coupon_id: int, user_id: int) -> bool:
+ stmt = select(CouponUsage).where(CouponUsage.coupon_id == coupon_id, CouponUsage.user_id == user_id)
result = await session.execute(stmt)
return result.scalar_one_or_none() is not None
@@ -109,9 +99,7 @@ async def update_coupon_usage_count(session: AsyncSession, coupon_id: int):
.where(Coupon.id == coupon_id)
.values(
usage_count=Coupon.usage_count + 1,
- is_used=case(
- (Coupon.usage_count + 1 >= Coupon.usage_limit, True), else_=False
- ),
+ is_used=case((Coupon.usage_count + 1 >= Coupon.usage_limit, True), else_=False),
)
)
await session.commit()
diff --git a/database/db.py b/database/db.py
index b7312561..9aaea5fc 100644
--- a/database/db.py
+++ b/database/db.py
@@ -3,10 +3,9 @@ from sqlalchemy.orm import declarative_base
from config import DATABASE_URL
+
engine = create_async_engine(DATABASE_URL, echo=False, future=True)
-async_session_maker = async_sessionmaker(
- bind=engine, expire_on_commit=False, class_=AsyncSession
-)
+async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
Base = declarative_base()
diff --git a/database/hot_leads.py b/database/hot_leads.py
index 1b5c0bc6..2e9c1063 100644
--- a/database/hot_leads.py
+++ b/database/hot_leads.py
@@ -8,11 +8,7 @@ async def get_hot_leads(session: AsyncSession):
"""
Возвращает пользователей, у которых есть успешные оплаты, но нет активных ключей.
"""
- subquery = (
- select(Key.tg_id)
- .where(Key.expiry_time > func.extract("epoch", func.now()) * 1000)
- .distinct()
- )
+ subquery = select(Key.tg_id).where(Key.expiry_time > func.extract("epoch", func.now()) * 1000).distinct()
stmt = (
select(Payment.tg_id)
diff --git a/database/importer.py b/database/importer.py
new file mode 100644
index 00000000..717ae347
--- /dev/null
+++ b/database/importer.py
@@ -0,0 +1,115 @@
+import json
+import sqlite3
+import time
+
+from datetime import datetime
+from itertools import cycle
+
+from sqlalchemy import select
+from sqlalchemy.exc import SQLAlchemyError
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from database.models import Key, Server, User
+
+
+async def import_keys_from_3xui_db(db_path: str, session: AsyncSession) -> tuple[int, int]:
+ imported = 0
+ skipped = 0
+
+ result = await session.execute(
+ select(Server.cluster_name)
+ .where(Server.enabled is True, Server.panel_type == "3x-ui", Server.cluster_name.isnot(None))
+ .distinct()
+ )
+ clusters = [row[0] for row in result.fetchall()]
+ if not clusters:
+ raise RuntimeError("❌ Не найдено доступных кластеров для 3x-ui")
+ cluster_cycle = cycle(clusters)
+
+ try:
+ conn = sqlite3.connect(db_path)
+ cursor = conn.cursor()
+ cursor.execute("SELECT id, remark, settings FROM inbounds")
+ inbounds = cursor.fetchall()
+ except Exception as e:
+ raise RuntimeError(f"Не удалось прочитать SQLite: {e}")
+ finally:
+ conn.close()
+
+ parsed_clients = []
+ for inbound_id, _remark, settings_raw in inbounds:
+ try:
+ settings = json.loads(settings_raw)
+ clients = settings.get("clients", [])
+ for c in clients:
+ expiry = c.get("expiryTime")
+ c["expiryTime"] = int(float(expiry)) if expiry else 0
+ c["limitIp"] = int(c.get("limitIp", 0) or 0)
+ c["inbound_id"] = inbound_id
+ parsed_clients.append(c)
+ except Exception:
+ continue
+
+ now_ts = int(time.time() * 1000)
+
+ for c in parsed_clients:
+ tg_id = c.get("tgId")
+ client_id = str(c.get("id"))
+ email = c.get("email")
+ expiry_time = int(c.get("expiryTime") or now_ts)
+ created_at = now_ts
+ server_id = next(cluster_cycle)
+
+ if not tg_id or not client_id:
+ continue
+
+ user_exists = await session.execute(select(User).where(User.tg_id == tg_id))
+ if not user_exists.scalar():
+ try:
+ session.add(
+ User(
+ tg_id=tg_id,
+ username=None,
+ first_name=None,
+ last_name=None,
+ language_code=None,
+ is_bot=False,
+ balance=0.0,
+ trial=1,
+ source_code=None,
+ created_at=datetime.utcnow(),
+ updated_at=datetime.utcnow(),
+ )
+ )
+ except SQLAlchemyError:
+ continue
+
+ key_exists = await session.execute(select(Key).where(Key.client_id == client_id))
+ if key_exists.scalar():
+ skipped += 1
+ continue
+
+ try:
+ session.add(
+ Key(
+ tg_id=tg_id,
+ client_id=client_id,
+ email=email,
+ created_at=created_at,
+ expiry_time=expiry_time,
+ key="",
+ server_id=server_id,
+ remnawave_link=None,
+ tariff_id=None,
+ is_frozen=False,
+ alias=None,
+ notified=False,
+ notified_24h=False,
+ )
+ )
+ imported += 1
+ except SQLAlchemyError:
+ continue
+
+ await session.commit()
+ return imported, skipped
diff --git a/database/init_db.py b/database/init_db.py
index ffe41379..c278d755 100644
--- a/database/init_db.py
+++ b/database/init_db.py
@@ -1,9 +1,12 @@
-from database.db import engine, async_session_maker
-from database.models import Base, Admin, User
-from sqlalchemy import select
-from config import ADMIN_ID
from datetime import datetime
+from sqlalchemy import select
+
+from config import ADMIN_ID
+from database.db import async_session_maker, engine
+from database.models import Admin, Base, User
+
+
async def init_db():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
@@ -11,22 +14,23 @@ async def init_db():
async with async_session_maker() as session:
result = await session.execute(select(User).where(User.tg_id == 0))
if not result.scalar_one_or_none():
- session.add(User(
- tg_id=0,
- username="system",
- first_name="System",
- is_bot=True,
- created_at=datetime.utcnow(),
- updated_at=datetime.utcnow()
- ))
-
+ session.add(
+ User(
+ tg_id=0,
+ username="system",
+ first_name="System",
+ is_bot=True,
+ created_at=datetime.utcnow(),
+ updated_at=datetime.utcnow(),
+ )
+ )
+
for tg_id in ADMIN_ID:
result = await session.execute(select(Admin).where(Admin.tg_id == tg_id))
if not result.scalar_one_or_none():
- session.add(Admin(
- tg_id=tg_id,
- role="superadmin",
- description="Imported from config",
- added_at=datetime.utcnow()
- ))
+ session.add(
+ Admin(
+ tg_id=tg_id, role="superadmin", description="Imported from config", added_at=datetime.utcnow()
+ )
+ )
await session.commit()
diff --git a/database/keys.py b/database/keys.py
index 0a4048de..d2a5bc7a 100644
--- a/database/keys.py
+++ b/database/keys.py
@@ -20,13 +20,9 @@ async def store_key(
tariff_id: int = None,
):
try:
- exists = await session.execute(
- select(Key).where(Key.tg_id == tg_id, Key.client_id == client_id)
- )
+ exists = await session.execute(select(Key).where(Key.tg_id == tg_id, Key.client_id == client_id))
if exists.scalar_one_or_none():
- logger.info(
- f"[Store Key] Ключ уже существует — пропускаем: tg_id={tg_id}, client_id={client_id}"
- )
+ logger.info(f"[Store Key] Ключ уже существует — пропускаем: tg_id={tg_id}, client_id={client_id}")
return
new_key = Key(
@@ -42,9 +38,7 @@ async def store_key(
)
session.add(new_key)
await session.commit()
- logger.info(
- f"✅ Ключ сохранён: tg_id={tg_id}, client_id={client_id}, server_id={server_id}"
- )
+ logger.info(f"✅ Ключ сохранён: tg_id={tg_id}, client_id={client_id}, server_id={server_id}")
except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при сохранении ключа: {e}")
await session.rollback()
@@ -67,9 +61,7 @@ async def get_key_by_server(session: AsyncSession, tg_id: int, client_id: str):
async def get_key_details(session: AsyncSession, email: str) -> dict | None:
- stmt = (
- select(Key, User).join(User, Key.tg_id == User.tg_id).where(Key.email == email)
- )
+ stmt = select(Key, User).join(User, Key.tg_id == User.tg_id).where(Key.email == email)
result = await session.execute(stmt)
row = result.first()
if not row:
@@ -110,26 +102,18 @@ async def get_key_details(session: AsyncSession, email: str) -> dict | None:
async def get_key_count(session: AsyncSession, tg_id: int) -> int:
- result = await session.execute(
- select(func.count()).select_from(Key).where(Key.tg_id == tg_id)
- )
+ result = await session.execute(select(func.count()).select_from(Key).where(Key.tg_id == tg_id))
return result.scalar() or 0
async def delete_key(session: AsyncSession, identifier: int | str):
- stmt = delete(Key).where(
- Key.tg_id == identifier
- if str(identifier).isdigit()
- else Key.client_id == identifier
- )
+ stmt = delete(Key).where(Key.tg_id == identifier if str(identifier).isdigit() else Key.client_id == identifier)
await session.execute(stmt)
await session.commit()
logger.info(f"Ключ с идентификатором {identifier} удалён")
-async def update_key_expiry(
- session: AsyncSession, client_id: str, new_expiry_time: int
-):
+async def update_key_expiry(session: AsyncSession, client_id: str, new_expiry_time: int):
await session.execute(
update(Key)
.where(Key.client_id == client_id)
@@ -145,17 +129,11 @@ async def get_client_id_by_email(session: AsyncSession, email: str):
async def update_key_notified(session: AsyncSession, tg_id: int, client_id: str):
- await session.execute(
- update(Key)
- .where(Key.tg_id == tg_id, Key.client_id == client_id)
- .values(notified=True)
- )
+ await session.execute(update(Key).where(Key.tg_id == tg_id, Key.client_id == client_id).values(notified=True))
await session.commit()
-async def mark_key_as_frozen(
- session: AsyncSession, tg_id: int, client_id: str, time_left: int
-):
+async def mark_key_as_frozen(session: AsyncSession, tg_id: int, client_id: str, time_left: int):
await session.execute(
text(
"""
@@ -170,9 +148,7 @@ async def mark_key_as_frozen(
)
-async def mark_key_as_unfrozen(
- session: AsyncSession, tg_id: int, client_id: str, new_expiry_time: int
-):
+async def mark_key_as_unfrozen(session: AsyncSession, tg_id: int, client_id: str, new_expiry_time: int):
await session.execute(
text(
"""
@@ -188,10 +164,6 @@ async def mark_key_as_unfrozen(
async def update_key_tariff(session: AsyncSession, client_id: str, tariff_id: int):
- await session.execute(
- update(Key)
- .where(Key.client_id == client_id)
- .values(tariff_id=tariff_id)
- )
+ await session.execute(update(Key).where(Key.client_id == client_id).values(tariff_id=tariff_id))
await session.commit()
logger.info(f"Тариф ключа {client_id} обновлён на {tariff_id}")
diff --git a/database/models.py b/database/models.py
index df4dec38..f66f8bfa 100644
--- a/database/models.py
+++ b/database/models.py
@@ -1,3 +1,6 @@
+import secrets
+import uuid
+
from datetime import datetime
from sqlalchemy import (
@@ -12,10 +15,9 @@ from sqlalchemy import (
String,
Text,
)
-import secrets
-import uuid
from sqlalchemy.orm import Mapped, declarative_base, mapped_column
+
Base = declarative_base()
@@ -27,9 +29,7 @@ class DictLikeMixin:
return getattr(self, key, default)
def to_dict(self):
- return {
- column.name: getattr(self, column.name) for column in self.__table__.columns
- }
+ return {column.name: getattr(self, column.name) for column in self.__table__.columns}
class User(DictLikeMixin, Base):
@@ -139,11 +139,7 @@ class Referral(DictLikeMixin, Base):
class Notification(DictLikeMixin, Base):
__tablename__ = "notifications"
- tg_id = Column(
- BigInteger,
- ForeignKey("users.tg_id", ondelete="CASCADE"),
- primary_key=True
- )
+ tg_id = Column(BigInteger, ForeignKey("users.tg_id", ondelete="CASCADE"), primary_key=True)
notification_type = Column(String, primary_key=True)
last_notification_time = Column(DateTime, default=datetime.utcnow)
@@ -219,4 +215,4 @@ class Admin(Base):
@staticmethod
def generate_token() -> str:
- return secrets.token_urlsafe(32)
\ No newline at end of file
+ return secrets.token_urlsafe(32)
diff --git a/database/notifications.py b/database/notifications.py
index 0e92ed76..0e938b81 100644
--- a/database/notifications.py
+++ b/database/notifications.py
@@ -25,17 +25,13 @@ async def add_notification(session: AsyncSession, tg_id: int, notification_type:
)
await session.execute(stmt)
await session.commit()
- logger.info(
- f"✅ Добавлено уведомление {notification_type} для пользователя {tg_id}"
- )
+ logger.info(f"✅ Добавлено уведомление {notification_type} для пользователя {tg_id}")
except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при добавлении уведомления: {e}")
await session.rollback()
-async def delete_notification(
- session: AsyncSession, tg_id: int, notification_type: str
-):
+async def delete_notification(session: AsyncSession, tg_id: int, notification_type: str):
await session.execute(
delete(Notification).where(
Notification.tg_id == tg_id,
@@ -46,9 +42,7 @@ async def delete_notification(
logger.info(f"🗑 Уведомление {notification_type} для пользователя {tg_id} удалено")
-async def check_notification_time(
- session: AsyncSession, tg_id: int, notification_type: str, hours: int = 12
-) -> bool:
+async def check_notification_time(session: AsyncSession, tg_id: int, notification_type: str, hours: int = 12) -> bool:
stmt = select(Notification.last_notification_time).where(
Notification.tg_id == tg_id, Notification.notification_type == notification_type
)
@@ -59,9 +53,7 @@ async def check_notification_time(
return datetime.utcnow() - last_time > timedelta(hours=hours)
-async def get_last_notification_time(
- session: AsyncSession, tg_id: int, notification_type: str
-) -> int | None:
+async def get_last_notification_time(session: AsyncSession, tg_id: int, notification_type: str) -> int | None:
stmt = select(Notification.last_notification_time).where(
Notification.tg_id == tg_id, Notification.notification_type == notification_type
)
@@ -79,16 +71,14 @@ async def check_notifications_bulk(
tg_ids: list[int] = None,
emails: list[str] = None,
) -> list[dict]:
- from sqlalchemy import and_, func, select
- from database.models import User, Key, Notification, BlockedUser
+ from sqlalchemy import select
+
+ from database.models import BlockedUser, Notification
try:
now = datetime.utcnow()
subq_last_notification = (
- select(
- Notification.tg_id,
- func.max(Notification.last_notification_time).label("last_notification_time")
- )
+ select(Notification.tg_id, func.max(Notification.last_notification_time).label("last_notification_time"))
.where(Notification.notification_type == notification_type)
.group_by(Notification.tg_id)
.subquery()
@@ -112,7 +102,7 @@ async def check_notifications_bulk(
and_(
User.trial.in_([0, -1]),
~User.tg_id.in_(select(BlockedUser.tg_id)),
- ~User.tg_id.in_(select(Key.tg_id.distinct()))
+ ~User.tg_id.in_(select(Key.tg_id.distinct())),
)
)
@@ -135,9 +125,7 @@ async def check_notifications_bulk(
"username": row.username,
"first_name": row.first_name,
"last_name": row.last_name,
- "last_notification_time": (
- int(last_time.timestamp() * 1000) if last_time else None
- )
+ "last_notification_time": (int(last_time.timestamp() * 1000) if last_time else None),
})
logger.info(f"Найдено {len(users)} пользователей, готовых к уведомлению типа {notification_type}")
diff --git a/database/payments.py b/database/payments.py
index ec20c673..65ff9696 100644
--- a/database/payments.py
+++ b/database/payments.py
@@ -1,9 +1,9 @@
from datetime import datetime
+from pytz import timezone
from sqlalchemy import insert, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
-from pytz import timezone
from database.models import Payment
from logger import logger
@@ -12,9 +12,7 @@ from logger import logger
MOSCOW_TZ = timezone("Europe/Moscow")
-async def add_payment(
- session: AsyncSession, tg_id: int, amount: float, payment_system: str
-):
+async def add_payment(session: AsyncSession, tg_id: int, amount: float, payment_system: str):
try:
now_moscow = datetime.now(MOSCOW_TZ).replace(tzinfo=None)
stmt = insert(Payment).values(
@@ -26,9 +24,7 @@ async def add_payment(
)
await session.execute(stmt)
await session.commit()
- logger.info(
- f"✅ Успешно добавлен платёж: {tg_id}, {amount}₽ через {payment_system}"
- )
+ logger.info(f"✅ Успешно добавлен платёж: {tg_id}, {amount}₽ через {payment_system}")
except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при добавлении платежа: {e}")
await session.rollback()
@@ -38,15 +34,10 @@ async def add_payment(
async def get_last_payments(session: AsyncSession, tg_id: int, limit: int = 3):
try:
result = await session.execute(
- select(Payment)
- .where(Payment.tg_id == tg_id)
- .order_by(Payment.created_at.desc())
- .limit(limit)
+ select(Payment).where(Payment.tg_id == tg_id).order_by(Payment.created_at.desc()).limit(limit)
)
payments = result.scalars().all()
- logger.info(
- f"✅ Получены последние платежи пользователя {tg_id}, всего: {len(payments)}"
- )
+ logger.info(f"✅ Получены последние платежи пользователя {tg_id}, всего: {len(payments)}")
return [dict(p.__dict__) for p in payments]
except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при получении платежей пользователя {tg_id}: {e}")
diff --git a/database/referrals.py b/database/referrals.py
index cdf4f38d..e29a1ee9 100644
--- a/database/referrals.py
+++ b/database/referrals.py
@@ -13,23 +13,17 @@ async def add_referral(session: AsyncSession, referred_tg_id: int, referrer_tg_i
logger.warning(f"⚠️ Попытка самореферала: {referred_tg_id}")
return
- stmt = insert(Referral).values(
- referred_tg_id=referred_tg_id, referrer_tg_id=referrer_tg_id
- )
+ stmt = insert(Referral).values(referred_tg_id=referred_tg_id, referrer_tg_id=referrer_tg_id)
await session.execute(stmt)
await session.commit()
- logger.info(
- f"✅ Добавлена реферальная связь: {referred_tg_id} → {referrer_tg_id}"
- )
+ logger.info(f"✅ Добавлена реферальная связь: {referred_tg_id} → {referrer_tg_id}")
except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при добавлении реферала: {e}")
await session.rollback()
raise
-async def get_referral_by_referred_id(
- session: AsyncSession, referred_tg_id: int
-) -> dict | None:
+async def get_referral_by_referred_id(session: AsyncSession, referred_tg_id: int) -> dict | None:
stmt = select(Referral).where(Referral.referred_tg_id == referred_tg_id)
result = await session.execute(stmt)
row = result.scalar_one_or_none()
@@ -37,11 +31,7 @@ async def get_referral_by_referred_id(
async def get_total_referrals(session: AsyncSession, referrer_tg_id: int) -> int:
- stmt = (
- select(func.count())
- .select_from(Referral)
- .where(Referral.referrer_tg_id == referrer_tg_id)
- )
+ stmt = select(func.count()).select_from(Referral).where(Referral.referrer_tg_id == referrer_tg_id)
result = await session.execute(stmt)
return result.scalar()
@@ -62,17 +52,11 @@ async def get_active_referrals(session: AsyncSession, referrer_tg_id: int) -> in
async def mark_referral_reward_issued(session: AsyncSession, referred_tg_id: int):
- await session.execute(
- update(Referral)
- .where(Referral.referred_tg_id == referred_tg_id)
- .values(reward_issued=True)
- )
+ await session.execute(update(Referral).where(Referral.referred_tg_id == referred_tg_id).values(reward_issued=True))
await session.commit()
-async def get_total_referral_bonus(
- session: AsyncSession, referrer_tg_id: int, max_levels: int
-) -> float:
+async def get_total_referral_bonus(session: AsyncSession, referrer_tg_id: int, max_levels: int) -> float:
if CHECK_REFERRAL_REWARD_ISSUED:
bonus_cte = """
WITH RECURSIVE
@@ -168,9 +152,7 @@ async def get_total_referral_bonus(
"""
)
- result = await session.execute(
- text(bonus_query), {"tg_id": referrer_tg_id, "max_levels": max_levels}
- )
+ result = await session.execute(text(bonus_query), {"tg_id": referrer_tg_id, "max_levels": max_levels})
total_bonus_raw = result.scalar()
total_bonus = round(float(total_bonus_raw or 0), 2)
@@ -178,9 +160,7 @@ async def get_total_referral_bonus(
return total_bonus
-async def get_referrals_by_level(
- session: AsyncSession, referrer_tg_id: int, max_levels: int
-) -> dict:
+async def get_referrals_by_level(session: AsyncSession, referrer_tg_id: int, max_levels: int) -> dict:
query = """
WITH RECURSIVE referral_levels AS (
SELECT referred_tg_id, referrer_tg_id, 1 AS level
@@ -200,9 +180,7 @@ async def get_referrals_by_level(
GROUP BY level
ORDER BY level
"""
- result = await session.execute(
- text(query), {"referrer_tg_id": referrer_tg_id, "max_levels": max_levels}
- )
+ result = await session.execute(text(query), {"referrer_tg_id": referrer_tg_id, "max_levels": max_levels})
return {
row["level"]: {
"total": row["level_count"],
@@ -214,19 +192,13 @@ async def get_referrals_by_level(
async def get_referral_stats(session: AsyncSession, referrer_tg_id: int):
try:
- logger.info(
- f"[ReferralStats] Получение статистики для пользователя {referrer_tg_id}"
- )
+ logger.info(f"[ReferralStats] Получение статистики для пользователя {referrer_tg_id}")
total_referrals = await get_total_referrals(session, referrer_tg_id)
active_referrals = await get_active_referrals(session, referrer_tg_id)
max_levels = len(REFERRAL_BONUS_PERCENTAGES)
- referrals_by_level = await get_referrals_by_level(
- session, referrer_tg_id, max_levels
- )
- total_referral_bonus = await get_total_referral_bonus(
- session, referrer_tg_id, max_levels
- )
+ referrals_by_level = await get_referrals_by_level(session, referrer_tg_id, max_levels)
+ total_referral_bonus = await get_total_referral_bonus(session, referrer_tg_id, max_levels)
return {
"total_referrals": total_referrals,
@@ -236,18 +208,12 @@ async def get_referral_stats(session: AsyncSession, referrer_tg_id: int):
}
except Exception as e:
- logger.error(
- f"[ReferralStats] Ошибка при получении статистики для пользователя {referrer_tg_id}: {e}"
- )
+ logger.error(f"[ReferralStats] Ошибка при получении статистики для пользователя {referrer_tg_id}: {e}")
raise
async def get_user_referral_count(session: AsyncSession, tg_id: int) -> int:
- result = await session.execute(
- select(func.count())
- .select_from(Referral)
- .where(Referral.referrer_tg_id == tg_id)
- )
+ result = await session.execute(select(func.count()).select_from(Referral).where(Referral.referrer_tg_id == tg_id))
return result.scalar_one() or 0
@@ -272,7 +238,4 @@ async def get_top_referrals(session: AsyncSession, limit: int = 5):
.limit(limit)
)
result = await session.execute(query)
- return [
- {"referrer_tg_id": row.referrer_tg_id, "referral_count": row.referral_count}
- for row in result.all()
- ]
+ return [{"referrer_tg_id": row.referrer_tg_id, "referral_count": row.referral_count} for row in result.all()]
diff --git a/database/servers.py b/database/servers.py
index f9296d35..5b0c0945 100644
--- a/database/servers.py
+++ b/database/servers.py
@@ -1,8 +1,8 @@
-from sqlalchemy import delete, insert, select, update, func
+from sqlalchemy import delete, func, insert, select, update
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
-from database.models import Server, Key
+from database.models import Key, Server
from logger import logger
@@ -54,19 +54,17 @@ async def get_servers(session: AsyncSession, include_enabled: bool = False) -> d
if not include_enabled and not s.enabled:
continue
cluster = s.cluster_name
- grouped.setdefault(cluster, []).append(
- {
- "server_name": s.server_name,
- "api_url": s.api_url,
- "subscription_url": s.subscription_url,
- "inbound_id": s.inbound_id,
- "panel_type": s.panel_type,
- "enabled": s.enabled,
- "max_keys": s.max_keys,
- "tariff_group": s.tariff_group,
- "cluster_name": cluster,
- }
- )
+ grouped.setdefault(cluster, []).append({
+ "server_name": s.server_name,
+ "api_url": s.api_url,
+ "subscription_url": s.subscription_url,
+ "inbound_id": s.inbound_id,
+ "panel_type": s.panel_type,
+ "enabled": s.enabled,
+ "max_keys": s.max_keys,
+ "tariff_group": s.tariff_group,
+ "cluster_name": cluster,
+ })
return grouped
except SQLAlchemyError as e:
@@ -80,9 +78,7 @@ async def get_clusters(session: AsyncSession) -> list[str]:
return [r[0] for r in result.all()]
-async def check_unique_server_name(
- session: AsyncSession, server_name: str, cluster_name: str | None = None
-) -> bool:
+async def check_unique_server_name(session: AsyncSession, server_name: str, cluster_name: str | None = None) -> bool:
stmt = select(Server).where(Server.server_name == server_name)
if cluster_name:
stmt = stmt.where(Server.cluster_name == cluster_name)
@@ -90,13 +86,9 @@ async def check_unique_server_name(
return result.scalar_one_or_none() is None
-async def check_server_name_by_cluster(
- session: AsyncSession, server_name: str
-) -> dict | None:
+async def check_server_name_by_cluster(session: AsyncSession, server_name: str) -> dict | None:
try:
- result = await session.execute(
- select(Server.cluster_name).where(Server.server_name == server_name)
- )
+ result = await session.execute(select(Server.cluster_name).where(Server.server_name == server_name))
row = result.first()
return {"cluster_name": row[0]} if row else None
except SQLAlchemyError as e:
@@ -104,14 +96,10 @@ async def check_server_name_by_cluster(
return None
-async def get_cluster_name_by_server(
- session: AsyncSession, server_id_or_name: str
-) -> str | None:
+async def get_cluster_name_by_server(session: AsyncSession, server_id_or_name: str) -> str | None:
stmt = (
select(Server.cluster_name)
- .where(
- (Server.id == server_id_or_name) | (Server.server_name == server_id_or_name)
- )
+ .where((Server.id == server_id_or_name) | (Server.server_name == server_id_or_name))
.limit(1)
)
@@ -125,7 +113,7 @@ async def get_server_by_name(session: AsyncSession, server_name: str) -> dict |
stmt = select(Server).where(Server.server_name == server_name)
result = await session.execute(stmt)
server = result.scalar_one_or_none()
-
+
if server:
return {
"id": server.id,
@@ -145,9 +133,7 @@ async def get_server_by_name(session: AsyncSession, server_name: str) -> dict |
return None
-async def update_server_field(
- session: AsyncSession, server_name: str, field: str, value: any
-) -> bool:
+async def update_server_field(session: AsyncSession, server_name: str, field: str, value: any) -> bool:
try:
stmt = update(Server).where(Server.server_name == server_name).values(**{field: value})
await session.execute(stmt)
@@ -160,11 +146,10 @@ async def update_server_field(
return False
-async def update_server_name_with_keys(
- session: AsyncSession, old_name: str, new_name: str
-) -> bool:
+async def update_server_name_with_keys(session: AsyncSession, old_name: str, new_name: str) -> bool:
try:
from sqlalchemy import update
+
from database.models import Key
if not await check_unique_server_name(session, new_name):
@@ -176,7 +161,7 @@ async def update_server_name_with_keys(
stmt_keys = update(Key).where(Key.server_id == old_name).values(server_id=new_name)
await session.execute(stmt_keys)
-
+
await session.commit()
logger.info(f"✅ Сервер переименован с {old_name} на {new_name}")
return True
@@ -196,16 +181,12 @@ async def get_available_clusters(session: AsyncSession) -> list[str]:
return []
-async def update_server_cluster(
- session: AsyncSession,
- server_name: str,
- new_cluster: str
-) -> bool:
+async def update_server_cluster(session: AsyncSession, server_name: str, new_cluster: str) -> bool:
try:
server_data = await get_server_by_name(session, server_name)
if not server_data:
return False
-
+
old_cluster = server_data["cluster_name"]
stmt_remaining = select(func.count()).where(
@@ -215,26 +196,21 @@ async def update_server_cluster(
remaining_servers = result.scalar_one()
if remaining_servers == 0:
- stmt_update_keys = update(Key).where(
- Key.server_id == old_cluster
- ).values(server_id=new_cluster)
+ stmt_update_keys = update(Key).where(Key.server_id == old_cluster).values(server_id=new_cluster)
await session.execute(stmt_update_keys)
- stmt_new_cluster = select(Server.tariff_group).where(
- Server.cluster_name == new_cluster
- ).limit(1)
+ stmt_new_cluster = select(Server.tariff_group).where(Server.cluster_name == new_cluster).limit(1)
result = await session.execute(stmt_new_cluster)
new_tariff_group = result.scalar_one_or_none()
- stmt_update = update(Server).where(
- Server.server_name == server_name
- ).values(
- cluster_name=new_cluster,
- tariff_group=new_tariff_group
+ stmt_update = (
+ update(Server)
+ .where(Server.server_name == server_name)
+ .values(cluster_name=new_cluster, tariff_group=new_tariff_group)
)
await session.execute(stmt_update)
await session.commit()
-
+
logger.info(f"✅ Сервер {server_name} перемещен в кластер {new_cluster} с обновлением тарифной группы")
return True
except SQLAlchemyError as e:
diff --git a/database/statistics.py b/database/statistics.py
index f58aa429..bea5295b 100644
--- a/database/statistics.py
+++ b/database/statistics.py
@@ -1,6 +1,6 @@
from datetime import date, datetime
-from sqlalchemy import and_, func, not_, select, exists
+from sqlalchemy import and_, exists, func, not_, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.models import Key, Payment, Referral, Tariff, User
@@ -11,24 +11,16 @@ async def count_total_users(session: AsyncSession) -> int:
async def count_users_updated_today(session: AsyncSession, today: date) -> int:
- return await session.scalar(
- select(func.count()).select_from(User).where(User.updated_at >= today)
- )
+ return await session.scalar(select(func.count()).select_from(User).where(User.updated_at >= today))
async def count_users_registered_since(session: AsyncSession, since: date) -> int:
- return await session.scalar(
- select(func.count()).select_from(User).where(User.created_at >= since)
- )
+ return await session.scalar(select(func.count()).select_from(User).where(User.created_at >= since))
-async def count_users_registered_between(
- session: AsyncSession, start: date, end: date
-) -> int:
+async def count_users_registered_between(session: AsyncSession, start: date, end: date) -> int:
return await session.scalar(
- select(func.count())
- .select_from(User)
- .where(User.created_at >= start, User.created_at < end)
+ select(func.count()).select_from(User).where(User.created_at >= start, User.created_at < end)
)
@@ -38,78 +30,55 @@ async def count_total_keys(session: AsyncSession) -> int:
async def count_active_keys(session: AsyncSession) -> int:
current_time_ms = int(datetime.utcnow().timestamp() * 1000)
- return await session.scalar(
- select(func.count()).select_from(Key).where(Key.expiry_time > current_time_ms)
- )
+ return await session.scalar(select(func.count()).select_from(Key).where(Key.expiry_time > current_time_ms))
async def count_trial_keys(session: AsyncSession) -> int:
subquery_success_payments = (
- select(Payment.tg_id)
- .where(and_(Payment.tg_id == Key.tg_id, Payment.status == "success"))
- .exists()
+ select(Payment.tg_id).where(and_(Payment.tg_id == Key.tg_id, Payment.status == "success")).exists()
)
- return await session.scalar(
- select(func.count()).select_from(Key).where(not_(subquery_success_payments))
- )
+ return await session.scalar(select(func.count()).select_from(Key).where(not_(subquery_success_payments)))
async def get_tariff_distribution(
session: AsyncSession, include_unbound: bool = False
) -> tuple[list[tuple[int, int]], list[dict]]:
result = await session.execute(
- select(Key.tariff_id, func.count(Key.client_id))
- .where(Key.tariff_id.isnot(None))
- .group_by(Key.tariff_id)
+ select(Key.tariff_id, func.count(Key.client_id)).where(Key.tariff_id.isnot(None)).group_by(Key.tariff_id)
)
tariff_counts = result.all()
if not include_unbound:
return tariff_counts
- result = await session.execute(
- select(Key.expiry_time)
- .where(Key.tariff_id.is_(None))
- )
+ result = await session.execute(select(Key.expiry_time).where(Key.tariff_id.is_(None)))
no_tariff_keys = [{"expiry_time": row[0]} for row in result.all()]
return tariff_counts, no_tariff_keys
-async def get_tariff_names(
- session: AsyncSession, tariff_ids: list[int]
-) -> dict[int, str]:
+async def get_tariff_names(session: AsyncSession, tariff_ids: list[int]) -> dict[int, str]:
if not tariff_ids:
return {}
- result = await session.execute(
- select(Tariff.id, Tariff.name).where(Tariff.id.in_(tariff_ids))
- )
+ result = await session.execute(select(Tariff.id, Tariff.name).where(Tariff.id.in_(tariff_ids)))
return dict(result.all())
-async def get_tariff_groups(
- session: AsyncSession, tariff_ids: list[int]
-) -> dict[int, str]:
+async def get_tariff_groups(session: AsyncSession, tariff_ids: list[int]) -> dict[int, str]:
if not tariff_ids:
return {}
- result = await session.execute(
- select(Tariff.id, Tariff.group_code).where(Tariff.id.in_(tariff_ids))
- )
+ result = await session.execute(select(Tariff.id, Tariff.group_code).where(Tariff.id.in_(tariff_ids)))
return dict(result.all())
-async def get_tariff_durations(
- session: AsyncSession, tariff_ids: list[int]
-) -> dict[int, int]:
+async def get_tariff_durations(session: AsyncSession, tariff_ids: list[int]) -> dict[int, int]:
if not tariff_ids:
return {}
- result = await session.execute(
- select(Tariff.id, Tariff.duration_days).where(Tariff.id.in_(tariff_ids))
- )
+ result = await session.execute(select(Tariff.id, Tariff.duration_days).where(Tariff.id.in_(tariff_ids)))
return dict(result.all())
@@ -120,10 +89,7 @@ async def count_total_referrals(session: AsyncSession) -> int:
async def sum_payments_since(session: AsyncSession, since: date) -> float:
result = await session.scalar(
select(func.coalesce(func.sum(Payment.amount), 0)).where(
- and_(
- Payment.created_at >= since,
- Payment.payment_system.notin_(["referral", "coupon", "cashback"])
- )
+ and_(Payment.created_at >= since, Payment.payment_system.notin_(["referral", "coupon", "cashback"]))
)
)
return round(float(result), 2)
@@ -135,7 +101,7 @@ async def sum_payments_between(session: AsyncSession, start: date, end: date) ->
and_(
Payment.created_at >= start,
Payment.created_at < end,
- Payment.payment_system.notin_(["referral", "coupon", "cashback"])
+ Payment.payment_system.notin_(["referral", "coupon", "cashback"]),
)
)
)
@@ -153,9 +119,7 @@ async def sum_total_payments(session: AsyncSession) -> float:
async def count_hot_leads(session: AsyncSession) -> int:
subquery_active_keys = (
- select(Key.tg_id)
- .where(Key.expiry_time > int(datetime.utcnow().timestamp() * 1000))
- .distinct()
+ select(Key.tg_id).where(Key.expiry_time > int(datetime.utcnow().timestamp() * 1000)).distinct()
)
stmt = (
@@ -168,4 +132,4 @@ async def count_hot_leads(session: AsyncSession) -> int:
)
result = await session.execute(select(func.count()).select_from(stmt.subquery()))
- return result.scalar()
\ No newline at end of file
+ return result.scalar()
diff --git a/database/tariffs.py b/database/tariffs.py
index a4bf9e26..c7750868 100644
--- a/database/tariffs.py
+++ b/database/tariffs.py
@@ -1,6 +1,7 @@
-from datetime import datetime
import hashlib
+from datetime import datetime
+
from sqlalchemy import delete, insert, select, update
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,7 +15,7 @@ def create_subgroup_hash(subgroup_title: str, group_code: str) -> str:
return ""
unique_key = f"{subgroup_title}:{group_code}"
- hash_object = hashlib.md5(unique_key.encode('utf-8'))
+ hash_object = hashlib.md5(unique_key.encode("utf-8"))
return hash_object.hexdigest()[:8]
@@ -25,24 +26,20 @@ async def find_subgroup_by_hash(session: AsyncSession, subgroup_hash: str, group
.distinct()
)
subgroups = [row[0] for row in result.fetchall()]
-
+
for subgroup_title in subgroups:
if create_subgroup_hash(subgroup_title, group_code) == subgroup_hash:
return subgroup_title
-
+
return None
-async def get_tariffs(
- session: AsyncSession, tariff_id: int = None, group_code: str = None
-):
+async def get_tariffs(session: AsyncSession, tariff_id: int = None, group_code: str = None):
try:
if tariff_id:
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
elif group_code:
- result = await session.execute(
- select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id)
- )
+ result = await session.execute(select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id))
else:
result = await session.execute(select(Tariff))
@@ -65,34 +62,26 @@ async def get_tariff_by_id(session: AsyncSession, tariff_id: int):
async def get_tariffs_for_cluster(session: AsyncSession, cluster_name: str):
try:
server_row = await session.execute(
- select(Server.tariff_group)
- .where(Server.cluster_name == cluster_name)
- .limit(1)
+ select(Server.tariff_group).where(Server.cluster_name == cluster_name).limit(1)
)
row = server_row.first()
-
+
if not row:
server_row = await session.execute(
- select(Server.tariff_group)
- .where(Server.server_name == cluster_name)
- .limit(1)
+ select(Server.tariff_group).where(Server.server_name == cluster_name).limit(1)
)
row = server_row.first()
-
+
if not row or not row[0]:
return []
group_code = row[0]
result = await session.execute(
- select(Tariff)
- .where(Tariff.group_code == group_code, Tariff.is_active.is_(True))
- .order_by(Tariff.id)
+ select(Tariff).where(Tariff.group_code == group_code, Tariff.is_active.is_(True)).order_by(Tariff.id)
)
return [dict(r.__dict__) for r in result.scalars().all()]
except SQLAlchemyError as e:
- logger.error(
- f"[TARIFF] Ошибка при получении тарифов для кластера {cluster_name}: {e}"
- )
+ logger.error(f"[TARIFF] Ошибка при получении тарифов для кластера {cluster_name}: {e}")
return []
@@ -116,9 +105,7 @@ async def update_tariff(session: AsyncSession, tariff_id: int, updates: dict):
return False
try:
updates["updated_at"] = datetime.utcnow()
- await session.execute(
- update(Tariff).where(Tariff.id == tariff_id).values(**updates)
- )
+ await session.execute(update(Tariff).where(Tariff.id == tariff_id).values(**updates))
await session.commit()
return True
except SQLAlchemyError as e:
@@ -140,10 +127,7 @@ async def delete_tariff(session: AsyncSession, tariff_id: int):
async def check_tariff_exists(session: AsyncSession, tariff_id: int):
try:
- result = await session.execute(
- select(Tariff)
- .where(Tariff.id == tariff_id, Tariff.is_active.is_(True))
- )
+ result = await session.execute(select(Tariff).where(Tariff.id == tariff_id, Tariff.is_active.is_(True)))
tariff = result.scalar_one_or_none()
if tariff:
logger.info(f"[TARIFF] Тариф {tariff_id} найден в БД: {tariff.group_code}")
diff --git a/database/temporary_data.py b/database/temporary_data.py
index bc4da1c1..55f34ed8 100644
--- a/database/temporary_data.py
+++ b/database/temporary_data.py
@@ -9,9 +9,7 @@ from database.models import TemporaryData
from logger import logger
-async def create_temporary_data(
- session: AsyncSession, tg_id: int, state: str, data: dict
-):
+async def create_temporary_data(session: AsyncSession, tg_id: int, state: str, data: dict):
try:
stmt = (
insert(TemporaryData)
diff --git a/database/tracking_sources.py b/database/tracking_sources.py
index 26423f8a..910ff45b 100644
--- a/database/tracking_sources.py
+++ b/database/tracking_sources.py
@@ -1,4 +1,4 @@
-from sqlalchemy import func, insert, select, not_
+from sqlalchemy import func, insert, not_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
@@ -6,9 +6,7 @@ from database.models import Payment, TrackingSource, User
from logger import logger
-async def create_tracking_source(
- session: AsyncSession, name: str, code: str, type_: str, created_by: int
-):
+async def create_tracking_source(session: AsyncSession, name: str, code: str, type_: str, created_by: int):
try:
stmt = insert(TrackingSource).values(
name=name,
@@ -42,9 +40,7 @@ async def get_all_tracking_sources(session: AsyncSession) -> list[dict]:
payments_subq = (
select(func.count(func.distinct(Payment.tg_id)))
.join(User, Payment.tg_id == User.tg_id)
- .where(
- (User.source_code == TrackingSource.code) & (Payment.status == "success")
- )
+ .where((User.source_code == TrackingSource.code) & (Payment.status == "success"))
.correlate(TrackingSource)
.scalar_subquery()
)
@@ -74,9 +70,7 @@ async def get_all_tracking_sources(session: AsyncSession) -> list[dict]:
async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict | None:
- source_result = await session.execute(
- select(TrackingSource.created_at).where(TrackingSource.code == code)
- )
+ source_result = await session.execute(select(TrackingSource.created_at).where(TrackingSource.code == code))
created_at_row = source_result.first()
if not created_at_row:
return None
@@ -85,20 +79,13 @@ async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict |
reg_subq = (
select(func.count(func.distinct(User.tg_id)))
- .where(
- (User.source_code == code) &
- (User.created_at >= created_at)
- )
+ .where((User.source_code == code) & (User.created_at >= created_at))
.scalar_subquery()
)
trial_subq = (
select(func.count(func.distinct(User.tg_id)))
- .where(
- (User.source_code == code) &
- (User.trial == 1) &
- (User.created_at >= created_at)
- )
+ .where((User.source_code == code) & (User.trial == 1) & (User.created_at >= created_at))
.scalar_subquery()
)
@@ -106,10 +93,10 @@ async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict |
select(func.count(func.distinct(Payment.tg_id)))
.join(User, Payment.tg_id == User.tg_id)
.where(
- (User.source_code == code) &
- (Payment.status == "success") &
- not_(Payment.payment_system.in_(["coupon", "referral", "cashback"])) &
- (Payment.created_at >= created_at)
+ (User.source_code == code)
+ & (Payment.status == "success")
+ & not_(Payment.payment_system.in_(["coupon", "referral", "cashback"]))
+ & (Payment.created_at >= created_at)
)
.scalar_subquery()
)
@@ -118,10 +105,10 @@ async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict |
select(func.coalesce(func.sum(Payment.amount), 0))
.join(User, Payment.tg_id == User.tg_id)
.where(
- (User.source_code == code) &
- (Payment.status == "success") &
- not_(Payment.payment_system.in_(["coupon", "referral", "cashback"])) &
- (Payment.created_at >= created_at)
+ (User.source_code == code)
+ & (Payment.status == "success")
+ & not_(Payment.payment_system.in_(["coupon", "referral", "cashback"]))
+ & (Payment.created_at >= created_at)
)
.scalar_subquery()
)
diff --git a/database/users.py b/database/users.py
index eb6d48e8..aa644281 100644
--- a/database/users.py
+++ b/database/users.py
@@ -10,12 +10,12 @@ from database.models import (
BlockedUser,
CouponUsage,
Gift,
+ GiftUsage,
Notification,
Payment,
Referral,
TemporaryData,
User,
- GiftUsage
)
from logger import logger
@@ -47,9 +47,7 @@ async def add_user(
await session.execute(stmt)
await session.commit()
- logger.info(
- f"[DB] Новый пользователь добавлен: {tg_id} (source: {source_code})"
- )
+ logger.info(f"[DB] Новый пользователь добавлен: {tg_id} (source: {source_code})")
except SQLAlchemyError as e:
logger.error(f"[DB] Ошибка при добавлении пользователя {tg_id}: {e}")
await session.rollback()
@@ -61,13 +59,9 @@ async def update_balance(session: AsyncSession, tg_id: int, amount: float) -> No
result = await session.execute(select(User.balance).where(User.tg_id == tg_id))
current = result.scalar_one_or_none() or 0
new_balance = current + amount
- await session.execute(
- update(User).where(User.tg_id == tg_id).values(balance=new_balance)
- )
+ await session.execute(update(User).where(User.tg_id == tg_id).values(balance=new_balance))
await session.commit()
- logger.info(
- f"[DB] Баланс пользователя {tg_id} обновлён: {current} → {new_balance}"
- )
+ logger.info(f"[DB] Баланс пользователя {tg_id} обновлён: {current} → {new_balance}")
except SQLAlchemyError as e:
logger.error(f"[DB] Ошибка при обновлении баланса пользователя {tg_id}: {e}")
await session.rollback()
@@ -87,9 +81,7 @@ async def get_balance(session: AsyncSession, tg_id: int) -> float:
async def set_user_balance(session: AsyncSession, tg_id: int, balance: float) -> None:
try:
- await session.execute(
- update(User).where(User.tg_id == tg_id).values(balance=balance)
- )
+ await session.execute(update(User).where(User.tg_id == tg_id).values(balance=balance))
await session.commit()
except SQLAlchemyError as e:
logger.error(f"Ошибка при установке баланса для пользователя {tg_id}: {e}")
@@ -98,9 +90,7 @@ async def set_user_balance(session: AsyncSession, tg_id: int, balance: float) ->
async def update_trial(session: AsyncSession, tg_id: int, status: int):
try:
- await session.execute(
- update(User).where(User.tg_id == tg_id).values(trial=status)
- )
+ await session.execute(update(User).where(User.tg_id == tg_id).values(trial=status))
await session.commit()
logger.info(f"[DB] Триал статус обновлён для пользователя {tg_id}: {status}")
except SQLAlchemyError as e:
@@ -181,29 +171,18 @@ async def delete_user_data(session: AsyncSession, tg_id: int):
try:
await session.execute(delete(Notification).where(Notification.tg_id == tg_id))
- result = await session.execute(
- select(Gift.gift_id).where(Gift.sender_tg_id == tg_id)
- )
+ result = await session.execute(select(Gift.gift_id).where(Gift.sender_tg_id == tg_id))
gift_ids = [row[0] for row in result.all()]
if gift_ids:
await session.execute(delete(GiftUsage).where(GiftUsage.gift_id.in_(gift_ids)))
await session.execute(delete(Gift).where(Gift.sender_tg_id == tg_id))
- await session.execute(
- update(Gift)
- .where(Gift.recipient_tg_id == tg_id)
- .values(recipient_tg_id=None)
- )
+ await session.execute(update(Gift).where(Gift.recipient_tg_id == tg_id).values(recipient_tg_id=None))
await session.execute(delete(Payment).where(Payment.tg_id == tg_id))
await session.execute(
- delete(Referral).where(
- or_(
- Referral.referrer_tg_id == tg_id,
- Referral.referred_tg_id == tg_id
- )
- )
+ delete(Referral).where(or_(Referral.referrer_tg_id == tg_id, Referral.referred_tg_id == tg_id))
)
await session.execute(delete(CouponUsage).where(CouponUsage.user_id == tg_id))
await delete_key(session, tg_id)
diff --git a/filters/admin.py b/filters/admin.py
index eba89e04..95bd781b 100644
--- a/filters/admin.py
+++ b/filters/admin.py
@@ -1,17 +1,16 @@
from aiogram.filters import BaseFilter
-from aiogram.types import Message, CallbackQuery
+from aiogram.types import CallbackQuery, Message
from sqlalchemy import select
-from database.models import Admin
from database.db import async_session_maker
+from database.models import Admin
+
class IsAdminFilter(BaseFilter):
async def __call__(self, event: Message | CallbackQuery) -> bool:
try:
async with async_session_maker() as session:
- result = await session.execute(
- select(Admin).where(Admin.tg_id == event.from_user.id)
- )
+ result = await session.execute(select(Admin).where(Admin.tg_id == event.from_user.id))
admin = result.scalar_one_or_none()
return admin is not None
except Exception:
diff --git a/handlers/__init__.py b/handlers/__init__.py
index 6aa97ddb..de7b59b3 100644
--- a/handlers/__init__.py
+++ b/handlers/__init__.py
@@ -15,6 +15,7 @@ from .profile import router as profile_router
from .refferal import router as refferal_router
from .start import router as start_router
+
router = Router(name="handlers_main_router")
router.include_routers(
diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py
index 9428cd31..a8cb4f74 100644
--- a/handlers/admin/__init__.py
+++ b/handlers/admin/__init__.py
@@ -7,6 +7,7 @@ from .backups import router as backups_router
from .bans import router as bans_router
from .clusters import router as clusters_router
from .coupons import router as coupons_router
+from .gifts import router as gifts_router
from .management import router as management_router
from .panel import router as panel_router
from .restart import router as restart_router
@@ -15,7 +16,7 @@ from .servers import router as servers_router
from .stats import router as stats_router
from .tariffs import router as tariffs_router
from .users import router as users_router
-from .gifts import router as gifts_router
+
router = Router(name="admins_main_router")
@@ -33,5 +34,5 @@ router.include_routers(
bans_router,
ads_router,
tariffs_router,
- gifts_router
+ gifts_router,
)
diff --git a/handlers/admin/ads/ads_handler.py b/handlers/admin/ads/ads_handler.py
index d7e254a6..a623db3f 100644
--- a/handlers/admin/ads/ads_handler.py
+++ b/handlers/admin/ads/ads_handler.py
@@ -1,10 +1,11 @@
+import re
+
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from sqlalchemy import delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession
-import re
from config import USERNAME_BOT
from database import create_tracking_source, get_tracking_source_stats
@@ -22,6 +23,7 @@ from .keyboard import (
build_cancel_input_kb,
)
+
router = Router()
@@ -32,9 +34,7 @@ class AdminAdsState(StatesGroup):
@router.callback_query(AdminPanelCallback.filter(F.action == "ads"), IsAdminFilter())
async def handle_ads_menu(callback_query: CallbackQuery):
- await callback_query.message.edit_text(
- text="📊 Аналитика рекламы:", reply_markup=build_ads_kb()
- )
+ await callback_query.message.edit_text(text="📊 Аналитика рекламы:", reply_markup=build_ads_kb())
@router.callback_query(AdminAdsCallback.filter(F.action == "create"), IsAdminFilter())
@@ -51,28 +51,24 @@ async def handle_ads_name_input(message: Message, state: FSMContext):
await state.update_data(name=name)
await state.set_state(AdminAdsState.waiting_for_new_code)
await message.answer(
- f"🔗 Введите код ссылки для: {name}.\n\n"
- f"💡 Правила: только латинские буквы и цифры",
+ f"🔗 Введите код ссылки для: {name}.\n\n💡 Правила: только латинские буквы и цифры",
reply_markup=build_cancel_input_kb(),
)
@router.message(AdminAdsState.waiting_for_new_code, IsAdminFilter())
-async def handle_ads_code_input(
- message: Message, state: FSMContext, session: AsyncSession
-):
+async def handle_ads_code_input(message: Message, state: FSMContext, session: AsyncSession):
code = message.text.strip()
data = await state.get_data()
name = data["name"]
- if not re.match(r'^[a-zA-Z0-9]+$', code):
+ if not re.match(r"^[a-zA-Z0-9]+$", code):
await message.answer(
- "❌ Код может содержать только латинские буквы и цифры\n"
- "Введите код заново:",
+ "❌ Код может содержать только латинские буквы и цифры\nВведите код заново:",
reply_markup=build_cancel_input_kb(),
)
return
-
+
code_with_prefix = f"utm_{code}"
try:
@@ -103,9 +99,7 @@ async def handle_ads_code_input(
@router.callback_query(AdminAdsCallback.filter(F.action == "list"), IsAdminFilter())
async def handle_ads_list(callback_query: CallbackQuery, session: AsyncSession, callback_data: AdminAdsCallback):
try:
- result = await session.execute(
- select(TrackingSource).order_by(TrackingSource.created_at.desc())
- )
+ result = await session.execute(select(TrackingSource).order_by(TrackingSource.created_at.desc()))
ads = result.scalars().all()
items_per_page = 6
if callback_data.code and callback_data.code.isdigit():
@@ -119,9 +113,7 @@ async def handle_ads_list(callback_query: CallbackQuery, session: AsyncSession,
)
except Exception as e:
logger.error(f"Ошибка при получении списка UTM: {e}", exc_info=True)
- await callback_query.message.edit_text(
- "❌ Произошла ошибка при получении списка."
- )
+ await callback_query.message.edit_text("❌ Произошла ошибка при получении списка.")
@router.callback_query(AdminAdsCallback.filter(F.action == "view"), IsAdminFilter())
@@ -134,25 +126,17 @@ async def handle_ads_view(
try:
stats = await get_tracking_source_stats(session, code)
if not stats:
- await callback_query.message.edit_text(
- "❌ Источник не найден или не содержит данных."
- )
+ await callback_query.message.edit_text("❌ Источник не найден или не содержит данных.")
return
msg = format_ads_stats(stats, USERNAME_BOT)
- await callback_query.message.edit_text(
- text=msg, reply_markup=build_ads_stats_kb(code)
- )
+ await callback_query.message.edit_text(text=msg, reply_markup=build_ads_stats_kb(code))
except Exception as e:
logger.error(f"Ошибка при просмотре статистики: {e}", exc_info=True)
await callback_query.message.edit_text("❌ Ошибка при получении статистики.")
-@router.callback_query(
- AdminAdsCallback.filter(F.action == "delete_confirm"), IsAdminFilter()
-)
-async def handle_ads_delete_confirm(
- callback_query: CallbackQuery, callback_data: AdminAdsCallback
-):
+@router.callback_query(AdminAdsCallback.filter(F.action == "delete_confirm"), IsAdminFilter())
+async def handle_ads_delete_confirm(callback_query: CallbackQuery, callback_data: AdminAdsCallback):
code = callback_data.code
await callback_query.message.edit_text(
text=f"Вы уверены, что хотите удалить ссылку {code}?",
@@ -168,9 +152,7 @@ async def handle_ads_delete(
):
code = callback_data.code
try:
- await session.execute(
- update(User).where(User.source_code == code).values(source_code=None)
- )
+ await session.execute(update(User).where(User.source_code == code).values(source_code=None))
await session.execute(delete(TrackingSource).where(TrackingSource.code == code))
await session.commit()
await callback_query.message.edit_text(
@@ -182,14 +164,10 @@ async def handle_ads_delete(
await callback_query.message.edit_text("❌ Не удалось удалить ссылку.")
-@router.callback_query(
- AdminAdsCallback.filter(F.action == "cancel_input"), IsAdminFilter()
-)
+@router.callback_query(AdminAdsCallback.filter(F.action == "cancel_input"), IsAdminFilter())
async def handle_ads_cancel_input(callback_query: CallbackQuery, state: FSMContext):
await state.clear()
- await callback_query.message.edit_text(
- text="📊 Аналитика рекламы:", reply_markup=build_ads_kb()
- )
+ await callback_query.message.edit_text(text="📊 Аналитика рекламы:", reply_markup=build_ads_kb())
def format_ads_stats(stats: dict, username_bot: str) -> str:
diff --git a/handlers/admin/ads/keyboard.py b/handlers/admin/ads/keyboard.py
index 1e2d3052..18b7249d 100644
--- a/handlers/admin/ads/keyboard.py
+++ b/handlers/admin/ads/keyboard.py
@@ -12,20 +12,14 @@ class AdminAdsCallback(CallbackData, prefix="admin_ads"):
def build_ads_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
- builder.button(
- text="➕ Новая ссылка", callback_data=AdminAdsCallback(action="create").pack()
- )
- builder.button(
- text="📊 Список", callback_data=AdminAdsCallback(action="list").pack()
- )
+ builder.button(text="➕ Новая ссылка", callback_data=AdminAdsCallback(action="create").pack())
+ builder.button(text="📊 Список", callback_data=AdminAdsCallback(action="list").pack())
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
-def build_ads_list_kb(
- ads: list, current_page: int, total_pages: int
-) -> InlineKeyboardMarkup:
+def build_ads_list_kb(ads: list, current_page: int, total_pages: int) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
items_per_page = 6
@@ -50,18 +44,14 @@ def build_ads_list_kb(
pagination_buttons.append(
InlineKeyboardButton(
text="⬅️ Назад",
- callback_data=AdminAdsCallback(
- action="list", code=f"{current_page - 1}"
- ).pack(),
+ callback_data=AdminAdsCallback(action="list", code=f"{current_page - 1}").pack(),
)
)
if current_page < total_pages:
pagination_buttons.append(
InlineKeyboardButton(
text="Вперед ➡️",
- callback_data=AdminAdsCallback(
- action="list", code=f"{current_page + 1}"
- ).pack(),
+ callback_data=AdminAdsCallback(action="list", code=f"{current_page + 1}").pack(),
)
)
if pagination_buttons:
diff --git a/handlers/admin/backups/backups_handler.py b/handlers/admin/backups/backups_handler.py
index 43d1106d..e602f6b0 100644
--- a/handlers/admin/backups/backups_handler.py
+++ b/handlers/admin/backups/backups_handler.py
@@ -6,6 +6,7 @@ from filters.admin import IsAdminFilter
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
+
router = Router()
diff --git a/handlers/admin/bans/bans_handler.py b/handlers/admin/bans/bans_handler.py
index dedc4673..39e6ea67 100644
--- a/handlers/admin/bans/bans_handler.py
+++ b/handlers/admin/bans/bans_handler.py
@@ -14,6 +14,7 @@ from logger import logger
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
from .keyboard import build_bans_kb
+
router = Router()
@@ -28,9 +29,7 @@ async def handle_bans(callback_query: CallbackQuery):
await callback_query.message.edit_text(text=text_, reply_markup=build_bans_kb())
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "bans_export"), IsAdminFilter()
-)
+@router.callback_query(AdminPanelCallback.filter(F.action == "bans_export"), IsAdminFilter())
async def handle_bans_export(callback_query: CallbackQuery, session: AsyncSession):
kb = build_admin_back_kb("management")
try:
@@ -45,9 +44,7 @@ async def handle_bans_export(callback_query: CallbackQuery, session: AsyncSessio
writer.writerow([user.tg_id])
csv_output.seek(0)
- document = BufferedInputFile(
- file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv"
- )
+ document = BufferedInputFile(file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv")
await callback_query.message.answer_document(
document=document,
@@ -60,12 +57,8 @@ async def handle_bans_export(callback_query: CallbackQuery, session: AsyncSessio
)
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "bans_delete_banned"), IsAdminFilter()
-)
-async def handle_bans_delete_banned(
- callback_query: CallbackQuery, session: AsyncSession
-):
+@router.callback_query(AdminPanelCallback.filter(F.action == "bans_delete_banned"), IsAdminFilter())
+async def handle_bans_delete_banned(callback_query: CallbackQuery, session: AsyncSession):
kb = build_admin_back_kb("bans")
try:
result = await session.execute(text("SELECT tg_id FROM blocked_users"))
@@ -99,17 +92,11 @@ async def handle_bans_delete_banned(
)
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "manual_bans_export"), IsAdminFilter()
-)
-async def handle_manual_bans_export(
- callback_query: CallbackQuery, session: AsyncSession
-):
+@router.callback_query(AdminPanelCallback.filter(F.action == "manual_bans_export"), IsAdminFilter())
+async def handle_manual_bans_export(callback_query: CallbackQuery, session: AsyncSession):
build_admin_back_kb("bans")
try:
- result = await session.execute(
- text("SELECT tg_id, banned_at, reason, until FROM manual_bans")
- )
+ result = await session.execute(text("SELECT tg_id, banned_at, reason, until FROM manual_bans"))
rows = result.all()
csv_output = io.StringIO()
@@ -120,9 +107,7 @@ async def handle_manual_bans_export(
writer.writerow([user.tg_id, user.banned_at, user.reason, user.until])
csv_output.seek(0)
- document = BufferedInputFile(
- file=csv_output.getvalue().encode("utf-8"), filename="manual_bans.csv"
- )
+ document = BufferedInputFile(file=csv_output.getvalue().encode("utf-8"), filename="manual_bans.csv")
await callback_query.message.answer_document(
document=document,
@@ -135,12 +120,8 @@ async def handle_manual_bans_export(
)
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "bans_delete_manual"), IsAdminFilter()
-)
-async def handle_delete_manual_banned(
- callback_query: CallbackQuery, session: AsyncSession
-):
+@router.callback_query(AdminPanelCallback.filter(F.action == "bans_delete_manual"), IsAdminFilter())
+async def handle_delete_manual_banned(callback_query: CallbackQuery, session: AsyncSession):
try:
await session.execute(delete(ManualBan))
await session.commit()
@@ -151,6 +132,4 @@ async def handle_delete_manual_banned(
logger.info("[BANS] Очищены записи из manual_bans")
except Exception as e:
logger.error(f"[BANS] Ошибка при очистке manual_bans: {e}")
- await callback_query.message.edit_text(
- "❌ Ошибка при удалении вручную забаненных пользователей."
- )
+ await callback_query.message.edit_text("❌ Ошибка при удалении вручную забаненных пользователей.")
diff --git a/handlers/admin/bans/keyboard.py b/handlers/admin/bans/keyboard.py
index e370bd7f..861ddf9d 100644
--- a/handlers/admin/bans/keyboard.py
+++ b/handlers/admin/bans/keyboard.py
@@ -23,9 +23,7 @@ def build_bans_kb():
text="🗑️ Очистить вручную забаненных",
callback_data=AdminPanelCallback(action="bans_delete_manual").pack(),
)
- builder.button(
- text="🔙 Назад", callback_data=AdminPanelCallback(action="management").pack()
- )
+ builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="management").pack())
builder.adjust(1)
return builder.as_markup()
diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py
index 9d9693a5..c01b78d6 100644
--- a/handlers/admin/clusters/clusters_handler.py
+++ b/handlers/admin/clusters/clusters_handler.py
@@ -1,4 +1,5 @@
import asyncio
+
from datetime import datetime, timezone
from typing import Any
@@ -42,6 +43,7 @@ from .keyboard import (
build_tariff_group_selection_kb,
)
+
router = Router()
@@ -89,9 +91,7 @@ async def handle_clusters_add(callback_query: CallbackQuery, state: FSMContext):
"Пример: cluster1 или us_east_1"
)
- await callback_query.message.edit_text(
- text=text, reply_markup=build_admin_back_kb("clusters")
- )
+ await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb("clusters"))
await state.set_state(AdminClusterStates.waiting_for_cluster_name)
@@ -249,9 +249,7 @@ async def handle_panel_type_selection(
subscription_url = user_data.get("subscription_url")
inbound_id = user_data.get("inbound_id")
- result = await session.execute(
- select(Server.tariff_group).where(Server.cluster_name == cluster_name).limit(1)
- )
+ result = await session.execute(select(Server.tariff_group).where(Server.cluster_name == cluster_name).limit(1))
row = result.first()
tariff_group = row[0] if row else None
@@ -275,9 +273,7 @@ async def handle_panel_type_selection(
await state.clear()
-@router.callback_query(
- AdminClusterCallback.filter(F.action == "manage"), IsAdminFilter()
-)
+@router.callback_query(AdminClusterCallback.filter(F.action == "manage"), IsAdminFilter())
async def handle_clusters_manage(
callback_query: types.CallbackQuery,
callback_data: AdminClusterCallback,
@@ -286,16 +282,12 @@ async def handle_clusters_manage(
cluster_name = callback_data.data
result = await session.execute(
- select(Server.tariff_group)
- .where(Server.cluster_name == cluster_name, Server.tariff_group.isnot(None))
- .limit(1)
+ select(Server.tariff_group).where(Server.cluster_name == cluster_name, Server.tariff_group.isnot(None)).limit(1)
)
row = result.first()
tariff_group = row[0] if row else "—"
- result = await session.execute(
- select(Server.server_name).where(Server.cluster_name == cluster_name)
- )
+ result = await session.execute(select(Server.server_name).where(Server.cluster_name == cluster_name))
server_names = [row[0] for row in result.all()]
result = await session.execute(
select(func.count(func.distinct(Key.tg_id))).where(
@@ -305,9 +297,7 @@ async def handle_clusters_manage(
user_count = result.scalar() or 0
result = await session.execute(
- select(func.count()).where(
- (Key.server_id == cluster_name) | (Key.server_id.in_(server_names))
- )
+ select(func.count()).where((Key.server_id == cluster_name) | (Key.server_id.in_(server_names)))
)
subscription_count = result.scalar() or 0
@@ -336,9 +326,7 @@ async def handle_cluster_servers(callback: CallbackQuery, session: AsyncSession)
)
-@router.callback_query(
- AdminClusterCallback.filter(F.action == "availability"), IsAdminFilter()
-)
+@router.callback_query(AdminClusterCallback.filter(F.action == "availability"), IsAdminFilter())
async def handle_cluster_availability(
callback_query: types.CallbackQuery,
callback_data: AdminClusterCallback,
@@ -349,9 +337,7 @@ async def handle_cluster_availability(
cluster_servers = servers.get(cluster_name, [])
if not cluster_servers:
- await callback_query.message.edit_text(
- text=f"Кластер '{cluster_name}' не содержит серверов."
- )
+ await callback_query.message.edit_text(text=f"Кластер '{cluster_name}' не содержит серверов.")
return
await callback_query.message.edit_text(
@@ -397,9 +383,7 @@ async def handle_cluster_availability(
remna = RemnawaveAPI(server["api_url"])
nodes_data = await remna.get_all_nodes_with_online(
- username=REMNAWAVE_LOGIN,
- password=REMNAWAVE_PASSWORD,
- inbound_id=server_inbound_id
+ username=REMNAWAVE_LOGIN, password=REMNAWAVE_PASSWORD, inbound_id=server_inbound_id
)
if nodes_data.get("error"):
@@ -412,34 +396,28 @@ async def handle_cluster_availability(
if len(nodes_info) > 1:
result_text += f"🌍 {prefix} {server_name} - {online_remna_users} онлайн\n"
for node_info in nodes_info:
- country_code = node_info.get('country_code', 'Unknown')
- node_name = node_info.get('name', 'Unknown')
- online_users = node_info.get('online_users', 0)
+ country_code = node_info.get("country_code", "Unknown")
+ node_name = node_info.get("name", "Unknown")
+ online_users = node_info.get("online_users", 0)
- if country_code != 'Unknown' and len(country_code) == 2:
- flag = ''.join(chr(ord(c) + 127397) for c in country_code.upper())
+ if country_code != "Unknown" and len(country_code) == 2:
+ flag = "".join(chr(ord(c) + 127397) for c in country_code.upper())
else:
flag = country_code
result_text += f" ↳ {flag} ({node_name}): {online_users} онлайн\n"
else:
- result_text += (
- f"🌍 {prefix} {server_name} - {online_remna_users} онлайн\n"
- )
+ result_text += f"🌍 {prefix} {server_name} - {online_remna_users} онлайн\n"
except Exception as e:
error_text = str(e) or "Сервер недоступен"
result_text += f"❌ {prefix} {server_name} - ошибка: {error_text}\n"
result_text += f"\n👥 Всего пользователей онлайн: {total_online_users}"
- await callback_query.message.edit_text(
- text=result_text, reply_markup=build_admin_back_kb("clusters")
- )
+ await callback_query.message.edit_text(text=result_text, reply_markup=build_admin_back_kb("clusters"))
-@router.callback_query(
- AdminClusterCallback.filter(F.action == "backup"), IsAdminFilter()
-)
+@router.callback_query(AdminClusterCallback.filter(F.action == "backup"), IsAdminFilter())
async def handle_clusters_backup(
callback_query: types.CallbackQuery,
callback_data: AdminClusterCallback,
@@ -490,9 +468,7 @@ async def handle_sync(
)
-@router.callback_query(
- AdminClusterCallback.filter(F.action == "sync-server"), IsAdminFilter()
-)
+@router.callback_query(AdminClusterCallback.filter(F.action == "sync-server"), IsAdminFilter())
async def handle_sync_server(
callback_query: types.CallbackQuery,
callback_data: AdminClusterCallback,
@@ -552,9 +528,7 @@ async def handle_sync_server(
)
await asyncio.sleep(0.6)
except Exception as e:
- logger.error(
- f"Ошибка при добавлении ключа {key['client_id']} в сервер {server_name}: {e}"
- )
+ logger.error(f"Ошибка при добавлении ключа {key['client_id']} в сервер {server_name}: {e}")
await callback_query.message.edit_text(
text=f"✅ Ключи успешно синхронизированы для сервера {server_name}",
@@ -568,9 +542,7 @@ async def handle_sync_server(
)
-@router.callback_query(
- AdminClusterCallback.filter(F.action == "sync-cluster"), IsAdminFilter()
-)
+@router.callback_query(AdminClusterCallback.filter(F.action == "sync-cluster"), IsAdminFilter())
async def handle_sync_cluster(
callback_query: CallbackQuery,
callback_data: AdminClusterCallback,
@@ -610,9 +582,7 @@ async def handle_sync_cluster(
try:
if only_remnawave:
expire_iso = (
- datetime.utcfromtimestamp(key["expiry_time"] / 1000)
- .replace(tzinfo=timezone.utc)
- .isoformat()
+ datetime.utcfromtimestamp(key["expiry_time"] / 1000).replace(tzinfo=timezone.utc).isoformat()
)
remna = RemnawaveAPI(cluster_servers[0]["api_url"])
@@ -634,11 +604,7 @@ async def handle_sync_cluster(
f"[Sync] Ключ {key['client_id']} с несуществующим тарифом ID={key['tariff_id']} — обновим без лимитов"
)
- inbound_ids = [
- s["inbound_id"]
- for s in cluster_servers
- if s.get("inbound_id")
- ]
+ inbound_ids = [s["inbound_id"] for s in cluster_servers if s.get("inbound_id")]
success = await remna.update_user(
uuid=key["client_id"],
@@ -651,17 +617,12 @@ async def handle_sync_cluster(
)
if not success:
- logger.warning(f"[Sync] ошибка обновления, пробуем пересоздать")
+ logger.warning("[Sync] ошибка обновления, пробуем пересоздать")
- await delete_key_from_cluster(
- cluster_name, key["email"], key["client_id"], session
- )
+ await delete_key_from_cluster(cluster_name, key["email"], key["client_id"], session)
await session.execute(
- delete(Key).where(
- Key.tg_id == key["tg_id"],
- Key.client_id == key["client_id"]
- )
+ delete(Key).where(Key.tg_id == key["tg_id"], Key.client_id == key["client_id"])
)
await create_key_on_cluster(
@@ -674,19 +635,14 @@ async def handle_sync_cluster(
session=session,
remnawave_link=key["remnawave_link"],
)
-
+
await asyncio.sleep(0.1)
else:
- await delete_key_from_cluster(
- cluster_name, key["email"], key["client_id"], session
- )
+ await delete_key_from_cluster(cluster_name, key["email"], key["client_id"], session)
await session.execute(
- delete(Key).where(
- Key.tg_id == key["tg_id"],
- Key.client_id == key["client_id"]
- )
+ delete(Key).where(Key.tg_id == key["tg_id"], Key.client_id == key["client_id"])
)
await create_key_on_cluster(
@@ -699,13 +655,11 @@ async def handle_sync_cluster(
session=session,
remnawave_link=key["remnawave_link"],
)
-
+
await asyncio.sleep(0.5)
except Exception as e:
- logger.error(
- f"[Sync] Ошибка при обработке ключа {key['client_id']} в {cluster_name}: {e}"
- )
+ logger.error(f"[Sync] Ошибка при обработке ключа {key['client_id']} в {cluster_name}: {e}")
await callback_query.message.edit_text(
text=f"✅ Ключи успешно синхронизированы для кластера {cluster_name}",
@@ -721,9 +675,7 @@ async def handle_sync_cluster(
@router.callback_query(AdminServerCallback.filter(F.action == "add"), IsAdminFilter())
-async def handle_add_server(
- callback_query: CallbackQuery, callback_data: AdminServerCallback, state: FSMContext
-):
+async def handle_add_server(callback_query: CallbackQuery, callback_data: AdminServerCallback, state: FSMContext):
cluster_name = callback_data.data
await state.update_data(cluster_name=cluster_name)
@@ -742,9 +694,7 @@ async def handle_add_server(
await state.set_state(AdminClusterStates.waiting_for_server_name)
-@router.callback_query(
- AdminClusterCallback.filter(F.action == "add_time"), IsAdminFilter()
-)
+@router.callback_query(AdminClusterCallback.filter(F.action == "add_time"), IsAdminFilter())
async def handle_add_time(
callback_query: CallbackQuery,
callback_data: AdminClusterCallback,
@@ -787,9 +737,7 @@ async def handle_days_input(message: Message, state: FSMContext, session: AsyncS
)
row = result.first()
if not row or not row[0]:
- await message.answer(
- "❌ Не удалось определить тарифную группу для этого кластера или сервера."
- )
+ await message.answer("❌ Не удалось определить тарифную группу для этого кластера или сервера.")
await state.clear()
return
@@ -813,16 +761,12 @@ async def handle_days_input(message: Message, state: FSMContext, session: AsyncS
total_gb = tariff.traffic_limit or 0
- server_stmt = select(Server.server_name).where(
- Server.cluster_name == cluster_name
- )
+ server_stmt = select(Server.server_name).where(Server.cluster_name == cluster_name)
server_rows = await session.execute(server_stmt)
server_names = [row[0] for row in server_rows.all()]
server_names.append(cluster_name)
- result = await session.execute(
- select(Key).where(Key.server_id.in_(server_names))
- )
+ result = await session.execute(select(Key).where(Key.server_id.in_(server_names)))
keys = result.scalars().all()
if not keys:
@@ -843,9 +787,7 @@ async def handle_days_input(message: Message, state: FSMContext, session: AsyncS
)
await update_key_expiry(session, key.client_id, new_expiry)
- logger.info(
- f"[Cluster Extend] {key.email} +{days}д → {datetime.utcfromtimestamp(new_expiry / 1000)}"
- )
+ logger.info(f"[Cluster Extend] {key.email} +{days}д → {datetime.utcfromtimestamp(new_expiry / 1000)}")
await message.answer(
f"✅ Время подписки продлено на {days} дней всем пользователям в кластере {cluster_name}."
@@ -860,9 +802,7 @@ async def handle_days_input(message: Message, state: FSMContext, session: AsyncS
await state.clear()
-@router.callback_query(
- AdminClusterCallback.filter(F.action == "rename"), IsAdminFilter()
-)
+@router.callback_query(AdminClusterCallback.filter(F.action == "rename"), IsAdminFilter())
async def handle_rename_cluster(
callback_query: CallbackQuery,
callback_data: AdminClusterCallback,
@@ -886,9 +826,7 @@ async def handle_rename_cluster(
@router.message(AdminClusterStates.waiting_for_new_cluster_name, IsAdminFilter())
-async def handle_new_cluster_name_input(
- message: Message, state: FSMContext, session: AsyncSession
-):
+async def handle_new_cluster_name_input(message: Message, state: FSMContext, session: AsyncSession):
if not message.text:
await message.answer(
text="❌ Имя кластера не может быть пустым! Попробуйте снова.",
@@ -909,9 +847,7 @@ async def handle_new_cluster_name_input(
try:
result = await session.execute(
- select(Server.cluster_name)
- .where(Server.cluster_name == new_cluster_name)
- .limit(1)
+ select(Server.cluster_name).where(Server.cluster_name == new_cluster_name).limit(1)
)
existing_cluster = result.scalar()
@@ -923,23 +859,17 @@ async def handle_new_cluster_name_input(
return
keys_count_result = await session.execute(
- select(func.count())
- .select_from(Key)
- .where(Key.server_id == old_cluster_name)
+ select(func.count()).select_from(Key).where(Key.server_id == old_cluster_name)
)
keys_count = keys_count_result.scalar()
await session.execute(
- update(Server)
- .where(Server.cluster_name == old_cluster_name)
- .values(cluster_name=new_cluster_name)
+ update(Server).where(Server.cluster_name == old_cluster_name).values(cluster_name=new_cluster_name)
)
if keys_count > 0:
await session.execute(
- update(Key)
- .where(Key.server_id == old_cluster_name)
- .values(server_id=new_cluster_name)
+ update(Key).where(Key.server_id == old_cluster_name).values(server_id=new_cluster_name)
)
await session.commit()
@@ -950,9 +880,7 @@ async def handle_new_cluster_name_input(
)
except Exception as e:
await session.rollback()
- logger.error(
- f"Ошибка при смене имени кластера {old_cluster_name} на {new_cluster_name}: {e}"
- )
+ logger.error(f"Ошибка при смене имени кластера {old_cluster_name} на {new_cluster_name}: {e}")
await message.answer(
text=f"❌ Произошла ошибка при смене имени кластера: {e}",
reply_markup=build_admin_back_kb("clusters"),
@@ -961,9 +889,7 @@ async def handle_new_cluster_name_input(
await state.clear()
-@router.callback_query(
- AdminServerCallback.filter(F.action == "rename"), IsAdminFilter()
-)
+@router.callback_query(AdminServerCallback.filter(F.action == "rename"), IsAdminFilter())
async def handle_rename_server(
callback_query: CallbackQuery,
callback_data: AdminServerCallback,
@@ -1006,9 +932,7 @@ async def handle_rename_server(
@router.message(AdminClusterStates.waiting_for_new_server_name, IsAdminFilter())
-async def handle_new_server_name_input(
- message: Message, state: FSMContext, session: AsyncSession
-):
+async def handle_new_server_name_input(message: Message, state: FSMContext, session: AsyncSession):
if not message.text:
await message.answer(
text="❌ Имя сервера не может быть пустым! Попробуйте снова.",
@@ -1045,11 +969,7 @@ async def handle_new_server_name_input(
)
return
- result = await session.execute(
- select(func.count())
- .select_from(Key)
- .where(Key.server_id == old_server_name)
- )
+ result = await session.execute(select(func.count()).select_from(Key).where(Key.server_id == old_server_name))
keys_count = result.scalar()
await session.execute(
@@ -1062,11 +982,7 @@ async def handle_new_server_name_input(
)
if keys_count > 0:
- await session.execute(
- update(Key)
- .where(Key.server_id == old_server_name)
- .values(server_id=new_server_name)
- )
+ await session.execute(update(Key).where(Key.server_id == old_server_name).values(server_id=new_server_name))
await session.commit()
@@ -1076,9 +992,7 @@ async def handle_new_server_name_input(
)
except Exception as e:
await session.rollback()
- logger.error(
- f"Ошибка при смене имени сервера {old_server_name} на {new_server_name}: {e}"
- )
+ logger.error(f"Ошибка при смене имени сервера {old_server_name} на {new_server_name}: {e}")
await message.answer(
text=f"❌ Произошла ошибка при смене имени сервера: {e}",
reply_markup=build_admin_back_kb("clusters"),
@@ -1088,9 +1002,7 @@ async def handle_new_server_name_input(
@router.callback_query(F.data.startswith("transfer_to_server|"))
-async def handle_server_transfer(
- callback_query: CallbackQuery, state: FSMContext, session: AsyncSession
-):
+async def handle_server_transfer(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
try:
data = callback_query.data.split("|")
new_server_name = data[1]
@@ -1099,11 +1011,7 @@ async def handle_server_transfer(
user_data = await state.get_data()
cluster_name = user_data.get("cluster_name")
- await session.execute(
- update(Key)
- .where(Key.server_id == old_server_name)
- .values(server_id=new_server_name)
- )
+ await session.execute(update(Key).where(Key.server_id == old_server_name).values(server_id=new_server_name))
await session.execute(
delete(Server).where(
@@ -1134,9 +1042,7 @@ async def handle_server_transfer(
@router.callback_query(F.data.startswith("transfer_to_cluster|"))
-async def handle_cluster_transfer(
- callback_query: CallbackQuery, state: FSMContext, session: AsyncSession
-):
+async def handle_cluster_transfer(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
try:
data = callback_query.data.split("|")
new_cluster_name = data[1]
@@ -1146,16 +1052,8 @@ async def handle_cluster_transfer(
user_data = await state.get_data()
cluster_name = user_data.get("cluster_name")
- await session.execute(
- update(Key)
- .where(Key.server_id == old_server_name)
- .values(server_id=new_cluster_name)
- )
- await session.execute(
- update(Key)
- .where(Key.server_id == old_cluster_name)
- .values(server_id=new_cluster_name)
- )
+ await session.execute(update(Key).where(Key.server_id == old_server_name).values(server_id=new_cluster_name))
+ await session.execute(update(Key).where(Key.server_id == old_cluster_name).values(server_id=new_cluster_name))
await session.execute(
delete(Server).where(
@@ -1185,17 +1083,11 @@ async def handle_cluster_transfer(
await state.clear()
-@router.callback_query(
- AdminClusterCallback.filter(F.action == "set_tariff"), IsAdminFilter()
-)
-async def show_tariff_group_selection(
- callback: CallbackQuery, callback_data: AdminClusterCallback, session
-):
+@router.callback_query(AdminClusterCallback.filter(F.action == "set_tariff"), IsAdminFilter())
+async def show_tariff_group_selection(callback: CallbackQuery, callback_data: AdminClusterCallback, session):
cluster_name = callback_data.data
result = await session.execute(
- select(Tariff.id, Tariff.group_code)
- .where(Tariff.group_code.isnot(None))
- .distinct(Tariff.group_code)
+ select(Tariff.id, Tariff.group_code).where(Tariff.group_code.isnot(None)).distinct(Tariff.group_code)
)
rows = result.mappings().all()
groups = [(r["id"], r["group_code"]) for r in rows]
@@ -1210,19 +1102,13 @@ async def show_tariff_group_selection(
)
-@router.callback_query(
- AdminClusterCallback.filter(F.action == "apply_tariff_group"), IsAdminFilter()
-)
-async def apply_tariff_group(
- callback: CallbackQuery, callback_data: AdminClusterCallback, session
-):
+@router.callback_query(AdminClusterCallback.filter(F.action == "apply_tariff_group"), IsAdminFilter())
+async def apply_tariff_group(callback: CallbackQuery, callback_data: AdminClusterCallback, session):
try:
cluster_name, group_id = callback_data.data.split("|", 1)
group_id = int(group_id)
- result = await session.execute(
- select(Tariff.group_code).where(Tariff.id == group_id)
- )
+ result = await session.execute(select(Tariff.group_code).where(Tariff.id == group_id))
row = result.mappings().first()
if not row:
@@ -1231,11 +1117,7 @@ async def apply_tariff_group(
group_code = row["group_code"]
- await session.execute(
- update(Server)
- .where(Server.cluster_name == cluster_name)
- .values(tariff_group=group_code)
- )
+ await session.execute(update(Server).where(Server.cluster_name == cluster_name).values(tariff_group=group_code))
await session.commit()
await callback.message.edit_text(
@@ -1245,6 +1127,4 @@ async def apply_tariff_group(
except Exception as e:
logger.error(f"Ошибка при применении тарифной группы: {e}")
- await callback.message.edit_text(
- "❌ Произошла ошибка при установке тарифной группы."
- )
\ No newline at end of file
+ await callback.message.edit_text("❌ Произошла ошибка при установке тарифной группы.")
diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py
index be962e04..8ac4ea8f 100644
--- a/handlers/admin/clusters/keyboard.py
+++ b/handlers/admin/clusters/keyboard.py
@@ -24,9 +24,7 @@ def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup:
row_buttons.append(
InlineKeyboardButton(
text=label,
- callback_data=AdminClusterCallback(
- action="manage", data=name
- ).pack(),
+ callback_data=AdminClusterCallback(action="manage", data=name).pack(),
)
)
builder.row(*row_buttons)
@@ -42,18 +40,14 @@ def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup:
return builder.as_markup()
-def build_manage_cluster_kb(
- cluster_servers: list, cluster_name: str
-) -> InlineKeyboardMarkup:
+def build_manage_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for server in cluster_servers:
builder.row(
InlineKeyboardButton(
text=f"🌍 {server['server_name']}",
- callback_data=AdminServerCallback(
- action="manage", data=server["server_name"]
- ).pack(),
+ callback_data=AdminServerCallback(action="manage", data=server["server_name"]).pack(),
)
)
@@ -67,9 +61,7 @@ def build_manage_cluster_kb(
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
- callback_data=AdminClusterCallback(
- action="manage", data=cluster_name
- ).pack(),
+ callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(),
)
)
@@ -88,9 +80,7 @@ def build_cluster_management_kb(cluster_name: str) -> InlineKeyboardMarkup:
builder.row(
InlineKeyboardButton(
text="🌐 Доступность",
- callback_data=AdminClusterCallback(
- action="availability", data=cluster_name
- ).pack(),
+ callback_data=AdminClusterCallback(action="availability", data=cluster_name).pack(),
)
)
builder.row(
@@ -102,65 +92,47 @@ def build_cluster_management_kb(cluster_name: str) -> InlineKeyboardMarkup:
builder.row(
InlineKeyboardButton(
text="💾 Создать бэкап",
- callback_data=AdminClusterCallback(
- action="backup", data=cluster_name
- ).pack(),
+ callback_data=AdminClusterCallback(action="backup", data=cluster_name).pack(),
)
)
builder.row(
InlineKeyboardButton(
text="⏳ Добавить время",
- callback_data=AdminClusterCallback(
- action="add_time", data=cluster_name
- ).pack(),
+ callback_data=AdminClusterCallback(action="add_time", data=cluster_name).pack(),
)
)
builder.row(
InlineKeyboardButton(
text="✏️ Сменить название",
- callback_data=AdminClusterCallback(
- action="rename", data=cluster_name
- ).pack(),
+ callback_data=AdminClusterCallback(action="rename", data=cluster_name).pack(),
)
)
builder.row(
InlineKeyboardButton(
text="💸 Тариф(Установить/изменить)",
- callback_data=AdminClusterCallback(
- action="set_tariff", data=cluster_name
- ).pack(),
- )
- )
- builder.row(
- InlineKeyboardButton(
- text="🔙 Назад", callback_data=AdminPanelCallback(action="clusters").pack()
+ callback_data=AdminClusterCallback(action="set_tariff", data=cluster_name).pack(),
)
)
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="clusters").pack()))
return builder.as_markup()
-def build_sync_cluster_kb(
- cluster_servers: list, cluster_name: str
-) -> InlineKeyboardMarkup:
+def build_sync_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for server in cluster_servers:
builder.row(
InlineKeyboardButton(
text=f"🔄 Синхронизировать {server['server_name']}",
- callback_data=AdminClusterCallback(
- action="sync-server", data=server["server_name"]
- ).pack(),
+ callback_data=AdminClusterCallback(action="sync-server", data=server["server_name"]).pack(),
)
)
builder.row(
InlineKeyboardButton(
text="📍 Синхронизировать кластер",
- callback_data=AdminClusterCallback(
- action="sync-cluster", data=cluster_name
- ).pack(),
+ callback_data=AdminClusterCallback(action="sync-cluster", data=cluster_name).pack(),
)
)
@@ -171,9 +143,7 @@ def build_sync_cluster_kb(
def build_panel_type_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
- builder.button(
- text="🌐 3X-UI", callback_data=AdminClusterCallback(action="panel_3xui").pack()
- )
+ builder.button(text="🌐 3X-UI", callback_data=AdminClusterCallback(action="panel_3xui").pack())
builder.button(
text="🌀 Remnawave",
callback_data=AdminClusterCallback(action="panel_remnawave").pack(),
@@ -182,23 +152,17 @@ def build_panel_type_kb() -> InlineKeyboardMarkup:
return builder.as_markup()
-def build_tariff_group_selection_kb(
- cluster_name: str, groups: list[tuple[int, str]]
-) -> InlineKeyboardMarkup:
+def build_tariff_group_selection_kb(cluster_name: str, groups: list[tuple[int, str]]) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for group_id, group_code in groups:
builder.button(
text=group_code,
- callback_data=AdminClusterCallback(
- action="apply_tariff_group", data=f"{cluster_name}|{group_id}"
- ).pack(),
+ callback_data=AdminClusterCallback(action="apply_tariff_group", data=f"{cluster_name}|{group_id}").pack(),
)
builder.row(
InlineKeyboardButton(
text="⬅️ Назад",
- callback_data=AdminClusterCallback(
- action="manage", data=cluster_name
- ).pack(),
+ callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(),
)
)
builder.adjust(2, 1)
diff --git a/handlers/admin/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py
index b41f266b..2c9aba64 100644
--- a/handlers/admin/coupons/coupons_handler.py
+++ b/handlers/admin/coupons/coupons_handler.py
@@ -28,6 +28,7 @@ from .keyboard import (
format_coupons_list,
)
+
router = Router()
@@ -42,9 +43,7 @@ class AdminCouponsState(StatesGroup):
IsAdminFilter(),
)
async def handle_coupons(callback_query: CallbackQuery):
- await callback_query.message.edit_text(
- text="🛠 Меню управления купонами:", reply_markup=build_coupons_kb()
- )
+ await callback_query.message.edit_text(text="🛠 Меню управления купонами:", reply_markup=build_coupons_kb())
@router.callback_query(
@@ -64,9 +63,7 @@ async def handle_coupons_create(callback_query: CallbackQuery, state: FSMContext
@router.callback_query(F.data == "coupon_type_balance")
-async def handle_balance_coupon_selection(
- callback_query: CallbackQuery, state: FSMContext
-):
+async def handle_balance_coupon_selection(callback_query: CallbackQuery, state: FSMContext):
text = (
"🎫 Введите данные для создания купона в формате:\n\n"
"📝 код 💰 сумма 🔢 лимит\n\n"
@@ -80,9 +77,7 @@ async def handle_balance_coupon_selection(
@router.callback_query(F.data == "coupon_type_days")
-async def handle_days_coupon_selection(
- callback_query: CallbackQuery, state: FSMContext
-):
+async def handle_days_coupon_selection(callback_query: CallbackQuery, state: FSMContext):
text = (
"🎫 Введите данные для создания купона в формате:\n\n"
"📝 код ⏳ дни 🔢 лимит\n\n"
@@ -96,9 +91,7 @@ async def handle_days_coupon_selection(
@router.message(AdminCouponsState.waiting_for_balance_data, IsAdminFilter())
-async def handle_balance_coupon_input(
- message: Message, state: FSMContext, session: Any
-):
+async def handle_balance_coupon_input(message: Message, state: FSMContext, session: Any):
text = message.text.strip()
parts = text.split()
@@ -147,9 +140,7 @@ async def handle_balance_coupon_input(
except Exception as e:
logger.error(f"Ошибка при создании купона: {e}")
- await message.answer(
- "❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup()
- )
+ await message.answer("❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup())
@router.message(AdminCouponsState.waiting_for_days_data, IsAdminFilter())
@@ -202,9 +193,7 @@ async def handle_days_coupon_input(message: Message, state: FSMContext, session:
except Exception as e:
logger.error(f"Ошибка при создании купона: {e}")
- await message.answer(
- "❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup()
- )
+ await message.answer("❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup())
@router.callback_query(
@@ -218,14 +207,10 @@ async def handle_coupons_list(callback_query: CallbackQuery, session: Any):
await update_coupons_list(callback_query.message, session, page)
except Exception as e:
logger.error(f"Ошибка при получении списка купонов: {e}")
- await callback_query.message.edit_text(
- "Произошла ошибка при получении списка купонов."
- )
+ await callback_query.message.edit_text("Произошла ошибка при получении списка купонов.")
-@router.callback_query(
- AdminCouponDeleteCallback.filter(F.confirm.is_(None)), IsAdminFilter()
-)
+@router.callback_query(AdminCouponDeleteCallback.filter(F.confirm.is_(None)), IsAdminFilter())
async def handle_coupon_delete(
callback_query: CallbackQuery,
callback_data: AdminCouponDeleteCallback,
@@ -235,15 +220,11 @@ async def handle_coupon_delete(
kb = InlineKeyboardBuilder()
kb.button(
text="✅ Да, удалить",
- callback_data=AdminCouponDeleteCallback(
- coupon_code=coupon_code, confirm=True
- ).pack(),
+ callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=True).pack(),
)
kb.button(
text="❌ Нет, отменить",
- callback_data=AdminCouponDeleteCallback(
- coupon_code=coupon_code, confirm=False
- ).pack(),
+ callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=False).pack(),
)
kb.adjust(1)
@@ -253,9 +234,7 @@ async def handle_coupon_delete(
)
-@router.callback_query(
- AdminCouponDeleteCallback.filter(F.confirm.is_not(None)), IsAdminFilter()
-)
+@router.callback_query(AdminCouponDeleteCallback.filter(F.confirm.is_not(None)), IsAdminFilter())
async def confirm_coupon_delete(
callback_query: CallbackQuery,
callback_data: AdminCouponDeleteCallback,
@@ -340,9 +319,7 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
id=coupon_code,
title=title,
description=description,
- input_message_content=InputTextMessageContent(
- message_text=message_text, parse_mode=ParseMode.HTML
- ),
+ input_message_content=InputTextMessageContent(message_text=message_text, parse_mode=ParseMode.HTML),
reply_markup=builder.as_markup(),
)
diff --git a/handlers/admin/coupons/keyboard.py b/handlers/admin/coupons/keyboard.py
index 67c558bc..807b4f79 100644
--- a/handlers/admin/coupons/keyboard.py
+++ b/handlers/admin/coupons/keyboard.py
@@ -35,9 +35,7 @@ def build_coupons_kb() -> InlineKeyboardMarkup:
return builder.as_markup()
-def build_coupons_list_kb(
- coupons: list, current_page: int, total_pages: int
-) -> InlineKeyboardMarkup:
+def build_coupons_list_kb(coupons: list, current_page: int, total_pages: int) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for coupon in coupons:
@@ -52,18 +50,14 @@ def build_coupons_list_kb(
pagination_buttons.append(
InlineKeyboardButton(
text=BACK,
- callback_data=AdminPanelCallback(
- action="coupons_list", page=current_page - 1
- ).pack(),
+ callback_data=AdminPanelCallback(action="coupons_list", page=current_page - 1).pack(),
)
)
if current_page < total_pages:
pagination_buttons.append(
InlineKeyboardButton(
text="Вперед ➡️",
- callback_data=AdminPanelCallback(
- action="coupons_list", page=current_page + 1
- ).pack(),
+ callback_data=AdminPanelCallback(action="coupons_list", page=current_page + 1).pack(),
)
)
if pagination_buttons:
diff --git a/handlers/admin/gifts/__init__.py b/handlers/admin/gifts/__init__.py
index 8de4fd7f..d6c1dd91 100644
--- a/handlers/admin/gifts/__init__.py
+++ b/handlers/admin/gifts/__init__.py
@@ -1,3 +1,3 @@
__all__ = ("router",)
-from .gifts_handler import router
\ No newline at end of file
+from .gifts_handler import router
diff --git a/handlers/admin/gifts/gifts_handler.py b/handlers/admin/gifts/gifts_handler.py
index ecd2893d..9eb35214 100644
--- a/handlers/admin/gifts/gifts_handler.py
+++ b/handlers/admin/gifts/gifts_handler.py
@@ -1,54 +1,49 @@
-from aiogram import F, Router, types, Bot
-from aiogram.types import CallbackQuery
+from aiogram import Bot, F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, delete, func
+from aiogram.types import CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
+from sqlalchemy import delete, func, select
+from sqlalchemy.ext.asyncio import AsyncSession
+from database.models import Gift, GiftUsage, Tariff
+from database.tariffs import create_subgroup_hash, find_subgroup_by_hash
+from handlers.utils import edit_or_send_message, format_days, format_months
+from logger import logger
from ..panel.keyboard import AdminPanelCallback
from .keyboard import build_admin_gifts_kb, build_gifts_list_kb
-from database.models import Tariff, Gift, GiftUsage
-from database.tariffs import create_subgroup_hash, find_subgroup_by_hash
-from handlers.utils import format_days, format_months, edit_or_send_message
-
-from logger import logger
router = Router()
+
class GiftCreationState(StatesGroup):
waiting_for_gift_limit = State()
waiting_for_limit_input_or_unlimited = State()
+
@router.callback_query(AdminPanelCallback.filter(F.action == "gifts"))
async def admin_gift_menu(callback: CallbackQuery):
await callback.message.edit_text(
- text="🎁 Подарки\nВыберите, что хотите сделать:",
- reply_markup=build_admin_gifts_kb()
+ text="🎁 Подарки\nВыберите, что хотите сделать:", reply_markup=build_admin_gifts_kb()
)
+
@router.callback_query(F.data == "admin_gift_create")
async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession):
- stmt = (
- select(Tariff)
- .where(Tariff.group_code == "gifts", Tariff.is_active == True)
- .order_by(Tariff.duration_days)
- )
+ stmt = select(Tariff).where(Tariff.group_code == "gifts", Tariff.is_active is True).order_by(Tariff.duration_days)
result = await session.execute(stmt)
tariffs = result.scalars().all()
if not tariffs:
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack())
- await callback.message.edit_text(
- "❌ Нет активных тарифов в группе 'gifts'.",
- reply_markup=builder.as_markup()
- )
+ await callback.message.edit_text("❌ Нет активных тарифов в группе 'gifts'.", reply_markup=builder.as_markup())
return
from collections import defaultdict
+
grouped_tariffs = defaultdict(list)
for t in tariffs:
grouped_tariffs[t.subgroup_title].append(t)
@@ -61,10 +56,7 @@ async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession
else:
duration_text = format_days(t.duration_days)
- builder.button(
- text=f"{t.name} – {duration_text}",
- callback_data=f"admin_gift_select|{t.id}"
- )
+ builder.button(text=f"{t.name} – {duration_text}", callback_data=f"admin_gift_select|{t.id}")
for subgroup in sorted(k for k in grouped_tariffs if k):
subgroup_hash = create_subgroup_hash(subgroup, "gifts")
@@ -75,16 +67,9 @@ async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession
)
)
- builder.row(
- types.InlineKeyboardButton(
- text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()
- )
- )
+ builder.row(types.InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()))
- await callback.message.edit_text(
- "🎁 Выберите тариф для подарка:",
- reply_markup=builder.as_markup()
- )
+ await callback.message.edit_text("🎁 Выберите тариф для подарка:", reply_markup=builder.as_markup())
@router.callback_query(F.data.startswith("admin_gift_subgroup|"))
@@ -98,9 +83,7 @@ async def admin_gift_show_tariffs_in_subgroup(callback: CallbackQuery, session:
return
stmt = (
- select(Tariff)
- .where(Tariff.group_code == "gifts", Tariff.is_active == True)
- .order_by(Tariff.duration_days)
+ select(Tariff).where(Tariff.group_code == "gifts", Tariff.is_active is True).order_by(Tariff.duration_days)
)
result = await session.execute(stmt)
tariffs = result.scalars().all()
@@ -124,9 +107,7 @@ async def admin_gift_show_tariffs_in_subgroup(callback: CallbackQuery, session:
)
)
- builder.row(
- types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_gift_create")
- )
+ builder.row(types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_gift_create"))
await edit_or_send_message(
target_message=callback.message,
@@ -148,22 +129,24 @@ async def handle_tariff_selection(callback: CallbackQuery, state: FSMContext):
kb = InlineKeyboardBuilder()
kb.button(text="🔙 Назад", callback_data="admin_gift_create")
await callback.message.edit_text(
- "🔢 Введите максимальное количество активаций подарка:",
- reply_markup=kb.as_markup()
+ "🔢 Введите максимальное количество активаций подарка:", reply_markup=kb.as_markup()
)
@router.callback_query(F.data == "gift_limit_unlimited")
async def handle_unlimited_gift(callback: CallbackQuery, state: FSMContext, bot: Bot):
from handlers.payments.gift import finalize_gift
+
data = await state.get_data()
session: AsyncSession = callback.bot["session"]
await state.clear()
await finalize_gift(callback.message, session, bot, data, is_unlimited=True)
+
@router.message(GiftCreationState.waiting_for_limit_input_or_unlimited)
async def handle_limited_gift_input(message: types.Message, session: AsyncSession, state: FSMContext, bot: Bot):
from handlers.payments.gift import finalize_gift
+
try:
max_usages = int(message.text.strip())
if max_usages <= 0:
@@ -198,14 +181,12 @@ async def show_gift_list(callback: CallbackQuery, session: AsyncSession, page: i
gifts = result.scalars().all()
from aiogram.utils.keyboard import InlineKeyboardBuilder
+
builder = InlineKeyboardBuilder()
if not gifts:
builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack())
- await callback.message.edit_text(
- "❌ Подарки не найдены.",
- reply_markup=builder.as_markup()
- )
+ await callback.message.edit_text("❌ Подарки не найдены.", reply_markup=builder.as_markup())
return
keyboard = build_gifts_list_kb(gifts, page, total=len(gifts))
@@ -213,10 +194,7 @@ async def show_gift_list(callback: CallbackQuery, session: AsyncSession, page: i
builder.inline_keyboard.extend(keyboard.inline_keyboard)
builder.row(types.InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()))
- await callback.message.edit_text(
- f"🎁 Список подарков\nСтраница {page}:",
- reply_markup=builder.as_markup()
- )
+ await callback.message.edit_text(f"🎁 Список подарков\nСтраница {page}:", reply_markup=builder.as_markup())
async def show_gift_list(callback: CallbackQuery, session: AsyncSession, page: int):
@@ -231,10 +209,7 @@ async def show_gift_list(callback: CallbackQuery, session: AsyncSession, page: i
if not gifts:
builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack())
- await callback.message.edit_text(
- "❌ Подарки не найдены.",
- reply_markup=builder.as_markup()
- )
+ await callback.message.edit_text("❌ Подарки не найдены.", reply_markup=builder.as_markup())
return
keyboard = build_gifts_list_kb(gifts, page, total=len(gifts))
@@ -242,10 +217,7 @@ async def show_gift_list(callback: CallbackQuery, session: AsyncSession, page: i
for row in keyboard.inline_keyboard:
builder.row(*row)
- await callback.message.edit_text(
- f"🎁 Список подарков\nСтраница {page}:",
- reply_markup=builder.as_markup()
- )
+ await callback.message.edit_text(f"🎁 Список подарков\nСтраница {page}:", reply_markup=builder.as_markup())
@router.callback_query(F.data.startswith("gift_view|"))
@@ -298,7 +270,4 @@ async def delete_gift(callback: CallbackQuery, session: AsyncSession):
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад к списку", callback_data="admin_gifts_all")
- await callback.message.edit_text(
- "✅ Подарок удалён.",
- reply_markup=builder.as_markup()
- )
+ await callback.message.edit_text("✅ Подарок удалён.", reply_markup=builder.as_markup())
diff --git a/handlers/admin/gifts/keyboard.py b/handlers/admin/gifts/keyboard.py
index c23433b9..61f34608 100644
--- a/handlers/admin/gifts/keyboard.py
+++ b/handlers/admin/gifts/keyboard.py
@@ -1,23 +1,21 @@
-from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
+from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from database.models import Tariff, Gift
-from ..panel.keyboard import AdminPanelCallback
-from handlers.utils import get_plural_form, format_months, format_days
+from database.models import Gift, Tariff
from handlers.buttons import BACK
+from handlers.utils import format_days, format_months, get_plural_form
+
+from ..panel.keyboard import AdminPanelCallback
+
def build_admin_gifts_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
-
- builder.row(
- InlineKeyboardButton(text="🎁 Создать подарок", callback_data="admin_gift_create")
- )
+
+ builder.row(InlineKeyboardButton(text="🎁 Создать подарок", callback_data="admin_gift_create"))
builder.row(
InlineKeyboardButton(text="📦 Все подарки", callback_data="admin_gifts_all"),
)
- builder.row(
- InlineKeyboardButton(text=BACK, callback_data="admin")
- )
+ builder.row(InlineKeyboardButton(text=BACK, callback_data="admin"))
return builder.as_markup()
@@ -66,8 +64,6 @@ def build_gifts_list_kb(gifts: list[Gift], page: int, total: int) -> InlineKeybo
if nav:
builder.row(*nav)
- builder.row(
- InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack())
- )
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()))
- return builder.as_markup()
\ No newline at end of file
+ return builder.as_markup()
diff --git a/handlers/admin/management/keyboard.py b/handlers/admin/management/keyboard.py
index 49718d1e..491790cb 100644
--- a/handlers/admin/management/keyboard.py
+++ b/handlers/admin/management/keyboard.py
@@ -35,11 +35,7 @@ def build_management_kb(admin_role: str) -> InlineKeyboardMarkup:
text="🔑 Восстановить пробники",
callback_data=AdminPanelCallback(action="restore_trials").pack(),
)
- maintenance_text = (
- "🛠️ Выключить тех. работы"
- if maintenance.maintenance_mode
- else "🛠️ Включить тех. работы"
- )
+ maintenance_text = "🛠️ Выключить тех. работы" if maintenance.maintenance_mode else "🛠️ Включить тех. работы"
builder.button(
text=maintenance_text,
callback_data=AdminPanelCallback(action="toggle_maintenance").pack(),
@@ -72,23 +68,16 @@ def build_database_kb() -> InlineKeyboardMarkup:
def build_back_to_db_menu() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
- builder.button(
- text="⬅️ Назад", callback_data=AdminPanelCallback(action="database").pack()
- )
+ builder.button(text="⬅️ Назад", callback_data=AdminPanelCallback(action="database").pack())
return builder.as_markup()
def build_export_db_sources_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
- builder.button(
- text="🌀 Remnawave",
- callback_data=AdminPanelCallback(action="export_remnawave").pack()
- )
- builder.button(
- text="🔙 Назад",
- callback_data=AdminPanelCallback(action="back_to_db_menu").pack()
- )
+ builder.button(text="🌀 Remnawave", callback_data=AdminPanelCallback(action="export_remnawave").pack())
+ builder.button(text="🧩 3x-ui", callback_data=AdminPanelCallback(action="request_3xui_file").pack())
+ builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="back_to_db_menu").pack())
builder.adjust(1)
return builder.as_markup()
@@ -99,14 +88,10 @@ def build_admins_kb(admins: list[tuple[int, str]]) -> InlineKeyboardMarkup:
for tg_id, role in admins:
builder.button(
- text=f"🧑 {tg_id} ({role})",
- callback_data=AdminPanelCallback(action=f"admin_menu|{tg_id}").pack()
+ text=f"🧑 {tg_id} ({role})", callback_data=AdminPanelCallback(action=f"admin_menu|{tg_id}").pack()
)
- builder.button(
- text="➕ Добавить админа",
- callback_data=AdminPanelCallback(action="add_admin").pack()
- )
+ builder.button(text="➕ Добавить админа", callback_data=AdminPanelCallback(action="add_admin").pack())
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
@@ -115,26 +100,16 @@ def build_admins_kb(admins: list[tuple[int, str]]) -> InlineKeyboardMarkup:
def build_single_admin_menu(tg_id: int, role: str = "moderator") -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
- builder.button(
- text="✏ Изменить роль",
- callback_data=AdminPanelCallback(action=f"edit_role|{tg_id}").pack()
- )
+ builder.button(text="✏ Изменить роль", callback_data=AdminPanelCallback(action=f"edit_role|{tg_id}").pack())
- builder.button(
- text="🗑 Удалить админа",
- callback_data=AdminPanelCallback(action=f"delete_admin|{tg_id}").pack()
- )
+ builder.button(text="🗑 Удалить админа", callback_data=AdminPanelCallback(action=f"delete_admin|{tg_id}").pack())
if role == "superadmin":
builder.button(
- text="🎟 Выпустить токен",
- callback_data=AdminPanelCallback(action=f"generate_token|{tg_id}").pack()
+ text="🎟 Выпустить токен", callback_data=AdminPanelCallback(action=f"generate_token|{tg_id}").pack()
)
- builder.button(
- text="🔙 Назад",
- callback_data=AdminPanelCallback(action="admins").pack()
- )
+ builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="admins").pack())
builder.adjust(1)
return builder.as_markup()
@@ -142,41 +117,41 @@ def build_single_admin_menu(tg_id: int, role: str = "moderator") -> InlineKeyboa
def build_role_selection_kb(tg_id: int) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
- builder.button(
- text="👑 superadmin",
- callback_data=AdminPanelCallback(action=f"set_role|{tg_id}|superadmin").pack()
- )
- builder.button(
- text="🛡 moderator",
- callback_data=AdminPanelCallback(action=f"set_role|{tg_id}|moderator").pack()
- )
- builder.button(
- text="🔙 Назад",
- callback_data=AdminPanelCallback(action=f"admin_menu|{tg_id}").pack()
- )
+ builder.button(text="👑 superadmin", callback_data=AdminPanelCallback(action=f"set_role|{tg_id}|superadmin").pack())
+ builder.button(text="🛡 moderator", callback_data=AdminPanelCallback(action=f"set_role|{tg_id}|moderator").pack())
+ builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action=f"admin_menu|{tg_id}").pack())
builder.adjust(1)
return builder.as_markup()
def build_admin_back_kb_to_admins() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
- builder.button(
- text="🔙 Назад",
- callback_data=AdminPanelCallback(action="admins").pack()
- )
+ builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="admins").pack())
builder.adjust(1)
return builder.as_markup()
def build_token_result_kb(token: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
- builder.button(
- text="📋 Скопировать токен",
- switch_inline_query_current_chat=token
- )
- builder.button(
- text="🔙 Назад",
- callback_data=AdminPanelCallback(action="admins").pack()
- )
+ builder.button(text="📋 Скопировать токен", switch_inline_query_current_chat=token)
+ builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="admins").pack())
builder.adjust(1)
- return builder.as_markup()
\ No newline at end of file
+ return builder.as_markup()
+
+
+def build_back_to_db_menu() -> InlineKeyboardMarkup:
+ builder = InlineKeyboardBuilder()
+
+ builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="back_to_db_menu").pack())
+
+ return builder.as_markup()
+
+
+def build_post_import_kb() -> InlineKeyboardMarkup:
+ builder = InlineKeyboardBuilder()
+ builder.button(
+ text="🔁 Перевыпустить подписки", callback_data=AdminPanelCallback(action="resync_after_import").pack()
+ )
+ builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="back_to_db_menu").pack())
+ builder.adjust(1)
+ return builder.as_markup()
diff --git a/handlers/admin/management/management_handler.py b/handlers/admin/management/management_handler.py
index 0446dd80..59492866 100644
--- a/handlers/admin/management/management_handler.py
+++ b/handlers/admin/management/management_handler.py
@@ -1,31 +1,46 @@
-from aiogram import F, Router
+import hashlib
+import json
+import os
+import subprocess
+import sys
+import time
+import traceback
+
+from asyncio import sleep
+from datetime import datetime
+from tempfile import NamedTemporaryFile
+
+from aiogram import Bot, F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
-from sqlalchemy import func, select, update, delete
-from sqlalchemy.ext.asyncio import AsyncSession
-import hashlib
-import time
from dateutil import parser
-
-import os, subprocess, sys
-import json
-from aiogram import Bot
-from panels.remnawave import RemnawaveAPI
-from tempfile import NamedTemporaryFile
-import traceback
-from datetime import datetime
+from sqlalchemy import delete, func, select, update
from sqlalchemy.exc import SQLAlchemyError
+from sqlalchemy.ext.asyncio import AsyncSession
-from database.models import Key, Admin, Server, User
+from config import DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD
+from database.models import Admin, Key, Server, User
from filters.admin import IsAdminFilter
+from handlers.keys.key_utils import update_subscription
from logger import logger
from middlewares import maintenance
+from panels.remnawave import RemnawaveAPI
from ..panel.keyboard import build_admin_back_kb
-from .keyboard import AdminPanelCallback, build_management_kb, build_export_db_sources_kb, build_admins_kb, build_back_to_db_menu, build_single_admin_menu, build_role_selection_kb, build_database_kb, build_admin_back_kb_to_admins, build_token_result_kb
-from asyncio import sleep
-from config import DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD
+from .keyboard import (
+ AdminPanelCallback,
+ build_admin_back_kb_to_admins,
+ build_admins_kb,
+ build_back_to_db_menu,
+ build_database_kb,
+ build_export_db_sources_kb,
+ build_management_kb,
+ build_post_import_kb,
+ build_role_selection_kb,
+ build_single_admin_menu,
+ build_token_result_kb,
+)
router = Router()
@@ -35,6 +50,10 @@ class AdminManagementStates(StatesGroup):
waiting_for_new_domain = State()
+class Import3xuiStates(StatesGroup):
+ waiting_for_file = State()
+
+
class DatabaseState(StatesGroup):
waiting_for_backup_file = State()
@@ -43,9 +62,7 @@ class AdminState(StatesGroup):
waiting_for_tg_id = State()
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "management"), IsAdminFilter()
-)
+@router.callback_query(AdminPanelCallback.filter(F.action == "management"), IsAdminFilter())
async def handle_management(callback_query: CallbackQuery, session: AsyncSession):
tg_id = callback_query.from_user.id
@@ -62,9 +79,7 @@ async def handle_management(callback_query: CallbackQuery, session: AsyncSession
)
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "change_domain"), IsAdminFilter()
-)
+@router.callback_query(AdminPanelCallback.filter(F.action == "change_domain"), IsAdminFilter())
async def request_new_domain(callback_query: CallbackQuery, state: FSMContext):
"""Запрашивает у администратора новый домен."""
await state.set_state(AdminManagementStates.waiting_for_new_domain)
@@ -74,14 +89,10 @@ async def request_new_domain(callback_query: CallbackQuery, state: FSMContext):
@router.message(AdminManagementStates.waiting_for_new_domain)
-async def process_new_domain(
- message: Message, state: FSMContext, session: AsyncSession
-):
+async def process_new_domain(message: Message, state: FSMContext, session: AsyncSession):
"""Обновляет домен в таблице keys."""
new_domain = message.text.strip()
- logger.info(
- f"[DomainChange] Новый домен, введённый администратором: '{new_domain}'"
- )
+ logger.info(f"[DomainChange] Новый домен, введённый администратором: '{new_domain}'")
if not new_domain or " " in new_domain or not new_domain.replace(".", "").isalnum():
logger.warning("[DomainChange] Некорректный домен")
@@ -102,8 +113,8 @@ async def process_new_domain(
remnawave_link=func.regexp_replace(Key.remnawave_link, r"^https://[^/]+", new_domain_url),
)
.where(
- (Key.key.startswith("https://") & ~Key.key.startswith(new_domain_url)) |
- (Key.remnawave_link.startswith("https://") & ~Key.remnawave_link.startswith(new_domain_url))
+ (Key.key.startswith("https://") & ~Key.key.startswith(new_domain_url))
+ | (Key.remnawave_link.startswith("https://") & ~Key.remnawave_link.startswith(new_domain_url))
)
)
await session.execute(stmt)
@@ -146,26 +157,20 @@ async def toggle_maintenance_mode(callback: CallbackQuery, session: AsyncSession
new_status = "включён" if maintenance.maintenance_mode else "выключен"
await callback.answer(f"🛠️ Режим обслуживания {new_status}.", show_alert=True)
- await callback.message.edit_reply_markup(
- reply_markup=build_management_kb(admin.role)
- )
+ await callback.message.edit_reply_markup(reply_markup=build_management_kb(admin.role))
@router.callback_query(AdminPanelCallback.filter(F.action == "admins"))
async def show_admins(callback: CallbackQuery, session: AsyncSession):
result = await session.execute(select(Admin.tg_id, Admin.role))
admins = result.all()
- await callback.message.edit_text(
- "👑 Список админов",
- reply_markup=build_admins_kb(admins)
- )
+ await callback.message.edit_text("👑 Список админов", reply_markup=build_admins_kb(admins))
@router.callback_query(AdminPanelCallback.filter(F.action == "add_admin"))
async def prompt_new_admin(callback: CallbackQuery, state: FSMContext):
await callback.message.edit_text(
- "Введите tg_id нового админа:",
- reply_markup=build_admin_back_kb_to_admins()
+ "Введите tg_id нового админа:", reply_markup=build_admin_back_kb_to_admins()
)
await state.set_state(AdminState.waiting_for_tg_id)
@@ -182,16 +187,9 @@ async def save_new_admin(message: Message, session: AsyncSession, state: FSMCont
if result.scalar_one_or_none():
await message.answer("⚠️ Такой админ уже существует.")
else:
- session.add(Admin(
- tg_id=tg_id,
- role="moderator",
- description="Добавлен вручную"
- ))
+ session.add(Admin(tg_id=tg_id, role="moderator", description="Добавлен вручную"))
await session.commit()
- await message.answer(
- f"✅ Админ {tg_id} добавлен.",
- reply_markup=build_admin_back_kb_to_admins()
- )
+ await message.answer(f"✅ Админ {tg_id} добавлен.", reply_markup=build_admin_back_kb_to_admins())
await state.clear()
@@ -205,8 +203,7 @@ async def open_admin_menu(callback: CallbackQuery, callback_data: AdminPanelCall
role = admin or "moderator"
await callback.message.edit_text(
- f"👤 Управление админом {tg_id}",
- reply_markup=build_single_admin_menu(tg_id, role)
+ f"👤 Управление админом {tg_id}", reply_markup=build_single_admin_menu(tg_id, role)
)
@@ -229,7 +226,7 @@ async def generate_token(callback: CallbackQuery, callback_data: AdminPanelCallb
f"🎟 Новый токен для {tg_id}:\n\n"
f"{token}\n\n"
f"⚠️ Это сообщение исчезнет через 5 минут.",
- reply_markup=build_token_result_kb(token)
+ reply_markup=build_token_result_kb(token),
)
await sleep(300)
@@ -243,8 +240,7 @@ async def generate_token(callback: CallbackQuery, callback_data: AdminPanelCallb
async def edit_admin_role(callback: CallbackQuery, callback_data: AdminPanelCallback):
tg_id = int(callback_data.action.split("|")[1])
await callback.message.edit_text(
- f"✏ Выберите новую роль для {tg_id}:",
- reply_markup=build_role_selection_kb(tg_id)
+ f"✏ Выберите новую роль для {tg_id}:", reply_markup=build_role_selection_kb(tg_id)
)
@@ -261,8 +257,7 @@ async def set_admin_role(callback: CallbackQuery, callback_data: AdminPanelCallb
if tg_id == callback.from_user.id:
await callback.message.edit_text(
- "🚫 Нельзя изменить свою собственную роль!",
- reply_markup=build_single_admin_menu(tg_id)
+ "🚫 Нельзя изменить свою собственную роль!", reply_markup=build_single_admin_menu(tg_id)
)
return
@@ -276,8 +271,7 @@ async def set_admin_role(callback: CallbackQuery, callback_data: AdminPanelCallb
await session.commit()
await callback.message.edit_text(
- f"✅ Роль админа {tg_id} изменена на {role}.",
- reply_markup=build_single_admin_menu(tg_id)
+ f"✅ Роль админа {tg_id} изменена на {role}.", reply_markup=build_single_admin_menu(tg_id)
)
@@ -289,8 +283,7 @@ async def delete_admin(callback: CallbackQuery, callback_data: AdminPanelCallbac
await session.commit()
await callback.message.edit_text(
- f"🗑 Админ {tg_id} удалён.",
- reply_markup=build_admin_back_kb_to_admins()
+ f"🗑 Админ {tg_id} удалён.", reply_markup=build_admin_back_kb_to_admins()
)
@@ -314,7 +307,6 @@ async def prompt_restore_db(callback: CallbackQuery, state: FSMContext):
@router.message(DatabaseState.waiting_for_backup_file)
async def restore_database(message: Message, state: FSMContext, bot: Bot):
-
document = message.document
if not document or not document.file_name.endswith(".sql"):
@@ -336,45 +328,70 @@ async def restore_database(message: Message, state: FSMContext, bot: Bot):
logger.info(f"[Restore] Определён формат: {'custom' if is_custom_dump else 'plain'}")
- subprocess.run([
- "sudo", "-u", "postgres", "psql", "-d", "postgres", "-c",
- f"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{DB_NAME}' AND pid <> pg_backend_pid();"
- ], check=True)
+ subprocess.run(
+ [
+ "sudo",
+ "-u",
+ "postgres",
+ "psql",
+ "-d",
+ "postgres",
+ "-c",
+ f"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{DB_NAME}' AND pid <> pg_backend_pid();",
+ ],
+ check=True,
+ )
- subprocess.run([
- "sudo", "-u", "postgres", "psql", "-d", "postgres", "-c",
- f"DROP DATABASE IF EXISTS {DB_NAME};"
- ], check=True)
+ subprocess.run(
+ ["sudo", "-u", "postgres", "psql", "-d", "postgres", "-c", f"DROP DATABASE IF EXISTS {DB_NAME};"],
+ check=True,
+ )
- subprocess.run([
- "sudo", "-u", "postgres", "psql", "-d", "postgres", "-c",
- f"CREATE DATABASE {DB_NAME} OWNER {DB_USER};"
- ], check=True)
+ subprocess.run(
+ ["sudo", "-u", "postgres", "psql", "-d", "postgres", "-c", f"CREATE DATABASE {DB_NAME} OWNER {DB_USER};"],
+ check=True,
+ )
logger.info("[Restore] База данных пересоздана")
os.environ["PGPASSWORD"] = DB_PASSWORD
if is_custom_dump:
- result = subprocess.run([
- "pg_restore",
- f"--dbname={DB_NAME}",
- "-U", DB_USER,
- "-h", PG_HOST,
- "-p", PG_PORT,
- "--no-owner",
- "--exit-on-error",
- tmp_path,
- ], capture_output=True, text=True)
+ result = subprocess.run(
+ [
+ "pg_restore",
+ f"--dbname={DB_NAME}",
+ "-U",
+ DB_USER,
+ "-h",
+ PG_HOST,
+ "-p",
+ PG_PORT,
+ "--no-owner",
+ "--exit-on-error",
+ tmp_path,
+ ],
+ capture_output=True,
+ text=True,
+ )
else:
- result = subprocess.run([
- "psql",
- "-U", DB_USER,
- "-h", PG_HOST,
- "-p", PG_PORT,
- "-d", DB_NAME,
- "-f", tmp_path,
- ], capture_output=True, text=True)
+ result = subprocess.run(
+ [
+ "psql",
+ "-U",
+ DB_USER,
+ "-h",
+ PG_HOST,
+ "-p",
+ PG_PORT,
+ "-d",
+ DB_NAME,
+ "-f",
+ tmp_path,
+ ],
+ capture_output=True,
+ text=True,
+ )
del os.environ["PGPASSWORD"]
@@ -416,19 +433,14 @@ async def handle_export_db(callback: CallbackQuery):
@router.callback_query(AdminPanelCallback.filter(F.action == "back_to_db_menu"))
async def back_to_database_menu(callback: CallbackQuery):
- await callback.message.edit_text(
- "📦 Управление базой данных:",
- reply_markup=build_database_kb()
- )
+ await callback.message.edit_text("📦 Управление базой данных:", reply_markup=build_database_kb())
@router.callback_query(AdminPanelCallback.filter(F.action == "export_remnawave"))
async def show_remnawave_clients(callback: CallbackQuery, session: AsyncSession):
await callback.answer()
- result = await session.execute(
- select(Server).where(Server.panel_type == "remnawave", Server.enabled == True)
- )
+ result = await session.execute(select(Server).where(Server.panel_type == "remnawave", Server.enabled is True))
servers = result.scalars().all()
if not servers:
@@ -529,22 +541,14 @@ async def import_remnawave_keys(session: AsyncSession, users: list[dict], server
logger.warning(f"[SKIP] Пропущен клиент: tg_id={tg_id}, client_id={client_id}")
continue
- exists_stmt = await session.execute(
- select(Key).where(Key.client_id == client_id)
- )
+ exists_stmt = await session.execute(select(Key).where(Key.client_id == client_id))
if exists_stmt.scalar():
logger.info(f"[SKIP] Ключ уже существует: {client_id}")
continue
try:
- created_ts = (
- int(parser.isoparse(created_at).timestamp() * 1000)
- if created_at else int(time.time() * 1000)
- )
- expire_ts = (
- int(parser.isoparse(expire_at).timestamp() * 1000)
- if expire_at else int(time.time() * 1000)
- )
+ created_ts = int(parser.isoparse(created_at).timestamp() * 1000) if created_at else int(time.time() * 1000)
+ expire_ts = int(parser.isoparse(expire_at).timestamp() * 1000) if expire_at else int(time.time() * 1000)
new_key = Key(
tg_id=tg_id,
@@ -571,4 +575,76 @@ async def import_remnawave_keys(session: AsyncSession, users: list[dict], server
await session.commit()
logger.info(f"[IMPORT] Всего добавлено ключей: {added}")
- return added
\ No newline at end of file
+ return added
+
+
+@router.callback_query(AdminPanelCallback.filter(F.action == "request_3xui_file"))
+async def prompt_for_3xui_file(callback: CallbackQuery, state: FSMContext):
+ await callback.message.edit_text(
+ "📂 Пришлите файл базы данных x-ui.db для восстановления подписок и клиентов.\n\n"
+ "Формат: SQLite-файл с таблицей inbounds.\n\n"
+ "⚠️ Важно! Убедитесь, что у всех подписок в панели прописан telegram_id.\n"
+ "После восстановления обязательно выполните синхронизацию с текущими серверами!",
+ reply_markup=build_back_to_db_menu(),
+ )
+ await state.set_state(Import3xuiStates.waiting_for_file)
+
+
+@router.message(Import3xuiStates.waiting_for_file, F.document)
+async def handle_3xui_db_upload(message: Message, state: FSMContext, session: AsyncSession):
+ file = message.document
+
+ if not file.file_name.endswith(".db"):
+ await message.reply("❌ Пожалуйста, пришли файл с расширением .db")
+ return
+
+ file_path = f"/tmp/{file.file_name}"
+ await message.bot.download(file, destination=file_path)
+
+ processing_message = await message.reply("📥 Файл получен. Начинаю восстановление...")
+
+ try:
+ from database.importer import import_keys_from_3xui_db
+
+ imported, skipped = await import_keys_from_3xui_db(file_path, session)
+
+ await processing_message.edit_text(
+ f"✅ Восстановление завершено:\n"
+ f"🔐 Импортировано подписок: {imported}\n"
+ f"⏭ Пропущено (уже есть): {skipped}",
+ reply_markup=build_post_import_kb(),
+ )
+
+ except Exception as e:
+ logger.error(f"[Import 3x-ui] Ошибка: {e}")
+ await processing_message.edit_text(
+ "❌ Произошла ошибка при импорте. Убедись, что это валидный файл x-ui.db",
+ reply_markup=build_back_to_db_menu(),
+ )
+
+ await state.clear()
+
+
+@router.callback_query(AdminPanelCallback.filter(F.action == "resync_after_import"))
+async def handle_resync_after_import(callback: CallbackQuery, session: AsyncSession):
+ await callback.answer("🔁 Начинаю перевыпуск подписок...")
+
+ result = await session.execute(select(Key.tg_id, Key.email))
+ keys = result.all()
+
+ success = 0
+ failed = 0
+
+ for tg_id, email in keys:
+ try:
+ await update_subscription(tg_id=tg_id, email=email, session=session)
+ success += 1
+ except Exception as e:
+ logger.error(f"[Resync] Ошибка при перевыпуске {email}: {e}")
+ failed += 1
+
+ await callback.message.edit_text(
+ f"🔁 Перевыпуск завершён:\n✅ Успешно: {success}\n❌ Ошибки: {failed}",
+ parse_mode="HTML",
+ reply_markup=build_back_to_db_menu(),
+ )
diff --git a/handlers/admin/panel/keyboard.py b/handlers/admin/panel/keyboard.py
index f88d05d7..46484d06 100644
--- a/handlers/admin/panel/keyboard.py
+++ b/handlers/admin/panel/keyboard.py
@@ -33,20 +33,12 @@ def build_panel_kb(admin_role: str) -> InlineKeyboardMarkup:
callback_data=AdminPanelCallback(action="clusters").pack(),
)
builder.row(
- InlineKeyboardButton(
- text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack()
- ),
- InlineKeyboardButton(
- text="🎟️ Купоны", callback_data=AdminPanelCallback(action="coupons").pack()
- ),
+ InlineKeyboardButton(text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack()),
+ InlineKeyboardButton(text="🎟️ Купоны", callback_data=AdminPanelCallback(action="coupons").pack()),
)
builder.row(
- InlineKeyboardButton(
- text="💸 Тарифы", callback_data=AdminPanelCallback(action="tariffs").pack()
- ),
- InlineKeyboardButton(
- text="🎁 Подарки", callback_data=AdminPanelCallback(action="gifts").pack()
- ),
+ InlineKeyboardButton(text="💸 Тарифы", callback_data=AdminPanelCallback(action="tariffs").pack()),
+ InlineKeyboardButton(text="🎁 Подарки", callback_data=AdminPanelCallback(action="gifts").pack()),
)
if admin_role == "superadmin":
@@ -99,6 +91,4 @@ def build_admin_back_btn(action: str = "admin") -> InlineKeyboardButton:
def build_admin_btn(text: str, action: str) -> InlineKeyboardButton:
- return InlineKeyboardButton(
- text=text, callback_data=AdminPanelCallback(action=action).pack()
- )
+ return InlineKeyboardButton(text=text, callback_data=AdminPanelCallback(action=action).pack())
diff --git a/handlers/admin/panel/panel_handler.py b/handlers/admin/panel/panel_handler.py
index 635c1c1c..a56ac18b 100644
--- a/handlers/admin/panel/panel_handler.py
+++ b/handlers/admin/panel/panel_handler.py
@@ -3,16 +3,17 @@ from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
-
-from bot import version
-from filters.admin import IsAdminFilter
-from database.models import Admin
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
+
+from bot import version
+from database.models import Admin
+from filters.admin import IsAdminFilter
from logger import logger
from .keyboard import AdminPanelCallback, build_panel_kb
+
router = Router()
@@ -22,9 +23,7 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC
await state.clear()
- result = await session.execute(
- select(Admin.role).where(Admin.tg_id == callback_query.from_user.id)
- )
+ result = await session.execute(select(Admin.role).where(Admin.tg_id == callback_query.from_user.id))
role = result.scalar_one_or_none() or "admin"
markup = build_panel_kb(admin_role=role)
@@ -38,9 +37,7 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC
)
except TelegramBadRequest as e:
if "message is not modified" in str(e):
- logger.warning(
- "🔄 Попытка редактировать сообщение без изменений — пропущено."
- )
+ logger.warning("🔄 Попытка редактировать сообщение без изменений — пропущено.")
else:
raise
else:
@@ -65,16 +62,13 @@ async def handle_admin_callback_query_simple(callback_query: CallbackQuery, stat
async def handle_admin_message(message: Message, state: FSMContext, session: AsyncSession):
text = f"🤖 Панель администратора\n\nВерсия бота:\n
{version}" - await state.clear() - result = await session.execute( - select(Admin.role).where(Admin.tg_id == message.from_user.id) - ) + result = await session.execute(select(Admin.role).where(Admin.tg_id == message.from_user.id)) role = result.scalar_one_or_none() or "admin" await message.answer( text=text, reply_markup=build_panel_kb(admin_role=role), disable_web_page_preview=True, - ) \ No newline at end of file + ) diff --git a/handlers/admin/restart/restart_handler.py b/handlers/admin/restart/restart_handler.py index 79aec009..218ee2f6 100644 --- a/handlers/admin/restart/restart_handler.py +++ b/handlers/admin/restart/restart_handler.py @@ -4,6 +4,7 @@ import subprocess import sys import psutil + from aiogram import F, Router from aiogram.types import CallbackQuery @@ -11,15 +12,12 @@ from filters.admin import IsAdminFilter from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb, build_restart_kb + router = Router() -@router.callback_query( - AdminPanelCallback.filter(F.action == "restart"), IsAdminFilter() -) -async def handle_restart_confirm( - callback_query: CallbackQuery, callback_data: AdminPanelCallback -): +@router.callback_query(AdminPanelCallback.filter(F.action == "restart"), IsAdminFilter()) +async def handle_restart_confirm(callback_query: CallbackQuery, callback_data: AdminPanelCallback): kb = build_admin_back_kb() await callback_query.message.edit_text("🔄 Перезапускаем бота...", reply_markup=kb) diff --git a/handlers/admin/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py index 540ac1e2..807544b1 100644 --- a/handlers/admin/sender/sender_handler.py +++ b/handlers/admin/sender/sender_handler.py @@ -1,12 +1,13 @@ import json import re + from datetime import datetime from aiogram import F, Router +from aiogram.exceptions import TelegramBadRequest from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import CallbackQuery, Message, InlineKeyboardMarkup, InlineKeyboardButton -from aiogram.exceptions import TelegramBadRequest +from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message from sqlalchemy import distinct, func, select from sqlalchemy.ext.asyncio import AsyncSession @@ -17,6 +18,7 @@ from logger import logger from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from .keyboard import AdminSenderCallback, build_clusters_kb, build_sender_kb + router = Router() @@ -32,23 +34,23 @@ def parse_message_buttons(text: str) -> tuple[str, InlineKeyboardMarkup | None]: parts = text.split("BUTTONS:", 1) clean_text = parts[0].strip() buttons_text = parts[1].strip() - + if not buttons_text: return clean_text, None buttons = [] - button_lines = [line.strip() for line in buttons_text.split('\n') if line.strip()] - + button_lines = [line.strip() for line in buttons_text.split("\n") if line.strip()] + for line in button_lines: try: - cleaned_line = re.sub(r'
{update_time}"
)
- await callback_query.message.edit_text(
- text=stats_message, reply_markup=build_stats_kb()
- )
+ await callback_query.message.edit_text(text=stats_message, reply_markup=build_stats_kb())
except TelegramBadRequest as e:
if "message is not modified" not in str(e):
logger.error(f"Error in user_stats_menu: {e}")
except Exception as e:
logger.error(f"Error in user_stats_menu: {e}")
- await callback_query.answer(
- "Произошла ошибка при получении статистики", show_alert=True
- )
+ await callback_query.answer("Произошла ошибка при получении статистики", show_alert=True)
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "stats_export_users_csv"), IsAdminFilter()
-)
+@router.callback_query(AdminPanelCallback.filter(F.action == "stats_export_users_csv"), IsAdminFilter())
async def handle_export_users_csv(callback_query: CallbackQuery, session: AsyncSession):
kb = build_admin_back_kb("stats")
try:
export = await export_users_csv(session)
- await callback_query.message.answer_document(
- document=export, caption="📅 Экспорт пользователей в CSV"
- )
+ await callback_query.message.answer_document(document=export, caption="📅 Экспорт пользователей в CSV")
except Exception as e:
logger.error(f"Ошибка при экспорте пользователей: {e}")
await callback_query.message.edit_text(text=f"❗ Ошибка: {e}", reply_markup=kb)
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "stats_export_payments_csv"), IsAdminFilter()
-)
-async def handle_export_payments_csv(
- callback_query: CallbackQuery, session: AsyncSession
-):
+@router.callback_query(AdminPanelCallback.filter(F.action == "stats_export_payments_csv"), IsAdminFilter())
+async def handle_export_payments_csv(callback_query: CallbackQuery, session: AsyncSession):
kb = build_admin_back_kb("stats")
try:
export = await export_payments_csv(session)
- await callback_query.message.answer_document(
- document=export, caption="📅 Экспорт платежей в CSV"
- )
+ await callback_query.message.answer_document(document=export, caption="📅 Экспорт платежей в CSV")
except Exception as e:
logger.error(f"Ошибка при экспорте платежей: {e}")
await callback_query.message.edit_text(text=f"❗ Ошибка: {e}", reply_markup=kb)
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "stats_export_hot_leads_csv"), IsAdminFilter()
-)
-async def handle_export_hot_leads_csv(
- callback_query: CallbackQuery, session: AsyncSession
-):
+@router.callback_query(AdminPanelCallback.filter(F.action == "stats_export_hot_leads_csv"), IsAdminFilter())
+async def handle_export_hot_leads_csv(callback_query: CallbackQuery, session: AsyncSession):
kb = build_admin_back_kb("stats")
try:
export = await export_hot_leads_csv(session)
- await callback_query.message.answer_document(
- document=export, caption="📅 Экспорт горящих лидов"
- )
+ await callback_query.message.answer_document(document=export, caption="📅 Экспорт горящих лидов")
except Exception as e:
logger.error(f"Ошибка при экспорте горящих лидов: {e}")
await callback_query.message.edit_text(text=f"❗ Ошибка: {e}", reply_markup=kb)
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "stats_export_keys_csv"), IsAdminFilter()
-)
+@router.callback_query(AdminPanelCallback.filter(F.action == "stats_export_keys_csv"), IsAdminFilter())
async def handle_export_keys_csv(callback_query: CallbackQuery, session: AsyncSession):
kb = build_admin_back_kb("stats")
try:
export = await export_keys_csv(session)
- await callback_query.message.answer_document(
- document=export, caption="📅 Экспорт подписок в CSV"
- )
+ await callback_query.message.answer_document(document=export, caption="📅 Экспорт подписок в CSV")
except Exception as e:
logger.error(f"Ошибка при экспорте подписок: {e}")
await callback_query.message.edit_text(text=f"❗ Ошибка: {e}", reply_markup=kb)
diff --git a/handlers/admin/tariffs/keyboard.py b/handlers/admin/tariffs/keyboard.py
index 22f55df8..ce358999 100644
--- a/handlers/admin/tariffs/keyboard.py
+++ b/handlers/admin/tariffs/keyboard.py
@@ -1,9 +1,11 @@
+from collections import defaultdict
+
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from collections import defaultdict
from database.tariffs import create_subgroup_hash
+
from ..panel.keyboard import AdminPanelCallback
@@ -25,23 +27,13 @@ def build_tariff_menu_kb() -> InlineKeyboardMarkup:
callback_data=AdminTariffCallback(action="list").pack(),
)
)
- builder.row(
- InlineKeyboardButton(
- text="⬅️ Назад", callback_data=AdminPanelCallback(action="admin").pack()
- )
- )
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=AdminPanelCallback(action="admin").pack()))
return builder.as_markup()
def build_cancel_kb() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
- inline_keyboard=[
- [
- InlineKeyboardButton(
- text="❌ Отменить", callback_data="cancel_tariff_creation"
- )
- ]
- ]
+ inline_keyboard=[[InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_tariff_creation")]]
)
@@ -81,13 +73,12 @@ def build_tariff_list_kb(tariffs: list[dict]) -> InlineKeyboardMarkup:
subgroup = t.get("subgroup_title")
grouped[subgroup].append(t)
- for subgroup_title, items in grouped.items():
+ for subgroup_title, _items in grouped.items():
if subgroup_title:
subgroup_hash = create_subgroup_hash(subgroup_title, group_code)
builder.row(
InlineKeyboardButton(
- text=f"{subgroup_title}",
- callback_data=f"view_subgroup|{subgroup_hash}|{group_code}"
+ text=f"{subgroup_title}", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}"
)
)
@@ -107,12 +98,7 @@ def build_tariff_list_kb(tariffs: list[dict]) -> InlineKeyboardMarkup:
)
)
- builder.row(
- InlineKeyboardButton(
- text="Сгруппировать в подгруппу",
- callback_data=f"start_subgrouping|{group_code}"
- )
- )
+ builder.row(InlineKeyboardButton(text="Сгруппировать в подгруппу", callback_data=f"start_subgrouping|{group_code}"))
builder.row(
InlineKeyboardButton(
@@ -130,15 +116,11 @@ def build_single_tariff_kb(tariff_id: int) -> InlineKeyboardMarkup:
[
InlineKeyboardButton(
text="✏️ Редактировать",
- callback_data=AdminTariffCallback(
- action=f"edit|{tariff_id}"
- ).pack(),
+ callback_data=AdminTariffCallback(action=f"edit|{tariff_id}").pack(),
),
InlineKeyboardButton(
text="🗑 Удалить",
- callback_data=AdminTariffCallback(
- action=f"delete|{tariff_id}"
- ).pack(),
+ callback_data=AdminTariffCallback(action=f"delete|{tariff_id}").pack(),
),
],
[
@@ -154,22 +136,14 @@ def build_single_tariff_kb(tariff_id: int) -> InlineKeyboardMarkup:
def build_edit_tariff_fields_kb(tariff_id: int) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
- [
- InlineKeyboardButton(
- text="📝 Название", callback_data=f"edit_field|{tariff_id}|name"
- )
- ],
+ [InlineKeyboardButton(text="📝 Название", callback_data=f"edit_field|{tariff_id}|name")],
[
InlineKeyboardButton(
text="📅 Длительность",
callback_data=f"edit_field|{tariff_id}|duration_days",
)
],
- [
- InlineKeyboardButton(
- text="💰 Цена", callback_data=f"edit_field|{tariff_id}|price_rub"
- )
- ],
+ [InlineKeyboardButton(text="💰 Цена", callback_data=f"edit_field|{tariff_id}|price_rub")],
[
InlineKeyboardButton(
text="📦 Трафик (ГБ или 0)",
@@ -182,14 +156,11 @@ def build_edit_tariff_fields_kb(tariff_id: int) -> InlineKeyboardMarkup:
callback_data=f"edit_field|{tariff_id}|device_limit",
)
],
+ [InlineKeyboardButton(text="🔘 Активность", callback_data=f"toggle_active|{tariff_id}")],
[
InlineKeyboardButton(
- text="🔘 Активность", callback_data=f"toggle_active|{tariff_id}"
+ text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack()
)
],
- [InlineKeyboardButton(
- text="⬅️ Назад",
- callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack()
- )]
]
)
diff --git a/handlers/admin/tariffs/tariffs_handler.py b/handlers/admin/tariffs/tariffs_handler.py
index a27b9aae..3af8f09b 100644
--- a/handlers/admin/tariffs/tariffs_handler.py
+++ b/handlers/admin/tariffs/tariffs_handler.py
@@ -29,6 +29,7 @@ from .keyboard import (
build_tariff_menu_kb,
)
+
router = Router()
@@ -74,30 +75,24 @@ def validate_subgroup_title(title: str) -> tuple[bool, str]:
return True, ""
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "tariffs"), IsAdminFilter()
-)
+@router.callback_query(AdminPanelCallback.filter(F.action == "tariffs"), IsAdminFilter())
async def handle_tariff_menu(callback_query: CallbackQuery):
text = (
"💸 Управление тарифами\n\n"
"Вы можете выполнить следующие действия:\n\n"
"🆕 Создать тариф\n"
- "• Установите длительность (в днях)\n"
+ "• Установите длительность (в днях)\n" "• Задайте цену (в рублях)\n" "• Задайте лимит устройств (hwid/ip_limit)\n" - "• Укажите лимит трафика (в ГБ)\n\n" + "• Укажите лимит трафика (в ГБ)\n\n" "📋 Редактировать тарифы\n" - "• Просматривайте список текущих тарифов\n" - "• Изменяйте параметры или удаляйте при необходимости" - ) - await callback_query.message.edit_text( - text=text, reply_markup=build_tariff_menu_kb() + "
• Просматривайте список текущих тарифов\n" + "• Изменяйте параметры или удаляйте при необходимости" ) + await callback_query.message.edit_text(text=text, reply_markup=build_tariff_menu_kb()) -@router.callback_query( - AdminTariffCallback.filter(F.action == "create"), IsAdminFilter() -) +@router.callback_query(AdminTariffCallback.filter(F.action == "create"), IsAdminFilter()) async def start_tariff_creation(callback: CallbackQuery, state: FSMContext): await state.set_state(TariffCreateState.group) await callback.message.edit_text( @@ -135,7 +130,7 @@ async def process_tariff_name(message: Message, state: FSMContext): reply_markup=build_cancel_kb(), ) return - + await state.update_data(name=name) await state.set_state(TariffCreateState.duration) await message.answer( @@ -151,9 +146,7 @@ async def process_tariff_duration(message: Message, state: FSMContext): if days <= 0: raise ValueError except ValueError: - await message.answer( - "❌ Введите корректное количество дней (целое число больше 0):" - ) + await message.answer("❌ Введите корректное количество дней (целое число больше 0):") return await state.update_data(duration_days=days) @@ -190,9 +183,7 @@ async def process_tariff_traffic(message: Message, state: FSMContext): if traffic < 0: raise ValueError except ValueError: - await message.answer( - "❌ Введите корректный лимит трафика (целое число 0 или больше):" - ) + await message.answer("❌ Введите корректный лимит трафика (целое число 0 или больше):") return await state.update_data(traffic_limit=traffic if traffic > 0 else None) @@ -204,17 +195,13 @@ async def process_tariff_traffic(message: Message, state: FSMContext): @router.message(TariffCreateState.device_limit, IsAdminFilter()) -async def process_tariff_device_limit( - message: Message, state: FSMContext, session: AsyncSession -): +async def process_tariff_device_limit(message: Message, state: FSMContext, session: AsyncSession): try: device_limit = int(message.text.strip()) if device_limit < 0: raise ValueError except ValueError: - await message.answer( - "❌ Введите корректный лимит устройств (целое число 0 или больше):" - ) + await message.answer("❌ Введите корректный лимит устройств (целое число 0 или больше):") return data = await state.get_data() @@ -239,9 +226,7 @@ async def process_tariff_device_limit( inline_keyboard=[ [ InlineKeyboardButton(text="✅ Да", callback_data="add_more_tariff"), - InlineKeyboardButton( - text="❌ Нет", callback_data="done_tariff_group" - ), + InlineKeyboardButton(text="❌ Нет", callback_data="done_tariff_group"), ] ] ), @@ -251,40 +236,30 @@ async def process_tariff_device_limit( @router.callback_query(F.data == "add_more_tariff", IsAdminFilter()) async def handle_add_more_tariff(callback: CallbackQuery, state: FSMContext): await state.set_state(TariffCreateState.name) - await callback.message.edit_text( - "📝 Введите название следующего тарифа:", reply_markup=build_cancel_kb() - ) + await callback.message.edit_text("📝 Введите название следующего тарифа:", reply_markup=build_cancel_kb()) @router.callback_query(F.data == "done_tariff_group", IsAdminFilter()) async def handle_done_tariff_group(callback: CallbackQuery, state: FSMContext): await state.clear() - await callback.message.edit_text( - "✅ Группа тарифов успешно завершена.", reply_markup=build_tariff_menu_kb() - ) + await callback.message.edit_text("✅ Группа тарифов успешно завершена.", reply_markup=build_tariff_menu_kb()) @router.callback_query(F.data == "cancel_tariff_creation", IsAdminFilter()) async def cancel_tariff_creation(callback: CallbackQuery, state: FSMContext): await state.clear() - await callback.message.edit_text( - "❌ Создание тарифа отменено.", reply_markup=build_tariff_menu_kb() - ) + await callback.message.edit_text("❌ Создание тарифа отменено.", reply_markup=build_tariff_menu_kb()) @router.callback_query(AdminTariffCallback.filter(F.action == "list"), IsAdminFilter()) async def show_tariff_groups(callback: CallbackQuery, session: AsyncSession): result = await session.execute( - select(distinct(Tariff.group_code)) - .where(Tariff.group_code.isnot(None)) - .order_by(Tariff.group_code) + select(distinct(Tariff.group_code)).where(Tariff.group_code.isnot(None)).order_by(Tariff.group_code) ) groups = [row[0] for row in result.fetchall()] if not groups: - await callback.message.edit_text( - "❌ Нет сохранённых тарифов.", reply_markup=build_tariff_menu_kb() - ) + await callback.message.edit_text("❌ Нет сохранённых тарифов.", reply_markup=build_tariff_menu_kb()) return special_groups = { @@ -310,21 +285,15 @@ def tariff_to_dict(tariff: Tariff) -> dict: "name": tariff.name, "price_rub": tariff.price_rub, "group_code": tariff.group_code, - "subgroup_title": tariff.subgroup_title + "subgroup_title": tariff.subgroup_title, } -@router.callback_query( - AdminTariffCallback.filter(F.action.startswith("group|")), IsAdminFilter() -) -async def show_tariffs_in_group( - callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession -): +@router.callback_query(AdminTariffCallback.filter(F.action.startswith("group|")), IsAdminFilter()) +async def show_tariffs_in_group(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): group_code = callback_data.action.split("|", 1)[1] - result = await session.execute( - select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id) - ) + result = await session.execute(select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id)) tariffs = result.scalars().all() if not tariffs: @@ -339,12 +308,8 @@ async def show_tariffs_in_group( ) -@router.callback_query( - AdminTariffCallback.filter(F.action.startswith("view|")), IsAdminFilter() -) -async def view_tariff( - callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession -): +@router.callback_query(AdminTariffCallback.filter(F.action.startswith("view|")), IsAdminFilter()) +async def view_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): tariff_id = int(callback_data.action.split("|", 1)[1]) result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) @@ -358,24 +323,16 @@ async def view_tariff( await callback.message.edit_text(text=text, reply_markup=markup) -@router.callback_query( - AdminTariffCallback.filter(F.action.startswith("delete|")), IsAdminFilter() -) -async def confirm_tariff_deletion( - callback: CallbackQuery, callback_data: AdminTariffCallback -): +@router.callback_query(AdminTariffCallback.filter(F.action.startswith("delete|")), IsAdminFilter()) +async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminTariffCallback): tariff_id = int(callback_data.action.split("|", 1)[1]) await callback.message.edit_text( "⚠️ Вы уверены, что хотите удалить этот тариф?", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ - InlineKeyboardButton( - text="✅ Да", callback_data=f"confirm_delete_tariff|{tariff_id}" - ), - InlineKeyboardButton( - text="❌ Отмена", callback_data=f"view|{tariff_id}" - ), + InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_tariff|{tariff_id}"), + InlineKeyboardButton(text="❌ Отмена", callback_data=f"view|{tariff_id}"), ] ] ), @@ -395,36 +352,22 @@ async def delete_tariff(callback: CallbackQuery, session: AsyncSession): group_code = tariff.group_code - await session.execute( - update(Key).where(Key.tariff_id == tariff_id).values(tariff_id=None) - ) + await session.execute(update(Key).where(Key.tariff_id == tariff_id).values(tariff_id=None)) await session.execute(delete(Tariff).where(Tariff.id == tariff_id)) - result = await session.execute( - select(Tariff).where(Tariff.group_code == group_code) - ) + result = await session.execute(select(Tariff).where(Tariff.group_code == group_code)) remaining_tariffs = result.scalars().all() if not remaining_tariffs: - await session.execute( - update(Server) - .where(Server.tariff_group == group_code) - .values(tariff_group=None) - ) + await session.execute(update(Server).where(Server.tariff_group == group_code).values(tariff_group=None)) await session.commit() - await callback.message.edit_text( - "🗑 Тариф успешно удалён.", reply_markup=build_tariff_menu_kb() - ) + await callback.message.edit_text("🗑 Тариф успешно удалён.", reply_markup=build_tariff_menu_kb()) -@router.callback_query( - AdminTariffCallback.filter(F.action.startswith("edit|")), IsAdminFilter() -) -async def start_edit_tariff( - callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext -): +@router.callback_query(AdminTariffCallback.filter(F.action.startswith("edit|")), IsAdminFilter()) +async def start_edit_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext): tariff_id = int(callback_data.action.split("|")[1]) await state.update_data(tariff_id=tariff_id) await state.set_state(TariffEditState.choosing_field) @@ -519,9 +462,7 @@ async def toggle_tariff_status(callback: CallbackQuery, session: AsyncSession): await callback.message.edit_text(text=text, reply_markup=markup) -@router.callback_query( - AdminTariffCallback.filter(F.action.startswith("create|")), IsAdminFilter() -) +@router.callback_query(AdminTariffCallback.filter(F.action.startswith("create|")), IsAdminFilter()) async def start_tariff_creation_existing_group( callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext ): @@ -535,12 +476,8 @@ async def start_tariff_creation_existing_group( def render_tariff_card(tariff: Tariff) -> tuple[str, InlineKeyboardMarkup]: - traffic_text = ( - f"{tariff.traffic_limit} ГБ" if tariff.traffic_limit else "Безлимит" - ) - device_text = ( - f"{tariff.device_limit}" if tariff.device_limit is not None else "Безлимит" - ) + traffic_text = f"{tariff.traffic_limit} ГБ" if tariff.traffic_limit else "Безлимит" + device_text = f"{tariff.device_limit}" if tariff.device_limit is not None else "Безлимит" text = ( f"📄 Тариф: {tariff.name}\n\n" @@ -561,10 +498,7 @@ async def start_subgrouping(callback: CallbackQuery, state: FSMContext, session: result = await session.execute( select(Tariff) - .where( - Tariff.group_code == group_code, - (Tariff.subgroup_title.is_(None) | (Tariff.subgroup_title == "")) - ) + .where(Tariff.group_code == group_code, (Tariff.subgroup_title.is_(None) | (Tariff.subgroup_title == ""))) .order_by(Tariff.id) ) tariffs = result.scalars().all() @@ -574,38 +508,30 @@ async def start_subgrouping(callback: CallbackQuery, state: FSMContext, session: "❌ Нет доступных тарифов для группировки.\n\nВсе тарифы уже находятся в подгруппах.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton( - text="⬅️ Назад", - callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() - )] + [ + InlineKeyboardButton( + text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() + ) + ] ] - ) + ), ) return await state.set_state(TariffSubgroupState.selecting_tariffs) - await state.update_data( - group_code=group_code, - selected_tariff_ids=[] - ) + await state.update_data(group_code=group_code, selected_tariff_ids=[]) builder = InlineKeyboardBuilder() for tariff in tariffs: - builder.row( - InlineKeyboardButton( - text=f"{tariff.name}", - callback_data=f"sub_select|{tariff.id}" - ) - ) + builder.row(InlineKeyboardButton(text=f"{tariff.name}", callback_data=f"sub_select|{tariff.id}")) builder.row( InlineKeyboardButton(text="➡️ Продолжить", callback_data="subgroup_continue"), - InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping") + InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping"), ) await callback.message.edit_text( - "Выберите тарифы, которые нужно объединить в подгруппу:", - reply_markup=builder.as_markup() + "Выберите тарифы, которые нужно объединить в подгруппу:", reply_markup=builder.as_markup() ) @@ -625,10 +551,7 @@ async def toggle_tariff_subgroup_selection(callback: CallbackQuery, state: FSMCo group_code = data["group_code"] result = await session.execute( select(Tariff) - .where( - Tariff.group_code == group_code, - (Tariff.subgroup_title.is_(None) | (Tariff.subgroup_title == "")) - ) + .where(Tariff.group_code == group_code, (Tariff.subgroup_title.is_(None) | (Tariff.subgroup_title == ""))) .order_by(Tariff.id) ) tariffs = result.scalars().all() @@ -637,16 +560,11 @@ async def toggle_tariff_subgroup_selection(callback: CallbackQuery, state: FSMCo for tariff in tariffs: is_selected = tariff.id in selected prefix = "✅ " if is_selected else "" - builder.row( - InlineKeyboardButton( - text=f"{prefix}{tariff.name}", - callback_data=f"sub_select|{tariff.id}" - ) - ) + builder.row(InlineKeyboardButton(text=f"{prefix}{tariff.name}", callback_data=f"sub_select|{tariff.id}")) builder.row( InlineKeyboardButton(text="➡️ Продолжить", callback_data="subgroup_continue"), - InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping") + InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping"), ) await callback.message.edit_reply_markup(reply_markup=builder.as_markup()) @@ -666,9 +584,7 @@ async def ask_subgroup_title(callback: CallbackQuery, state: FSMContext): await state.set_state(TariffSubgroupState.entering_subgroup_title) keyboard = InlineKeyboardMarkup( - inline_keyboard=[ - [InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping")] - ] + inline_keyboard=[[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping")]] ) await callback.message.edit_text( @@ -686,13 +602,11 @@ async def apply_subgroup_title(message: Message, state: FSMContext, session: Asy await message.answer( f"❌ {error_msg}\n\nПовторите ввод:", reply_markup=InlineKeyboardMarkup( - inline_keyboard=[ - [InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping")] - ] + inline_keyboard=[[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping")]] ), ) return - + data = await state.get_data() selected_ids = data.get("selected_tariff_ids", []) @@ -702,14 +616,15 @@ async def apply_subgroup_title(message: Message, state: FSMContext, session: Asy return await session.execute( - update(Tariff) - .where(Tariff.id.in_(selected_ids)) - .values(subgroup_title=title, updated_at=datetime.utcnow()) + update(Tariff).where(Tariff.id.in_(selected_ids)).values(subgroup_title=title, updated_at=datetime.utcnow()) ) await session.commit() await state.clear() - await message.answer(f"✅ {len(selected_ids)} тарифов сгруппированы в подгруппу: {title}.", reply_markup=build_tariff_menu_kb()) + await message.answer( + f"✅ {len(selected_ids)} тарифов сгруппированы в подгруппу: {title}.", + reply_markup=build_tariff_menu_kb(), + ) @router.callback_query(F.data == "cancel_subgrouping", IsAdminFilter()) @@ -723,16 +638,15 @@ async def view_subgroup_tariffs(callback: CallbackQuery, session: AsyncSession): _, subgroup_hash, group_code = callback.data.split("|", 2) subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) - + if not subgroup_title: await callback.message.edit_text("❌ Подгруппа не найдена.") return result = await session.execute( - select(Tariff).where( - Tariff.group_code == group_code, - Tariff.subgroup_title == subgroup_title - ).order_by(Tariff.id) + select(Tariff) + .where(Tariff.group_code == group_code, Tariff.subgroup_title == subgroup_title) + .order_by(Tariff.id) ) tariffs = result.scalars().all() @@ -789,7 +703,7 @@ async def start_rename_subgroup(callback: CallbackQuery, state: FSMContext, sess _, subgroup_hash, group_code = callback.data.split("|", 2) subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) - + if not subgroup_title: await callback.message.edit_text("❌ Подгруппа не найдена.") return @@ -807,7 +721,7 @@ async def start_rename_subgroup(callback: CallbackQuery, state: FSMContext, sess inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}")] ] - ) + ), ) @@ -820,17 +734,21 @@ async def save_new_subgroup_title(message: Message, state: FSMContext, session: data = await state.get_data() subgroup_hash = data.get("subgroup_hash") group_code = data.get("group_code") - + await message.answer( f"❌ {error_msg}\n\nПовторите ввод:", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}")] + [ + InlineKeyboardButton( + text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}" + ) + ] ] ), ) return - + data = await state.get_data() old_title = data["subgroup_title"] group_code = data["group_code"] @@ -846,15 +764,19 @@ async def save_new_subgroup_title(message: Message, state: FSMContext, session: await session.commit() await state.clear() - new_hash = create_subgroup_hash(new_title, group_code) + create_subgroup_hash(new_title, group_code) await message.answer( f"✅ Подгруппа {old_title} переименована в {new_title}.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton(text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack())] + [ + InlineKeyboardButton( + text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() + ) + ] ] - ) + ), ) @@ -863,7 +785,7 @@ async def confirm_delete_subgroup(callback: CallbackQuery, state: FSMContext, se _, subgroup_hash, group_code = callback.data.split("|", 2) subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) - + if not subgroup_title: await callback.message.edit_text("❌ Подгруппа не найдена.") return @@ -882,10 +804,10 @@ async def confirm_delete_subgroup(callback: CallbackQuery, state: FSMContext, se inline_keyboard=[ [ InlineKeyboardButton(text="✅ Удалить", callback_data="confirm_subgroup_deletion"), - InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}") + InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}"), ] ] - ) + ), ) @@ -897,10 +819,7 @@ async def perform_subgroup_deletion(callback: CallbackQuery, state: FSMContext, await session.execute( update(Tariff) - .where( - Tariff.group_code == group_code, - Tariff.subgroup_title == subgroup_title - ) + .where(Tariff.group_code == group_code, Tariff.subgroup_title == subgroup_title) .values(subgroup_title=None) ) await session.commit() @@ -910,9 +829,13 @@ async def perform_subgroup_deletion(callback: CallbackQuery, state: FSMContext, f"✅ Подгруппа {subgroup_title} удалена.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton(text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack())] + [ + InlineKeyboardButton( + text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() + ) + ] ] - ) + ), ) @@ -921,7 +844,7 @@ async def start_edit_subgroup_tariffs(callback: CallbackQuery, state: FSMContext _, subgroup_hash, group_code = callback.data.split("|", 2) subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) - + if not subgroup_title: await callback.message.edit_text("❌ Подгруппа не найдена.") return @@ -930,11 +853,7 @@ async def start_edit_subgroup_tariffs(callback: CallbackQuery, state: FSMContext select(Tariff) .where( Tariff.group_code == group_code, - or_( - Tariff.subgroup_title == subgroup_title, - Tariff.subgroup_title.is_(None), - Tariff.subgroup_title == '' - ) + or_(Tariff.subgroup_title == subgroup_title, Tariff.subgroup_title.is_(None), Tariff.subgroup_title == ""), ) .order_by(Tariff.id) ) @@ -947,12 +866,9 @@ async def start_edit_subgroup_tariffs(callback: CallbackQuery, state: FSMContext "❌ Нет доступных тарифов для редактирования.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton( - text="⬅️ Назад", - callback_data=f"view_subgroup|{subgroup_hash}|{group_code}" - )] + [InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}")] ] - ) + ), ) return @@ -961,30 +877,25 @@ async def start_edit_subgroup_tariffs(callback: CallbackQuery, state: FSMContext subgroup_title=subgroup_title, group_code=group_code, subgroup_hash=subgroup_hash, - selected_tariff_ids=list(subgroup_tariff_ids) + selected_tariff_ids=list(subgroup_tariff_ids), ) builder = InlineKeyboardBuilder() for tariff in all_tariffs_to_show: is_in_subgroup = tariff.id in subgroup_tariff_ids prefix = "✅ " if is_in_subgroup else "" - builder.row( - InlineKeyboardButton( - text=f"{prefix}{tariff.name}", - callback_data=f"edit_sub_toggle|{tariff.id}" - ) - ) + builder.row(InlineKeyboardButton(text=f"{prefix}{tariff.name}", callback_data=f"edit_sub_toggle|{tariff.id}")) builder.row( InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"), - InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}") + InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}"), ) await callback.message.edit_text( f"✏️ Редактирование подгруппы: {subgroup_title}\n\n" "✅ - тарифы в подгруппе\n\n" "Нажмите на тариф, чтобы добавить/убрать его:", - reply_markup=builder.as_markup() + reply_markup=builder.as_markup(), ) @@ -1009,11 +920,7 @@ async def toggle_tariff_in_subgroup_edit(callback: CallbackQuery, state: FSMCont select(Tariff) .where( Tariff.group_code == group_code, - or_( - Tariff.subgroup_title == subgroup_title, - Tariff.subgroup_title.is_(None), - Tariff.subgroup_title == '' - ) + or_(Tariff.subgroup_title == subgroup_title, Tariff.subgroup_title.is_(None), Tariff.subgroup_title == ""), ) .order_by(Tariff.id) ) @@ -1023,16 +930,11 @@ async def toggle_tariff_in_subgroup_edit(callback: CallbackQuery, state: FSMCont for tariff in all_tariffs_to_show: is_selected = tariff.id in selected_ids prefix = "✅ " if is_selected else "" - builder.row( - InlineKeyboardButton( - text=f"{prefix}{tariff.name}", - callback_data=f"edit_sub_toggle|{tariff.id}" - ) - ) + builder.row(InlineKeyboardButton(text=f"{prefix}{tariff.name}", callback_data=f"edit_sub_toggle|{tariff.id}")) builder.row( InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"), - InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}") + InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}"), ) await callback.message.edit_reply_markup(reply_markup=builder.as_markup()) @@ -1047,11 +949,7 @@ async def save_subgroup_tariffs_changes(callback: CallbackQuery, state: FSMConte selected_tariff_ids = set(data.get("selected_tariff_ids", [])) result = await session.execute( - select(Tariff) - .where( - Tariff.group_code == group_code, - Tariff.subgroup_title == subgroup_title - ) + select(Tariff).where(Tariff.group_code == group_code, Tariff.subgroup_title == subgroup_title) ) current_subgroup_tariffs = result.scalars().all() current_tariff_ids = {t.id for t in current_subgroup_tariffs} @@ -1061,9 +959,7 @@ async def save_subgroup_tariffs_changes(callback: CallbackQuery, state: FSMConte if to_remove: await session.execute( - update(Tariff) - .where(Tariff.id.in_(to_remove)) - .values(subgroup_title=None, updated_at=datetime.utcnow()) + update(Tariff).where(Tariff.id.in_(to_remove)).values(subgroup_title=None, updated_at=datetime.utcnow()) ) if to_add: @@ -1081,12 +977,14 @@ async def save_subgroup_tariffs_changes(callback: CallbackQuery, state: FSMConte f"✅ Подгруппа {subgroup_title} была расформирована.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton( - text="⬅️ Назад к группе тарифов", - callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() - )] + [ + InlineKeyboardButton( + text="⬅️ Назад к группе тарифов", + callback_data=AdminTariffCallback(action=f"group|{group_code}").pack(), + ) + ] ] - ) + ), ) return @@ -1113,14 +1011,14 @@ async def save_subgroup_tariffs_changes(callback: CallbackQuery, state: FSMConte changes_text.append("Изменений не было") await callback.message.edit_text( - f"✅ Подгруппа обновлена: {subgroup_title}\n\n" - f"{chr(10).join(changes_text)}", + f"✅ Подгруппа обновлена: {subgroup_title}\n\n{chr(10).join(changes_text)}", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton( - text="⬅️ Назад к подгруппе", - callback_data=f"view_subgroup|{subgroup_hash}|{group_code}" - )] + [ + InlineKeyboardButton( + text="⬅️ Назад к подгруппе", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}" + ) + ] ] - ) + ), ) diff --git a/handlers/admin/users/keyboard.py b/handlers/admin/users/keyboard.py index 96b62229..98040035 100644 --- a/handlers/admin/users/keyboard.py +++ b/handlers/admin/users/keyboard.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from sqlalchemy import select, or_ +from sqlalchemy import or_, select from sqlalchemy.ext.asyncio import AsyncSession from config import HWID_RESET_BUTTON @@ -30,18 +30,14 @@ class AdminUserKeyEditorCallback(CallbackData, prefix="admin_users_key"): edit: bool = False -def build_user_edit_kb( - tg_id: int, key_records: list, is_banned: bool = False -) -> InlineKeyboardMarkup: +def build_user_edit_kb(tg_id: int, key_records: list, is_banned: bool = False) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() current_time = datetime.now(tz=timezone.utc) builder.row( InlineKeyboardButton( text="➕ Создать подписку", - callback_data=AdminUserEditorCallback( - action="users_create_key", tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_create_key", tg_id=tg_id).pack(), ) ) @@ -52,51 +48,39 @@ def build_user_edit_kb( builder.row( InlineKeyboardButton( text=f"🔑 {email} ({'<1' if days < 1 else days} дн.)", - callback_data=AdminUserEditorCallback( - action="users_key_edit", tg_id=tg_id, data=str(email) - ).pack(), + callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=str(email)).pack(), ) ) builder.row( InlineKeyboardButton( text="✉️ Сообщение", - callback_data=AdminUserEditorCallback( - action="users_send_message", tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_send_message", tg_id=tg_id).pack(), ), InlineKeyboardButton( text="💸 Баланс", - callback_data=AdminUserEditorCallback( - action="users_balance_edit", tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_balance_edit", tg_id=tg_id).pack(), ), ) builder.row( InlineKeyboardButton( text="🤝 Выгрузить рефералов", - callback_data=AdminUserEditorCallback( - action="users_export_referrals", tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_export_referrals", tg_id=tg_id).pack(), ) ) builder.row( InlineKeyboardButton( text="♻️ Восстановить триал", - callback_data=AdminUserEditorCallback( - action="users_trial_restore", tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_trial_restore", tg_id=tg_id).pack(), ) ) builder.row( InlineKeyboardButton( text="❌ Удалить", - callback_data=AdminUserEditorCallback( - action="users_delete_user", tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_delete_user", tg_id=tg_id).pack(), ), InlineKeyboardButton( text="✅ Разблокировать" if is_banned else "🚫 Заблокировать", @@ -116,55 +100,41 @@ def build_users_balance_change_kb(tg_id: int) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( text=BACK, - callback_data=AdminUserEditorCallback( - action="users_balance_edit", tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_balance_edit", tg_id=tg_id).pack(), ) return builder.as_markup() -async def build_users_balance_kb( - session: AsyncSession, tg_id: int -) -> InlineKeyboardMarkup: +async def build_users_balance_kb(session: AsyncSession, tg_id: int) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() for amount in [100, 250, 500, 1000]: builder.row( InlineKeyboardButton( text=f"+ {amount}₽", - callback_data=AdminUserEditorCallback( - action="users_balance_add", tg_id=tg_id, data=amount - ).pack(), + callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=amount).pack(), ), InlineKeyboardButton( text=f"- {amount}₽", - callback_data=AdminUserEditorCallback( - action="users_balance_add", tg_id=tg_id, data=-amount - ).pack(), + callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=-amount).pack(), ), ) builder.row( InlineKeyboardButton( text="💵 Добавить", - callback_data=AdminUserEditorCallback( - action="users_balance_add", tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id).pack(), ), InlineKeyboardButton( text="💵 Вычесть", - callback_data=AdminUserEditorCallback( - action="users_balance_take", tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_balance_take", tg_id=tg_id).pack(), ), ) builder.row( InlineKeyboardButton( text="💵 Установить баланс", - callback_data=AdminUserEditorCallback( - action="users_balance_set", tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_balance_set", tg_id=tg_id).pack(), ) ) @@ -177,21 +147,17 @@ def build_users_key_show_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( text=BACK, - callback_data=AdminUserEditorCallback( - action="users_key_edit", tg_id=tg_id, data=email, edit=True - ).pack(), + callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email, edit=True).pack(), ) return builder.as_markup() -async def build_users_key_expiry_kb( - session: AsyncSession, tg_id: int, email: str -) -> InlineKeyboardMarkup: +async def build_users_key_expiry_kb(session: AsyncSession, tg_id: int, email: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() result = await session.execute(select(Key.server_id, Key.tariff_id).where(Key.email == email)) row = result.first() - server_id, tariff_id = (row if row else (None, None)) + _server_id, tariff_id = row if row else (None, None) if tariff_id: result = await session.execute(select(Tariff.group_code).where(Tariff.id == tariff_id)) @@ -199,8 +165,7 @@ async def build_users_key_expiry_kb( if row and row[0]: group_code = row[0] result = await session.execute( - select(Tariff) - .where(Tariff.group_code == group_code, Tariff.is_active.is_(True)) + select(Tariff).where(Tariff.group_code == group_code, Tariff.is_active.is_(True)) ) tariffs = result.scalars().all() unique_durations = set() @@ -228,32 +193,24 @@ async def build_users_key_expiry_kb( builder.row( InlineKeyboardButton( text="⏳ Добавить дни", - callback_data=AdminUserKeyEditorCallback( - action="add", tg_id=tg_id, data=email - ).pack(), + callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email).pack(), ), InlineKeyboardButton( text="⏳ Вычесть дни", - callback_data=AdminUserKeyEditorCallback( - action="take", tg_id=tg_id, data=email - ).pack(), + callback_data=AdminUserKeyEditorCallback(action="take", tg_id=tg_id, data=email).pack(), ), ) builder.row( InlineKeyboardButton( text="⏳ Установить дату истечения", - callback_data=AdminUserKeyEditorCallback( - action="set", tg_id=tg_id, data=email - ).pack(), + callback_data=AdminUserKeyEditorCallback(action="set", tg_id=tg_id, data=email).pack(), ) ) builder.row( InlineKeyboardButton( text=BACK, - callback_data=AdminUserEditorCallback( - action="users_key_edit", tg_id=tg_id, data=email - ).pack(), + callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(), ) ) @@ -264,9 +221,7 @@ def build_user_delete_kb(tg_id: int): builder = InlineKeyboardBuilder() builder.button( text="❌ Да, удалить!", - callback_data=AdminUserEditorCallback( - action="users_delete_user_confirm", tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_delete_user_confirm", tg_id=tg_id).pack(), ) builder.row(build_editor_back_btn(tg_id, True)) builder.adjust(1) @@ -277,9 +232,7 @@ def build_user_key_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( text=BACK, - callback_data=AdminUserEditorCallback( - action="users_key_edit", tg_id=tg_id, data=email - ).pack(), + callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(), ) builder.adjust(1) return builder.as_markup() @@ -296,27 +249,19 @@ def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup: ) builder.button( text="🔄 Перевыпустить", - callback_data=AdminUserEditorCallback( - action="users_update_key", data=email, tg_id=key_details["tg_id"] - ).pack(), + callback_data=AdminUserEditorCallback(action="users_update_key", data=email, tg_id=key_details["tg_id"]).pack(), ) builder.button( text="📦 Тариф", - callback_data=AdminUserEditorCallback( - action="users_renew", data=email, tg_id=key_details["tg_id"] - ).pack(), + callback_data=AdminUserEditorCallback(action="users_renew", data=email, tg_id=key_details["tg_id"]).pack(), ) builder.button( text="❌ Удалить", - callback_data=AdminUserEditorCallback( - action="users_delete_key", data=email, tg_id=key_details["tg_id"] - ).pack(), + callback_data=AdminUserEditorCallback(action="users_delete_key", data=email, tg_id=key_details["tg_id"]).pack(), ) builder.button( text="📊 Трафик", - callback_data=AdminUserEditorCallback( - action="users_traffic", data=email, tg_id=key_details["tg_id"] - ).pack(), + callback_data=AdminUserEditorCallback(action="users_traffic", data=email, tg_id=key_details["tg_id"]).pack(), ) builder.button( text="♻️ Сбросить трафик", @@ -341,15 +286,11 @@ def build_hwid_menu_kb(email: str, tg_id: int) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( text="♻️ Сбросить HWID", - callback_data=AdminUserEditorCallback( - action="users_hwid_reset", data=email, tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_hwid_reset", data=email, tg_id=tg_id).pack(), ) builder.button( text="🔙 Назад", - callback_data=AdminUserEditorCallback( - action="users_key_edit", data=email, tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id).pack(), ) builder.adjust(1) return builder.as_markup() @@ -360,9 +301,7 @@ def build_key_delete_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: builder.row( InlineKeyboardButton( text="✅ Да, удалить", - callback_data=AdminUserEditorCallback( - action="users_delete_key_confirm", data=email, tg_id=tg_id - ).pack(), + callback_data=AdminUserEditorCallback(action="users_delete_key_confirm", data=email, tg_id=tg_id).pack(), ) ) builder.row(build_editor_back_btn(tg_id)) @@ -374,9 +313,7 @@ def build_editor_kb(tg_id: int, edit: bool = False) -> InlineKeyboardMarkup: return build_editor_singleton_kb(BACK, tg_id, edit) -def build_editor_singleton_kb( - text: str, tg_id: int, edit: bool = False -) -> InlineKeyboardMarkup: +def build_editor_singleton_kb(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.row(build_editor_btn(text, tg_id, edit)) return builder.as_markup() @@ -389,28 +326,19 @@ def build_editor_back_btn(tg_id: int, edit: bool = False) -> InlineKeyboardButto def build_editor_btn(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardButton: return InlineKeyboardButton( text=text, - callback_data=AdminUserEditorCallback( - action="users_editor", tg_id=tg_id, edit=edit - ).pack(), + callback_data=AdminUserEditorCallback(action="users_editor", tg_id=tg_id, edit=edit).pack(), ) -async def build_cluster_selection_kb( - session, tg_id: int, email: str, action: str -) -> InlineKeyboardMarkup: +async def build_cluster_selection_kb(session, tg_id: int, email: str, action: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() clusters = await get_clusters(session) for cluster_id in clusters: - builder.button( - text=cluster_id, callback_data=f"{action}|{tg_id}|{email}|{cluster_id}" - ) + builder.button(text=cluster_id, callback_data=f"{action}|{tg_id}|{email}|{cluster_id}") builder.button( - text=BACK, - callback_data=AdminUserEditorCallback( - action="users_key_edit", tg_id=tg_id, data=email - ).pack() + text=BACK, callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack() ) builder.adjust(1) return builder.as_markup() diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index 36fca181..e1240e11 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -1,9 +1,11 @@ import asyncio import uuid + from datetime import datetime, timedelta, timezone from typing import Any import pytz + from aiogram import F, Router, types from aiogram.exceptions import TelegramBadRequest from aiogram.fsm.context import FSMContext @@ -53,6 +55,7 @@ from .keyboard import ( AdminUserEditorCallback, AdminUserKeyEditorCallback, build_cluster_selection_kb, + build_editor_btn, build_editor_kb, build_hwid_menu_kb, build_key_delete_kb, @@ -63,9 +66,9 @@ from .keyboard import ( build_users_balance_kb, build_users_key_expiry_kb, build_users_key_show_kb, - build_editor_btn ) + MOSCOW_TZ = pytz.timezone("Europe/Moscow") router = Router() @@ -93,9 +96,7 @@ class BanUserStates(StatesGroup): waiting_for_ban_duration = State() -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_hwid_menu"), IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_hwid_menu"), IsAdminFilter()) async def handle_hwid_menu( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, @@ -106,9 +107,7 @@ async def handle_hwid_menu( client_id = await get_client_id_by_email(session, email) if not client_id: - await callback_query.message.edit_text( - "🚫 Не удалось найти client_id по email." - ) + await callback_query.message.edit_text("🚫 Не удалось найти client_id по email.") return servers = await get_servers(session=session) @@ -123,8 +122,7 @@ async def handle_hwid_menu( if not remna_server: await callback_query.message.edit_text( - "🚫 Нет доступного сервера Remnawave.", - reply_markup=build_editor_kb(tg_id) + "🚫 Нет доступного сервера Remnawave.", reply_markup=build_editor_kb(tg_id) ) return @@ -151,14 +149,10 @@ async def handle_hwid_menu( f"└ 🔄 Обновлено: {updated}\n\n" ) - await callback_query.message.edit_text( - text, reply_markup=build_hwid_menu_kb(email, tg_id) - ) + await callback_query.message.edit_text(text, reply_markup=build_hwid_menu_kb(email, tg_id)) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_hwid_reset"), IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_hwid_reset"), IsAdminFilter()) async def handle_hwid_reset( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, @@ -169,9 +163,7 @@ async def handle_hwid_reset( client_id = await get_client_id_by_email(session, email) if not client_id: - await callback_query.message.edit_text( - "🚫 Не удалось найти client_id по email." - ) + await callback_query.message.edit_text("🚫 Не удалось найти client_id по email.") return servers = await get_servers(session=session) @@ -186,8 +178,7 @@ async def handle_hwid_reset( if not remna_server: await callback_query.message.edit_text( - "🚫 Нет доступного сервера Remnawave.", - reply_markup=build_editor_kb(tg_id) + "🚫 Нет доступного сервера Remnawave.", reply_markup=build_editor_kb(tg_id) ) return @@ -229,9 +220,7 @@ async def handle_search_user(callback_query: CallbackQuery, state: FSMContext): ) await state.set_state(UserEditorState.waiting_for_user_data) - await callback_query.message.edit_text( - text=text, reply_markup=build_admin_back_kb() - ) + await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb()) @router.callback_query( @@ -240,9 +229,7 @@ async def handle_search_user(callback_query: CallbackQuery, state: FSMContext): ) async def handle_search_key(callback_query: CallbackQuery, state: FSMContext): await state.set_state(UserEditorState.waiting_for_key_name) - await callback_query.message.edit_text( - text="🔑 Введите имя ключа для поиска:", reply_markup=build_admin_back_kb() - ) + await callback_query.message.edit_text(text="🔑 Введите имя ключа для поиска:", reply_markup=build_admin_back_kb()) @router.message(UserEditorState.waiting_for_key_name, IsAdminFilter()) @@ -250,27 +237,21 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An kb = build_admin_back_kb() if not message.text: - await message.answer( - text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb - ) + await message.answer(text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb) return key_name = sanitize_key_name(message.text) key_details = await get_key_details(session, key_name) if not key_details: - await message.answer( - text="🚫 Пользователь с указанным именем ключа не найден.", reply_markup=kb - ) + await message.answer(text="🚫 Пользователь с указанным именем ключа не найден.", reply_markup=kb) return await process_user_search(message, state, session, key_details["tg_id"]) @router.message(UserEditorState.waiting_for_user_data, IsAdminFilter()) -async def handle_user_data_input( - message: Message, state: FSMContext, session: AsyncSession -): +async def handle_user_data_input(message: Message, state: FSMContext, session: AsyncSession): kb = build_admin_back_kb() if message.forward_from: @@ -279,9 +260,7 @@ async def handle_user_data_input( return if not message.text: - await message.answer( - text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb - ) + await message.answer(text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb) return if message.text.isdigit(): @@ -341,8 +320,7 @@ async def handle_message_text_input(message: Message, state: FSMContext): max_len = 1024 if photo else 4096 if len(text_message) > max_len: await message.answer( - f"⚠️ Сообщение слишком длинное.\n" - f"Максимум: {max_len} символов, сейчас: {len(text_message)}.", + f"⚠️ Сообщение слишком длинное.\nМаксимум: {max_len} символов, сейчас: {len(text_message)}.", reply_markup=build_editor_kb(tg_id), ) await state.clear() @@ -358,12 +336,14 @@ async def handle_message_text_input(message: Message, state: FSMContext): await message.answer( "👀 Это предпросмотр сообщения. Отправить?", - reply_markup=InlineKeyboardMarkup(inline_keyboard=[ - [ - InlineKeyboardButton(text="📤 Отправить", callback_data="send_user_message"), - InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_user_message"), + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="📤 Отправить", callback_data="send_user_message"), + InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_user_message"), + ] ] - ]), + ), ) @@ -402,9 +382,7 @@ async def handle_send_user_message(callback_query: CallbackQuery, state: FSMCont async def handle_cancel_user_message(callback_query: CallbackQuery, state: FSMContext): data = await state.get_data() tg_id = data.get("tg_id") - await callback_query.message.edit_text( - text="🚫 Отправка сообщения отменена.", reply_markup=build_editor_kb(tg_id) - ) + await callback_query.message.edit_text(text="🚫 Отправка сообщения отменена.", reply_markup=build_editor_kb(tg_id)) await state.clear() @@ -420,14 +398,10 @@ async def handle_trial_restore( tg_id = callback_data.tg_id await update_trial(session, tg_id, 0) - await callback_query.message.edit_text( - text="✅ Триал успешно восстановлен!", reply_markup=build_editor_kb(tg_id) - ) + await callback_query.message.edit_text(text="✅ Триал успешно восстановлен!", reply_markup=build_editor_kb(tg_id)) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_edit"), IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_edit"), IsAdminFilter()) async def handle_balance_change( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, @@ -436,9 +410,7 @@ async def handle_balance_change( tg_id = callback_data.tg_id stmt = ( - select( - Payment.amount, Payment.payment_system, Payment.status, Payment.created_at - ) + select(Payment.amount, Payment.payment_system, Payment.status, Payment.created_at) .where(Payment.tg_id == tg_id) .order_by(Payment.created_at.desc()) .limit(5) @@ -467,14 +439,10 @@ async def handle_balance_change( else: text += "\n 🚫 Отсутствуют" - await callback_query.message.edit_text( - text=text, reply_markup=await build_users_balance_kb(session, tg_id) - ) + await callback_query.message.edit_text(text=text, reply_markup=await build_users_balance_kb(session, tg_id)) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_add"), IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_add"), IsAdminFilter()) async def handle_balance_add( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, @@ -505,9 +473,7 @@ async def handle_balance_add( ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_take"), IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_take"), IsAdminFilter()) async def handle_balance_take( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, @@ -524,9 +490,7 @@ async def handle_balance_take( ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_set"), IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_set"), IsAdminFilter()) async def handle_balance_set( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, @@ -574,9 +538,7 @@ async def handle_balance_input(message: Message, state: FSMContext, session: Any await message.answer(text=text, reply_markup=build_users_balance_change_kb(tg_id)) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_key_edit"), IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_key_edit"), IsAdminFilter()) async def handle_key_edit( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback | AdminUserKeyEditorCallback, @@ -628,15 +590,11 @@ async def handle_key_edit( text += "" if not update or not callback_data.edit: - await callback_query.message.edit_text( - text=text, reply_markup=build_key_edit_kb(key_details, email) - ) + await callback_query.message.edit_text(text=text, reply_markup=build_key_edit_kb(key_details, email)) else: await callback_query.message.edit_text( text=text, - reply_markup=await build_users_key_expiry_kb( - session, callback_data.tg_id, email - ), + reply_markup=await build_users_key_expiry_kb(session, callback_data.tg_id, email), ) @@ -651,9 +609,7 @@ async def handle_back_to_key_menu( tg_id = data["tg_id"] await state.clear() - callback_data = AdminUserEditorCallback( - action="users_key_edit", data=email, tg_id=tg_id - ) + callback_data = AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id) await handle_key_edit( callback_query=callback_query, callback_data=callback_data, @@ -662,9 +618,7 @@ async def handle_back_to_key_menu( ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_renew"), IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_renew"), IsAdminFilter()) async def handle_user_choose_tariff_group( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, @@ -703,9 +657,7 @@ async def handle_user_choose_tariff( await state.set_state(RenewTariffState.selecting_tariff) result = await session.execute( - select(Tariff) - .where(Tariff.group_code == group_code, Tariff.is_active == True) - .order_by(Tariff.id) + select(Tariff).where(Tariff.group_code == group_code, Tariff.is_active is True).order_by(Tariff.id) ) tariffs = result.scalars().all() @@ -715,10 +667,7 @@ async def handle_user_choose_tariff( builder = InlineKeyboardBuilder() for tariff in tariffs: - builder.button( - text=f"{tariff.name} – {int(tariff.price_rub)}₽", - callback_data=f"confirm:{tariff.id}" - ) + builder.button(text=f"{tariff.name} – {int(tariff.price_rub)}₽", callback_data=f"confirm:{tariff.id}") builder.button(text="🔙 Назад", callback_data="back:group") builder.adjust(1) @@ -739,18 +688,12 @@ async def handle_user_renew_confirm( email = data["email"] tg_id = data["tg_id"] - stmt = ( - update(Key) - .where(Key.tg_id == tg_id, Key.email == email) - .values(tariff_id=tariff_id) - ) + stmt = update(Key).where(Key.tg_id == tg_id, Key.email == email).values(tariff_id=tariff_id) await session.execute(stmt) await session.commit() await state.clear() - callback_data = AdminUserEditorCallback( - action="users_key_edit", data=email, tg_id=tg_id - ) + callback_data = AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id) await handle_key_edit( callback_query=callback_query, @@ -766,7 +709,7 @@ async def handle_back_to_group( state: FSMContext, session: AsyncSession, ): - data = await state.get_data() + await state.get_data() result = await session.execute(select(Tariff.group_code).distinct()) groups = [row[0] for row in result.fetchall()] @@ -784,9 +727,7 @@ async def handle_back_to_group( await state.set_state(RenewTariffState.selecting_group) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_expiry_edit"), IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_expiry_edit"), IsAdminFilter()) async def handle_change_expiry( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, @@ -795,14 +736,10 @@ async def handle_change_expiry( tg_id = callback_data.tg_id email = callback_data.data - await callback_query.message.edit_reply_markup( - reply_markup=await build_users_key_expiry_kb(session, tg_id, email) - ) + await callback_query.message.edit_reply_markup(reply_markup=await build_users_key_expiry_kb(session, tg_id, email)) -@router.callback_query( - AdminUserKeyEditorCallback.filter(F.action == "add"), IsAdminFilter() -) +@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "add"), IsAdminFilter()) async def handle_expiry_add( callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, @@ -823,9 +760,7 @@ async def handle_expiry_add( return if days: - await change_expiry_time( - key_details["expiry_time"] + days * 24 * 3600 * 1000, email, session - ) + await change_expiry_time(key_details["expiry_time"] + days * 24 * 3600 * 1000, email, session) await handle_key_edit(callback_query, callback_data, session, True) return @@ -838,9 +773,7 @@ async def handle_expiry_add( ) -@router.callback_query( - AdminUserKeyEditorCallback.filter(F.action == "take"), IsAdminFilter() -) +@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "take"), IsAdminFilter()) async def handle_expiry_take( callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, @@ -858,9 +791,7 @@ async def handle_expiry_take( ) -@router.callback_query( - AdminUserKeyEditorCallback.filter(F.action == "set"), IsAdminFilter() -) +@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "set"), IsAdminFilter()) async def handle_expiry_set( callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, @@ -888,9 +819,7 @@ async def handle_expiry_set( f"\n\n📄 Текущая дата: {datetime.fromtimestamp(key_details['expiry_time'] / 1000).strftime('%Y-%m-%d %H:%M')}" ) - await callback_query.message.edit_text( - text=text, reply_markup=build_users_key_show_kb(tg_id, email) - ) + await callback_query.message.edit_text(text=text, reply_markup=build_users_key_show_kb(tg_id, email)) @router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter()) @@ -917,9 +846,7 @@ async def handle_expiry_time_input(message: Message, state: FSMContext, session: return try: - current_expiry_time = datetime.fromtimestamp( - key_details["expiry_time"] / 1000, tz=MOSCOW_TZ - ) + current_expiry_time = datetime.fromtimestamp(key_details["expiry_time"] / 1000, tz=MOSCOW_TZ) if op_type == "add": days = int(message.text) @@ -947,27 +874,19 @@ async def handle_expiry_time_input(message: Message, state: FSMContext, session: await message.answer(text=text, reply_markup=build_users_key_show_kb(tg_id, email)) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_update_key"), IsAdminFilter() -) -async def handle_update_key( - callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any -): +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_update_key"), IsAdminFilter()) +async def handle_update_key(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any): tg_id = callback_data.tg_id email = callback_data.data await callback_query.message.edit_text( text=f"📡 Выберите кластер, на котором пересоздать ключ {email}:", - reply_markup=await build_cluster_selection_kb( - session, tg_id, email, action="confirm_admin_key_reissue" - ), + reply_markup=await build_cluster_selection_kb(session, tg_id, email, action="confirm_admin_key_reissue"), ) @router.callback_query(F.data.startswith("confirm_admin_key_reissue|"), IsAdminFilter()) -async def confirm_admin_key_reissue( - callback_query: CallbackQuery, session: Any, state: FSMContext -): +async def confirm_admin_key_reissue(callback_query: CallbackQuery, session: Any, state: FSMContext): _, tg_id, email, cluster_id = callback_query.data.split("|") tg_id = int(tg_id) @@ -980,16 +899,13 @@ async def confirm_admin_key_reissue( builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( - text="🔗 Привязать тариф", - callback_data=AdminPanelCallback(action="clusters").pack() + text="🔗 Привязать тариф", callback_data=AdminPanelCallback(action="clusters").pack() ) ) builder.row( InlineKeyboardButton( text="🔙 Назад", - callback_data=AdminUserEditorCallback( - action="users_key_edit", tg_id=tg_id, data=email - ).pack() + callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(), ) ) await callback_query.message.edit_text( @@ -998,7 +914,7 @@ async def confirm_admin_key_reissue( f"🌐 Кластер:
{cluster_id}\n"
f"⚠️ Статус: Нет привязанного тарифа\n"
f"💡 Привяжите тариф к кластеру",
- reply_markup=builder.as_markup()
+ reply_markup=builder.as_markup(),
)
return
@@ -1011,20 +927,14 @@ async def confirm_admin_key_reissue(
text=country,
callback_data=f"admin_reissue_country|{tg_id}|{email}|{country}",
)
- builder.row(
- InlineKeyboardButton(
- text="Назад", callback_data=f"users_key_edit|{email}"
- )
- )
+ builder.row(InlineKeyboardButton(text="Назад", callback_data=f"users_key_edit|{email}"))
await callback_query.message.edit_text(
"🌍 Выберите сервер (страну) для пересоздания подписки:",
reply_markup=builder.as_markup(),
)
return
- result = await session.execute(
- select(Key.remnawave_link).where(Key.email == email)
- )
+ result = await session.execute(select(Key.remnawave_link).where(Key.email == email))
remnawave_link = result.scalar_one_or_none()
await update_subscription(tg_id, email, session, cluster_override=cluster_id, remnawave_link=remnawave_link)
@@ -1049,23 +959,20 @@ async def admin_reissue_country(callback_query: CallbackQuery, session: AsyncSes
try:
data = await state.get_data()
cluster_id = data.get("cluster_id")
-
+
if cluster_id:
tariffs = await get_tariffs_for_cluster(session, cluster_id)
if not tariffs:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
- text="🔗 Привязать тариф",
- callback_data=AdminPanelCallback(action="clusters").pack()
+ text="🔗 Привязать тариф", callback_data=AdminPanelCallback(action="clusters").pack()
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
- callback_data=AdminUserEditorCallback(
- action="users_key_edit", tg_id=tg_id, data=email
- ).pack()
+ callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(),
)
)
await callback_query.message.edit_text(
@@ -1074,14 +981,12 @@ async def admin_reissue_country(callback_query: CallbackQuery, session: AsyncSes
f"🌐 Кластер: {cluster_id}\n"
f"⚠️ Статус: Нет привязанного тарифа\n"
f"💡 Привяжите тариф к кластеру",
- reply_markup=builder.as_markup()
+ reply_markup=builder.as_markup(),
)
return
- result = await session.execute(
- select(Key.remnawave_link, Key.tariff_id).where(Key.email == email)
- )
- remnawave_link, tariff_id = result.one_or_none() or (None, None)
+ result = await session.execute(select(Key.remnawave_link, Key.tariff_id).where(Key.email == email))
+ remnawave_link, _tariff_id = result.one_or_none() or (None, None)
await update_subscription(
tg_id=tg_id,
@@ -1102,12 +1007,8 @@ async def admin_reissue_country(callback_query: CallbackQuery, session: AsyncSes
await callback_query.message.answer(f"❗ Ошибка: {e}")
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_delete_key"), IsAdminFilter()
-)
-async def handle_delete_key(
- callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
-):
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_key"), IsAdminFilter())
+async def handle_delete_key(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
email = callback_data.data
result = await session.execute(select(Key.client_id).where(Key.email == email))
@@ -1148,29 +1049,19 @@ async def handle_delete_key_confirm(
tasks = []
for cluster_name, cluster_servers in clusters.items():
for _ in cluster_servers:
- tasks.append(
- delete_key_from_cluster(cluster_name, email, client_id, session)
- )
+ tasks.append(delete_key_from_cluster(cluster_name, email, client_id, session))
await asyncio.gather(*tasks, return_exceptions=True)
await delete_key_from_servers()
await delete_key(session, client_id)
- await callback_query.message.edit_text(
- text="✅ Ключ успешно удален.", reply_markup=kb
- )
+ await callback_query.message.edit_text(text="✅ Ключ успешно удален.", reply_markup=kb)
else:
- await callback_query.message.edit_text(
- text="🚫 Ключ не найден или уже удален.", reply_markup=kb
- )
+ await callback_query.message.edit_text(text="🚫 Ключ не найден или уже удален.", reply_markup=kb)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_delete_user"), IsAdminFilter()
-)
-async def handle_delete_user(
- callback_query: CallbackQuery, callback_data: AdminUserEditorCallback
-):
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_user"), IsAdminFilter())
+async def handle_delete_user(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback):
tg_id = callback_data.tg_id
await callback_query.message.edit_text(
text=f"❗️ Вы уверены, что хотите удалить пользователя с ID {tg_id}?",
@@ -1189,9 +1080,7 @@ async def handle_delete_user_confirm(
):
tg_id = callback_data.tg_id
- result = await session.execute(
- select(Key.email, Key.client_id).where(Key.tg_id == tg_id)
- )
+ result = await session.execute(select(Key.email, Key.client_id).where(Key.tg_id == tg_id))
key_records = result.all()
async def delete_keys_from_servers():
@@ -1200,14 +1089,10 @@ async def handle_delete_user_confirm(
servers = await get_servers(session=session)
for email, client_id in key_records:
for cluster_id, _cluster in servers.items():
- tasks.append(
- delete_key_from_cluster(cluster_id, email, client_id, session)
- )
+ tasks.append(delete_key_from_cluster(cluster_id, email, client_id, session))
await asyncio.gather(*tasks, return_exceptions=True)
except Exception as e:
- logger.error(
- f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}"
- )
+ logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}")
await delete_keys_from_servers()
@@ -1219,9 +1104,7 @@ async def handle_delete_user_confirm(
reply_markup=build_editor_kb(callback_data.tg_id),
)
except Exception as e:
- logger.error(
- f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}"
- )
+ logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}")
await callback_query.message.edit_text(
text=f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова."
)
@@ -1236,9 +1119,7 @@ async def process_user_search(
) -> None:
await state.clear()
- stmt_user = select(
- User.username, User.balance, User.created_at, User.updated_at
- ).where(User.tg_id == tg_id)
+ stmt_user = select(User.username, User.balance, User.created_at, User.updated_at).where(User.tg_id == tg_id)
result_user = await session.execute(stmt_user)
user_data = result_user.first()
@@ -1254,19 +1135,11 @@ async def process_user_search(
created_at_str = created_at.replace(tzinfo=pytz.UTC).astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y")
updated_at_str = updated_at.replace(tzinfo=pytz.UTC).astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y")
- stmt_ref_count = (
- select(func.count())
- .select_from(Referral)
- .where(Referral.referrer_tg_id == tg_id)
- )
+ stmt_ref_count = select(func.count()).select_from(Referral).where(Referral.referrer_tg_id == tg_id)
result_ref = await session.execute(stmt_ref_count)
referral_count = result_ref.scalar_one()
- stmt_ref_by = (
- select(Referral.referrer_tg_id)
- .where(Referral.referred_tg_id == tg_id)
- .limit(1)
- )
+ stmt_ref_by = select(Referral.referrer_tg_id).where(Referral.referred_tg_id == tg_id).limit(1)
result_ref_by = await session.execute(stmt_ref_by)
referrer_tg_id = result_ref_by.scalar_one_or_none()
@@ -1285,10 +1158,7 @@ async def process_user_search(
stmt_ban = (
select(1)
- .where(
- (ManualBan.tg_id == tg_id)
- & (or_(ManualBan.until.is_(None), ManualBan.until > func.now()))
- )
+ .where((ManualBan.tg_id == tg_id) & (or_(ManualBan.until.is_(None), ManualBan.until > func.now())))
.limit(1)
)
result_ban = await session.execute(stmt_ban)
@@ -1319,14 +1189,12 @@ async def process_user_search(
await message.answer(text=text, reply_markup=kb)
-async def change_expiry_time(
- expiry_time: int, email: str, session: AsyncSession
-) -> Exception | None:
+async def change_expiry_time(expiry_time: int, email: str, session: AsyncSession) -> Exception | None:
result = await session.execute(select(Key.client_id, Key.tariff_id, Key.server_id).where(Key.email == email))
row = result.first()
if not row:
return ValueError(f"User with email {email} was not found")
-
+
client_id, tariff_id, server_id = row
if server_id is None:
return ValueError(f"Key with client_id {client_id} was not found")
@@ -1335,8 +1203,7 @@ async def change_expiry_time(
device_limit = None
if tariff_id:
result = await session.execute(
- select(Tariff.traffic_limit, Tariff.device_limit)
- .where(Tariff.id == tariff_id, Tariff.is_active.is_(True))
+ select(Tariff.traffic_limit, Tariff.device_limit).where(Tariff.id == tariff_id, Tariff.is_active.is_(True))
)
tariff = result.first()
if tariff:
@@ -1353,7 +1220,7 @@ async def change_expiry_time(
if any(s.get("server_name") == server_id for s in cluster_servers):
target_cluster = cluster_name
break
-
+
if not target_cluster:
return ValueError(f"No suitable cluster found for server {server_id}")
@@ -1367,14 +1234,12 @@ async def change_expiry_time(
hwid_device_limit=device_limit,
reset_traffic=False,
)
-
+
await update_key_expiry(session, client_id, expiry_time)
return None
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_traffic"), IsAdminFilter()
-)
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_traffic"), IsAdminFilter())
async def handle_user_traffic(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
@@ -1387,16 +1252,12 @@ async def handle_user_traffic(
tg_id = callback_data.tg_id
email = callback_data.data
- await callback_query.message.edit_text(
- "⏳ Получаем данные о трафике, пожалуйста, подождите..."
- )
+ await callback_query.message.edit_text("⏳ Получаем данные о трафике, пожалуйста, подождите...")
traffic_data = await get_user_traffic(session, tg_id, email)
if traffic_data["status"] == "error":
- await callback_query.message.edit_text(
- traffic_data["message"], reply_markup=build_editor_kb(tg_id, True)
- )
+ await callback_query.message.edit_text(traffic_data["message"], reply_markup=build_editor_kb(tg_id, True))
return
total_traffic = 0
@@ -1412,14 +1273,10 @@ async def handle_user_traffic(
result_text += f"\n🔢 Общий трафик: {total_traffic:.2f} ГБ"
- await callback_query.message.edit_text(
- result_text, reply_markup=build_editor_kb(tg_id, True)
- )
+ await callback_query.message.edit_text(result_text, reply_markup=build_editor_kb(tg_id, True))
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "restore_trials"), IsAdminFilter()
-)
+@router.callback_query(AdminPanelCallback.filter(F.action == "restore_trials"), IsAdminFilter())
async def confirm_restore_trials(callback_query: types.CallbackQuery):
"""
Меню подтверждения перед восстановлением пробников.
@@ -1438,21 +1295,10 @@ async def confirm_restore_trials(callback_query: types.CallbackQuery):
)
-@router.callback_query(
- AdminPanelCallback.filter(F.action == "confirm_restore_trials"), IsAdminFilter()
-)
+@router.callback_query(AdminPanelCallback.filter(F.action == "confirm_restore_trials"), IsAdminFilter())
async def restore_trials(callback_query: types.CallbackQuery, session: AsyncSession):
- active_keys_subq = (
- select(Key.tg_id)
- .where(Key.expiry_time > func.extract("epoch", func.now()) * 1000)
- .subquery()
- )
- stmt = (
- update(User)
- .where(~User.tg_id.in_(select(active_keys_subq.c.tg_id)))
- .where(User.trial != 0)
- .values(trial=0)
- )
+ active_keys_subq = select(Key.tg_id).where(Key.expiry_time > func.extract("epoch", func.now()) * 1000).subquery()
+ stmt = update(User).where(~User.tg_id.in_(select(active_keys_subq.c.tg_id))).where(User.trial != 0).values(trial=0)
await session.execute(stmt)
await session.commit()
@@ -1494,9 +1340,7 @@ async def handle_users_export_referrals(
)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_create_key"), IsAdminFilter()
-)
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_create_key"), IsAdminFilter())
async def handle_create_key_start(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
@@ -1557,18 +1401,14 @@ async def handle_create_key_start(
@router.callback_query(UserEditorState.selecting_country, IsAdminFilter())
-async def handle_create_key_country(
- callback_query: CallbackQuery, state: FSMContext, session
-):
+async def handle_create_key_country(callback_query: CallbackQuery, state: FSMContext, session):
country = callback_query.data
await state.update_data(country=country)
await state.set_state(UserEditorState.selecting_duration)
builder = InlineKeyboardBuilder()
- result = await session.execute(
- select(Server.cluster_name).where(Server.server_name == country)
- )
+ result = await session.execute(select(Server.cluster_name).where(Server.server_name == country))
row = result.mappings().first()
if not row:
@@ -1583,10 +1423,7 @@ async def handle_create_key_country(
for tariff in tariffs:
if tariff["duration_days"] < 1:
continue
- builder.button(
- text=f"{tariff['name']} — {tariff['price_rub']}₽",
- callback_data=f"tariff_{tariff['id']}"
- )
+ builder.button(text=f"{tariff['name']} — {tariff['price_rub']}₽", callback_data=f"tariff_{tariff['id']}")
builder.adjust(1)
builder.row(build_admin_back_btn())
@@ -1598,9 +1435,7 @@ async def handle_create_key_country(
@router.callback_query(UserEditorState.selecting_cluster, IsAdminFilter())
-async def handle_create_key_cluster(
- callback_query: CallbackQuery, state: FSMContext, session
-):
+async def handle_create_key_cluster(callback_query: CallbackQuery, state: FSMContext, session):
cluster_name = callback_query.data
data = await state.get_data()
@@ -1619,10 +1454,7 @@ async def handle_create_key_cluster(
for tariff in tariffs:
if tariff["duration_days"] < 1:
continue
- builder.button(
- text=f"{tariff['name']} — {tariff['price_rub']}₽",
- callback_data=f"tariff_{tariff['id']}"
- )
+ builder.button(text=f"{tariff['name']} — {tariff['price_rub']}₽", callback_data=f"tariff_{tariff['id']}")
builder.adjust(1)
builder.row(build_admin_back_btn())
@@ -1634,9 +1466,7 @@ async def handle_create_key_cluster(
@router.callback_query(UserEditorState.selecting_duration, IsAdminFilter())
-async def handle_create_key_duration(
- callback_query: CallbackQuery, state: FSMContext, session
-):
+async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMContext, session):
data = await state.get_data()
tg_id = data.get("tg_id", callback_query.from_user.id)
@@ -1692,9 +1522,7 @@ async def handle_create_key_duration(
)
else:
- await callback_query.message.edit_text(
- "❌ Не удалось определить источник — страна или кластер."
- )
+ await callback_query.message.edit_text("❌ Не удалось определить источник — страна или кластер.")
except Exception as e:
logger.error(f"[CreateKey] Ошибка при создании ключа: {e}")
@@ -1704,9 +1532,7 @@ async def handle_create_key_duration(
)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_reset_traffic"), IsAdminFilter()
-)
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_reset_traffic"), IsAdminFilter())
async def handle_reset_traffic(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
@@ -1715,16 +1541,12 @@ async def handle_reset_traffic(
tg_id = callback_data.tg_id
email = callback_data.data
- stmt = select(Key.server_id, Key.client_id).where(
- (Key.tg_id == tg_id) & (Key.email == email)
- )
+ stmt = select(Key.server_id, Key.client_id).where((Key.tg_id == tg_id) & (Key.email == email))
result = await session.execute(stmt)
record = result.first()
if not record:
- await callback_query.message.edit_text(
- "❌ Ключ не найден в базе данных.", reply_markup=build_editor_kb(tg_id)
- )
+ await callback_query.message.edit_text("❌ Ключ не найден в базе данных.", reply_markup=build_editor_kb(tg_id))
return
cluster_id, _client_id = record
@@ -1743,12 +1565,8 @@ async def handle_reset_traffic(
)
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_ban"), IsAdminFilter()
-)
-async def handle_user_ban(
- callback: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext
-):
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_ban"), IsAdminFilter())
+async def handle_user_ban(callback: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext):
await state.set_state(BanUserStates.waiting_for_reason)
await state.update_data(tg_id=callback_data.tg_id)
@@ -1779,9 +1597,7 @@ async def handle_ban_reason_input(message: Message, state: FSMContext):
@router.message(BanUserStates.waiting_for_ban_duration, IsAdminFilter())
-async def handle_ban_duration_input(
- message: Message, state: FSMContext, session: AsyncSession
-):
+async def handle_ban_duration_input(message: Message, state: FSMContext, session: AsyncSession):
user_data = await state.get_data()
tg_id = user_data.get("tg_id")
reason = user_data.get("reason")
@@ -1832,29 +1648,23 @@ async def handle_ban_duration_input(
await state.clear()
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_unban"), IsAdminFilter()
-)
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_unban"), IsAdminFilter())
async def handle_user_unban(
callback: CallbackQuery,
callback_data: AdminUserEditorCallback,
session: AsyncSession,
):
- await session.execute(
- delete(ManualBan).where(ManualBan.tg_id == callback_data.tg_id)
- )
+ await session.execute(delete(ManualBan).where(ManualBan.tg_id == callback_data.tg_id))
await session.commit()
- text = f"✅ Пользователь {callback_data.tg_id} разблокирован. Нажмите кнопку ниже для возврата в профиль."
-
- await callback.message.edit_text(
- text=text, reply_markup=build_editor_kb(callback_data.tg_id, edit=True)
+ text = (
+ f"✅ Пользователь {callback_data.tg_id} разблокирован. Нажмите кнопку ниже для возврата в профиль."
)
+ await callback.message.edit_text(text=text, reply_markup=build_editor_kb(callback_data.tg_id, edit=True))
-@router.callback_query(
- AdminUserEditorCallback.filter(F.action == "users_editor"), IsAdminFilter()
-)
+
+@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_editor"), IsAdminFilter())
async def handle_users_editor(
callback: CallbackQuery,
callback_data: AdminUserEditorCallback,
diff --git a/handlers/captcha.py b/handlers/captcha.py
index 01459d6a..880bc76b 100644
--- a/handlers/captcha.py
+++ b/handlers/captcha.py
@@ -1,5 +1,6 @@
import random
import secrets
+
from typing import Any
from aiogram import F, Router
@@ -12,14 +13,13 @@ from logger import logger
from .utils import edit_or_send_message
+
router = Router()
async def generate_captcha(message: Message, state: FSMContext):
correct_emoji, correct_text = secrets.choice(list(CAPTCHA_EMOJIS.items()))
- wrong_emojis = random.sample(
- [e for e in CAPTCHA_EMOJIS.keys() if e != correct_emoji], 3
- )
+ wrong_emojis = random.sample([e for e in CAPTCHA_EMOJIS.keys() if e != correct_emoji], 3)
all_emojis = [correct_emoji] + wrong_emojis
random.shuffle(all_emojis)
@@ -29,9 +29,7 @@ async def generate_captcha(message: Message, state: FSMContext):
if "user_data" not in state_data:
from_user = message.from_user
if not from_user:
- logger.warning(
- "[CAPTCHA] ❗ from_user отсутствует — невозможно сохранить user_data"
- )
+ logger.warning("[CAPTCHA] ❗ from_user отсутствует — невозможно сохранить user_data")
return None
await state.update_data(
@@ -69,9 +67,7 @@ async def generate_captcha(message: Message, state: FSMContext):
@router.callback_query(F.data.startswith("captcha_"))
-async def check_captcha(
- callback: CallbackQuery, state: FSMContext, session: Any, admin: bool
-):
+async def check_captcha(callback: CallbackQuery, state: FSMContext, session: Any, admin: bool):
from handlers.start import process_start_logic
selected_emoji = callback.data.split("captcha_")[1]
diff --git a/handlers/coupons.py b/handlers/coupons.py
index 9c8dcc8f..ea030db2 100644
--- a/handlers/coupons.py
+++ b/handlers/coupons.py
@@ -1,8 +1,10 @@
import html
+
from datetime import datetime
from typing import Any
import pytz
+
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
@@ -13,27 +15,27 @@ from sqlalchemy.ext.asyncio import AsyncSession
from config import ADMIN_ID
from database import (
+ add_payment,
add_user,
check_coupon_usage,
check_user_exists,
create_coupon_usage,
get_coupon_by_code,
get_keys,
+ get_tariff_by_id,
update_balance,
update_coupon_usage_count,
update_key_expiry,
- add_payment,
- get_tariff_by_id,
)
from handlers.buttons import MAIN_MENU
from handlers.keys.key_utils import renew_key_in_cluster
from handlers.profile import process_callback_view_profile
from handlers.texts import (
+ COUPONS_DAYS_MESSAGE,
COUPON_ALREADY_USED_MSG,
+ COUPON_DAYS_ACTIVATED_MSG,
COUPON_INPUT_PROMPT,
COUPON_NOT_FOUND_MSG,
- COUPONS_DAYS_MESSAGE,
- COUPON_DAYS_ACTIVATED_MSG
)
from handlers.utils import edit_or_send_message, format_days
from logger import logger
@@ -49,9 +51,7 @@ router = Router()
@router.callback_query(F.data == "activate_coupon")
@router.message(F.text == "/activate_coupon")
-async def handle_activate_coupon(
- callback_query_or_message: Message | CallbackQuery, state: FSMContext
-):
+async def handle_activate_coupon(callback_query_or_message: Message | CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
@@ -88,9 +88,7 @@ async def activate_coupon(
if not coupon:
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text=MAIN_MENU, callback_data="exit_coupon_input")
- )
+ builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="exit_coupon_input"))
await message.answer(COUPON_NOT_FOUND_MSG, reply_markup=builder.as_markup())
return
@@ -128,15 +126,8 @@ async def activate_coupon(
await update_balance(session, user_id, coupon.amount)
await update_coupon_usage_count(session, coupon.id)
await create_coupon_usage(session, coupon.id, user_id)
- await add_payment(
- session,
- tg_id=user_id,
- amount=coupon.amount,
- payment_system="coupon"
- )
- await message.answer(
- f"✅ Купон активирован, на баланс начислено {coupon.amount} рублей."
- )
+ await add_payment(session, tg_id=user_id, amount=coupon.amount, payment_system="coupon")
+ await message.answer(f"✅ Купон активирован, на баланс начислено {coupon.amount} рублей.")
await state.clear()
except Exception as e:
logger.error(f"Ошибка при активации купона на баланс: {e}")
@@ -160,9 +151,9 @@ async def activate_coupon(
for key in active_keys:
key_display = html.escape((key.alias or key.email).strip())
- expiry_date = datetime.fromtimestamp(
- key.expiry_time / 1000, tz=moscow_tz
- ).strftime("до %d.%m.%y, %H:%M")
+ expiry_date = datetime.fromtimestamp(key.expiry_time / 1000, tz=moscow_tz).strftime(
+ "до %d.%m.%y, %H:%M"
+ )
response_message += f"• {key_display} ({expiry_date})\n"
builder.button(
text=key_display,
@@ -204,9 +195,7 @@ async def handle_key_extension(
result = await session.execute(select(Coupon).where(Coupon.id == coupon_id))
coupon = result.scalar_one_or_none()
if not coupon or coupon.usage_count >= coupon.usage_limit:
- await callback_query.message.edit_text(
- "❌ Купон недействителен или лимит исчерпан."
- )
+ await callback_query.message.edit_text("❌ Купон недействителен или лимит исчерпан.")
await state.clear()
return
@@ -216,14 +205,10 @@ async def handle_key_extension(
await state.clear()
return
- result = await session.execute(
- select(Key).where(Key.tg_id == tg_id, Key.client_id == client_id)
- )
+ result = await session.execute(select(Key).where(Key.tg_id == tg_id, Key.client_id == client_id))
key = result.scalar_one_or_none()
if not key or key.is_frozen:
- await callback_query.message.edit_text(
- "❌ Выбранная подписка не найдена или заморожена."
- )
+ await callback_query.message.edit_text("❌ Выбранная подписка не найдена или заморожена.")
await state.clear()
return
@@ -252,19 +237,13 @@ async def handle_key_extension(
await create_coupon_usage(session, coupon.id, tg_id)
alias = key.alias or key.email
- expiry_date = datetime.fromtimestamp(
- new_expiry / 1000, tz=pytz.timezone("Europe/Moscow")
- ).strftime("%d.%m.%y, %H:%M")
+ expiry_date = datetime.fromtimestamp(new_expiry / 1000, tz=pytz.timezone("Europe/Moscow")).strftime(
+ "%d.%m.%y, %H:%M"
+ )
await callback_query.message.answer(
- COUPON_DAYS_ACTIVATED_MSG.format(
- alias=alias,
- days=format_days(coupon.days),
- expiry=expiry_date
- )
- )
- await process_callback_view_profile(
- callback_query.message, state, admin, session
+ COUPON_DAYS_ACTIVATED_MSG.format(alias=alias, days=format_days(coupon.days), expiry=expiry_date)
)
+ await process_callback_view_profile(callback_query.message, state, admin, session)
await state.clear()
except Exception as e:
logger.error(f"Ошибка при продлении ключа: {e}")
@@ -292,6 +271,4 @@ async def handle_exit_coupon_input(
):
await state.clear()
is_admin = callback_query.from_user.id in ADMIN_ID
- await process_callback_view_profile(
- callback_query.message, state, admin=is_admin, session=session
- )
+ await process_callback_view_profile(callback_query.message, state, admin=is_admin, session=session)
diff --git a/handlers/donate.py b/handlers/donate.py
index 6214582d..347749b9 100644
--- a/handlers/donate.py
+++ b/handlers/donate.py
@@ -31,11 +31,7 @@ async def process_donate(callback_query: CallbackQuery, state: FSMContext):
await state.clear()
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(
- text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"
- )
- )
+ builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"))
builder.row(
InlineKeyboardButton(
text="💰 Ввести сумму доната",
@@ -77,9 +73,7 @@ async def process_donate_amount_input(message: Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount // RUB_TO_XTR <= 0:
- await message.answer(
- f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:"
- )
+ await message.answer(f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:")
return
await state.update_data(amount=amount)
diff --git a/handlers/fallback_router.py b/handlers/fallback_router.py
index 4ba4d953..f28f1e15 100644
--- a/handlers/fallback_router.py
+++ b/handlers/fallback_router.py
@@ -6,6 +6,7 @@ from config import SUPPORT_CHAT_URL
from handlers.buttons import MAIN_MENU, SUPPORT
from handlers.texts import FALLBACK_MESSAGE
+
fallback_router = Router()
diff --git a/handlers/instructions/__init__.py b/handlers/instructions/__init__.py
index 06350a7c..3756810c 100644
--- a/handlers/instructions/__init__.py
+++ b/handlers/instructions/__init__.py
@@ -4,6 +4,7 @@ from aiogram import Router
from .instructions import router as instructions_router
+
router = Router(name="instructions_main_router")
router.include_routers(
diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py
index 95e89126..d10f931a 100644
--- a/handlers/instructions/instructions.py
+++ b/handlers/instructions/instructions.py
@@ -1,4 +1,5 @@
import os
+
from typing import Any
from aiogram import F, Router
@@ -29,14 +30,15 @@ from handlers.buttons import (
from handlers.texts import (
CHOOSE_DEVICE_TEXT,
CONNECT_TV_TEXT,
+ INSTRUCTIONS,
INSTRUCTION_MACOS,
INSTRUCTION_PC,
- INSTRUCTIONS,
KEY_MESSAGE,
SUBSCRIPTION_DETAILS_TEXT,
)
from handlers.utils import edit_or_send_message
+
router = Router()
@@ -79,12 +81,8 @@ async def process_connect_pc(callback_query: CallbackQuery, session: Any):
return
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text=PC_PC, callback_data=f"windows_menu|{key_name}")
- )
- builder.row(
- InlineKeyboardButton(text=PC_MACOS, callback_data=f"macos_menu|{key_name}")
- )
+ builder.row(InlineKeyboardButton(text=PC_PC, callback_data=f"windows_menu|{key_name}"))
+ builder.row(InlineKeyboardButton(text=PC_MACOS, callback_data=f"macos_menu|{key_name}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
await edit_or_send_message(
@@ -105,9 +103,7 @@ async def process_windows_menu(callback_query: CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=DOWNLOAD_PC_BUTTON, url=DOWNLOAD_PC))
- builder.row(
- InlineKeyboardButton(text=CONNECT_WINDOWS_BUTTON, url=f"{CONNECT_WINDOWS}{key}")
- )
+ builder.row(InlineKeyboardButton(text=CONNECT_WINDOWS_BUTTON, url=f"{CONNECT_WINDOWS}{key}"))
builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_pc|{key_name}"))
@@ -129,9 +125,7 @@ async def process_macos_menu(callback_query: CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=DOWNLOAD_MACOS_BUTTON, url=DOWNLOAD_MACOS))
- builder.row(
- InlineKeyboardButton(text=CONNECT_MACOS_BUTTON, url=f"{CONNECT_MACOS}{key}")
- )
+ builder.row(InlineKeyboardButton(text=CONNECT_MACOS_BUTTON, url=f"{CONNECT_MACOS}{key}"))
builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_pc|{key_name}"))
@@ -148,9 +142,7 @@ async def process_connect_tv(callback_query: CallbackQuery, session: Any):
key_name = callback_query.data.split("|")[1]
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text=TV_CONTINUE, callback_data=f"continue_tv|{key_name}")
- )
+ builder.row(InlineKeyboardButton(text=TV_CONTINUE, callback_data=f"continue_tv|{key_name}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
@@ -179,4 +171,4 @@ async def process_continue_tv(callback_query: CallbackQuery, session: Any):
text=message_text,
reply_markup=builder.as_markup(),
media_path=None,
- )
\ No newline at end of file
+ )
diff --git a/handlers/keys/__init__.py b/handlers/keys/__init__.py
index 32190dc0..959efc08 100644
--- a/handlers/keys/__init__.py
+++ b/handlers/keys/__init__.py
@@ -9,6 +9,7 @@ from .key_renew import router as renew_router
from .key_view import router as view_router
from .keys import router as keys_router
+
router = Router(name="keys_main_router")
router.include_routers(
diff --git a/handlers/keys/key_connect.py b/handlers/keys/key_connect.py
index 201a55b8..4cd392ee 100644
--- a/handlers/keys/key_connect.py
+++ b/handlers/keys/key_connect.py
@@ -1,7 +1,9 @@
import os
+
from io import BytesIO
import qrcode
+
from aiogram import F, Router, types
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -38,6 +40,7 @@ from handlers.texts import (
from handlers.utils import edit_or_send_message
from logger import logger
+
router = Router()
@@ -47,24 +50,12 @@ async def handle_connect_device(callback_query: CallbackQuery):
key_name = callback_query.data.split("|")[1]
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text=IPHONE, callback_data=f"connect_ios|{key_name}")
- )
- builder.row(
- InlineKeyboardButton(
- text=ANDROID, callback_data=f"connect_android|{key_name}"
- )
- )
- builder.row(
- InlineKeyboardButton(text=PC, callback_data=f"connect_pc|{key_name}")
- )
- builder.row(
- InlineKeyboardButton(text=TV, callback_data=f"connect_tv|{key_name}")
- )
+ builder.row(InlineKeyboardButton(text=IPHONE, callback_data=f"connect_ios|{key_name}"))
+ builder.row(InlineKeyboardButton(text=ANDROID, callback_data=f"connect_android|{key_name}"))
+ builder.row(InlineKeyboardButton(text=PC, callback_data=f"connect_pc|{key_name}"))
+ builder.row(InlineKeyboardButton(text=TV, callback_data=f"connect_tv|{key_name}"))
# builder.row(InlineKeyboardButton(text=ROUTER, callback_data=f"connect_router|{key_name}"))
- builder.row(
- InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}")
- )
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
await edit_or_send_message(
target_message=callback_query.message,
@@ -78,9 +69,7 @@ async def handle_connect_device(callback_query: CallbackQuery):
@router.callback_query(F.data.startswith("connect_phone|"))
-async def process_callback_connect_phone(
- callback_query: CallbackQuery, session: AsyncSession
-):
+async def process_callback_connect_phone(callback_query: CallbackQuery, session: AsyncSession):
email = callback_query.data.split("|")[1]
try:
@@ -110,9 +99,7 @@ async def process_callback_connect_phone(
InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key_link}"),
)
if INSTRUCTIONS_BUTTON:
- builder.row(
- InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions")
- )
+ builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions"))
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{email}"))
await edit_or_send_message(
@@ -124,9 +111,7 @@ async def process_callback_connect_phone(
@router.callback_query(F.data.startswith("connect_ios|"))
-async def process_callback_connect_ios(
- callback_query: CallbackQuery, session: AsyncSession
-):
+async def process_callback_connect_ios(callback_query: CallbackQuery, session: AsyncSession):
email = callback_query.data.split("|")[1]
try:
@@ -148,12 +133,8 @@ async def process_callback_connect_ios(
builder.row(InlineKeyboardButton(text=DOWNLOAD_IOS_BUTTON, url=DOWNLOAD_IOS))
builder.row(InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{key_link}"))
if INSTRUCTIONS_BUTTON:
- builder.row(
- InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions")
- )
- builder.row(
- InlineKeyboardButton(text=BACK, callback_data=f"connect_device|{email}")
- )
+ builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions"))
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_device|{email}"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
@@ -165,9 +146,7 @@ async def process_callback_connect_ios(
@router.callback_query(F.data.startswith("connect_android|"))
-async def process_callback_connect_android(
- callback_query: CallbackQuery, session: AsyncSession
-):
+async def process_callback_connect_android(callback_query: CallbackQuery, session: AsyncSession):
email = callback_query.data.split("|")[1]
try:
@@ -186,19 +165,11 @@ async def process_callback_connect_android(
description = ANDROID_DESCRIPTION_TEMPLATE.format(key_link=key_link)
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID)
- )
- builder.row(
- InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key_link}")
- )
+ builder.row(InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID))
+ builder.row(InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key_link}"))
if INSTRUCTIONS_BUTTON:
- builder.row(
- InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions")
- )
- builder.row(
- InlineKeyboardButton(text=BACK, callback_data=f"connect_device|{email}")
- )
+ builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions"))
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_device|{email}"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
@@ -224,9 +195,7 @@ async def show_qr_code(callback_query: types.CallbackQuery, session: AsyncSessio
qr_data = record.key or record.remnawave_link
if not qr_data:
- await callback_query.message.answer(
- "❌ У этой подписки отсутствует ссылка для подключения."
- )
+ await callback_query.message.answer("❌ У этой подписки отсутствует ссылка для подключения.")
return
qr = qrcode.QRCode(version=1, box_size=10, border=4)
@@ -243,9 +212,7 @@ async def show_qr_code(callback_query: types.CallbackQuery, session: AsyncSessio
f.write(buffer.read())
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text=BACK, callback_data=f"view_key|{record.email}")
- )
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{record.email}"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
diff --git a/handlers/keys/key_freeze.py b/handlers/keys/key_freeze.py
index 8f0563c2..be275389 100644
--- a/handlers/keys/key_freeze.py
+++ b/handlers/keys/key_freeze.py
@@ -1,4 +1,5 @@
import time
+
from typing import Any
from aiogram import F, Router
@@ -25,13 +26,12 @@ from handlers.texts import (
from handlers.utils import edit_or_send_message, handle_error
from logger import logger
+
router = Router()
@router.callback_query(F.data.startswith("unfreeze_subscription|"))
-async def process_callback_unfreeze_subscription(
- callback_query: CallbackQuery, session: Any
-):
+async def process_callback_unfreeze_subscription(callback_query: CallbackQuery, session: Any):
key_name = callback_query.data.split("|")[1]
confirm_text = UNFREEZE_SUBSCRIPTION_CONFIRM_MSG
@@ -55,9 +55,7 @@ async def process_callback_unfreeze_subscription(
@router.callback_query(F.data.startswith("unfreeze_subscription_confirm|"))
-async def process_callback_unfreeze_subscription_confirm(
- callback_query: CallbackQuery, session: Any
-):
+async def process_callback_unfreeze_subscription_confirm(callback_query: CallbackQuery, session: Any):
"""
Размораживает (включает) подписку.
"""
@@ -74,9 +72,7 @@ async def process_callback_unfreeze_subscription_confirm(
client_id = record["client_id"]
cluster_id = record["server_id"]
- result = await toggle_client_on_cluster(
- cluster_id, email, client_id, enable=True, session=session
- )
+ result = await toggle_client_on_cluster(cluster_id, email, client_id, enable=True, session=session)
if result["status"] != "success":
logger.warning(f"Не удалось включить подписку: {result.get('error') or result.get('results')}")
@@ -86,20 +82,14 @@ async def process_callback_unfreeze_subscription_confirm(
if not cluster_servers:
text_error = "Сервер не найден."
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}")
- )
- await edit_or_send_message(
- callback_query.message, text_error, builder.as_markup()
- )
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
+ await edit_or_send_message(callback_query.message, text_error, builder.as_markup())
return
tariff = await get_tariff_by_id(session, record["tariff_id"]) if record.get("tariff_id") else None
if not tariff:
- logger.info(
- "[Unfreeze] Тариф не найден — возможно ключ триальный. Применяем дефолтные значения."
- )
+ logger.info("[Unfreeze] Тариф не найден — возможно ключ триальный. Применяем дефолтные значения.")
total_gb = TRIAL_CONFIG["traffic_limit_gb"]
hwid_limit = TRIAL_CONFIG["hwid_limit"]
else:
@@ -116,7 +106,7 @@ async def process_callback_unfreeze_subscription_confirm(
await mark_key_as_unfrozen(session, record["tg_id"], client_id, new_expiry_time)
await session.commit()
- added_days = max(leftover / (1000 * 86400), 0.01)
+ max(leftover / (1000 * 86400), 0.01)
logger.info(
f"[Unfreeze Debug] Запуск renew_key_in_cluster с expiry={new_expiry_time}, gb={total_gb}, hwid={hwid_limit}"
)
@@ -134,9 +124,7 @@ async def process_callback_unfreeze_subscription_confirm(
text_ok = SUBSCRIPTION_UNFROZEN_MSG
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}")
- )
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
await edit_or_send_message(callback_query.message, text_ok, builder.as_markup())
except Exception as e:
@@ -144,9 +132,7 @@ async def process_callback_unfreeze_subscription_confirm(
@router.callback_query(F.data.startswith("freeze_subscription|"))
-async def process_callback_freeze_subscription(
- callback_query: CallbackQuery, session: Any
-):
+async def process_callback_freeze_subscription(callback_query: CallbackQuery, session: Any):
"""
Показывает пользователю диалог подтверждения заморозки (отключения) подписки.
"""
@@ -174,9 +160,7 @@ async def process_callback_freeze_subscription(
@router.callback_query(F.data.startswith("freeze_subscription_confirm|"))
-async def process_callback_freeze_subscription_confirm(
- callback_query: CallbackQuery, session: Any
-):
+async def process_callback_freeze_subscription_confirm(callback_query: CallbackQuery, session: Any):
"""
Замораживает (отключает) подписку.
"""
@@ -193,9 +177,7 @@ async def process_callback_freeze_subscription_confirm(
client_id = record["client_id"]
cluster_id = record["server_id"]
- result = await toggle_client_on_cluster(
- cluster_id, email, client_id, enable=False, session=session
- )
+ result = await toggle_client_on_cluster(cluster_id, email, client_id, enable=False, session=session)
if result["status"] == "success":
now_ms = int(time.time() * 1000)
@@ -208,9 +190,7 @@ async def process_callback_freeze_subscription_confirm(
text_ok = SUBSCRIPTION_FROZEN_MSG
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}")
- )
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
await edit_or_send_message(
target_message=callback_query.message,
text=text_ok,
@@ -218,11 +198,11 @@ async def process_callback_freeze_subscription_confirm(
)
else:
- text_error = f"Произошла ошибка при заморозке подписки.\nДетали: {result.get('error') or result.get('results')}"
- builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}")
+ text_error = (
+ f"Произошла ошибка при заморозке подписки.\nДетали: {result.get('error') or result.get('results')}"
)
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
await edit_or_send_message(
target_message=callback_query.message,
text=text_error,
@@ -231,4 +211,3 @@ async def process_callback_freeze_subscription_confirm(
except Exception as e:
await handle_error(tg_id, callback_query, f"Ошибка при заморозке подписки: {e}")
-
diff --git a/handlers/keys/key_mode/__init__.py b/handlers/keys/key_mode/__init__.py
index 0d25df74..5fbf209f 100644
--- a/handlers/keys/key_mode/__init__.py
+++ b/handlers/keys/key_mode/__init__.py
@@ -7,6 +7,7 @@ from .key_country_mode import router as country_router
from .key_create import router as create_router
from .key_discount_mode import router as discount_router
+
router = Router(name="key_mode_router")
router.include_routers(create_router, cluster_router, country_router, discount_router)
diff --git a/handlers/keys/key_mode/key_cluster_mode.py b/handlers/keys/key_mode/key_cluster_mode.py
index 8070d73b..c70844e3 100644
--- a/handlers/keys/key_mode/key_cluster_mode.py
+++ b/handlers/keys/key_mode/key_cluster_mode.py
@@ -1,7 +1,9 @@
import uuid
+
from datetime import datetime
import pytz
+
from aiogram import Router
from aiogram.types import (
CallbackQuery,
@@ -34,14 +36,15 @@ from handlers.keys.key_utils import create_key_on_cluster
from handlers.texts import key_message_success
from handlers.utils import (
edit_or_send_message,
+ format_days,
+ format_months,
generate_random_email,
get_least_loaded_cluster,
is_full_remnawave_cluster,
- format_days,
- format_months,
)
from logger import logger
+
router = Router()
moscow_tz = pytz.timezone("Europe/Moscow")
@@ -97,7 +100,7 @@ async def key_cluster_mode(
except ValueError as e:
logger.error(f"Нет доступных кластеров: {e}")
error_message = str(e)
-
+
if safe_to_edit:
await edit_or_send_message(
target_message=target_message,
@@ -121,9 +124,7 @@ async def key_cluster_mode(
is_trial=is_trial,
)
- logger.info(
- f"[Key Creation] Ключ создан на кластере {least_loaded_cluster} для пользователя {tg_id}"
- )
+ logger.info(f"[Key Creation] Ключ создан на кластере {least_loaded_cluster} для пользователя {tg_id}")
key_record = await get_key_details(session, email)
if not key_record:
@@ -146,9 +147,7 @@ async def key_cluster_mode(
except Exception as e:
logger.error(f"[Error] Ошибка при создании ключа для пользователя {tg_id}: {e}")
- error_message = (
- "❌ Произошла ошибка при создании подписки. Пожалуйста, попробуйте снова."
- )
+ error_message = "❌ Произошла ошибка при создании подписки. Пожалуйста, попробуйте снова."
if safe_to_edit:
await edit_or_send_message(
@@ -162,42 +161,27 @@ async def key_cluster_mode(
builder = InlineKeyboardBuilder()
if await is_full_remnawave_cluster(least_loaded_cluster, session):
- builder.row(
- InlineKeyboardButton(
- text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link)
- )
- )
- builder.row(
- InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}")
- )
+ builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link)))
+ builder.row(InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"))
elif CONNECT_PHONE_BUTTON:
- builder.row(
- InlineKeyboardButton(
- text=CONNECT_PHONE, callback_data=f"connect_phone|{key_name}"
- )
- )
+ builder.row(InlineKeyboardButton(text=CONNECT_PHONE, callback_data=f"connect_phone|{key_name}"))
builder.row(
InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"),
InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"),
)
else:
- builder.row(
- InlineKeyboardButton(
- text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}"
- )
- )
+ builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}"))
builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=f"view_key|{key_name}"))
builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
expiry_time_local = expiry_time.astimezone(moscow_tz)
- remaining_time = expiry_time_local - datetime.now(moscow_tz)
- days = remaining_time.days
+ expiry_time_local - datetime.now(moscow_tz)
tariff_info = None
if plan:
tariff_info = await get_tariff_by_id(session, plan)
-
+
if is_trial:
trial_days = TRIAL_CONFIG.get("duration_days", 1)
if trial_days >= 30:
@@ -209,18 +193,18 @@ async def key_cluster_mode(
final_link,
tariff_name=tariff_duration,
traffic_limit=TRIAL_CONFIG.get("traffic_limit_gb", 100),
- device_limit=TRIAL_CONFIG.get("hwid_limit", 0)
+ device_limit=TRIAL_CONFIG.get("hwid_limit", 0),
)
else:
tariff_duration = tariff_info["name"]
subgroup_title = tariff_info.get("subgroup_title", "") if tariff_info else ""
-
+
key_message_text = key_message_success(
final_link,
tariff_name=tariff_duration,
traffic_limit=tariff_info.get("traffic_limit", 0) if tariff_info else 0,
device_limit=tariff_info.get("device_limit", 0) if tariff_info else 0,
- subgroup_title=subgroup_title
+ subgroup_title=subgroup_title,
)
default_media_path = "img/pic.jpg"
diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py
index 4c117480..9c994211 100644
--- a/handlers/keys/key_mode/key_country_mode.py
+++ b/handlers/keys/key_mode/key_country_mode.py
@@ -1,9 +1,11 @@
import asyncio
import uuid
+
from datetime import datetime
from typing import Any
import pytz
+
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message, WebAppInfo
@@ -34,30 +36,22 @@ from database import (
update_trial,
)
from database.models import Key, Server, Tariff
-from handlers.buttons import (
- BACK,
- CONNECT_DEVICE,
- CONNECT_PHONE,
- MAIN_MENU,
- PC_BUTTON,
- SUPPORT,
- TV_BUTTON,
- MY_SUB
-)
+from handlers.buttons import BACK, CONNECT_DEVICE, CONNECT_PHONE, MAIN_MENU, MY_SUB, PC_BUTTON, SUPPORT, TV_BUTTON
from handlers.keys.key_utils import create_client_on_server
from handlers.texts import SELECT_COUNTRY_MSG, key_message_success
from handlers.utils import (
edit_or_send_message,
+ format_days,
+ format_months,
generate_random_email,
get_least_loaded_cluster,
is_full_remnawave_cluster,
- format_days,
- format_months,
)
from logger import logger
from panels.remnawave import RemnawaveAPI
from panels.three_xui import delete_client, get_xui_instance
+
router = Router()
moscow_tz = pytz.timezone("Europe/Moscow")
@@ -91,9 +85,7 @@ async def key_country_mode(
logger.error(f"Нет доступных кластеров: {e}")
text = str(e)
if safe_to_edit:
- await edit_or_send_message(
- target_message=target_message, text=text, reply_markup=None
- )
+ await edit_or_send_message(target_message=target_message, text=text, reply_markup=None)
else:
await bot.send_message(chat_id=tg_id, text=text)
return
@@ -113,18 +105,13 @@ async def key_country_mode(
logger.error(f"❌ Нет серверов в кластере {least_loaded_cluster}")
text = "❌ Нет доступных серверов в выбранном кластере."
if safe_to_edit:
- await edit_or_send_message(
- target_message=target_message, text=text, reply_markup=None
- )
+ await edit_or_send_message(target_message=target_message, text=text, reply_markup=None)
else:
await bot.send_message(chat_id=tg_id, text=text)
return
available_servers = []
- tasks = [
- asyncio.create_task(check_server_availability(server, session))
- for server in servers
- ]
+ tasks = [asyncio.create_task(check_server_availability(server, session)) for server in servers]
results = await asyncio.gather(*tasks, return_exceptions=True)
for server, result in zip(servers, results, strict=False):
@@ -132,21 +119,15 @@ async def key_country_mode(
available_servers.append(server["server_name"])
if not available_servers:
- logger.warning(
- f"[Country Selection] Нет доступных серверов в кластере {least_loaded_cluster}"
- )
+ logger.warning(f"[Country Selection] Нет доступных серверов в кластере {least_loaded_cluster}")
text = "❌ Нет доступных серверов в выбранном кластере."
if safe_to_edit:
- await edit_or_send_message(
- target_message=target_message, text=text, reply_markup=None
- )
+ await edit_or_send_message(target_message=target_message, text=text, reply_markup=None)
else:
await bot.send_message(chat_id=tg_id, text=text)
return
- logger.info(
- f"[Country Selection] Доступные сервера в кластере {least_loaded_cluster}: {available_servers}"
- )
+ logger.info(f"[Country Selection] Доступные сервера в кластере {least_loaded_cluster}: {available_servers}")
builder = InlineKeyboardBuilder()
ts = int(expiry_time.timestamp())
@@ -194,9 +175,7 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
cluster_info = await check_server_name_by_cluster(session, current_server)
if not cluster_info:
- await callback_query.answer(
- "❌ Кластер для текущего сервера не найден", show_alert=True
- )
+ await callback_query.answer("❌ Кластер для текущего сервера не найден", show_alert=True)
return
cluster_name = cluster_info["cluster_name"]
@@ -219,9 +198,7 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
.all()
)
if not servers:
- await callback_query.answer(
- "❌ Доступных серверов в кластере не найдено", show_alert=True
- )
+ await callback_query.answer("❌ Доступных серверов в кластере не найдено", show_alert=True)
return
available_servers = []
@@ -245,9 +222,7 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
available_servers.append(server["server_name"])
if not available_servers:
- await callback_query.answer(
- "❌ Нет доступных серверов для смены локации", show_alert=True
- )
+ await callback_query.answer("❌ Нет доступных серверов для смены локации", show_alert=True)
return
logger.info(f"Доступные страны для смены локации: {available_servers}")
@@ -256,9 +231,7 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
for country in available_servers:
callback_data = f"select_country|{country}|{ts}|{old_key_name}"
builder.row(InlineKeyboardButton(text=country, callback_data=callback_data))
- builder.row(
- InlineKeyboardButton(text=BACK, callback_data=f"view_key|{old_key_name}")
- )
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{old_key_name}"))
await edit_or_send_message(
target_message=callback_query.message,
@@ -267,18 +240,12 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
media_path=None,
)
except Exception as e:
- logger.error(
- f"Ошибка при смене локации для пользователя {callback_query.from_user.id}: {e}"
- )
- await callback_query.answer(
- "❌ Ошибка смены локации. Попробуйте снова.", show_alert=True
- )
+ logger.error(f"Ошибка при смене локации для пользователя {callback_query.from_user.id}: {e}")
+ await callback_query.answer("❌ Ошибка смены локации. Попробуйте снова.", show_alert=True)
@router.callback_query(F.data.startswith("select_country|"))
-async def handle_country_selection(
- callback_query: CallbackQuery, session: Any, state: FSMContext
-):
+async def handle_country_selection(callback_query: CallbackQuery, session: Any, state: FSMContext):
"""
Обрабатывает выбор страны.
Формат callback data:
@@ -294,9 +261,7 @@ async def handle_country_selection(
try:
ts = int(data[2])
except ValueError:
- await callback_query.message.answer(
- "❌ Некорректное время истечения. Попробуйте снова."
- )
+ await callback_query.message.answer("❌ Некорректное время истечения. Попробуйте снова.")
return
expiry_time = datetime.fromtimestamp(ts, tz=moscow_tz)
@@ -354,7 +319,7 @@ async def finalize_key_creation(
client_id = old_key_details["client_id"]
email = old_key_details["email"]
expiry_timestamp = old_key_details["expiry_time"]
- tariff_id = old_key_details.get("tariff_id") or tariff_id
+ tariff_id = old_key_details.get("tariff_id") or tariff_id
else:
while True:
key_name = generate_random_email()
@@ -388,9 +353,7 @@ async def finalize_key_creation(
created_at = int(datetime.now(moscow_tz).timestamp() * 1000)
try:
- result = await session.execute(
- select(Server).where(Server.server_name == selected_country)
- )
+ result = await session.execute(select(Server).where(Server.server_name == selected_country))
server_info = result.scalar_one_or_none()
if not server_info:
raise ValueError(f"Сервер {selected_country} не найден")
@@ -413,9 +376,7 @@ async def finalize_key_creation(
xui = await get_xui_instance(old_server_info.api_url)
await delete_client(xui, old_server_info.inbound_id, email, client_id)
await session.execute(
- update(Key)
- .where(Key.tg_id == tg_id, Key.email == email)
- .values(key=None)
+ update(Key).where(Key.tg_id == tg_id, Key.email == email).values(key=None)
)
elif old_server_info.panel_type.lower() == "remnawave":
remna = RemnawaveAPI(old_server_info.api_url)
@@ -529,21 +490,15 @@ async def finalize_key_creation(
web_app=WebAppInfo(url=public_link or remnawave_link),
)
)
- builder.row(
- InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}")
- )
+ builder.row(InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"))
elif CONNECT_PHONE_BUTTON:
- builder.row(
- InlineKeyboardButton(text=CONNECT_PHONE, callback_data=f"connect_phone|{key_name}")
- )
+ builder.row(InlineKeyboardButton(text=CONNECT_PHONE, callback_data=f"connect_phone|{key_name}"))
builder.row(
InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"),
InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"),
)
else:
- builder.row(
- InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}")
- )
+ builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}"))
builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=f"view_key|{key_name}"))
builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
@@ -555,7 +510,7 @@ async def finalize_key_creation(
if tariff_id:
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff_info = result.scalar_one_or_none()
-
+
if is_trial:
trial_days = TRIAL_CONFIG.get("duration_days", 1)
if trial_days >= 30:
@@ -567,18 +522,18 @@ async def finalize_key_creation(
link_to_show,
tariff_name=tariff_duration,
traffic_limit=TRIAL_CONFIG.get("traffic_limit_gb", 100),
- device_limit=TRIAL_CONFIG.get("hwid_limit", 0)
+ device_limit=TRIAL_CONFIG.get("hwid_limit", 0),
)
else:
tariff_duration = tariff_info["name"] if tariff_info else None
subgroup_title = tariff_info.get("subgroup_title", "") if tariff_info else ""
-
+
key_message_text = key_message_success(
link_to_show,
tariff_name=tariff_duration,
traffic_limit=tariff_info.get("traffic_limit", 0) if tariff_info else 0,
device_limit=tariff_info.get("device_limit", 0) if tariff_info else 0,
- subgroup_title=subgroup_title
+ subgroup_title=subgroup_title,
)
await edit_or_send_message(
@@ -604,31 +559,21 @@ async def check_server_availability(server_info: dict, session: AsyncSession) ->
try:
if max_keys is not None:
- result = await session.execute(
- select(func.count())
- .select_from(Key)
- .where(Key.server_id == server_name)
- )
+ result = await session.execute(select(func.count()).select_from(Key).where(Key.server_id == server_name))
key_count = result.scalar()
if key_count >= max_keys:
- logger.info(
- f"[Ping] Сервер {server_name} достиг лимита ключей: {key_count}/{max_keys}."
- )
+ logger.info(f"[Ping] Сервер {server_name} достиг лимита ключей: {key_count}/{max_keys}.")
return False
except SQLAlchemyError as e:
- logger.warning(
- f"[Ping] Ошибка при проверке лимита ключей на сервере {server_name}: {e}"
- )
+ logger.warning(f"[Ping] Ошибка при проверке лимита ключей на сервере {server_name}: {e}")
return False
try:
if panel_type == "remnawave":
remna = RemnawaveAPI(server_info["api_url"])
- await asyncio.wait_for(
- remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD), timeout=5.0
- )
+ await asyncio.wait_for(remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD), timeout=5.0)
logger.info(f"[Ping] Remnawave сервер {server_name} доступен.")
return True
diff --git a/handlers/keys/key_mode/key_create.py b/handlers/keys/key_mode/key_create.py
index ec058791..50cf195d 100644
--- a/handlers/keys/key_mode/key_create.py
+++ b/handlers/keys/key_mode/key_create.py
@@ -1,15 +1,16 @@
-from datetime import datetime, timedelta
-from typing import Any
from collections import defaultdict
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select
+from datetime import datetime, timedelta
+from math import ceil
+from typing import Any
import pytz
+
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from math import ceil
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
from config import (
NOTIFY_EXTRA_DAYS,
@@ -46,6 +47,7 @@ from logger import logger
from .key_cluster_mode import key_cluster_mode
from .key_country_mode import key_country_mode
+
router = Router()
moscow_tz = pytz.timezone("Europe/Moscow")
@@ -58,16 +60,14 @@ class Form(FSMContext):
@router.callback_query(F.data == "create_key")
@router.callback_query(F.data == "buy")
@router.message(F.text == "/buy")
-async def confirm_create_new_key(
- callback_query_or_message: CallbackQuery | Message, state: FSMContext, session: Any
-):
+async def confirm_create_new_key(callback_query_or_message: CallbackQuery | Message, state: FSMContext, session: Any):
if isinstance(callback_query_or_message, CallbackQuery):
tg_id = callback_query_or_message.message.chat.id
message_or_query = callback_query_or_message
else:
tg_id = callback_query_or_message.chat.id
message_or_query = callback_query_or_message
-
+
await handle_key_creation(tg_id, state, session, message_or_query)
@@ -91,9 +91,7 @@ async def handle_key_creation(
await edit_or_send_message(
target_message=(
- message_or_query.message
- if isinstance(message_or_query, CallbackQuery)
- else message_or_query
+ message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query
),
text=CREATING_CONNECTION_MSG,
reply_markup=None,
@@ -109,14 +107,12 @@ async def handle_key_creation(
logger.error(f"Нет доступных кластеров: {e}")
await edit_or_send_message(
target_message=(
- message_or_query.message
- if isinstance(message_or_query, CallbackQuery)
- else message_or_query
+ message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query
),
text=str(e),
- reply_markup=InlineKeyboardBuilder().row(
- InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")
- ).as_markup(),
+ reply_markup=InlineKeyboardBuilder()
+ .row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
+ .as_markup(),
)
return
@@ -125,17 +121,16 @@ async def handle_key_creation(
if not tariffs:
result = await session.execute(select(Admin).where(Admin.tg_id == tg_id))
is_admin = result.scalar_one_or_none() is not None
-
+
if is_admin:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
- text="🔗 Привязать тариф",
- callback_data=AdminPanelCallback(action="clusters").pack()
+ text="🔗 Привязать тариф", callback_data=AdminPanelCallback(action="clusters").pack()
)
)
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
-
+
text = (
f"🚫 Невозможно создать подписку\n\n"
f"📊 Информация о кластере:\n" @@ -147,12 +142,10 @@ async def handle_key_creation( builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) text = "❌ Нет доступных тарифов для выбора." - + await edit_or_send_message( target_message=( - message_or_query.message - if isinstance(message_or_query, CallbackQuery) - else message_or_query + message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query ), text=text, reply_markup=builder.as_markup(), @@ -163,9 +156,7 @@ async def handle_key_creation( if not group_code: await edit_or_send_message( target_message=( - message_or_query.message - if isinstance(message_or_query, CallbackQuery) - else message_or_query + message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query ), text="❌ Не удалось определить группу тарифов.", reply_markup=None, @@ -198,11 +189,7 @@ async def handle_key_creation( builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) - target_message = ( - message_or_query.message - if isinstance(message_or_query, CallbackQuery) - else message_or_query - ) + target_message = message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query await edit_or_send_message( target_message=target_message, @@ -249,12 +236,7 @@ async def show_tariffs_in_subgroup_user(callback: CallbackQuery, state: FSMConte callback_data=f"select_tariff_plan|{t['id']}", ) ) - builder.row( - InlineKeyboardButton( - text="⬅️ Назад", - callback_data="back_to_tariff_group_list" - ) - ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_tariff_group_list")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) await edit_or_send_message( @@ -266,7 +248,7 @@ async def show_tariffs_in_subgroup_user(callback: CallbackQuery, state: FSMConte @router.callback_query(F.data == "back_to_tariff_group_list") async def back_to_tariff_group_list(callback: CallbackQuery, state: FSMContext, session: Any): - data = await state.get_data() + await state.get_data() tg_id = callback.from_user.id await handle_key_creation( tg_id=tg_id, @@ -277,9 +259,7 @@ async def back_to_tariff_group_list(callback: CallbackQuery, state: FSMContext, @router.callback_query(F.data.startswith("select_tariff_plan|")) -async def select_tariff_plan( - callback_query: CallbackQuery, session: Any, state: FSMContext -): +async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state: FSMContext): tg_id = callback_query.from_user.id tariff_id = int(callback_query.data.split("|")[1]) @@ -327,9 +307,7 @@ async def select_tariff_plan( return builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⏳ Подождите...", callback_data="creating_key") - ) + builder.row(InlineKeyboardButton(text="⏳ Подождите...", callback_data="creating_key")) await edit_or_send_message( target_message=callback_query.message, text=CREATING_CONNECTION_MSG, @@ -351,11 +329,7 @@ async def create_key( plan: int = None, ): if not await check_user_exists(session, tg_id): - from_user = ( - message_or_query.from_user - if isinstance(message_or_query, CallbackQuery | Message) - else None - ) + from_user = message_or_query.from_user if isinstance(message_or_query, CallbackQuery | Message) else None if from_user: await add_user( tg_id=from_user.id, diff --git a/handlers/keys/key_mode/key_discount_mode.py b/handlers/keys/key_mode/key_discount_mode.py index 1e0f9f4b..9e696ae9 100644 --- a/handlers/keys/key_mode/key_discount_mode.py +++ b/handlers/keys/key_mode/key_discount_mode.py @@ -14,6 +14,7 @@ from logger import logger from .key_create import select_tariff_plan + router = Router() diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index 6386fde6..d4eb3815 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -1,19 +1,21 @@ -from datetime import datetime, timedelta -from typing import Any -from aiogram.fsm.context import FSMContext from collections import defaultdict +from datetime import datetime, timedelta +from math import ceil +from typing import Any + +import pytz from aiogram import F, Router +from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from sqlalchemy import or_, select, update from sqlalchemy.ext.asyncio import AsyncSession -from math import ceil -import pytz from bot import bot from config import USE_NEW_PAYMENT_FLOW from database import ( + check_tariff_exists, create_temporary_data, get_balance, get_key_by_server, @@ -22,11 +24,10 @@ from database import ( get_tariffs, update_balance, update_key_expiry, - check_tariff_exists, ) -from database.models import Server, Key +from database.models import Key, Server from database.tariffs import create_subgroup_hash, find_subgroup_by_hash -from handlers.buttons import BACK, MAIN_MENU, PAYMENT +from handlers.buttons import BACK, MAIN_MENU, MY_SUB, PAYMENT from handlers.keys.key_utils import renew_key_in_cluster from handlers.payments.robokassa_pay import handle_custom_amount_input from handlers.payments.stars_pay import process_custom_amount_input_stars @@ -38,10 +39,10 @@ from handlers.texts import ( PLAN_SELECTION_MSG, get_renewal_message, ) -from handlers.buttons import MY_SUB from handlers.utils import edit_or_send_message, format_days, format_months, get_russian_month from logger import logger + router = Router() moscow_tz = pytz.timezone("Europe/Moscow") @@ -62,10 +63,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo server_id = record["server_id"] tariff_id = record.get("tariff_id") - await state.update_data( - renew_key_name=key_name, - renew_client_id=client_id - ) + await state.update_data(renew_key_name=key_name, renew_client_id=client_id) logger.info(f"[RENEW] Получение тарифов для server_id={server_id}") @@ -82,9 +80,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo Server.cluster_name == server_id, ) - row = await session.execute( - select(Server.tariff_group).where(filter_condition).limit(1) - ) + row = await session.execute(select(Server.tariff_group).where(filter_condition).limit(1)) row = row.first() if not row or not row[0]: logger.warning(f"[RENEW] Тарифная группа не найдена для server_id={server_id}") @@ -134,9 +130,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo balance = await get_balance(session, tg_id) response_message = PLAN_SELECTION_MSG.format( balance=balance, - expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime( - "%Y-%m-%d %H:%M:%S" - ), + expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"), ) await edit_or_send_message( @@ -182,9 +176,7 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont Server.cluster_name == server_id, ) - row = await session.execute( - select(Server.tariff_group).where(filter_condition).limit(1) - ) + row = await session.execute(select(Server.tariff_group).where(filter_condition).limit(1)) row = row.first() if not row or not row[0]: logger.warning(f"[RENEW_SUBGROUP] Тарифная группа не найдена для server_id={server_id}") @@ -197,7 +189,7 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont if not subgroup: await callback.message.answer("❌ Подгруппа не найдена.") return - + tariffs = await get_tariffs(session, group_code=group_code) filtered = [t for t in tariffs if t["subgroup_title"] == subgroup and t["is_active"]] @@ -218,12 +210,7 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont ) ) - builder.row( - InlineKeyboardButton( - text="⬅️ Назад", - callback_data="renew_menu" - ) - ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="renew_menu")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) await edit_or_send_message( @@ -272,13 +259,9 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, state: FSMC current_time = datetime.utcnow().timestamp() * 1000 if expiry_time <= current_time: - new_expiry_time = int( - current_time + timedelta(days=duration_days).total_seconds() * 1000 - ) + new_expiry_time = int(current_time + timedelta(days=duration_days).total_seconds() * 1000) else: - new_expiry_time = int( - expiry_time + timedelta(days=duration_days).total_seconds() * 1000 - ) + new_expiry_time = int(expiry_time + timedelta(days=duration_days).total_seconds() * 1000) balance = round(await get_balance(session, tg_id), 2) cost = round(cost, 2) @@ -312,21 +295,15 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, state: FSMC else: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay")) - builder.row( - InlineKeyboardButton(text=MAIN_MENU, callback_data="profile") - ) + builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) await edit_or_send_message( target_message=callback_query.message, - text=INSUFFICIENT_FUNDS_RENEWAL_MSG.format( - required_amount=required_amount - ), + text=INSUFFICIENT_FUNDS_RENEWAL_MSG.format(required_amount=required_amount), reply_markup=builder.as_markup(), ) return - logger.info( - f"[RENEW] Продление ключа для пользователя {tg_id} на {duration_days} дней" - ) + logger.info(f"[RENEW] Продление ключа для пользователя {tg_id} на {duration_days} дней") await complete_key_renewal( session, tg_id, @@ -340,26 +317,16 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, state: FSMC ) except Exception as e: - logger.error( - f"[RENEW] Ошибка при продлении ключа для пользователя {tg_id}: {e}" - ) + logger.error(f"[RENEW] Ошибка при продлении ключа для пользователя {tg_id}: {e}") -async def resolve_cluster_name( - session: AsyncSession, server_or_cluster: str -) -> str | None: - result = await session.execute( - select(Server).where(Server.cluster_name == server_or_cluster).limit(1) - ) +async def resolve_cluster_name(session: AsyncSession, server_or_cluster: str) -> str | None: + result = await session.execute(select(Server).where(Server.cluster_name == server_or_cluster).limit(1)) server = result.scalars().first() if server: return server_or_cluster - result = await session.execute( - select(Server.cluster_name) - .where(Server.server_name == server_or_cluster) - .limit(1) - ) + result = await session.execute(select(Server.cluster_name).where(Server.server_name == server_or_cluster).limit(1)) row = result.scalar() return row @@ -376,9 +343,7 @@ async def complete_key_renewal( tariff_id: int, ): try: - logger.info( - f"[Info] Продление ключа {client_id} по тарифу ID={tariff_id} (Start)" - ) + logger.info(f"[Info] Продление ключа {client_id} по тарифу ID={tariff_id} (Start)") tariff = await get_tariff_by_id(session, tariff_id) if not tariff: @@ -387,14 +352,12 @@ async def complete_key_renewal( builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=f"view_key|{email}")) - - formatted_expiry_date = datetime.fromtimestamp( - new_expiry_time / 1000, tz=moscow_tz - ).strftime("%d %B %Y, %H:%M") - + + formatted_expiry_date = datetime.fromtimestamp(new_expiry_time / 1000, tz=moscow_tz).strftime("%d %B %Y, %H:%M") + formatted_expiry_date = formatted_expiry_date.replace( datetime.fromtimestamp(new_expiry_time / 1000, tz=moscow_tz).strftime("%B"), - get_russian_month(datetime.fromtimestamp(new_expiry_time / 1000, tz=moscow_tz)) + get_russian_month(datetime.fromtimestamp(new_expiry_time / 1000, tz=moscow_tz)), ) response_message = get_renewal_message( @@ -402,7 +365,7 @@ async def complete_key_renewal( traffic_limit=tariff.get("traffic_limit") if tariff.get("traffic_limit") is not None else 0, device_limit=tariff.get("device_limit") if tariff.get("device_limit") is not None else 0, expiry_date=formatted_expiry_date, - subgroup_title=tariff.get("subgroup_title", "") + subgroup_title=tariff.get("subgroup_title", ""), ) if callback_query: @@ -414,13 +377,9 @@ async def complete_key_renewal( ) except Exception as e: logger.error(f"[Error] Ошибка при редактировании сообщения: {e}") - await callback_query.message.answer( - response_message, reply_markup=builder.as_markup() - ) + await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) else: - await bot.send_message( - tg_id, response_message, reply_markup=builder.as_markup() - ) + await bot.send_message(tg_id, response_message, reply_markup=builder.as_markup()) key_info = await get_key_details(session, email) if not key_info: @@ -445,16 +404,10 @@ async def complete_key_renewal( ) await update_key_expiry(session, client_id, new_expiry_time) - await session.execute( - update(Key) - .where(Key.client_id == client_id) - .values(tariff_id=tariff_id) - ) + await session.execute(update(Key).where(Key.client_id == client_id).values(tariff_id=tariff_id)) await update_balance(session, tg_id, -cost) - logger.info( - f"[Info] Продление ключа {client_id} завершено успешно (User: {tg_id})" - ) + logger.info(f"[Info] Продление ключа {client_id} завершено успешно (User: {tg_id})") except Exception as e: - logger.error(f"[Error] Ошибка в complete_key_renewal: {e}") \ No newline at end of file + logger.error(f"[Error] Ошибка в complete_key_renewal: {e}") diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 915e87f3..3852b907 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -1,4 +1,5 @@ import asyncio + from datetime import datetime, timezone from typing import Any @@ -50,44 +51,34 @@ async def create_key_on_cluster( cluster = found_servers server_id_to_store = found_servers[0].get("server_name") else: - raise ValueError( - f"Кластер или сервер с ID/именем {cluster_id} не найден." - ) + raise ValueError(f"Кластер или сервер с ID/именем {cluster_id} не найден.") enabled_servers = [s for s in cluster if s.get("enabled", True)] if not enabled_servers: - logger.warning( - f"[Key Creation] Нет доступных серверов в кластере {cluster_id}" - ) + logger.warning(f"[Key Creation] Нет доступных серверов в кластере {cluster_id}") return if plan is not None and traffic_limit_bytes is None: tariff = await get_tariff_by_id(session, plan) if not tariff: raise ValueError(f"Тариф с id={plan} не найден.") - traffic_limit_bytes = ( - int(tariff["traffic_limit"]) if tariff["traffic_limit"] else None - ) + traffic_limit_bytes = int(tariff["traffic_limit"]) if tariff["traffic_limit"] else None if hwid_limit is None and tariff.get("device_limit") is not None: hwid_limit = int(tariff["device_limit"]) remnawave_servers = [ s for s in enabled_servers - if s.get("panel_type", "3x-ui").lower() == "remnawave" - and await check_server_key_limit(s, session) + if s.get("panel_type", "3x-ui").lower() == "remnawave" and await check_server_key_limit(s, session) ] xui_servers = [ s for s in enabled_servers - if s.get("panel_type", "3x-ui").lower() == "3x-ui" - and await check_server_key_limit(s, session) + if s.get("panel_type", "3x-ui").lower() == "3x-ui" and await check_server_key_limit(s, session) ] if not remnawave_servers and not xui_servers: - logger.warning( - f"[Key Creation] Нет серверов с доступным лимитом в кластере {cluster_id}" - ) + logger.warning(f"[Key Creation] Нет серверов с доступным лимитом в кластере {cluster_id}") return semaphore = asyncio.Semaphore(2) @@ -101,14 +92,8 @@ async def create_key_on_cluster( if not logged_in: logger.error("Не удалось войти в Remnawave API") else: - expire_at = ( - datetime.utcfromtimestamp(expiry_timestamp / 1000).isoformat() + "Z" - ) - inbound_ids = [ - s.get("inbound_id") - for s in remnawave_servers - if s.get("inbound_id") - ] + expire_at = datetime.utcfromtimestamp(expiry_timestamp / 1000).isoformat() + "Z" + inbound_ids = [s.get("inbound_id") for s in remnawave_servers if s.get("inbound_id")] if not inbound_ids: logger.warning("Нет inbound_id у серверов Remnawave") @@ -141,15 +126,11 @@ async def create_key_on_cluster( remnawave_created = True remnawave_key = result.get("subscriptionUrl") remnawave_client_id = result.get("uuid") - logger.info( - f"[Key Creation] Пользователь создан в Remnawave: {result}" - ) + logger.info(f"[Key Creation] Пользователь создан в Remnawave: {result}") public_link = f"{PUBLIC_LINK}{email}/{tg_id}" if xui_servers else None final_client_id = remnawave_client_id or client_id - logger.info( - f"[Debug] 3x-ui servers для кластера {cluster_id}: {[s['server_name'] for s in xui_servers]}" - ) + logger.info(f"[Debug] 3x-ui servers для кластера {cluster_id}: {[s['server_name'] for s in xui_servers]}") if xui_servers: if SUPERNODE: @@ -303,25 +284,17 @@ async def renew_key_in_cluster( if found_servers: cluster = found_servers else: - raise ValueError( - f"Кластер или сервер с ID/именем {cluster_id} не найден." - ) + raise ValueError(f"Кластер или сервер с ID/именем {cluster_id} не найден.") - result = await session.execute( - select(Key.tg_id, Key.server_id).where(Key.client_id == client_id).limit(1) - ) + result = await session.execute(select(Key.tg_id, Key.server_id).where(Key.client_id == client_id).limit(1)) row = result.first() if not row: - logger.error( - f"Не найден пользователь с client_id={client_id} в таблице keys." - ) + logger.error(f"Не найден пользователь с client_id={client_id} в таблице keys.") return False tg_id, server_id = row - result = await session.execute( - select(Server.tariff_group).where(Server.server_name == server_id) - ) + result = await session.execute(select(Server.tariff_group).where(Server.server_name == server_id)) tariff_group_row = result.scalar_one_or_none() if tariff_group_row: @@ -348,18 +321,14 @@ async def renew_key_in_cluster( ( s for s in cluster - if s.get("panel_type", "").lower() == "remnawave" - and s.get("inbound_id") in remnawave_inbound_ids + if s.get("panel_type", "").lower() == "remnawave" and s.get("inbound_id") in remnawave_inbound_ids ), None, ) if remnawave_server: remna = RemnawaveAPI(remnawave_server["api_url"]) if await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD): - expire_iso = ( - datetime.utcfromtimestamp(new_expiry_time // 1000).isoformat() - + "Z" - ) + expire_iso = datetime.utcfromtimestamp(new_expiry_time // 1000).isoformat() + "Z" traffic_limit_bytes = total_gb * 1024 * 1024 * 1024 if total_gb else 0 updated = await remna.update_user( uuid=client_id, @@ -379,8 +348,8 @@ async def renew_key_in_cluster( ) row = result.one_or_none() remnawave_link = row[0] if row else None - old_key = row[1] if row else None - + row[1] if row else None + user_data = { "username": email, "trafficLimitStrategy": "NO_RESET", @@ -400,14 +369,11 @@ async def renew_key_in_cluster( new_client_id = result.get("uuid") new_remnawave_link = result.get("subscriptionUrl") logger.info(f"Пользователь Remnawave {client_id} успешно создан") - + await session.execute( update(Key) .where(Key.client_id == client_id) - .values( - client_id=new_client_id, - remnawave_link=new_remnawave_link - ) + .values(client_id=new_client_id, remnawave_link=new_remnawave_link) ) await session.commit() else: @@ -424,9 +390,7 @@ async def renew_key_in_cluster( server_name = server_info.get("server_name", "unknown") if not inbound_id: - logger.warning( - f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск." - ) + logger.warning(f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск.") continue if SUPERNODE: @@ -450,7 +414,7 @@ async def renew_key_in_cluster( tg_id=tg_id, limit_ip=hwid_device_limit, ) - + if not updated: logger.warning(f"Не удалось обновить клиента {unique_email}, пробуем создать") config = ClientConfig( @@ -478,15 +442,11 @@ async def renew_key_in_cluster( logger.info(f"🧹 Уведомления для ключа {email} очищены при продлении.") except Exception as e: - logger.error( - f"Не удалось продлить ключ {client_id} в кластере/на сервере {cluster_id}: {e}" - ) + logger.error(f"Не удалось продлить ключ {client_id} в кластере/на сервере {cluster_id}: {e}") raise -async def delete_key_from_cluster( - cluster_id: str, email: str, client_id: str, session: AsyncSession -): +async def delete_key_from_cluster(cluster_id: str, email: str, client_id: str, session: AsyncSession): """Удаление ключа с серверов в кластере или с конкретного сервера""" try: servers = await get_servers(session) @@ -502,9 +462,7 @@ async def delete_key_from_cluster( if found_servers: cluster = found_servers else: - raise ValueError( - f"Кластер или сервер с ID/именем {cluster_id} не найден." - ) + raise ValueError(f"Кластер или сервер с ID/именем {cluster_id} не найден.") for server_info in cluster: panel_type = server_info.get("panel_type", "3x-ui").lower() @@ -513,29 +471,21 @@ async def delete_key_from_cluster( if panel_type == "remnawave": remna = RemnawaveAPI(server_info["api_url"]) if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD): - logger.error( - f"[Remnawave] Не удалось войти на сервер {server_name}" - ) + logger.error(f"[Remnawave] Не удалось войти на сервер {server_name}") continue success = await remna.delete_user(client_id) if success: - logger.info( - f"[Remnawave] Клиент {client_id} успешно удалён с {server_name}" - ) + logger.info(f"[Remnawave] Клиент {client_id} успешно удалён с {server_name}") else: - logger.warning( - f"[Remnawave] Не удалось удалить клиента {client_id} с {server_name}" - ) + logger.warning(f"[Remnawave] Не удалось удалить клиента {client_id} с {server_name}") elif panel_type == "3x-ui": xui = await get_xui_instance(server_info["api_url"]) inbound_id = server_info.get("inbound_id") if not inbound_id: - logger.warning( - f"[3x-ui] INBOUND_ID отсутствует на сервере {server_name}. Пропуск." - ) + logger.warning(f"[3x-ui] INBOUND_ID отсутствует на сервере {server_name}. Пропуск.") continue await delete_client( @@ -544,19 +494,13 @@ async def delete_key_from_cluster( email=email, client_id=client_id, ) - logger.info( - f"[3x-ui] Клиент {client_id} удалён с сервера {server_name}" - ) + logger.info(f"[3x-ui] Клиент {client_id} удалён с сервера {server_name}") else: - logger.warning( - f"[Unknown] Неизвестный тип панели '{panel_type}' для сервера {server_name}" - ) + logger.warning(f"[Unknown] Неизвестный тип панели '{panel_type}' для сервера {server_name}") except Exception as e: - logger.error( - f"❌ Ошибка при удалении ключа {client_id} из кластера/сервера {cluster_id}: {e}" - ) + logger.error(f"❌ Ошибка при удалении ключа {client_id} из кластера/сервера {cluster_id}: {e}") raise @@ -588,30 +532,18 @@ async def update_key_on_cluster( if found_servers: cluster = found_servers else: - raise ValueError( - f"Кластер или сервер с ID/именем {cluster_id} не найден." - ) + raise ValueError(f"Кластер или сервер с ID/именем {cluster_id} не найден.") - expire_iso = ( - datetime.utcfromtimestamp(expiry_time / 1000) - .replace(tzinfo=timezone.utc) - .isoformat() - ) + expire_iso = datetime.utcfromtimestamp(expiry_time / 1000).replace(tzinfo=timezone.utc).isoformat() - remnawave_servers = [ - s for s in cluster if s.get("panel_type", "3x-ui").lower() == "remnawave" - ] - xui_servers = [ - s for s in cluster if s.get("panel_type", "3x-ui").lower() == "3x-ui" - ] + remnawave_servers = [s for s in cluster if s.get("panel_type", "3x-ui").lower() == "remnawave"] + xui_servers = [s for s in cluster if s.get("panel_type", "3x-ui").lower() == "3x-ui"] remnawave_client_id = None remnawave_key = None if remnawave_servers: - inbound_ids = [ - s["inbound_id"] for s in remnawave_servers if s.get("inbound_id") - ] + inbound_ids = [s["inbound_id"] for s in remnawave_servers if s.get("inbound_id")] remna = RemnawaveAPI(remnawave_servers[0]["api_url"]) if await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD): await remna.delete_user(client_id) @@ -626,7 +558,7 @@ async def update_key_on_cluster( .order_by(Tariff.duration_days.desc()) .limit(1) ) - tariff = result.scalar_one_or_none() + result.scalar_one_or_none() short_uuid = None if remnawave_link and "/" in remnawave_link: @@ -641,7 +573,7 @@ async def update_key_on_cluster( "activeUserInbounds": inbound_ids, } if traffic_limit is not None: - user_data["trafficLimitBytes"] = traffic_limit * 1024 ** 3 + user_data["trafficLimitBytes"] = traffic_limit * 1024**3 if device_limit is not None: user_data["hwidDeviceLimit"] = device_limit if short_uuid: @@ -652,18 +584,14 @@ async def update_key_on_cluster( if result: remnawave_client_id = result.get("uuid") remnawave_key = result.get("subscriptionUrl") - logger.info( - f"[Update] Remnawave: клиент заново создан, новый UUID: {remnawave_client_id}" - ) + logger.info(f"[Update] Remnawave: клиент заново создан, новый UUID: {remnawave_client_id}") else: logger.error("[Update] Ошибка создания Remnawave клиента") else: logger.error("[Update] Не удалось авторизоваться в Remnawave") if not remnawave_client_id: - logger.warning( - f"[Update] Remnawave client_id не получен. Используется исходный: {client_id}" - ) + logger.warning(f"[Update] Remnawave client_id не получен. Используется исходный: {client_id}") remnawave_client_id = client_id tasks = [] @@ -672,9 +600,7 @@ async def update_key_on_cluster( inbound_id = server_info.get("inbound_id") if not inbound_id: - logger.warning( - f"[Update] INBOUND_ID отсутствует для сервера {server_name}. Пропуск." - ) + logger.warning(f"[Update] INBOUND_ID отсутствует для сервера {server_name}. Пропуск.") continue xui = await get_xui_instance(server_info["api_url"]) @@ -692,9 +618,9 @@ async def update_key_on_cluster( .order_by(Tariff.duration_days.desc()) .limit(1) ) - tariff = result.scalar_one_or_none() + result.scalar_one_or_none() - total_gb_bytes = int(traffic_limit * 1024 ** 3) if traffic_limit else 0 + total_gb_bytes = int(traffic_limit * 1024**3) if traffic_limit else 0 device_limit_value = device_limit if device_limit is not None else None config = ClientConfig( @@ -715,15 +641,11 @@ async def update_key_on_cluster( if tasks: await asyncio.gather(*tasks, return_exceptions=True) - logger.info( - f"[Update] Ключ {remnawave_client_id} обновлён на всех серверах кластера {cluster_id}" - ) + logger.info(f"[Update] Ключ {remnawave_client_id} обновлён на всех серверах кластера {cluster_id}") return remnawave_client_id, remnawave_key except Exception as e: - logger.error( - f"[Update Error] Ошибка при обновлении ключа {client_id} на {cluster_id}: {e}" - ) + logger.error(f"[Update Error] Ошибка при обновлении ключа {client_id} на {cluster_id}: {e}") raise @@ -735,9 +657,7 @@ async def update_subscription( country_override: str = None, remnawave_link: str = None, ) -> None: - result = await session.execute( - select(Key).where(Key.tg_id == tg_id, Key.email == email) - ) + result = await session.execute(select(Key).where(Key.tg_id == tg_id, Key.email == email)) record = result.scalar_one_or_none() if not record: @@ -753,9 +673,7 @@ async def update_subscription( traffic_limit = None device_limit = None if tariff_id: - result = await session.execute( - select(Tariff).where(Tariff.id == tariff_id, Tariff.is_active.is_(True)) - ) + result = await session.execute(select(Tariff).where(Tariff.id == tariff_id, Tariff.is_active.is_(True))) tariff = result.scalar_one_or_none() if tariff: traffic_limit = int(tariff.traffic_limit) if tariff.traffic_limit is not None else None @@ -763,7 +681,7 @@ async def update_subscription( else: logger.warning(f"[LOG] update_subscription: тариф с id={tariff_id} не найден!") else: - logger.warning(f"[LOG] update_subscription: tariff_id отсутствует!") + logger.warning("[LOG] update_subscription: tariff_id отсутствует!") await delete_key_from_cluster(old_cluster_id, email, client_id, session=session) await session.execute(delete(Key).where(Key.tg_id == tg_id, Key.email == email)) @@ -787,7 +705,7 @@ async def update_subscription( session=session, traffic_limit=traffic_limit, device_limit=device_limit, - remnawave_link=remnawave_link + remnawave_link=remnawave_link, ) servers = await get_servers(session) @@ -820,18 +738,12 @@ async def update_subscription( ) -async def get_user_traffic( - session: AsyncSession, tg_id: int, email: str -) -> dict[str, Any]: +async def get_user_traffic(session: AsyncSession, tg_id: int, email: str) -> dict[str, Any]: """ Получает трафик пользователя на всех серверах, где у него есть ключ (3x-ui и Remnawave). Для Remnawave трафик считается один раз и отображается как "Remnawave (общий):". """ - result = await session.execute( - select(Key.client_id, Key.server_id).where( - Key.tg_id == tg_id, Key.email == email - ) - ) + result = await session.execute(select(Key.client_id, Key.server_id).where(Key.tg_id == tg_id, Key.email == email)) rows = result.all() if not rows: return {"status": "error", "message": "У пользователя нет активных ключей."} @@ -840,8 +752,7 @@ async def get_user_traffic( result = await session.execute( select(Server).where( - (Server.server_name.in_(server_ids) | Server.cluster_name.in_(server_ids)), - Server.enabled == True + (Server.server_name.in_(server_ids) | Server.cluster_name.in_(server_ids)), Server.enabled is True ) ) server_rows = result.scalars().all() @@ -894,9 +805,7 @@ async def get_user_traffic( server_id = row.server_id matched_servers = [ - s - for s in servers_map.values() - if s["server_name"] == server_id or s["cluster_name"] == server_id + s for s in servers_map.values() if s["server_name"] == server_id or s["cluster_name"] == server_id ] for server_info in matched_servers: panel_type = server_info.get("panel_type", "3x-ui").lower() @@ -940,9 +849,7 @@ async def toggle_client_on_cluster( ) -> dict[str, Any]: try: if session is None: - raise ValueError( - "[Cluster Toggle] Не передан объект сессии для toggle_client_on_cluster" - ) + raise ValueError("[Cluster Toggle] Не передан объект сессии для toggle_client_on_cluster") servers = await get_servers(session) cluster = servers.get(cluster_id) @@ -955,9 +862,7 @@ async def toggle_client_on_cluster( if found_servers: cluster = found_servers else: - raise ValueError( - f"Кластер или сервер с ID/именем '{cluster_id}' не найден." - ) + raise ValueError(f"Кластер или сервер с ID/именем '{cluster_id}' не найден.") results = {} tasks = [] @@ -969,25 +874,19 @@ async def toggle_client_on_cluster( if panel_type == "3x-ui": inbound_id = server_info.get("inbound_id") if not inbound_id: - logger.warning( - f"[3x-ui] INBOUND_ID отсутствует для сервера {server_name}. Пропуск." - ) + logger.warning(f"[3x-ui] INBOUND_ID отсутствует для сервера {server_name}. Пропуск.") results[server_name] = False continue xui = await get_xui_instance(server_info["api_url"]) unique_email = f"{email}_{server_name.lower()}" if SUPERNODE else email - tasks.append( - toggle_client(xui, int(inbound_id), unique_email, client_id, enable) - ) + tasks.append(toggle_client(xui, int(inbound_id), unique_email, client_id, enable)) elif panel_type == "remnawave": remna = RemnawaveAPI(server_info["api_url"]) if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD): - logger.error( - f"[Remnawave] Авторизация не удалась на сервере {server_name}" - ) + logger.error(f"[Remnawave] Авторизация не удалась на сервере {server_name}") results[server_name] = False continue @@ -1005,17 +904,13 @@ async def toggle_client_on_cluster( for server_info, result in zip(cluster, task_results, strict=False): server_name = server_info.get("server_name", "unknown") if isinstance(result, Exception): - logger.error( - f"[Cluster Toggle] Ошибка на сервере {server_name}: {result}" - ) + logger.error(f"[Cluster Toggle] Ошибка на сервере {server_name}: {result}") results[server_name] = False else: results[server_name] = result status = "включен" if enable else "отключен" - logger.info( - f"[Cluster Toggle] Клиент {email} {status} на серверах кластера {cluster_id}" - ) + logger.info(f"[Cluster Toggle] Клиент {email} {status} на серверах кластера {cluster_id}") logger.info(f"[Cluster Toggle DEBUG] Результаты: {results}") return { @@ -1024,15 +919,11 @@ async def toggle_client_on_cluster( } except Exception as e: - logger.error( - f"[Cluster Toggle] Ошибка при изменении состояния клиента {email} в кластере {cluster_id}: {e}" - ) + logger.error(f"[Cluster Toggle] Ошибка при изменении состояния клиента {email} в кластере {cluster_id}: {e}") return {"status": "error", "error": str(e)} -async def reset_traffic_in_cluster( - cluster_id: str, email: str, session: AsyncSession -) -> None: +async def reset_traffic_in_cluster(cluster_id: str, email: str, session: AsyncSession) -> None: try: servers = await get_servers(session) cluster = servers.get(cluster_id) @@ -1046,9 +937,7 @@ async def reset_traffic_in_cluster( if found_servers: cluster = found_servers else: - raise ValueError( - f"Кластер или сервер с ID/именем {cluster_id} не найден." - ) + raise ValueError(f"Кластер или сервер с ID/именем {cluster_id} не найден.") tasks = [] remnawave_done = False @@ -1061,25 +950,19 @@ async def reset_traffic_in_cluster( if panel_type == "remnawave" and not remnawave_done: result = await session.execute( - select(Key.client_id) - .where(Key.email == email, Key.server_id == cluster_id) - .limit(1) + select(Key.client_id).where(Key.email == email, Key.server_id == cluster_id).limit(1) ) row = result.first() if not row: - logger.warning( - f"[Remnawave Reset] client_id не найден для {email} на {server_name}" - ) + logger.warning(f"[Remnawave Reset] client_id не найден для {email} на {server_name}") continue client_id = row[0] remna = RemnawaveAPI(api_url) if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD): - logger.warning( - f"[Reset Traffic] Не удалось авторизоваться в Remnawave ({server_name})" - ) + logger.warning(f"[Reset Traffic] Не удалось авторизоваться в Remnawave ({server_name})") continue tasks.append(remna.reset_user_traffic(client_id)) @@ -1088,26 +971,18 @@ async def reset_traffic_in_cluster( if panel_type == "3x-ui": if not inbound_id: - logger.warning( - f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск." - ) + logger.warning(f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск.") continue xui = await get_xui_instance(api_url) unique_email = f"{email}_{server_name.lower()}" if SUPERNODE else email tasks.append(xui.client.reset_stats(int(inbound_id), unique_email)) else: - logger.warning( - f"[Reset Traffic] Неизвестный тип панели '{panel_type}' на {server_name}" - ) + logger.warning(f"[Reset Traffic] Неизвестный тип панели '{panel_type}' на {server_name}") await asyncio.gather(*tasks, return_exceptions=True) - logger.info( - f"[Reset Traffic] Трафик клиента {email} успешно сброшен в кластере {cluster_id}" - ) + logger.info(f"[Reset Traffic] Трафик клиента {email} успешно сброшен в кластере {cluster_id}") except Exception as e: - logger.error( - f"[Reset Traffic] Ошибка при сбросе трафика клиента {email} в кластере {cluster_id}: {e}" - ) - raise \ No newline at end of file + logger.error(f"[Reset Traffic] Ошибка при сбросе трафика клиента {email} в кластере {cluster_id}: {e}") + raise diff --git a/handlers/keys/key_view.py b/handlers/keys/key_view.py index 00ba27d1..73139abf 100644 --- a/handlers/keys/key_view.py +++ b/handlers/keys/key_view.py @@ -1,10 +1,12 @@ import html import os import re + from datetime import datetime from typing import Any import pytz + from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup @@ -37,11 +39,20 @@ from handlers.buttons import ( MAIN_MENU, PC_BUTTON, QR, + RENEW_SUB, TV_BUTTON, UNFREEZE, - RENEW_SUB ) -from handlers.texts import FROZEN_SUBSCRIPTION_MSG, NO_SUBSCRIPTIONS_MSG, key_message, KEYS_HEADER, KEYS_FOOTER, RENAME_KEY_PROMPT, DAYS_LEFT_MESSAGE, SELECT_SUBS +from handlers.texts import ( + DAYS_LEFT_MESSAGE, + FROZEN_SUBSCRIPTION_MSG, + KEYS_FOOTER, + KEYS_HEADER, + NO_SUBSCRIPTIONS_MSG, + RENAME_KEY_PROMPT, + SELECT_SUBS, + key_message, +) from handlers.utils import ( edit_or_send_message, format_days, @@ -52,6 +63,7 @@ from handlers.utils import ( ) from logger import logger + router = Router() @@ -61,9 +73,7 @@ class RenameKeyState(StatesGroup): @router.callback_query(F.data == "view_keys") @router.message(F.text == "/subs") -async def process_callback_or_message_view_keys( - callback_query_or_message: Message | CallbackQuery, session: Any -): +async def process_callback_or_message_view_keys(callback_query_or_message: Message | CallbackQuery, session: Any): if isinstance(callback_query_or_message, CallbackQuery): target_message = callback_query_or_message.message else: @@ -105,19 +115,13 @@ def build_keys_response(records): key_display = html.escape(alias.strip() if alias else email) if expiry_time: - expiry_date_full = datetime.fromtimestamp( - expiry_time / 1000, tz=moscow_tz - ) + expiry_date_full = datetime.fromtimestamp(expiry_time / 1000, tz=moscow_tz) formatted_date_full = expiry_date_full.strftime("до %d.%m.%y, %H:%M") else: formatted_date_full = "без срока действия" - key_button = InlineKeyboardButton( - text=f"🔑 {key_display}", callback_data=f"view_key|{email}" - ) - rename_button = InlineKeyboardButton( - text=ALIAS, callback_data=f"rename_key|{client_id}" - ) + key_button = InlineKeyboardButton(text=f"🔑 {key_display}", callback_data=f"view_key|{email}") + rename_button = InlineKeyboardButton(text=ALIAS, callback_data=f"rename_key|{client_id}") builder.row(key_button, rename_button) response_message += f"• {key_display} ({formatted_date_full})\n" @@ -139,9 +143,7 @@ async def handle_rename_key(callback: CallbackQuery, state: FSMContext): await state.update_data(client_id=client_id, target_message=callback.message) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text=BACK, callback_data="cancel_and_back_to_view_keys") - ) + builder.row(InlineKeyboardButton(text=BACK, callback_data="cancel_and_back_to_view_keys")) await edit_or_send_message( target_message=callback.message, @@ -151,24 +153,18 @@ async def handle_rename_key(callback: CallbackQuery, state: FSMContext): @router.callback_query(F.data == "cancel_and_back_to_view_keys") -async def cancel_and_back( - callback: CallbackQuery, state: FSMContext, session: AsyncSession -): +async def cancel_and_back(callback: CallbackQuery, state: FSMContext, session: AsyncSession): await state.clear() await callback.answer() await process_callback_or_message_view_keys(callback, session) @router.message(F.text, RenameKeyState.waiting_for_new_alias) -async def handle_new_alias_input( - message: Message, state: FSMContext, session: AsyncSession -): +async def handle_new_alias_input(message: Message, state: FSMContext, session: AsyncSession): alias = message.text.strip() if len(alias) > 10: - await message.answer( - "❌ Имя слишком длинное. Введите до 10 символов.\nПовторите ввод." - ) + await message.answer("❌ Имя слишком длинное. Введите до 10 символов.\nПовторите ввод.") return if not alias or not re.match(r"^[a-zA-Zа-яА-ЯёЁ0-9@._-]+$", alias): @@ -182,9 +178,7 @@ async def handle_new_alias_input( try: await session.execute( - update(Key) - .where(Key.tg_id == message.chat.id, Key.client_id == client_id) - .values(alias=alias) + update(Key).where(Key.tg_id == message.chat.id, Key.client_id == client_id).values(alias=alias) ) await session.commit() @@ -204,9 +198,7 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any) await render_key_info(callback_query.message, session, key_name, image_path) -async def render_key_info( - message: Message, session: Any, key_name: str, image_path: str -): +async def render_key_info(message: Message, session: Any, key_name: str, image_path: str): from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD from panels.remnawave import RemnawaveAPI @@ -225,11 +217,7 @@ async def render_key_info( builder = InlineKeyboardBuilder() if is_frozen: - builder.row( - InlineKeyboardButton( - text=UNFREEZE, callback_data=f"unfreeze_subscription|{key_name}" - ) - ) + builder.row(InlineKeyboardButton(text=UNFREEZE, callback_data=f"unfreeze_subscription|{key_name}")) builder.row(InlineKeyboardButton(text=BACK, callback_data="view_keys")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) await edit_or_send_message( @@ -252,9 +240,13 @@ async def render_key_info( days = total_seconds // 86400 hours = (total_seconds % 86400) // 3600 minutes = (total_seconds % 3600) // 60 - days_left_message = f"⏳ Осталось: {format_days(days)}, {format_hours(hours)}, {format_minutes(minutes)}" + days_left_message = ( + f"⏳ Осталось: {format_days(days)}, {format_hours(hours)}, {format_minutes(minutes)}" + ) - formatted_expiry_date = f"{expiry_date.strftime('%d')} {get_russian_month(expiry_date)} {expiry_date.strftime('%Y')} года" + formatted_expiry_date = ( + f"{expiry_date.strftime('%d')} {get_russian_month(expiry_date)} {expiry_date.strftime('%Y')} года" + ) hwid_count = 0 is_full_remnawave = await is_full_remnawave_cluster(server_name, session) @@ -262,12 +254,7 @@ async def render_key_info( try: servers = await get_servers(session) remna_server = next( - ( - srv - for cl in servers.values() - for srv in cl - if srv.get("panel_type") == "remnawave" - ), + (srv for cl in servers.values() for srv in cl if srv.get("panel_type") == "remnawave"), None, ) if remna_server: @@ -303,7 +290,7 @@ async def render_key_info( tariff_name=tariff_duration, traffic_limit=traffic_limit, device_limit=device_limit, - subgroup_title=subgroup_title + subgroup_title=subgroup_title, ) if ENABLE_UPDATE_SUBSCRIPTION_BUTTON: @@ -315,35 +302,17 @@ async def render_key_info( ) if is_full_remnawave and final_link: - builder.row( - InlineKeyboardButton( - text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link) - ) - ) - builder.row( - InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{key_name}") - ) + builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link))) + builder.row(InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{key_name}")) else: if CONNECT_PHONE_BUTTON: + builder.row(InlineKeyboardButton(text=CONNECT_PHONE, callback_data=f"connect_phone|{key_name}")) builder.row( - InlineKeyboardButton( - text=CONNECT_PHONE, callback_data=f"connect_phone|{key_name}" - ) - ) - builder.row( - InlineKeyboardButton( - text=PC_BUTTON, callback_data=f"connect_pc|{key_name}" - ), - InlineKeyboardButton( - text=TV_BUTTON, callback_data=f"connect_tv|{key_name}" - ), + InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{key_name}"), + InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{key_name}"), ) else: - builder.row( - InlineKeyboardButton( - text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}" - ) - ) + builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}")) if HWID_RESET_BUTTON and hwid_count > 0: builder.row( @@ -362,18 +331,10 @@ async def render_key_info( ) if USE_COUNTRY_SELECTION: - builder.row( - InlineKeyboardButton( - text=CHANGE_LOCATION, callback_data=f"change_location|{key_name}" - ) - ) + builder.row(InlineKeyboardButton(text=CHANGE_LOCATION, callback_data=f"change_location|{key_name}")) if TOGGLE_CLIENT: - builder.row( - InlineKeyboardButton( - text=FREEZE, callback_data=f"freeze_subscription|{key_name}" - ) - ) + builder.row(InlineKeyboardButton(text=FREEZE, callback_data=f"freeze_subscription|{key_name}")) builder.row(InlineKeyboardButton(text=BACK, callback_data="view_keys")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) @@ -399,19 +360,12 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: Any): client_id = record.get("client_id") if not client_id: - await callback_query.answer( - "❌ У ключа отсутствует client_id.", show_alert=True - ) + await callback_query.answer("❌ У ключа отсутствует client_id.", show_alert=True) return servers = await get_servers(session=session) remna_server = next( - ( - srv - for cl in servers.values() - for srv in cl - if srv.get("panel_type") == "remnawave" - ), + (srv for cl in servers.values() for srv in cl if srv.get("panel_type") == "remnawave"), None, ) if not remna_server: @@ -420,9 +374,7 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: Any): api = RemnawaveAPI(remna_server["api_url"]) if not await api.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD): - await callback_query.answer( - "❌ Авторизация в Remnawave не удалась.", show_alert=True - ) + await callback_query.answer("❌ Авторизация в Remnawave не удалась.", show_alert=True) return devices = await api.get_user_hwid_devices(client_id) @@ -433,9 +385,7 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: Any): for device in devices: if await api.delete_user_hwid_device(client_id, device["hwid"]): deleted += 1 - await callback_query.answer( - f"✅ Устройства сброшены ({deleted})", show_alert=True - ) + await callback_query.answer(f"✅ Устройства сброшены ({deleted})", show_alert=True) image_path = os.path.join("img", "pic_view.jpg") await render_key_info(callback_query.message, session, key_name, image_path) @@ -452,7 +402,7 @@ async def process_renew_menu(callback_query_or_message: CallbackQuery | Message, else: target_message = callback_query_or_message tg_id = callback_query_or_message.from_user.id - + records = await get_keys(session, tg_id) servers_dict = await get_servers(session) all_server_names = set() @@ -463,7 +413,7 @@ async def process_renew_menu(callback_query_or_message: CallbackQuery | Message, moscow_tz = pytz.timezone("Europe/Moscow") if records: for record in records: - if getattr(record, 'is_frozen', False): + if getattr(record, "is_frozen", False): continue alias = record.alias email = record.email diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index cfca39ff..eff7d1ac 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -13,13 +13,12 @@ from handlers.texts import DELETE_KEY_CONFIRM_MSG, KEY_DELETED_MSG_SIMPLE from handlers.utils import edit_or_send_message, handle_error from logger import logger + router = Router() @router.callback_query(F.data.startswith("update_subscription|")) -async def process_callback_update_subscription( - callback_query: CallbackQuery, session: AsyncSession -): +async def process_callback_update_subscription(callback_query: CallbackQuery, session: AsyncSession): tg_id = callback_query.message.chat.id email = callback_query.data.split("|")[1] @@ -34,9 +33,7 @@ async def process_callback_update_subscription( await process_callback_view_key(callback_query, session) except Exception as e: logger.error(f"Ошибка при обновлении ключа {email} пользователем: {e}") - await handle_error( - tg_id, callback_query, f"Ошибка при обновлении подписки: {e}" - ) + await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}") @router.callback_query(F.data.startswith("delete_key|")) @@ -45,11 +42,7 @@ async def process_callback_delete_key(callback_query: CallbackQuery): try: confirmation_keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ - [ - types.InlineKeyboardButton( - text=APPLY, callback_data=f"confirm_delete|{client_id}" - ) - ], + [types.InlineKeyboardButton(text=APPLY, callback_data=f"confirm_delete|{client_id}")], [types.InlineKeyboardButton(text=CANCEL, callback_data="view_keys")], ] ) @@ -59,18 +52,14 @@ async def process_callback_delete_key(callback_query: CallbackQuery): caption=DELETE_KEY_CONFIRM_MSG, reply_markup=confirmation_keyboard ) else: - await callback_query.message.edit_text( - text=DELETE_KEY_CONFIRM_MSG, reply_markup=confirmation_keyboard - ) + await callback_query.message.edit_text(text=DELETE_KEY_CONFIRM_MSG, reply_markup=confirmation_keyboard) except Exception as e: logger.error(f"Ошибка при подготовке удаления ключа {client_id}: {e}") @router.callback_query(F.data.startswith("confirm_delete|")) -async def process_callback_confirm_delete( - callback_query: CallbackQuery, session: AsyncSession -): +async def process_callback_confirm_delete(callback_query: CallbackQuery, session: AsyncSession): email = callback_query.data.split("|")[1] try: record = await get_key_details(session, email) @@ -78,9 +67,7 @@ async def process_callback_confirm_delete( client_id = record["client_id"] server_id = record["server_id"] response_message = KEY_DELETED_MSG_SIMPLE - back_button = types.InlineKeyboardButton( - text=BACK, callback_data="view_keys" - ) + back_button = types.InlineKeyboardButton(text=BACK, callback_data="view_keys") keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) await delete_key(session, client_id) @@ -94,15 +81,11 @@ async def process_callback_confirm_delete( try: await delete_key_from_cluster(server_id, email, client_id, session) except Exception as e: - logger.error( - f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}" - ) + logger.error(f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}") else: response_message = "Ключ не найден или уже удален." - back_button = types.InlineKeyboardButton( - text=BACK, callback_data="view_keys" - ) + back_button = types.InlineKeyboardButton(text=BACK, callback_data="view_keys") keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) await edit_or_send_message( target_message=callback_query.message, diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 6a84c51a..a1c2a433 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -6,6 +6,7 @@ import time import urllib.parse import aiohttp + from aiohttp import web from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -15,19 +16,17 @@ from config import ( RANDOM_SUBSCRIPTIONS, SUPERNODE, SUPPORT_CHAT_URL, - USE_COUNTRY_SELECTION, USERNAME_BOT, + USE_COUNTRY_SELECTION, ) from database import get_key_details, get_servers from database.models import Server +from handlers.texts import HAPP_ANNOUNCE, HIDDIFY_PROFILE_TITLE, SUBSCRIPTION_INFO_TEXT, V2RAYTUN_ANNOUNCE from handlers.utils import convert_to_bytes -from handlers.texts import SUBSCRIPTION_INFO_TEXT, HAPP_ANNOUNCE, V2RAYTUN_ANNOUNCE, HIDDIFY_PROFILE_TITLE from logger import logger -async def fetch_url_content( - url: str, identifier: str -) -> tuple[list[str], dict[str, str]]: +async def fetch_url_content(url: str, identifier: str) -> tuple[list[str], dict[str, str]]: try: timeout = aiohttp.ClientTimeout(total=5) async with aiohttp.ClientSession(timeout=timeout) as session: @@ -36,9 +35,7 @@ async def fetch_url_content( content = await response.text() lines = base64.b64decode(content).decode("utf-8").split("\n") headers = {k.lower(): v for k, v in response.headers.items()} - logger.debug( - f"Fetched {url}: {len(lines)} lines, headers: {headers}" - ) + logger.debug(f"Fetched {url}: {len(lines)} lines, headers: {headers}") return lines, headers return [], {} except Exception as e: @@ -50,9 +47,7 @@ async def combine_unique_lines( urls: list[str], identifier: str, query_string: str ) -> tuple[list[str], list[dict[str, str]]]: if SUPERNODE: - logger.info( - f"Режим SUPERNODE активен. Возвращаем первую ссылку для идентификатора: {identifier}" - ) + logger.info(f"Режим SUPERNODE активен. Возвращаем первую ссылку для идентификатора: {identifier}") if not urls: return [], [] url_with_query = f"{urls[0]}?{query_string}" if query_string else urls[0] @@ -78,7 +73,7 @@ async def get_subscription_urls( urls = [] if USE_COUNTRY_SELECTION: result = await session.execute( - select(Server.subscription_url).where(Server.server_name == server_id, Server.enabled == True) + select(Server.subscription_url).where(Server.server_name == server_id, Server.enabled is True) ) server_data = result.scalar() if server_data: @@ -101,9 +96,7 @@ def calculate_traffic( expiry_time_ms: int | None, headers_list: list[dict[str, str]], ) -> str: - logger.debug( - f"Calculating traffic with subscriptions: {cleaned_subscriptions}, headers: {headers_list}" - ) + logger.debug(f"Calculating traffic with subscriptions: {cleaned_subscriptions}, headers: {headers_list}") expire_timestamp = int(expiry_time_ms / 1000) if expiry_time_ms else 0 upload = 0 @@ -144,9 +137,7 @@ def calculate_traffic( country_remaining[country] = remaining_bytes logger.debug(f"Found traffic: {value}{unit} for {country}") - consumed_traffic_bytes = ( - total - sum(country_remaining.values()) if country_remaining else download - ) + consumed_traffic_bytes = total - sum(country_remaining.values()) if country_remaining else download if consumed_traffic_bytes < 0: consumed_traffic_bytes = 0 download = max(download, consumed_traffic_bytes) @@ -201,11 +192,9 @@ def prepare_headers( "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", "profile-update-interval": "3", - "profile-title": "base64:" - + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), + "profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), "support-url": SUPPORT_CHAT_URL, - "announce": "base64:" - + base64.b64encode(announce_str.encode("utf-8")).decode("utf-8"), + "announce": "base64:" + base64.b64encode(announce_str.encode("utf-8")).decode("utf-8"), "profile-web-page-url": f"https://t.me/{USERNAME_BOT}", "subscription-userinfo": subscription_userinfo, } @@ -215,8 +204,7 @@ def prepare_headers( encoded_project_name = HIDDIFY_PROFILE_TITLE.format(project_name=project_name, key_info=key_info) return { "profile-update-interval": "3", - "profile-title": "base64:" - + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), + "profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), "subscription-userinfo": subscription_userinfo, } elif "v2raytun" in user_agent: @@ -226,11 +214,9 @@ def prepare_headers( "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", "update-always": "true", - "announce": "base64:" - + base64.b64encode(announce_str.encode("utf-8")).decode("utf-8"), + "announce": "base64:" + base64.b64encode(announce_str.encode("utf-8")).decode("utf-8"), "announce-url": f"{SUPPORT_CHAT_URL}", - "profile-title": "base64:" - + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), + "profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), "subscription-userinfo": subscription_userinfo, } else: @@ -239,8 +225,7 @@ def prepare_headers( "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", "profile-update-interval": "3", - "profile-title": "base64:" - + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), + "profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), } @@ -257,14 +242,10 @@ async def handle_subscription(request: web.Request) -> web.Response: try: key = await get_key_details(session, email) if not key: - return web.Response( - text="❌ Клиент с таким email не найден.", status=404 - ) + return web.Response(text="❌ Клиент с таким email не найден.", status=404) if int(tg_id) != int(key["tg_id"]): - return web.Response( - text="❌ Неверные данные. Получите свой ключ в боте.", status=403 - ) + return web.Response(text="❌ Неверные данные. Получите свой ключ в боте.", status=403) expiry_time_ms = key["expiry_time"] server_id = key["server_id"] @@ -272,38 +253,26 @@ async def handle_subscription(request: web.Request) -> web.Response: time_left = format_time_left(expiry_time_ms) - urls = await get_subscription_urls( - server_id, email, session, include_remnawave_key=remnawave_link - ) + urls = await get_subscription_urls(server_id, email, session, include_remnawave_key=remnawave_link) if not urls: return web.Response(text="❌ Сервер не найден.", status=404) query_string = request.query_string - combined_subscriptions, headers_list = await combine_unique_lines( - urls, tg_id or email, query_string - ) + combined_subscriptions, headers_list = await combine_unique_lines(urls, tg_id or email, query_string) if RANDOM_SUBSCRIPTIONS: random.shuffle(combined_subscriptions) - cleaned_subscriptions = [ - clean_subscription_line(line) for line in combined_subscriptions - ] + cleaned_subscriptions = [clean_subscription_line(line) for line in combined_subscriptions] - base64_encoded = base64.b64encode( - "\n".join(cleaned_subscriptions).encode("utf-8") - ).decode("utf-8") + base64_encoded = base64.b64encode("\n".join(cleaned_subscriptions).encode("utf-8")).decode("utf-8") subscription_info = SUBSCRIPTION_INFO_TEXT.format(email=email, time_left=time_left) user_agent = request.headers.get("User-Agent", "") - subscription_userinfo = calculate_traffic( - cleaned_subscriptions, expiry_time_ms, headers_list - ) - headers = prepare_headers( - user_agent, PROJECT_NAME, subscription_info, subscription_userinfo - ) + subscription_userinfo = calculate_traffic(cleaned_subscriptions, expiry_time_ms, headers_list) + headers = prepare_headers(user_agent, PROJECT_NAME, subscription_info, subscription_userinfo) return web.Response(text=base64_encoded, headers=headers) except Exception as e: logger.error(f"Ошибка в handle_subscription: {e}", exc_info=True) - return web.Response(text=f"❌ Ошибка сервера: {e}", status=500) \ No newline at end of file + return web.Response(text=f"❌ Ошибка сервера: {e}", status=500) diff --git a/handlers/notifications/__init__.py b/handlers/notifications/__init__.py index ee8f833d..6b90acab 100644 --- a/handlers/notifications/__init__.py +++ b/handlers/notifications/__init__.py @@ -5,6 +5,7 @@ from aiogram import Router from .general_notifications import router as general_notifications_router from .special_notifications import router as special_notifications_router + router = Router(name="notifications_main_router") router.include_routers(general_notifications_router, special_notifications_router) diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index 643ac368..1b3a387f 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -1,7 +1,9 @@ import asyncio + from datetime import datetime, timedelta import pytz + from aiogram import Bot, Router from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker @@ -19,17 +21,17 @@ from database import ( add_notification, check_notification_time, check_notifications_bulk, + check_tariff_exists, delete_key, delete_notification, get_all_keys, get_balance, get_last_notification_time, + get_tariff_by_id, get_tariffs_for_cluster, update_balance, update_key_expiry, update_key_tariff, - check_tariff_exists, - get_tariff_by_id, ) from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster from handlers.notifications.notify_kb import ( @@ -44,13 +46,14 @@ from handlers.texts import ( KEY_EXPIRY_24H, get_renewal_message, ) -from handlers.utils import format_hours, format_minutes, get_russian_month, format_months, format_days +from handlers.utils import format_days, format_hours, format_minutes, format_months, get_russian_month from logger import logger from .hot_leads_notifications import notify_hot_leads from .notify_utils import send_messages_with_limit, send_notification from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic + router = Router() moscow_tz = pytz.timezone("Europe/Moscow") notification_lock = asyncio.Lock() @@ -69,13 +72,8 @@ async def periodic_notifications(bot: Bot, *, sessionmaker: async_sessionmaker): logger.info("🔔 Запуск обработки уведомлений") current_time = int(datetime.now(moscow_tz).timestamp() * 1000) - threshold_10h = int( - (datetime.now(moscow_tz) + timedelta(hours=10)).timestamp() - * 1000 - ) - threshold_24h = int( - (datetime.now(moscow_tz) + timedelta(days=1)).timestamp() * 1000 - ) + threshold_10h = int((datetime.now(moscow_tz) + timedelta(hours=10)).timestamp() * 1000) + threshold_24h = int((datetime.now(moscow_tz) + timedelta(days=1)).timestamp() * 1000) try: keys = await get_all_keys(session=session) @@ -91,16 +89,12 @@ async def periodic_notifications(bot: Bot, *, sessionmaker: async_sessionmaker): logger.error(f"Ошибка в notify_inactive_trial_users: {e}") try: - await notify_24h_keys( - bot, session, current_time, threshold_24h, keys - ) + await notify_24h_keys(bot, session, current_time, threshold_24h, keys) except Exception as e: logger.error(f"Ошибка в notify_24h_keys: {e}") try: - await notify_10h_keys( - bot, session, current_time, threshold_10h, keys - ) + await notify_10h_keys(bot, session, current_time, threshold_10h, keys) except Exception as e: logger.error(f"Ошибка в notify_10h_keys: {e}") @@ -111,9 +105,7 @@ async def periodic_notifications(bot: Bot, *, sessionmaker: async_sessionmaker): if NOTIFY_INACTIVE_TRAFFIC: try: - await notify_users_no_traffic( - bot, session, current_time, keys - ) + await notify_users_no_traffic(bot, session, current_time, keys) except Exception as e: logger.error(f"Ошибка в notify_users_no_traffic: {e}") @@ -142,19 +134,13 @@ async def notify_24h_keys( """ logger.info("Начало проверки подписок, истекающих через 24 часа.") - expiring_keys = [ - key - for key in keys - if key.expiry_time and current_time < key.expiry_time <= threshold_time_24h - ] + expiring_keys = [key for key in keys if key.expiry_time and current_time < key.expiry_time <= threshold_time_24h] logger.info(f"Найдено {len(expiring_keys)} подписок, истекающих через 24 часа.") tg_ids = [key["tg_id"] for key in expiring_keys] emails = [key.email or "" for key in expiring_keys] - users = await check_notifications_bulk( - session, "key_24h", 24, tg_ids=tg_ids, emails=emails - ) + users = await check_notifications_bulk(session, "key_24h", 24, tg_ids=tg_ids, emails=emails) messages = [] @@ -163,24 +149,18 @@ async def notify_24h_keys( email = key.email or "" notification_id = f"{email}_key_24h" - can_notify = await check_notification_time( - session, tg_id, notification_id, hours=24 - ) + can_notify = await check_notification_time(session, tg_id, notification_id, hours=24) if not can_notify: continue - user = next( - (u for u in users if u["tg_id"] == tg_id and u["email"] == email), None - ) + user = next((u for u in users if u["tg_id"] == tg_id and u["email"] == email), None) if not user: continue expiry_timestamp = key.expiry_time hours_left = int((expiry_timestamp - current_time) / (1000 * 3600)) hours_left_formatted = ( - f"⏳ Осталось времени: {format_hours(hours_left)}" - if hours_left > 0 - else "⏳ Последний день подписки!" + f"⏳ Осталось времени: {format_hours(hours_left)}" if hours_left > 0 else "⏳ Последний день подписки!" ) expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz) @@ -204,22 +184,18 @@ async def notify_24h_keys( notification_text, ) except Exception as e: - logger.error( - f"Ошибка авто-продления/уведомления для пользователя {tg_id}: {e}" - ) + logger.error(f"Ошибка авто-продления/уведомления для пользователя {tg_id}: {e}") continue else: keyboard = build_notification_kb(email) - messages.append( - { - "tg_id": tg_id, - "text": notification_text, - "photo": "notify_24h.jpg", - "keyboard": keyboard, - "notification_id": notification_id, - "email": email, - } - ) + messages.append({ + "tg_id": tg_id, + "text": notification_text, + "photo": "notify_24h.jpg", + "keyboard": keyboard, + "notification_id": notification_id, + "email": email, + }) if messages: results = await send_messages_with_limit(bot, messages, session=session) @@ -229,16 +205,12 @@ async def notify_24h_keys( if result: await add_notification(session, tg_id, msg["notification_id"]) sent_count += 1 - logger.info( - f"📢 Отправлено уведомление об истекающей подписке {msg['email']} пользователю {tg_id}." - ) + logger.info(f"📢 Отправлено уведомление об истекающей подписке {msg['email']} пользователю {tg_id}.") else: logger.warning( f"📢 Не удалось отправить уведомление об истекающей подписке {msg['email']} пользователю {tg_id}." ) - logger.info( - f"Отправлено {sent_count} уведомлений об истечении подписки через 24 часа." - ) + logger.info(f"Отправлено {sent_count} уведомлений об истечении подписки через 24 часа.") logger.info("Обработка всех уведомлений за 24 часа завершена.") await asyncio.sleep(1) @@ -253,19 +225,13 @@ async def notify_10h_keys( ): logger.info("Начало проверки подписок, истекающих через 10 часов.") - expiring_keys = [ - key - for key in keys - if key.expiry_time and current_time < key.expiry_time <= threshold_time_10h - ] + expiring_keys = [key for key in keys if key.expiry_time and current_time < key.expiry_time <= threshold_time_10h] logger.info(f"Найдено {len(expiring_keys)} подписок, истекающих через 10 часов.") tg_ids = [key.tg_id for key in expiring_keys] emails = [key.email or "" for key in expiring_keys] - users = await check_notifications_bulk( - session, "key_10h", 10, tg_ids=tg_ids, emails=emails - ) + users = await check_notifications_bulk(session, "key_10h", 10, tg_ids=tg_ids, emails=emails) messages = [] for key in expiring_keys: @@ -273,24 +239,18 @@ async def notify_10h_keys( email = key.email or "" notification_id = f"{email}_key_10h" - can_notify = await check_notification_time( - session, tg_id, notification_id, hours=10 - ) + can_notify = await check_notification_time(session, tg_id, notification_id, hours=10) if not can_notify: continue - user = next( - (u for u in users if u["tg_id"] == tg_id and u["email"] == email), None - ) + user = next((u for u in users if u["tg_id"] == tg_id and u["email"] == email), None) if not user: continue expiry_timestamp = key.expiry_time hours_left = int((expiry_timestamp - current_time) / (1000 * 3600)) hours_left_formatted = ( - f"⏳ Осталось времени: {format_hours(hours_left)}" - if hours_left > 0 - else "⏳ Последний день подписки!" + f"⏳ Осталось времени: {format_hours(hours_left)}" if hours_left > 0 else "⏳ Последний день подписки!" ) expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz) @@ -314,22 +274,18 @@ async def notify_10h_keys( notification_text, ) except Exception as e: - logger.error( - f"Ошибка авто-продления/уведомления для пользователя {tg_id}: {e}" - ) + logger.error(f"Ошибка авто-продления/уведомления для пользователя {tg_id}: {e}") continue else: keyboard = build_notification_kb(email) - messages.append( - { - "tg_id": tg_id, - "text": notification_text, - "photo": "notify_10h.jpg", - "keyboard": keyboard, - "notification_id": notification_id, - "email": email, - } - ) + messages.append({ + "tg_id": tg_id, + "text": notification_text, + "photo": "notify_10h.jpg", + "keyboard": keyboard, + "notification_id": notification_id, + "email": email, + }) if messages: results = await send_messages_with_limit(bot, messages, session=session) @@ -339,16 +295,12 @@ async def notify_10h_keys( if result: await add_notification(session, tg_id, msg["notification_id"]) sent_count += 1 - logger.info( - f"📢 Отправлено уведомление об истекающей подписке {msg['email']} пользователю {tg_id}." - ) + logger.info(f"📢 Отправлено уведомление об истекающей подписке {msg['email']} пользователю {tg_id}.") else: logger.warning( f"📢 Не удалось отправить уведомление об истекающей подписке {msg['email']} пользователю {tg_id}." ) - logger.info( - f"Отправлено {sent_count} уведомлений об истечении подписки через 10 часов." - ) + logger.info(f"Отправлено {sent_count} уведомлений об истечении подписки через 10 часов.") logger.info("Обработка всех уведомлений за 10 часов завершена.") await asyncio.sleep(1) @@ -362,16 +314,12 @@ async def handle_expired_keys( ): logger.info("Начало обработки истекших ключей.") - expired_keys = [ - key for key in keys if key.expiry_time and key.expiry_time < current_time - ] + expired_keys = [key for key in keys if key.expiry_time and key.expiry_time < current_time] logger.info(f"Найдено {len(expired_keys)} истекших ключей.") tg_ids = [key.tg_id for key in expired_keys] emails = [key.email or "" for key in expired_keys] - users = await check_notifications_bulk( - session, "key_expired", 0, tg_ids=tg_ids, emails=emails - ) + users = await check_notifications_bulk(session, "key_expired", 0, tg_ids=tg_ids, emails=emails) messages = [] @@ -382,9 +330,7 @@ async def handle_expired_keys( server_id = key.server_id notification_id = f"{email}_key_expired" - last_notification_time = await get_last_notification_time( - session, tg_id, notification_id - ) + last_notification_time = await get_last_notification_time(session, tg_id, notification_id) if NOTIFY_RENEW_EXPIRED: try: @@ -404,7 +350,7 @@ async def handle_expired_keys( tariff_name=tariff.get("name", ""), traffic_limit=tariff.get("traffic_limit") if tariff.get("traffic_limit") is not None else 0, device_limit=tariff.get("device_limit") if tariff.get("device_limit") is not None else 0, - subgroup_title=tariff.get("subgroup_title", "") + subgroup_title=tariff.get("subgroup_title", ""), ), ) continue @@ -417,9 +363,7 @@ async def handle_expired_keys( delete_after_delay = False if last_notification_time is not None: - delete_after_delay = ( - current_time - last_notification_time - ) / (1000 * 60) >= NOTIFY_DELETE_DELAY + delete_after_delay = (current_time - last_notification_time) / (1000 * 60) >= NOTIFY_DELETE_DELAY logger.info( f"Прошло минут={(current_time - last_notification_time) / (1000 * 60):.2f} " f"NOTIFY_DELETE_DELAY={NOTIFY_DELETE_DELAY}" @@ -429,30 +373,22 @@ async def handle_expired_keys( try: await delete_key_from_cluster(server_id, email, client_id, session) await delete_key(session, client_id) - logger.info( - f"🗑 Ключ {client_id} для пользователя {tg_id} успешно удалён." - ) + logger.info(f"🗑 Ключ {client_id} для пользователя {tg_id} успешно удалён.") keyboard = build_notification_expired_kb() - messages.append( - { - "tg_id": tg_id, - "text": KEY_DELETED_MSG.format(email=email), - "photo": "notify_expired.jpg", - "keyboard": keyboard, - "notification_id": notification_id, - "email": email, - } - ) + messages.append({ + "tg_id": tg_id, + "text": KEY_DELETED_MSG.format(email=email), + "photo": "notify_expired.jpg", + "keyboard": keyboard, + "notification_id": notification_id, + "email": email, + }) except Exception as e: - logger.error( - f"Ошибка удаления ключа {client_id} для пользователя {tg_id}: {e}" - ) + logger.error(f"Ошибка удаления ключа {client_id} для пользователя {tg_id}: {e}") continue - if last_notification_time is None and any( - u["tg_id"] == tg_id and u["email"] == email for u in users - ): + if last_notification_time is None and any(u["tg_id"] == tg_id and u["email"] == email for u in users): keyboard = build_notification_kb(email) if NOTIFY_DELETE_DELAY > 0: @@ -464,24 +400,19 @@ async def handle_expired_keys( time_formatted = format_hours(hours) else: time_formatted = format_minutes(minutes) - - delay_message = KEY_EXPIRED_DELAY_MSG.format( - email=email, - time_formatted=time_formatted - ) + + delay_message = KEY_EXPIRED_DELAY_MSG.format(email=email, time_formatted=time_formatted) else: delay_message = KEY_EXPIRED_NO_DELAY_MSG.format(email=email) - messages.append( - { - "tg_id": tg_id, - "text": delay_message, - "photo": "notify_expired.jpg", - "keyboard": keyboard, - "notification_id": notification_id, - "email": email, - } - ) + messages.append({ + "tg_id": tg_id, + "text": delay_message, + "photo": "notify_expired.jpg", + "keyboard": keyboard, + "notification_id": notification_id, + "email": email, + }) if messages: results = await send_messages_with_limit(bot, messages, session=session) @@ -490,9 +421,7 @@ async def handle_expired_keys( await add_notification(session, msg["tg_id"], msg["notification_id"]) if result: sent_count += 1 - logger.info( - f"📢 Уведомление об истекшем ключе {msg['email']} отправлено пользователю {msg['tg_id']}." - ) + logger.info(f"📢 Уведомление об истекшем ключе {msg['email']} отправлено пользователю {msg['tg_id']}.") else: logger.warning( f"📢 Не удалось отправить уведомление об истекшем ключе {msg['email']} пользователю {msg['tg_id']}." @@ -518,9 +447,7 @@ async def process_auto_renew_or_notify( renew_notification_id = f"{email}_renew" try: - can_renew = await check_notification_time( - conn, tg_id, renew_notification_id, hours=24 - ) + can_renew = await check_notification_time(conn, tg_id, renew_notification_id, hours=24) if not can_renew: logger.info( f"⏳ Подписка {email} уже продлевалась в течение последних 24 часов, повторное продление отменено." @@ -533,9 +460,7 @@ async def process_auto_renew_or_notify( tariffs = await get_tariffs_for_cluster(conn, server_id) if not tariffs: - logger.warning( - f"⛔ Нет доступных тарифов для продления подписки {email} (сервер: {server_id})" - ) + logger.warning(f"⛔ Нет доступных тарифов для продления подписки {email} (сервер: {server_id})") return selected_tariff = None @@ -573,15 +498,13 @@ async def process_auto_renew_or_notify( if not selected_tariff: keyboard = build_notification_kb(email) await add_notification(conn, tg_id, notification_id) - await send_notification( - bot, tg_id, standard_photo, standard_caption, keyboard - ) + await send_notification(bot, tg_id, standard_photo, standard_caption, keyboard) return client_id = key.client_id current_expiry = key.expiry_time duration_days = selected_tariff["duration_days"] - tariff_duration = selected_tariff["name"] + selected_tariff["name"] renewal_cost = selected_tariff["price_rub"] traffic_limit = selected_tariff["traffic_limit"] device_limit = selected_tariff["device_limit"] @@ -593,13 +516,11 @@ async def process_auto_renew_or_notify( else datetime.utcnow().timestamp() * 1000 ) + duration_days * 24 * 60 * 60 * 1000 - formatted_expiry_date = datetime.fromtimestamp( - new_expiry_time / 1000, tz=moscow_tz - ).strftime("%d %B %Y, %H:%M") + formatted_expiry_date = datetime.fromtimestamp(new_expiry_time / 1000, tz=moscow_tz).strftime("%d %B %Y, %H:%M") formatted_expiry_date = formatted_expiry_date.replace( datetime.fromtimestamp(new_expiry_time / 1000, tz=moscow_tz).strftime("%B"), - get_russian_month(datetime.fromtimestamp(new_expiry_time / 1000, tz=moscow_tz)) + get_russian_month(datetime.fromtimestamp(new_expiry_time / 1000, tz=moscow_tz)), ) logger.info( @@ -613,7 +534,7 @@ async def process_auto_renew_or_notify( new_expiry_time=int(new_expiry_time), total_gb=total_gb, hwid_device_limit=device_limit, - session=conn + session=conn, ) await update_balance(conn, tg_id, -renewal_cost) await update_key_expiry(conn, client_id, int(new_expiry_time)) @@ -623,25 +544,20 @@ async def process_auto_renew_or_notify( renewed_message = get_renewal_message( tariff_name=selected_tariff["name"], - traffic_limit=selected_tariff.get("traffic_limit") if selected_tariff.get("traffic_limit") is not None else 0, + traffic_limit=selected_tariff.get("traffic_limit") + if selected_tariff.get("traffic_limit") is not None + else 0, device_limit=selected_tariff.get("device_limit") if selected_tariff.get("device_limit") is not None else 0, expiry_date=formatted_expiry_date, - subgroup_title=selected_tariff.get("subgroup_title", "") + subgroup_title=selected_tariff.get("subgroup_title", ""), ) keyboard = build_notification_expired_kb() - result = await send_notification( - bot, tg_id, "notify_expired.jpg", renewed_message, keyboard - ) + result = await send_notification(bot, tg_id, "notify_expired.jpg", renewed_message, keyboard) if result: - logger.info( - f"✅ Уведомление о продлении подписки {email} отправлено пользователю {tg_id}." - ) + logger.info(f"✅ Уведомление о продлении подписки {email} отправлено пользователю {tg_id}.") else: - logger.warning( - f"📢 Не удалось отправить уведомление о продлении подписки {email} пользователю {tg_id}." - ) + logger.warning(f"📢 Не удалось отправить уведомление о продлении подписки {email} пользователю {tg_id}.") except Exception as e: logger.error(f"❌ Ошибка в process_auto_renew_or_notify: {e}") - \ No newline at end of file diff --git a/handlers/notifications/hot_leads_notifications.py b/handlers/notifications/hot_leads_notifications.py index 0c3995f5..fd1cd94d 100644 --- a/handlers/notifications/hot_leads_notifications.py +++ b/handlers/notifications/hot_leads_notifications.py @@ -27,11 +27,7 @@ async def notify_hot_leads(bot: Bot, session: AsyncSession): for tg_id in leads: has_step_1 = await session.scalar( - select( - select(Notification) - .filter_by(tg_id=tg_id, notification_type="hot_lead_step_1") - .exists() - ) + select(select(Notification).filter_by(tg_id=tg_id, notification_type="hot_lead_step_1").exists()) ) if not has_step_1: await add_notification(session, tg_id, "hot_lead_step_1") @@ -39,11 +35,7 @@ async def notify_hot_leads(bot: Bot, session: AsyncSession): continue has_step_2 = await session.scalar( - select( - select(Notification) - .filter_by(tg_id=tg_id, notification_type="hot_lead_step_2") - .exists() - ) + select(select(Notification).filter_by(tg_id=tg_id, notification_type="hot_lead_step_2").exists()) ) if not has_step_2: can_send = await check_notification_time( @@ -56,9 +48,7 @@ async def notify_hot_leads(bot: Bot, session: AsyncSession): continue keyboard = build_hot_lead_kb() - result = await send_notification( - bot, tg_id, None, HOT_LEAD_MESSAGE, keyboard - ) + result = await send_notification(bot, tg_id, None, HOT_LEAD_MESSAGE, keyboard) if result: await add_notification(session, tg_id, "hot_lead_step_2") logger.info(f"🔥 Шаг 2 — отправлено первое уведомление: {tg_id}") @@ -66,17 +56,11 @@ async def notify_hot_leads(bot: Bot, session: AsyncSession): continue has_step_3 = await session.scalar( - select( - select(Notification) - .filter_by(tg_id=tg_id, notification_type="hot_lead_step_3") - .exists() - ) + select(select(Notification).filter_by(tg_id=tg_id, notification_type="hot_lead_step_3").exists()) ) has_expired_notification = await session.scalar( select( - select(Notification) - .filter_by(tg_id=tg_id, notification_type="hot_lead_step_2_expired") - .exists() + select(Notification).filter_by(tg_id=tg_id, notification_type="hot_lead_step_2_expired").exists() ) ) if not has_step_3 and not has_expired_notification: @@ -88,20 +72,12 @@ async def notify_hot_leads(bot: Bot, session: AsyncSession): ) if expired: builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text=MAIN_MENU, callback_data="profile") - ) + builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) - result = await send_notification( - bot, tg_id, None, HOT_LEAD_LOST_OPPORTUNITY, builder.as_markup() - ) + result = await send_notification(bot, tg_id, None, HOT_LEAD_LOST_OPPORTUNITY, builder.as_markup()) if result: - await add_notification( - session, tg_id, "hot_lead_step_2_expired" - ) - logger.info( - f"📭 Скидка упущена — отправлено уведомление: {tg_id}" - ) + await add_notification(session, tg_id, "hot_lead_step_2_expired") + logger.info(f"📭 Скидка упущена — отправлено уведомление: {tg_id}") continue if not has_step_3: @@ -115,9 +91,7 @@ async def notify_hot_leads(bot: Bot, session: AsyncSession): continue keyboard = build_hot_lead_kb(final=True) - result = await send_notification( - bot, tg_id, None, HOT_LEAD_FINAL_MESSAGE, keyboard - ) + result = await send_notification(bot, tg_id, None, HOT_LEAD_FINAL_MESSAGE, keyboard) if result: await add_notification(session, tg_id, "hot_lead_step_3") logger.info(f"⚡ Шаг 3 — отправлено финальное уведомление: {tg_id}") diff --git a/handlers/notifications/notify_kb.py b/handlers/notifications/notify_kb.py index 030355fd..4e6bf097 100644 --- a/handlers/notifications/notify_kb.py +++ b/handlers/notifications/notify_kb.py @@ -35,18 +35,14 @@ def build_hot_lead_kb(final: bool = False) -> InlineKeyboardMarkup: [ InlineKeyboardButton( text=DISCOUNT_TARIFF if not final else MAX_DISCOUNT_TARIFF, - callback_data=( - "hot_lead_discount" if not final else "hot_lead_final_discount" - ), + callback_data=("hot_lead_discount" if not final else "hot_lead_final_discount"), ) ] ] ) -def build_tariffs_keyboard( - tariffs: list[dict], prefix: str = "tariff" -) -> InlineKeyboardMarkup: +def build_tariffs_keyboard(tariffs: list[dict], prefix: str = "tariff") -> InlineKeyboardMarkup: buttons = [ [ InlineKeyboardButton( diff --git a/handlers/notifications/notify_utils.py b/handlers/notifications/notify_utils.py index 94883d57..7800903b 100644 --- a/handlers/notifications/notify_utils.py +++ b/handlers/notifications/notify_utils.py @@ -2,6 +2,7 @@ import asyncio import os import aiofiles + from aiogram import Bot from aiogram.exceptions import ( TelegramBadRequest, @@ -32,10 +33,7 @@ async def send_messages_with_limit( for i in range(0, len(messages), batch_size): batch = messages[i : i + batch_size] tasks = [ - send_notification( - bot, msg["tg_id"], msg.get("photo"), msg["text"], msg.get("keyboard") - ) - for msg in batch + send_notification(bot, msg["tg_id"], msg.get("photo"), msg["text"], msg.get("keyboard")) for msg in batch ] batch_results = await asyncio.gather(*tasks, return_exceptions=True) @@ -48,17 +46,12 @@ async def send_messages_with_limit( logger.warning(f"🚫 Бот заблокирован пользователем {tg_id}.") await try_add_blocked_user(tg_id, session, source_file) results.append(False) - elif ( - isinstance(result, TelegramBadRequest) - and "chat not found" in str(result).lower() - ): + elif isinstance(result, TelegramBadRequest) and "chat not found" in str(result).lower(): logger.warning(f"🚫 Чат не найден для пользователя {tg_id}.") await try_add_blocked_user(tg_id, session, source_file) results.append(False) else: - logger.warning( - f"📩 Не удалось отправить уведомление пользователю {tg_id}." - ) + logger.warning(f"📩 Не удалось отправить уведомление пользователю {tg_id}.") await try_add_blocked_user(tg_id, session, source_file) results.append(False) @@ -67,9 +60,7 @@ async def send_messages_with_limit( return results -async def try_add_blocked_user( - tg_id: int, session: AsyncSession, source_file: str | None -): +async def try_add_blocked_user(tg_id: int, session: AsyncSession, source_file: str | None): """ Добавляет пользователя в список заблокировавших бота, если требуется. """ @@ -121,9 +112,7 @@ async def send_notification( photo_path = os.path.join("img", image_filename) if os.path.isfile(photo_path): - return await _send_photo_notification( - bot, tg_id, photo_path, image_filename, caption, keyboard - ) + return await _send_photo_notification(bot, tg_id, photo_path, image_filename, caption, keyboard) else: logger.warning(f"Файл с изображением не найден: {photo_path}") return await _send_text_notification(bot, tg_id, caption, keyboard) @@ -143,9 +132,7 @@ async def _send_photo_notification( async with aiofiles.open(photo_path, "rb") as image_file: image_data = await image_file.read() buffered_photo = BufferedInputFile(image_data, filename=image_filename) - await bot.send_photo( - tg_id, buffered_photo, caption=caption, reply_markup=keyboard - ) + await bot.send_photo(tg_id, buffered_photo, caption=caption, reply_markup=keyboard) return True except (TelegramForbiddenError, TelegramBadRequest): return False @@ -168,7 +155,5 @@ async def _send_text_notification( except (TelegramForbiddenError, TelegramBadRequest): return False except Exception as e: - logger.error( - f"Неизвестная ошибка при отправке сообщения для пользователя {tg_id}: {e}" - ) + logger.error(f"Неизвестная ошибка при отправке сообщения для пользователя {tg_id}: {e}") return False diff --git a/handlers/notifications/special_notifications.py b/handlers/notifications/special_notifications.py index faca9840..74e8e121 100644 --- a/handlers/notifications/special_notifications.py +++ b/handlers/notifications/special_notifications.py @@ -1,16 +1,18 @@ +from datetime import datetime, timedelta + import pytz + from aiogram import Bot, Router, types from aiogram.types import InlineKeyboardButton, WebAppInfo from aiogram.utils.keyboard import InlineKeyboardBuilder from sqlalchemy.ext.asyncio import AsyncSession -from datetime import datetime, timedelta from config import ( + CONNECT_PHONE_BUTTON, NOTIFY_EXTRA_DAYS, NOTIFY_INACTIVE, NOTIFY_INACTIVE_TRAFFIC, SUPPORT_CHAT_URL, - CONNECT_PHONE_BUTTON, TRIAL_CONFIG, ) from database import ( @@ -20,7 +22,7 @@ from database import ( update_key_notified, ) from database.models import Key -from handlers.buttons import MAIN_MENU, CONNECT_DEVICE, CONNECT_PHONE, PC_BUTTON, TV_BUTTON +from handlers.buttons import CONNECT_DEVICE, CONNECT_PHONE, MAIN_MENU, PC_BUTTON, TV_BUTTON from handlers.keys.key_utils import get_user_traffic from handlers.notifications.notify_utils import send_messages_with_limit from handlers.texts import ( @@ -52,11 +54,7 @@ async def notify_inactive_trial_users(bot: Bot, session: AsyncSession): display_name = username or first_name or last_name or "Пользователь" builder = InlineKeyboardBuilder() - builder.row( - types.InlineKeyboardButton( - text="🚀 Активировать пробный период", callback_data="create_key" - ) - ) + builder.row(types.InlineKeyboardButton(text="🚀 Активировать пробный период", callback_data="create_key")) builder.row(types.InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) keyboard = builder.as_markup() @@ -76,14 +74,12 @@ async def notify_inactive_trial_users(bot: Bot, session: AsyncSession): trial_time_formatted=format_days(trial_days), ) - messages.append( - { - "tg_id": tg_id, - "text": message, - "keyboard": keyboard, - "notification_id": "inactive_trial", - } - ) + messages.append({ + "tg_id": tg_id, + "text": message, + "keyboard": keyboard, + "notification_id": "inactive_trial", + }) if messages: results = await send_messages_with_limit( @@ -102,9 +98,7 @@ async def notify_inactive_trial_users(bot: Bot, session: AsyncSession): logger.info("✅ Проверка пользователей с неактивным пробным периодом завершена.") -async def notify_users_no_traffic( - bot: Bot, session: AsyncSession, current_time: int, keys: list -): +async def notify_users_no_traffic(bot: Bot, session: AsyncSession, current_time: int, keys: list): logger.info("Проверка пользователей с нулевым трафиком...") current_dt = datetime.fromtimestamp(current_time / 1000, tz=moscow_tz) messages = [] @@ -120,19 +114,13 @@ async def notify_users_no_traffic( if created_at is None or notified: continue - created_at_dt = pytz.utc.localize( - datetime.fromtimestamp(created_at / 1000) - ).astimezone(moscow_tz) + created_at_dt = pytz.utc.localize(datetime.fromtimestamp(created_at / 1000)).astimezone(moscow_tz) if current_dt < created_at_dt + timedelta(hours=NOTIFY_INACTIVE_TRAFFIC): continue if expiry_time: - expiry_dt = pytz.utc.localize( - datetime.fromtimestamp(expiry_time / 1000) - ).astimezone(moscow_tz) - if (current_dt - (expiry_dt - timedelta(days=30))) < timedelta( - hours=NOTIFY_INACTIVE_TRAFFIC - ): + expiry_dt = pytz.utc.localize(datetime.fromtimestamp(expiry_time / 1000)).astimezone(moscow_tz) + if (current_dt - (expiry_dt - timedelta(days=30))) < timedelta(hours=NOTIFY_INACTIVE_TRAFFIC): continue try: @@ -142,14 +130,11 @@ async def notify_users_no_traffic( continue if traffic_data.get("status") != "success": - logger.warning( - f"⚠ Ошибка при получении трафика для {email}: {traffic_data.get('message')}" - ) + logger.warning(f"⚠ Ошибка при получении трафика для {email}: {traffic_data.get('message')}") continue total_traffic = sum( - value if isinstance(value, int | float) else 0 - for value in traffic_data.get("traffic", {}).values() + value if isinstance(value, int | float) else 0 for value in traffic_data.get("traffic", {}).values() ) try: @@ -159,69 +144,39 @@ async def notify_users_no_traffic( continue if total_traffic == 0: - logger.info( - f"⚠ У пользователя {tg_id} ({email}) 0 ГБ трафика. Отправляем уведомление." - ) + logger.info(f"⚠ У пользователя {tg_id} ({email}) 0 ГБ трафика. Отправляем уведомление.") builder = InlineKeyboardBuilder() server_id = key.server_id try: is_full_remnawave = await is_full_remnawave_cluster(server_id, session) final_link = key.key or key.remnawave_link - + if is_full_remnawave and final_link: - builder.row( - InlineKeyboardButton( - text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link) - ) - ) + builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link))) else: if CONNECT_PHONE_BUTTON: + builder.row(InlineKeyboardButton(text=CONNECT_PHONE, callback_data=f"connect_phone|{email}")) builder.row( - InlineKeyboardButton( - text=CONNECT_PHONE, callback_data=f"connect_phone|{email}" - ) - ) - builder.row( - InlineKeyboardButton( - text=PC_BUTTON, callback_data=f"connect_pc|{email}" - ), - InlineKeyboardButton( - text=TV_BUTTON, callback_data=f"connect_tv|{email}" - ), + InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"), + InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"), ) else: - builder.row( - InlineKeyboardButton( - text=CONNECT_DEVICE, callback_data=f"connect_device|{email}" - ) - ) + builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{email}")) except Exception as e: logger.error(f"Ошибка при определении типа панели для {email}: {e}") - builder.row( - InlineKeyboardButton( - text=CONNECT_DEVICE, callback_data=f"connect_device|{email}" - ) - ) - - builder.row( - InlineKeyboardButton( - text="🔧 Написать в поддержку", url=SUPPORT_CHAT_URL - ) - ) - builder.row( - InlineKeyboardButton(text=MAIN_MENU, callback_data="profile") - ) + builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{email}")) + + builder.row(InlineKeyboardButton(text="🔧 Написать в поддержку", url=SUPPORT_CHAT_URL)) + builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) keyboard = builder.as_markup() message = ZERO_TRAFFIC_MSG.format(email=email) - messages.append( - { - "tg_id": tg_id, - "text": message, - "keyboard": keyboard, - "client_id": client_id, - } - ) + messages.append({ + "tg_id": tg_id, + "text": message, + "keyboard": keyboard, + "client_id": client_id, + }) if messages: results = await send_messages_with_limit( diff --git a/handlers/pay.py b/handlers/pay.py index c1cd642e..72e39b0c 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -1,4 +1,5 @@ import os + from typing import Any from aiogram import F, Router @@ -14,11 +15,11 @@ from config import ( FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, - YOOKASSA_ENABLE, - YOOMONEY_ENABLE, + WATA_INT_ENABLE, WATA_RU_ENABLE, WATA_SBP_ENABLE, - WATA_INT_ENABLE, + YOOKASSA_ENABLE, + YOOMONEY_ENABLE, ) from database import get_last_payments from database.models import User @@ -31,31 +32,29 @@ from handlers.buttons import ( PAYMENT, ROBOKASSA, STARS, - YOOKASSA, - YOOMONEY, + WATA_INT, WATA_RU, WATA_SBP, - WATA_INT, + YOOKASSA, + YOOMONEY, ) from handlers.payments.cryprobot_pay import process_callback_pay_cryptobot from handlers.payments.freekassa_pay import process_callback_pay_freekassa from handlers.payments.robokassa_pay import process_callback_pay_robokassa from handlers.payments.stars_pay import process_callback_pay_stars +from handlers.payments.wata import process_callback_pay_wata from handlers.payments.yookassa_pay import process_callback_pay_yookassa from handlers.payments.yoomoney_pay import process_callback_pay_yoomoney -from handlers.payments.wata import process_callback_pay_wata from handlers.texts import BALANCE_MANAGEMENT_TEXT, PAYMENT_METHODS_MSG from .utils import edit_or_send_message + router = Router() @router.callback_query(F.data == "pay") -async def handle_pay( - callback_query: CallbackQuery, state: FSMContext, session: AsyncSession -): - +async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession): payment_handlers = [] if YOOKASSA_ENABLE: @@ -98,9 +97,7 @@ async def handle_pay( if WATA_INT_ENABLE: builder.row(InlineKeyboardButton(text=WATA_INT, callback_data="pay_wata_int")) if DONATIONS_ENABLE: - builder.row( - InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate") - ) + builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) @@ -120,9 +117,7 @@ async def balance_handler(callback_query: CallbackQuery, session: AsyncSession): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay")) - builder.row( - InlineKeyboardButton(text=BALANCE_HISTORY, callback_data="balance_history") - ) + builder.row(InlineKeyboardButton(text=BALANCE_HISTORY, callback_data="balance_history")) builder.row(InlineKeyboardButton(text=COUPON, callback_data="activate_coupon")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) @@ -166,14 +161,17 @@ async def balance_history_handler(callback_query: CallbackQuery, session: Any): disable_web_page_preview=False, ) + @router.callback_query(F.data == "pay_wata_ru") async def handle_pay_wata_ru(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession): await process_callback_pay_wata(callback_query, state, session, cassa_name="ru") + @router.callback_query(F.data == "pay_wata_sbp") async def handle_pay_wata_sbp(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession): await process_callback_pay_wata(callback_query, state, session, cassa_name="sbp") + @router.callback_query(F.data == "pay_wata_int") async def handle_pay_wata_int(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession): await process_callback_pay_wata(callback_query, state, session, cassa_name="int") diff --git a/handlers/profile.py b/handlers/profile.py index 8f5b51bc..46f13808 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -21,11 +21,11 @@ from handlers.buttons import ( ADD_SUB, BACK, BALANCE, - RENEW_KEY, GIFTS, INSTRUCTIONS, INVITE, MY_SUBS, + RENEW_KEY, TRIAL_SUB, ) from handlers.texts import ADD_SUBSCRIPTION_HINT @@ -35,6 +35,7 @@ from .admin.panel.keyboard import AdminPanelCallback from .texts import profile_message_send from .utils import edit_or_send_message + router = Router() @@ -102,9 +103,7 @@ async def process_callback_view_profile( builder.row(*row_buttons) if INSTRUCTIONS_BUTTON: - builder.row( - InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions") - ) + builder.row(InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions")) if admin: builder.row( InlineKeyboardButton( diff --git a/handlers/refferal.py b/handlers/refferal.py index 8beff9fe..954bbdfa 100644 --- a/handlers/refferal.py +++ b/handlers/refferal.py @@ -1,7 +1,9 @@ import os + from io import BytesIO import qrcode + from aiogram import F, Router from aiogram.enums import ParseMode from aiogram.fsm.context import FSMContext @@ -18,7 +20,7 @@ from sqlalchemy import desc, func, select from sqlalchemy.ext.asyncio import AsyncSession from bot import bot -from config import ADMIN_ID, INLINE_MODE, TOP_REFERRAL_BUTTON, TRIAL_CONFIG, USERNAME_BOT, REFERRAL_BONUS_PERCENTAGES +from config import ADMIN_ID, INLINE_MODE, REFERRAL_BONUS_PERCENTAGES, TOP_REFERRAL_BUTTON, TRIAL_CONFIG, USERNAME_BOT from database import ( add_referral, add_user, @@ -29,26 +31,25 @@ from database import ( from database.models import Referral from handlers.buttons import BACK, INVITE, MAIN_MENU, QR, TOP_FIVE from handlers.texts import ( + INVITE_MESSAGE_TEMPLATE, INVITE_TEXT_NON_INLINE, NEW_REFERRAL_NOTIFICATION, REFERRAL_OFFERS, REFERRAL_SUCCESS_MSG, TOP_REFERRALS_TEXT, - INVITE_MESSAGE_TEMPLATE, ) from logger import logger from .texts import get_referral_link from .utils import edit_or_send_message, format_days + router = Router() @router.callback_query(F.data == "invite") @router.message(F.text == "/invite") -async def invite_handler( - callback_query_or_message: Message | CallbackQuery, session: AsyncSession -): +async def invite_handler(callback_query_or_message: Message | CallbackQuery, session: AsyncSession): if isinstance(callback_query_or_message, CallbackQuery): chat_id = callback_query_or_message.message.chat.id target_message = callback_query_or_message.message @@ -68,7 +69,7 @@ async def invite_handler( bonuses_block = "\n".join(bonuses_lines) details_lines = [] - for level, stats in referral_stats['referrals_by_level'].items(): + for level, stats in referral_stats["referrals_by_level"].items(): bonus_value = REFERRAL_BONUS_PERCENTAGES.get(level) if isinstance(bonus_value, float): bonus_str = f"{int(bonus_value * 100)}%" @@ -80,9 +81,9 @@ async def invite_handler( invite_message = INVITE_MESSAGE_TEMPLATE.format( referral_link=referral_link, bonuses_block=bonuses_block, - total_referrals=referral_stats['total_referrals'], + total_referrals=referral_stats["total_referrals"], details_block=details_block, - total_referral_bonus=referral_stats['total_referral_bonus'], + total_referral_bonus=referral_stats["total_referral_bonus"], ) image_path = os.path.join("img", "pic_invite.jpg") @@ -109,18 +110,14 @@ async def invite_handler( @router.inline_query(F.query.in_(["referral", "ref", "invite"])) async def inline_referral_handler(inline_query: InlineQuery): - referral_link = ( - f"https://t.me/{USERNAME_BOT}?start=referral_{inline_query.from_user.id}" - ) + referral_link = f"https://t.me/{USERNAME_BOT}?start=referral_{inline_query.from_user.id}" trial_days = TRIAL_CONFIG["duration_days"] trial_time_formatted = format_days(trial_days) results: list[InlineQueryResultArticle] = [] for index, offer in enumerate(REFERRAL_OFFERS): - message_text = offer["message"].format( - trial_time_formatted=trial_time_formatted - )[:4096] + message_text = offer["message"].format(trial_time_formatted=trial_time_formatted)[:4096] title = offer["title"].format(trial_time_formatted=trial_time_formatted) description = offer["description"] @@ -132,9 +129,7 @@ async def inline_referral_handler(inline_query: InlineQuery): id=str(index), title=title, description=description, - input_message_content=InputTextMessageContent( - message_text=message_text, parse_mode=ParseMode.HTML - ), + input_message_content=InputTextMessageContent(message_text=message_text, parse_mode=ParseMode.HTML), reply_markup=builder.as_markup(), ) ) @@ -175,9 +170,7 @@ async def show_referral_qr(callback_query: CallbackQuery): os.remove(qr_path) except Exception as e: - logger.error( - f"Ошибка при генерации QR-кода для реферальной ссылки: {e}", exc_info=True - ) + logger.error(f"Ошибка при генерации QR-кода для реферальной ссылки: {e}", exc_info=True) await callback_query.message.answer("❌ Произошла ошибка при создании QR-кода.") @@ -185,11 +178,7 @@ async def show_referral_qr(callback_query: CallbackQuery): async def top_referrals_handler(callback_query: CallbackQuery, session: AsyncSession): user_id = callback_query.from_user.id - result = await session.execute( - select(func.count()) - .select_from(Referral) - .where(Referral.referrer_tg_id == user_id) - ) + result = await session.execute(select(func.count()).select_from(Referral).where(Referral.referrer_tg_id == user_id)) user_referral_count = result.scalar_one() or 0 personal_block = "Твоё место в рейтинге:\n" @@ -254,9 +243,7 @@ async def handle_referral_link( user_id = user["tg_id"] if isinstance(user, dict) else user.id if referrer_tg_id == user_id: - await message.answer( - "❌ Вы не можете быть реферальной ссылкой самого себя." - ) + await message.answer("❌ Вы не можете быть реферальной ссылкой самого себя.") return existing_referral = await get_referral_by_referred_id(session, user_id) @@ -290,9 +277,7 @@ async def handle_referral_link( NEW_REFERRAL_NOTIFICATION.format(referred_id=user_id), ) except Exception as e: - logger.error( - f"Не удалось отправить уведомление пригласившему ({referrer_tg_id}): {e}" - ) + logger.error(f"Не удалось отправить уведомление пригласившему ({referrer_tg_id}): {e}") await message.answer(REFERRAL_SUCCESS_MSG.format(referrer_tg_id=referrer_tg_id)) diff --git a/handlers/start.py b/handlers/start.py index 0b328d04..421221bf 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -1,4 +1,5 @@ import os + from typing import Any from aiogram import F, Router @@ -23,9 +24,9 @@ from config import ( from database import ( add_user, check_user_exists, - get_trial, - get_key_count, get_coupon_by_code, + get_key_count, + get_trial, ) from database.models import TrackingSource, User from handlers.buttons import ( @@ -56,6 +57,7 @@ from .admin.panel.keyboard import AdminPanelCallback from .refferal import handle_referral_link from .utils import edit_or_send_message + router = Router() processing_gifts = set() @@ -73,9 +75,7 @@ async def handle_start_callback_query( @router.message(Command("start")) -async def start_command( - message: Message, state: FSMContext, session: Any, admin: bool, captcha: bool = True -): +async def start_command(message: Message, state: FSMContext, session: Any, admin: bool, captcha: bool = True): logger.info(f"Вызвана функция start_command для пользователя {message.chat.id}") if CAPTCHA_ENABLE and captcha: @@ -95,12 +95,7 @@ async def start_command( @router.callback_query(F.data == "check_subscription") -async def check_subscription_callback( - callback_query: CallbackQuery, - state: FSMContext, - session: Any, - admin: bool -): +async def check_subscription_callback(callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool): user_id = callback_query.from_user.id logger.info(f"[CALLBACK] Получен callback 'check_subscription' от пользователя {user_id}") @@ -112,11 +107,7 @@ async def check_subscription_callback( await callback_query.answer(NOT_SUBSCRIBED_YET_MSG, show_alert=True) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=SUB_CHANELL, url=CHANNEL_URL)) - builder.row( - InlineKeyboardButton( - text=SUB_CHANELL_DONE, callback_data="check_subscription" - ) - ) + builder.row(InlineKeyboardButton(text=SUB_CHANELL_DONE, callback_data="check_subscription")) await callback_query.message.edit_text( SUBSCRIPTION_REQUIRED_MSG, reply_markup=builder.as_markup(), @@ -178,9 +169,7 @@ async def process_start_logic( text = text_to_process or message.text or message.caption if not text: - logger.info( - f"[StartLogic] Текста нет — вызываю стартовое меню для {user_data['tg_id']}" - ) + logger.info(f"[StartLogic] Текста нет — вызываю стартовое меню для {user_data['tg_id']}") await show_start_menu(message, admin, session) return @@ -200,7 +189,7 @@ async def process_start_logic( coupon = await get_coupon_by_code(session, coupon_code) if not coupon: continue - + await activate_coupon( message, state, @@ -209,7 +198,7 @@ async def process_start_logic( admin=admin, user_data=user_data, ) - + if coupon.days: return continue @@ -229,25 +218,21 @@ async def process_start_logic( return await process_callback_view_profile(message, state, admin, session) processing_gifts.add(gift_id) - + try: logger.info(f"[GIFT] Обнаружен подарок {gift_id} от {sender_id}") - await handle_gift_link( - gift_id, message, state, session, user_data=user_data - ) + await handle_gift_link(gift_id, message, state, session, user_data=user_data) gift_detected = True finally: processing_gifts.discard(gift_id) - + break if "referral" in part: referrer_tg_id = part.split("referral")[1].strip("_") try: referrer_tg_id = int(referrer_tg_id) - await handle_referral_link( - referrer_tg_id, message, state, session, user_data=user_data - ) + await handle_referral_link(referrer_tg_id, message, state, session, user_data=user_data) except (ValueError, IndexError): pass continue @@ -255,9 +240,7 @@ async def process_start_logic( if "utm" in part: utm_code = part logger.info(f"[UTM] Обнаружена ссылка на UTM: {utm_code}") - await handle_utm_link( - utm_code, message, state, session, user_data=user_data - ) + await handle_utm_link(utm_code, message, state, session, user_data=user_data) continue await state.clear() @@ -297,9 +280,7 @@ async def handle_utm_link( ): user_id = user_data["tg_id"] - result = await session.execute( - select(TrackingSource).where(TrackingSource.code == utm_code) - ) + result = await session.execute(select(TrackingSource).where(TrackingSource.code == utm_code)) utm_exists = result.scalar_one_or_none() if not utm_exists: @@ -314,9 +295,7 @@ async def handle_utm_link( logger.info(f"[UTM] Привязана {utm_code} к пользователю {user_id}") elif not user: await add_user(session=session, source_code=utm_code, **user_data) - logger.info( - f"[UTM] Зарегистрирован и привязан {utm_code} к пользователю {user_id}" - ) + logger.info(f"[UTM] Зарегистрирован и привязан {utm_code} к пользователю {user_id}") async def show_start_menu(message: Message, admin: bool, session: AsyncSession): @@ -376,9 +355,7 @@ async def handle_about_vpn(callback_query: CallbackQuery, session: AsyncSession) builder = InlineKeyboardBuilder() if DONATIONS_ENABLE: - builder.row( - InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate") - ) + builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) support_btn = InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL) if CHANNEL_EXISTS: diff --git a/handlers/utils.py b/handlers/utils.py index 6fdaf697..eb3cfe65 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -2,9 +2,11 @@ import os import re import secrets import string + from datetime import datetime import aiofiles + from aiogram.types import ( BufferedInputFile, InlineKeyboardMarkup, @@ -17,7 +19,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from bot import bot from config import ADMIN_ID from database import get_servers -from database.models import Key, Server, Notification +from database.models import Key, Notification, Server from logger import logger @@ -25,14 +27,7 @@ def generate_random_email(length: int = 8) -> str: """ Генерирует случайный email с заданной длиной. """ - return ( - "".join( - secrets.choice(string.ascii_lowercase + string.digits) - for _ in range(length) - ) - if length > 0 - else "" - ) + return "".join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(length)) if length > 0 else "" async def get_least_loaded_cluster(session: AsyncSession) -> str: @@ -56,11 +51,8 @@ async def get_least_loaded_cluster(session: AsyncSession) -> str: available_clusters = {} for cluster_name, cluster_servers in servers.items(): - enabled_servers = [ - server for server in cluster_servers - if server.get("enabled", True) - ] - + enabled_servers = [server for server in cluster_servers if server.get("enabled", True)] + if not enabled_servers: continue @@ -78,9 +70,7 @@ async def get_least_loaded_cluster(session: AsyncSession) -> str: logger.warning("❌ Нет доступных кластеров с лимитом ключей!") raise ValueError("⚠️ Сервисы временно недоступны. Попробуйте позже.") - least_loaded_cluster = min( - available_clusters, key=lambda k: (available_clusters[k], k) - ) + least_loaded_cluster = min(available_clusters, key=lambda k: (available_clusters[k], k)) logger.info( f"✅ Выбран наименее загруженный кластер: {least_loaded_cluster} (загрузка: {available_clusters[least_loaded_cluster]})" ) @@ -97,15 +87,11 @@ async def check_server_key_limit(server_info: dict, session: AsyncSession) -> bo identifier = cluster_name if cluster_name else server_name - result = await session.execute( - select(func.count()).select_from(Key).where(Key.server_id == identifier) - ) + result = await session.execute(select(func.count()).select_from(Key).where(Key.server_id == identifier)) total_keys = result.scalar() or 0 if total_keys >= max_keys: - logger.warning( - f"[Key Limit] Сервер {server_name} достиг лимита: {total_keys}/{max_keys}" - ) + logger.warning(f"[Key Limit] Сервер {server_name} достиг лимита: {total_keys}/{max_keys}") return False usage_percent = total_keys / max_keys @@ -114,9 +100,7 @@ async def check_server_key_limit(server_info: dict, session: AsyncSession) -> bo notif_key = f"server_warn_{server_name}" result = await session.execute( - select(Notification).where( - Notification.tg_id == 0, Notification.notification_type == notif_key - ) + select(Notification).where(Notification.tg_id == 0, Notification.notification_type == notif_key) ) already_sent = result.scalar_one_or_none() @@ -137,18 +121,14 @@ async def check_server_key_limit(server_info: dict, session: AsyncSession) -> bo return True -async def handle_error( - tg_id: int, callback_query: object | None = None, message: str = "" -) -> None: +async def handle_error(tg_id: int, callback_query: object | None = None, message: str = "") -> None: """ Обрабатывает ошибку, отправляя сообщение пользователю. """ try: if callback_query and hasattr(callback_query, "message"): try: - await bot.delete_message( - chat_id=tg_id, message_id=callback_query.message.message_id - ) + await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) except Exception as delete_error: logger.warning(f"Не удалось удалить сообщение: {delete_error}") @@ -226,9 +206,7 @@ async def edit_or_send_message( return except Exception: await target_message.answer_photo( - photo=BufferedInputFile( - image_data, filename=os.path.basename(media_path) - ), + photo=BufferedInputFile(image_data, filename=os.path.basename(media_path)), caption=text, reply_markup=reply_markup, disable_web_page_preview=disable_web_page_preview, @@ -237,9 +215,7 @@ async def edit_or_send_message( else: if not force_text and target_message.caption is not None: try: - await target_message.edit_caption( - caption=text, reply_markup=reply_markup - ) + await target_message.edit_caption(caption=text, reply_markup=reply_markup) return except Exception as e: logger.error(f"Ошибка редактирования подписи: {e}") @@ -271,17 +247,13 @@ def convert_to_bytes(value: float, unit: str) -> int: async def is_full_remnawave_cluster(cluster_id: str, session: AsyncSession) -> bool: - result = await session.execute( - select(Server.panel_type).where(Server.cluster_name == cluster_id) - ) + result = await session.execute(select(Server.panel_type).where(Server.cluster_name == cluster_id)) panel_types = result.scalars().all() if panel_types: return all(pt.lower() == "remnawave" for pt in panel_types) - result = await session.execute( - select(Server.panel_type).where(Server.server_name == cluster_id) - ) + result = await session.execute(select(Server.panel_type).where(Server.server_name == cluster_id)) panel_type = result.scalar_one_or_none() return panel_type and panel_type.lower() == "remnawave" diff --git a/logger.py b/logger.py index cfef8fb1..fbd90605 100644 --- a/logger.py +++ b/logger.py @@ -1,12 +1,14 @@ import logging import os import sys + from datetime import timedelta from loguru import logger from config import LOG_ROTATION_TIME + log_folder = "logs" if not os.path.exists(log_folder): diff --git a/middlewares/__init__.py b/middlewares/__init__.py index e8b05b8b..354f7c94 100644 --- a/middlewares/__init__.py +++ b/middlewares/__init__.py @@ -25,9 +25,7 @@ def register_middleware( """Регистрирует middleware в диспетчере.""" if middlewares is None: available_middlewares = { - "session": ( - SessionMiddleware(sessionmaker) if sessionmaker else SessionMiddleware() - ), + "session": (SessionMiddleware(sessionmaker) if sessionmaker else SessionMiddleware()), "admin": AdminMiddleware(), "maintenance": MaintenanceModeMiddleware(), "logging": LoggingMiddleware(), @@ -40,11 +38,7 @@ def register_middleware( dispatcher.update.outer_middleware(BanCheckerMiddleware(sessionmaker)) exclude_set = set(exclude or []) - middlewares = [ - middleware - for name, middleware in available_middlewares.items() - if name not in exclude_set - ] + middlewares = [middleware for name, middleware in available_middlewares.items() if name not in exclude_set] handlers = [ dispatcher.message, diff --git a/middlewares/admin.py b/middlewares/admin.py index ba476ccb..123a7228 100644 --- a/middlewares/admin.py +++ b/middlewares/admin.py @@ -16,9 +16,7 @@ class AdminMiddleware(BaseMiddleware): является ли пользователь администратором. """ - _admin_ids: set[int] = ( - set(ADMIN_ID) if isinstance(ADMIN_ID, list | tuple) else {ADMIN_ID} - ) + _admin_ids: set[int] = set(ADMIN_ID) if isinstance(ADMIN_ID, list | tuple) else {ADMIN_ID} async def __call__( self, diff --git a/middlewares/ban_checker.py b/middlewares/ban_checker.py index adf20dcf..86899bd7 100644 --- a/middlewares/ban_checker.py +++ b/middlewares/ban_checker.py @@ -12,6 +12,7 @@ from config import SUPPORT_CHAT_URL from database.models import ManualBan from logger import logger + TZ = timezone("Europe/Moscow") @@ -56,9 +57,7 @@ class BanCheckerMiddleware(BaseMiddleware): reason = ban.reason or "не указана" until = ban.until - logger.warning( - f"[BanChecker] Пользователь {tg_id} заблокирован (до: {until}, причина: {reason})" - ) + logger.warning(f"[BanChecker] Пользователь {tg_id} заблокирован (до: {until}, причина: {reason})") if until: until_local = until.astimezone(TZ).strftime("%Y-%m-%d %H:%M") diff --git a/middlewares/maintenance.py b/middlewares/maintenance.py index 6f1d5f9f..09a41ae3 100644 --- a/middlewares/maintenance.py +++ b/middlewares/maintenance.py @@ -3,6 +3,7 @@ from aiogram.types import CallbackQuery, Message from config import ADMIN_ID + maintenance_mode = False @@ -16,9 +17,7 @@ class MaintenanceModeMiddleware(BaseMiddleware): user_id = event.from_user.id if user_id and user_id not in ADMIN_ID: - await event.answer( - "⚙️ Бот временно недоступен. Ведутся технические работы." - ) + await event.answer("⚙️ Бот временно недоступен. Ведутся технические работы.") return return await handler(event, data) diff --git a/middlewares/subscription.py b/middlewares/subscription.py index 45beef5d..c878d5f7 100644 --- a/middlewares/subscription.py +++ b/middlewares/subscription.py @@ -1,9 +1,10 @@ -from typing import Any, Awaitable, Callable +from collections.abc import Awaitable, Callable +from typing import Any from aiogram import BaseMiddleware +from aiogram.fsm.context import FSMContext from aiogram.types import InlineKeyboardButton, Message, Update from aiogram.utils.keyboard import InlineKeyboardBuilder -from aiogram.fsm.context import FSMContext from bot import bot from config import CHANNEL_EXISTS, CHANNEL_ID, CHANNEL_REQUIRED, CHANNEL_URL @@ -75,9 +76,7 @@ class SubscriptionMiddleware(BaseMiddleware): return await self._ask_to_subscribe(message) except Exception as e: - logger.warning( - f"[SubMiddleware] Ошибка при проверке подписки для {tg_id}: {e}" - ) + logger.warning(f"[SubMiddleware] Ошибка при проверке подписки для {tg_id}: {e}") return await self._ask_to_subscribe(message) return await handler(event, data) @@ -85,11 +84,7 @@ class SubscriptionMiddleware(BaseMiddleware): async def _ask_to_subscribe(self, message: Message): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=SUB_CHANELL, url=CHANNEL_URL)) - builder.row( - InlineKeyboardButton( - text=SUB_CHANELL_DONE, callback_data="check_subscription" - ) - ) + builder.row(InlineKeyboardButton(text=SUB_CHANELL_DONE, callback_data="check_subscription")) await edit_or_send_message( target_message=message, diff --git a/middlewares/throttling.py b/middlewares/throttling.py index 4c507a1e..710744c5 100644 --- a/middlewares/throttling.py +++ b/middlewares/throttling.py @@ -16,10 +16,7 @@ class ThrottlingMiddleware(BaseMiddleware): current_count = self.cache.get(user_id, 0) if current_count >= 3: - if ( - isinstance(event, CallbackQuery) - and user_id not in self.throttle_notice_cache - ): + if isinstance(event, CallbackQuery) and user_id not in self.throttle_notice_cache: self.throttle_notice_cache[user_id] = None bot: Bot = data["bot"] await bot.answer_callback_query( diff --git a/panels/three_xui.py b/panels/three_xui.py index 921c8ae5..13fa7c49 100644 --- a/panels/three_xui.py +++ b/panels/three_xui.py @@ -1,9 +1,11 @@ import time + from dataclasses import dataclass from typing import Any import httpx import py3xui + from py3xui import AsyncApi from config import ADMIN_PASSWORD, ADMIN_USERNAME, SUPERNODE, USE_XUI_TOKEN, XUI_TOKEN @@ -82,9 +84,7 @@ async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, An except Exception as e: error_message = str(e) if "Duplicate email" in error_message: - logger.warning( - f"Дублированный email: {config.email}. Пропуск. Сообщение: {error_message}" - ) + logger.warning(f"Дублированный email: {config.email}. Пропуск. Сообщение: {error_message}") return {"status": "duplicate", "email": config.email} logger.error(f"Ошибка при добавлении клиента {config.email}: {error_message}") @@ -108,9 +108,7 @@ async def extend_client_key( logger.warning(f"Клиент с email {email} не найден или не имеет ID.") return None - logger.info( - f"Обновление ключа клиента {email} с ID {client.id} до {new_expiry_time}" - ) + logger.info(f"Обновление ключа клиента {email} с ID {client.id} до {new_expiry_time}") client.id = client_id client.expiry_time = new_expiry_time @@ -216,14 +214,10 @@ async def toggle_client( except httpx.ConnectTimeout as e: status = "включении" if enable else "отключении" - logger.error( - f"Ошибка при {status} клиента с email {email} и ID {client_id}: {e}" - ) + logger.error(f"Ошибка при {status} клиента с email {email} и ID {client_id}: {e}") return False except Exception as e: status = "включении" if enable else "отключении" - logger.error( - f"Ошибка при {status} клиента с email {email} и ID {client_id}: {e}" - ) + logger.error(f"Ошибка при {status} клиента с email {email} и ID {client_id}: {e}") return False diff --git a/servers.py b/servers.py index 89c21099..817c9958 100644 --- a/servers.py +++ b/servers.py @@ -1,8 +1,9 @@ import asyncio import re -from datetime import datetime, timedelta import ssl +from datetime import datetime, timedelta + from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from ping3 import ping @@ -14,6 +15,7 @@ from database import get_servers from handlers.admin.servers.keyboard import AdminServerCallback from logger import logger + last_ping_times = {} last_down_times = {} notified_servers = set() @@ -36,7 +38,7 @@ async def check_tcp_connection(host: str, port: int) -> bool: """Проверяет доступность сервера через TCP с попыткой SSL-соединения.""" try: ssl_context = ssl.create_default_context() - reader, writer = await asyncio.open_connection(host, port, ssl=ssl_context) + _reader, writer = await asyncio.open_connection(host, port, ssl=ssl_context) writer.close() await writer.wait_closed() return True @@ -81,9 +83,7 @@ async def notify_admin(server_name: str, status: str, down_duration: timedelta = message = f"✅ Сервер '{server_name}' снова в сети!\n\n⏳ Время простоя: {downtime}." for admin_id in ADMIN_ID: - logger.info( - f"📨 Отправляем уведомление '{status}' администратору {admin_id} о сервере {server_name}" - ) + logger.info(f"📨 Отправляем уведомление '{status}' администратору {admin_id} о сервере {server_name}") await bot.send_message(admin_id, message, reply_markup=builder.as_markup()) @@ -116,9 +116,7 @@ async def check_servers(session: AsyncSession): restored_servers = set() online_servers = set() - for (server_name, server_host), result in zip( - server_info_list, results, strict=False - ): + for (server_name, server_host), result in zip(server_info_list, results, strict=False): is_online = bool(result) if not isinstance(result, Exception) else False if is_online: @@ -140,9 +138,7 @@ async def check_servers(session: AsyncSession): last_ping_times[server_name] = current_time last_down_times[server_name] = current_time - if last_ping_time and ( - current_time - last_ping_time > timedelta(seconds=PING_TIME * 3) - ): + if last_ping_time and (current_time - last_ping_time > timedelta(seconds=PING_TIME * 3)): if server_name not in notified_servers: logger.warning( f"🚨 Уведомление: сервер {server_name} не отвечает более {PING_TIME * 3} секунд!" @@ -155,18 +151,12 @@ async def check_servers(session: AsyncSession): all_servers = {name for name, _ in server_info_list} true_offline_servers = all_servers - online_servers - logger.info( - f"✅ Доступно серверов: {len(online_servers)}, ❌ Недоступно: {len(true_offline_servers)}" - ) + logger.info(f"✅ Доступно серверов: {len(online_servers)}, ❌ Недоступно: {len(true_offline_servers)}") if true_offline_servers: - logger.warning( - f"🚨 Не отвечает {len(true_offline_servers)} серверов: {', '.join(true_offline_servers)}" - ) + logger.warning(f"🚨 Не отвечает {len(true_offline_servers)} серверов: {', '.join(true_offline_servers)}") if restored_servers: - logger.info( - f"✅ Восстановились {len(restored_servers)} серверов: {', '.join(restored_servers)}" - ) + logger.info(f"✅ Восстановились {len(restored_servers)} серверов: {', '.join(restored_servers)}") await asyncio.sleep(PING_TIME) @@ -174,4 +164,4 @@ async def check_servers(session: AsyncSession): def extract_host(api_url: str) -> str: """Извлекает хост из `api_url`.""" match = re.match(r"(https?://)?([^:/]+)", api_url) - return match.group(2) if match else api_url \ No newline at end of file + return match.group(2) if match else api_url diff --git a/utils/csv_export.py b/utils/csv_export.py index 0bdbc6d3..32425cac 100644 --- a/utils/csv_export.py +++ b/utils/csv_export.py @@ -1,9 +1,10 @@ import csv + from datetime import datetime from io import StringIO from aiogram.types import BufferedInputFile -from sqlalchemy import func, join, select, exists, not_ +from sqlalchemy import exists, func, join, not_, select from sqlalchemy.ext.asyncio import AsyncSession from database.models import Key, Payment, Referral, Tariff, User @@ -27,27 +28,23 @@ async def export_users_csv(session: AsyncSession) -> BufferedInputFile: buffer = StringIO() writer = csv.writer(buffer) - writer.writerow( - [ - "tg_id", - "username", - "first_name", - "last_name", - "language_code", - "is_bot", - "balance", - "trial", - "created_at", - ] - ) + writer.writerow([ + "tg_id", + "username", + "first_name", + "last_name", + "language_code", + "is_bot", + "balance", + "trial", + "created_at", + ]) for user in users: writer.writerow(user) buffer.seek(0) - return BufferedInputFile( - file=buffer.getvalue().encode("utf-8-sig"), filename="users_export.csv" - ) + return BufferedInputFile(file=buffer.getvalue().encode("utf-8-sig"), filename="users_export.csv") async def export_payments_csv(session: AsyncSession) -> BufferedInputFile: @@ -73,9 +70,7 @@ async def export_payments_csv(session: AsyncSession) -> BufferedInputFile: return _export_payments_csv(payments, "payments_export.csv") -async def export_user_payments_csv( - tg_id: int, session: AsyncSession -) -> BufferedInputFile: +async def export_user_payments_csv(tg_id: int, session: AsyncSession) -> BufferedInputFile: j = join(User, Payment, User.tg_id == Payment.tg_id) query = ( select( @@ -102,31 +97,25 @@ async def export_user_payments_csv( def _export_payments_csv(payments, filename: str) -> BufferedInputFile: buffer = StringIO() writer = csv.writer(buffer) - writer.writerow( - [ - "tg_id", - "username", - "first_name", - "last_name", - "amount", - "payment_system", - "status", - "created_at", - ] - ) + writer.writerow([ + "tg_id", + "username", + "first_name", + "last_name", + "amount", + "payment_system", + "status", + "created_at", + ]) for payment in payments: writer.writerow(payment) buffer.seek(0) - return BufferedInputFile( - file=buffer.getvalue().encode("utf-8-sig"), filename=filename - ) + return BufferedInputFile(file=buffer.getvalue().encode("utf-8-sig"), filename=filename) -async def export_referrals_csv( - referrer_tg_id: int, session: AsyncSession -) -> BufferedInputFile | None: +async def export_referrals_csv(referrer_tg_id: int, session: AsyncSession) -> BufferedInputFile | None: j = join(Referral, User, Referral.referred_tg_id == User.tg_id) query = ( select( @@ -175,18 +164,8 @@ async def export_hot_leads_csv(session: AsyncSession) -> BufferedInputFile: User.updated_at, ) .where( - exists( - select(Payment.tg_id) - .where(Payment.tg_id == User.tg_id) - .where(Payment.status == "success") - ), - not_( - exists( - select(Key.client_id) - .where(Key.tg_id == User.tg_id) - .where(Key.expiry_time > now_ts) - ) - ) + exists(select(Payment.tg_id).where(Payment.tg_id == User.tg_id).where(Payment.status == "success")), + not_(exists(select(Key.client_id).where(Key.tg_id == User.tg_id).where(Key.expiry_time > now_ts))), ) .order_by(User.updated_at.desc()) ) @@ -231,54 +210,40 @@ async def export_keys_csv(session: AsyncSession) -> BufferedInputFile: buffer = StringIO() writer = csv.writer(buffer) - writer.writerow( - [ - "tg_id", - "client_id", - "email", - "created_at", - "expiry_time", - "key", - "server_id", - "is_frozen", - "alias", - "tariff", - ] - ) + writer.writerow([ + "tg_id", + "client_id", + "email", + "created_at", + "expiry_time", + "key", + "server_id", + "is_frozen", + "alias", + "tariff", + ]) for row in keys: created_at = ( - datetime.utcfromtimestamp(row.created_at / 1000).strftime( - "%Y-%m-%d %H:%M:%S" - ) - if row.created_at - else "" + datetime.utcfromtimestamp(row.created_at / 1000).strftime("%Y-%m-%d %H:%M:%S") if row.created_at else "" ) expiry_time = ( - datetime.utcfromtimestamp(row.expiry_time / 1000).strftime( - "%Y-%m-%d %H:%M:%S" - ) - if row.expiry_time - else "" + datetime.utcfromtimestamp(row.expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S") if row.expiry_time else "" ) tariff = row.tariff_name or "—" - writer.writerow( - [ - row.tg_id, - row.client_id, - row.email, - created_at, - expiry_time, - row.key, - row.server_id, - row.is_frozen, - row.alias or "", - tariff, - ] - ) + writer.writerow([ + row.tg_id, + row.client_id, + row.email, + created_at, + expiry_time, + row.key, + row.server_id, + row.is_frozen, + row.alias or "", + tariff, + ]) buffer.seek(0) - return BufferedInputFile( - file=buffer.getvalue().encode("utf-8-sig"), filename="keys_export.csv" - ) + return BufferedInputFile(file=buffer.getvalue().encode("utf-8-sig"), filename="keys_export.csv") diff --git a/web/__init__.py b/web/__init__.py index 135413cc..753601aa 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -1,14 +1,16 @@ from aiohttp.web_urldispatcher import UrlDispatcher import bot + from config import TBLOCKER_WEBHOOK_PATH from .tblocker import tblocker_webhook from .wata_payment import wata_payment_webhook + WATA_WEBHOOK_PATH = "/wata/webhook" + async def register_web_routes(router: UrlDispatcher) -> None: router.add_post(TBLOCKER_WEBHOOK_PATH, tblocker_webhook) router.add_post(WATA_WEBHOOK_PATH, wata_payment_webhook) - diff --git a/web/tblocker.py b/web/tblocker.py index 7002f443..420f2d11 100644 --- a/web/tblocker.py +++ b/web/tblocker.py @@ -1,4 +1,5 @@ import datetime + from datetime import datetime from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError @@ -13,6 +14,7 @@ from handlers.buttons import MAIN_MENU from handlers.texts import TORRENT_BLOCKED_MSG, TORRENT_UNBLOCKED_MSG from logger import logger + last_unblock_data = {} @@ -50,27 +52,19 @@ def handle_telegram_errors(func): @handle_telegram_errors -async def send_notification( - tg_id: int, username: str, ip: str, server: str, action: str, timestamp: str -): +async def send_notification(tg_id: int, username: str, ip: str, server: str, action: str, timestamp: str): country = get_country_from_server(server) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) if action == "block": - message = TORRENT_BLOCKED_MSG.format( - username=username, country=country, duration=BLOCK_DURATION - ) + message = TORRENT_BLOCKED_MSG.format(username=username, country=country, duration=BLOCK_DURATION) else: message = TORRENT_UNBLOCKED_MSG.format(username=username, country=country) - await bot.send_message( - chat_id=tg_id, text=message, parse_mode="HTML", reply_markup=builder.as_markup() - ) - logger.info( - f"Отправлено уведомление пользователю {tg_id} о {action} для подписки {username}" - ) + await bot.send_message(chat_id=tg_id, text=message, parse_mode="HTML", reply_markup=builder.as_markup()) + logger.info(f"Отправлено уведомление пользователю {tg_id} о {action} для подписки {username}") return True @@ -93,17 +87,13 @@ async def tblocker_webhook(request: web.Request): current_time = datetime.now().timestamp() last_unblock_data = { - k: v - for k, v in last_unblock_data.items() - if current_time - v["received_at"] <= TIMESTAMP_TTL + k: v for k, v in last_unblock_data.items() if current_time - v["received_at"] <= TIMESTAMP_TTL } cache_key = f"{username}:{server}" if action == "unblock" and cache_key in last_unblock_data: if timestamp == last_unblock_data[cache_key]["timestamp"]: - return web.json_response( - {"status": "ok", "message": "duplicate unblock skipped"} - ) + return web.json_response({"status": "ok", "message": "duplicate unblock skipped"}) if action == "unblock": last_unblock_data[cache_key] = { @@ -129,9 +119,7 @@ async def tblocker_webhook(request: web.Request): ) if not success: - logger.warning( - f"Не удалось отправить уведомление пользователю {key_info['tg_id']}" - ) + logger.warning(f"Не удалось отправить уведомление пользователю {key_info['tg_id']}") return web.json_response({"status": "ok"}) diff --git a/web/wata_payment.py b/web/wata_payment.py index 14159f79..1d52c3d6 100644 --- a/web/wata_payment.py +++ b/web/wata_payment.py @@ -1,13 +1,16 @@ import base64 -import aiohttp -from aiohttp import web import json -from database import async_session_maker, update_balance, add_payment -from handlers.payments.utils import send_payment_success_notification -from logger import logger + +import aiohttp + +from aiohttp import web +from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding -from cryptography.hazmat.backends import default_backend + +from database import add_payment, async_session_maker, update_balance +from handlers.payments.utils import send_payment_success_notification +from logger import logger PUBLIC_KEY_URL = "https://api.wata.pro/api/h2h/public-key" @@ -24,12 +27,7 @@ async def verify_signature(raw_json: bytes, signature: str, public_key_pem: byte try: public_key = serialization.load_pem_public_key(public_key_pem, backend=default_backend()) signature_bytes = base64.b64decode(signature) - public_key.verify( - signature_bytes, - raw_json, - padding.PKCS1v15(), - hashes.SHA512() - ) + public_key.verify(signature_bytes, raw_json, padding.PKCS1v15(), hashes.SHA512()) return True except Exception as e: logger.error(f"Ошибка проверки подписи WATA: {e}") @@ -50,7 +48,9 @@ async def wata_payment_webhook(request: web.Request): return web.Response(status=400) logger.info(f"WATA webhook: {json.dumps(data, ensure_ascii=False)}") - logger.info(f"transactionId={data.get('transactionId')}, status={data.get('transactionStatus')}, orderId={data.get('orderId')}, amount={data.get('amount')}, currency={data.get('currency')}, errorCode={data.get('errorCode')}, errorDescription={data.get('errorDescription')}") + logger.info( + f"transactionId={data.get('transactionId')}, status={data.get('transactionStatus')}, orderId={data.get('orderId')}, amount={data.get('amount')}, currency={data.get('currency')}, errorCode={data.get('errorCode')}, errorDescription={data.get('errorDescription')}" + ) if data.get("transactionStatus") == "Paid": tg_id = data.get("orderId") amount = data.get("amount")