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'([^<]*)', r'\1', line) - + cleaned_line = re.sub(r'([^<]*)', r"\1", line) + button_data = json.loads(cleaned_line) if not isinstance(button_data, dict) or "text" not in button_data: logger.warning(f"Неверный формат кнопки: {line}") continue - + text_btn = button_data["text"] if "callback" in button_data: @@ -63,19 +65,19 @@ def parse_message_buttons(text: str) -> tuple[str, InlineKeyboardMarkup | None]: else: logger.warning(f"Кнопка без действия: {line}") continue - + buttons.append([button]) - + except json.JSONDecodeError as e: logger.warning(f"Ошибка парсинга JSON кнопки: {line} - {e}") continue except Exception as e: logger.error(f"Ошибка создания кнопки: {line} - {e}") continue - + if not buttons: return clean_text, None - + keyboard = InlineKeyboardMarkup(inline_keyboard=buttons) return clean_text, keyboard @@ -151,42 +153,30 @@ async def handle_message_input(message: Message, state: FSMContext): max_len = 1024 if photo else 4096 if len(clean_text) > max_len: await message.answer( - f"⚠️ Сообщение слишком длинное.\n" - f"Максимум: {max_len} символов, сейчас: {len(clean_text)}.", + f"⚠️ Сообщение слишком длинное.\nМаксимум: {max_len} символов, сейчас: {len(clean_text)}.", reply_markup=build_admin_back_kb("sender"), ) await state.clear() return - await state.update_data( - text=clean_text, - photo=photo, - keyboard=keyboard.model_dump() if keyboard else None - ) + await state.update_data(text=clean_text, photo=photo, keyboard=keyboard.model_dump() if keyboard else None) await state.set_state(AdminSender.preview) if photo: - await message.answer_photo( - photo=photo, - caption=clean_text, - parse_mode="HTML", - reply_markup=keyboard - ) + await message.answer_photo(photo=photo, caption=clean_text, parse_mode="HTML", reply_markup=keyboard) else: - await message.answer( - text=clean_text, - parse_mode="HTML", - reply_markup=keyboard - ) + await message.answer(text=clean_text, parse_mode="HTML", reply_markup=keyboard) await message.answer( "👀 Это предпросмотр рассылки.\nОтправить?", - reply_markup=InlineKeyboardMarkup(inline_keyboard=[ - [ - InlineKeyboardButton(text="📤 Отправить", callback_data="send_message"), - InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_message"), + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="📤 Отправить", callback_data="send_message"), + InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_message"), + ] ] - ]), + ), ) @@ -226,9 +216,7 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext, query = select(distinct(subquery.c.tg_id)) elif send_to == "untrial": subquery = select(Key.tg_id) - query = select(distinct(User.tg_id)).where( - ~User.tg_id.in_(subquery) & User.trial.in_([0, -1]) - ) + query = select(distinct(User.tg_id)).where(~User.tg_id.in_(subquery) & User.trial.in_([0, -1])) elif send_to == "cluster": query = ( select(distinct(User.tg_id)) @@ -253,9 +241,7 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext, total_users = len(tg_ids) success_count = 0 - await callback_query.message.edit_text( - f"📤 Рассылка начата!\n👥 Количество получателей: {total_users}" - ) + await callback_query.message.edit_text(f"📤 Рассылка начата!\n👥 Количество получателей: {total_users}") for tg_id in tg_ids: try: @@ -296,4 +282,4 @@ async def handle_send_cancel(callback_query: CallbackQuery, state: FSMContext): "🚫 Рассылка отменена.", reply_markup=build_admin_back_kb("sender"), ) - await state.clear() \ No newline at end of file + await state.clear() diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py index c1871286..98480301 100644 --- a/handlers/admin/servers/keyboard.py +++ b/handlers/admin/servers/keyboard.py @@ -1,5 +1,5 @@ from aiogram.filters.callback_data import CallbackData -from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from handlers.buttons import BACK @@ -10,10 +10,7 @@ class AdminServerCallback(CallbackData, prefix="admin_server"): data: str -def build_manage_server_kb( - server_name: str, cluster_name: str, enabled: bool -) -> InlineKeyboardMarkup: - +def build_manage_server_kb(server_name: str, cluster_name: str, enabled: bool) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() toggle_text = "🔴 Отключить" if enabled else "🟢 Включить" @@ -21,9 +18,7 @@ def build_manage_server_kb( builder.button( text=toggle_text, - callback_data=AdminServerCallback( - action=toggle_action, data=server_name - ).pack(), + callback_data=AdminServerCallback(action=toggle_action, data=server_name).pack(), ) builder.button( @@ -54,55 +49,30 @@ def build_edit_server_fields_kb(server_name: str, server_data: dict) -> InlineKe builder = InlineKeyboardBuilder() builder.row( - InlineKeyboardButton( - text="📝 Имя сервера", - callback_data=f"edit_server_field|{server_name}|server_name" - ) - ) - - builder.row( - InlineKeyboardButton( - text="🗂 Кластер", - callback_data=f"edit_server_field|{server_name}|cluster_name" - ) - ) - - builder.row( - InlineKeyboardButton( - text="🌐 API URL", - callback_data=f"edit_server_field|{server_name}|api_url" - ) + InlineKeyboardButton(text="📝 Имя сервера", callback_data=f"edit_server_field|{server_name}|server_name") ) + builder.row(InlineKeyboardButton(text="🗂 Кластер", callback_data=f"edit_server_field|{server_name}|cluster_name")) + + builder.row(InlineKeyboardButton(text="🌐 API URL", callback_data=f"edit_server_field|{server_name}|api_url")) + if server_data.get("subscription_url"): builder.row( InlineKeyboardButton( - text="📡 Subscription URL", - callback_data=f"edit_server_field|{server_name}|subscription_url" + text="📡 Subscription URL", callback_data=f"edit_server_field|{server_name}|subscription_url" ) ) - + + builder.row(InlineKeyboardButton(text="🔑 Inbound ID", callback_data=f"edit_server_field|{server_name}|inbound_id")) + + builder.row(InlineKeyboardButton(text="⚙️ Тип панели", callback_data=f"select_panel_type|{server_name}")) + builder.row( InlineKeyboardButton( - text="🔑 Inbound ID", - callback_data=f"edit_server_field|{server_name}|inbound_id" + text="⬅️ Назад", callback_data=AdminServerCallback(action="manage", data=server_name).pack() ) ) - - builder.row( - InlineKeyboardButton( - text="⚙️ Тип панели", - callback_data=f"select_panel_type|{server_name}" - ) - ) - - builder.row( - InlineKeyboardButton( - text="⬅️ Назад", - callback_data=AdminServerCallback(action="manage", data=server_name).pack() - ) - ) - + return builder.as_markup() @@ -111,44 +81,23 @@ def build_panel_type_selection_kb(server_name: str) -> InlineKeyboardMarkup: inline_keyboard=[ [InlineKeyboardButton(text="🌐 3x-ui", callback_data=f"set_panel_type|{server_name}|3x-ui")], [InlineKeyboardButton(text="🌀 remnawave", callback_data=f"set_panel_type|{server_name}|remnawave")], - [InlineKeyboardButton( - text="⬅️ Назад", - callback_data=f"edit_server|{server_name}" - )] + [InlineKeyboardButton(text="⬅️ Назад", callback_data=f"edit_server|{server_name}")], ] ) def build_cluster_selection_kb(server_name: str, clusters: list[str]) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - + for cluster in clusters: - builder.row( - InlineKeyboardButton( - text=cluster, - callback_data=f"set_cluster|{server_name}|{cluster}" - ) - ) - - builder.row( - InlineKeyboardButton( - text="⬅️ Назад", - callback_data=f"edit_server|{server_name}" - ) - ) - + builder.row(InlineKeyboardButton(text=cluster, callback_data=f"set_cluster|{server_name}|{cluster}")) + + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"edit_server|{server_name}")) + return builder.as_markup() def build_cancel_edit_kb(server_name: str) -> InlineKeyboardMarkup: return InlineKeyboardMarkup( - inline_keyboard=[ - [ - InlineKeyboardButton( - text="❌ Отменить", - callback_data=f"edit_server|{server_name}" - ) - ] - ] + inline_keyboard=[[InlineKeyboardButton(text="❌ Отменить", callback_data=f"edit_server|{server_name}")]] ) - diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py index 2652f9f1..4e84b6bb 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -9,25 +9,26 @@ from sqlalchemy.ext.asyncio import AsyncSession from database import get_servers from database.models import Key, Server from database.servers import ( + get_available_clusters, get_server_by_name, + update_server_cluster, update_server_field, update_server_name_with_keys, - get_available_clusters, - update_server_cluster, ) from filters.admin import IsAdminFilter from handlers.buttons import BACK from ..panel.keyboard import build_admin_back_kb from .keyboard import ( - AdminServerCallback, - build_manage_server_kb, - build_edit_server_fields_kb, - build_panel_type_selection_kb, - build_cluster_selection_kb, + AdminServerCallback, build_cancel_edit_kb, + build_cluster_selection_kb, + build_edit_server_fields_kb, + build_manage_server_kb, + build_panel_type_selection_kb, ) + router = Router() @@ -40,9 +41,7 @@ class ServerEditState(StatesGroup): editing_value = State() -@router.callback_query( - AdminServerCallback.filter(F.action == "manage"), IsAdminFilter() -) +@router.callback_query(AdminServerCallback.filter(F.action == "manage"), IsAdminFilter()) async def handle_server_manage( callback_query: CallbackQuery, callback_data: AdminServerCallback, @@ -52,12 +51,7 @@ async def handle_server_manage( servers = await get_servers(session=session, include_enabled=True) cluster_name, server = next( - ( - (c, s) - for c, cs in servers.items() - for s in cs - if s["server_name"] == server_name - ), + ((c, s) for c, cs in servers.items() for s in cs if s["server_name"] == server_name), (None, None), ) @@ -69,9 +63,7 @@ async def handle_server_manage( max_keys = server.get("max_keys") limit_display = f"{max_keys}" if max_keys else "не задан" - result = await session.execute( - select(func.count()).where(Key.server_id == server_name) - ) + result = await session.execute(select(func.count()).where(Key.server_id == server_name)) subscription_count = result.scalar() or 0 text = ( @@ -97,17 +89,13 @@ async def handle_server_manage( await callback_query.message.edit_text( text=text, - reply_markup=build_manage_server_kb( - server_name, cluster_name, enabled=server.get("enabled", True) - ), + reply_markup=build_manage_server_kb(server_name, cluster_name, enabled=server.get("enabled", True)), ) else: await callback_query.message.edit_text(text="❌ Сервер не найден.") -@router.callback_query( - AdminServerCallback.filter(F.action == "delete"), IsAdminFilter() -) +@router.callback_query(AdminServerCallback.filter(F.action == "delete"), IsAdminFilter()) async def process_callback_delete_server( callback_query: CallbackQuery, callback_data: AdminServerCallback, @@ -139,16 +127,9 @@ async def process_callback_delete_server( if keys_count > 0: await state.update_data(server_name=server_name, cluster_name=cluster_name) - subq = ( - select(func.count()) - .where(Key.server_id == Server.server_name) - .correlate(Server) - .scalar_subquery() - ) + subq = select(func.count()).where(Key.server_id == Server.server_name).correlate(Server).scalar_subquery() - stmt_all_servers = select(Server.server_name, subq.label("key_count")).where( - Server.server_name != server_name - ) + stmt_all_servers = select(Server.server_name, subq.label("key_count")).where(Server.server_name != server_name) result = await session.execute(stmt_all_servers) all_servers = result.all() @@ -164,9 +145,7 @@ async def process_callback_delete_server( builder.row( InlineKeyboardButton( text=BACK, - callback_data=AdminServerCallback( - action="manage", data=server_name - ).pack(), + callback_data=AdminServerCallback(action="manage", data=server_name).pack(), ) ) @@ -184,31 +163,20 @@ async def process_callback_delete_server( remaining_servers = result.scalar_one() if remaining_servers == 0: - stmt_other_clusters = ( - select(Server.cluster_name) - .distinct() - .where(Server.cluster_name != cluster_name) - ) + stmt_other_clusters = select(Server.cluster_name).distinct().where(Server.cluster_name != cluster_name) result = await session.execute(stmt_other_clusters) other_clusters = result.scalars().all() if other_clusters: - stmt_cluster_keys = select(func.count()).where( - Key.server_id == cluster_name - ) + stmt_cluster_keys = select(func.count()).where(Key.server_id == cluster_name) result = await session.execute(stmt_cluster_keys) cluster_keys_count = result.scalar_one() if cluster_keys_count > 0: - await state.update_data( - server_name=server_name, cluster_name=cluster_name - ) + await state.update_data(server_name=server_name, cluster_name=cluster_name) subq_cluster = ( - select(func.count()) - .where(Key.server_id == Server.cluster_name) - .correlate(Server) - .scalar_subquery() + select(func.count()).where(Key.server_id == Server.cluster_name).correlate(Server).scalar_subquery() ) stmt_all_clusters = ( @@ -230,9 +198,7 @@ async def process_callback_delete_server( builder.row( InlineKeyboardButton( text=BACK, - callback_data=AdminServerCallback( - action="manage", data=server_name - ).pack(), + callback_data=AdminServerCallback(action="manage", data=server_name).pack(), ) ) @@ -243,9 +209,7 @@ async def process_callback_delete_server( await state.set_state(AdminClusterStates.waiting_for_cluster_transfer) return - stmt_delete = delete(Server).where( - (Server.cluster_name == cluster_name) & (Server.server_name == server_name) - ) + stmt_delete = delete(Server).where((Server.cluster_name == cluster_name) & (Server.server_name == server_name)) await session.execute(stmt_delete) await session.commit() await callback_query.message.edit_text( @@ -253,9 +217,7 @@ async def process_callback_delete_server( reply_markup=build_admin_back_kb("clusters"), ) else: - stmt_delete = delete(Server).where( - (Server.cluster_name == cluster_name) & (Server.server_name == server_name) - ) + stmt_delete = delete(Server).where((Server.cluster_name == cluster_name) & (Server.server_name == server_name)) await session.execute(stmt_delete) await session.commit() await callback_query.message.edit_text( @@ -264,9 +226,7 @@ async def process_callback_delete_server( ) -@router.callback_query( - AdminServerCallback.filter(F.action.in_(["enable", "disable"])), IsAdminFilter() -) +@router.callback_query(AdminServerCallback.filter(F.action.in_(["enable", "disable"])), IsAdminFilter()) async def toggle_server_enabled( callback_query: CallbackQuery, callback_data: AdminServerCallback, @@ -277,22 +237,13 @@ async def toggle_server_enabled( new_status = action == "enable" - await session.execute( - update(Server) - .where(Server.server_name == server_name) - .values(enabled=new_status) - ) + await session.execute(update(Server).where(Server.server_name == server_name).values(enabled=new_status)) await session.commit() servers = await get_servers(session=session, include_enabled=True) cluster_name, server = next( - ( - (c, s) - for c, cs in servers.items() - for s in cs - if s["server_name"] == server_name - ), + ((c, s) for c, cs in servers.items() for s in cs if s["server_name"] == server_name), (None, None), ) @@ -313,18 +264,12 @@ async def toggle_server_enabled( await callback_query.message.edit_text( text=text, - reply_markup=build_manage_server_kb( - server_name, cluster_name, enabled=new_status - ), + reply_markup=build_manage_server_kb(server_name, cluster_name, enabled=new_status), ) -@router.callback_query( - AdminServerCallback.filter(F.action == "set_limit"), IsAdminFilter() -) -async def ask_server_limit( - callback_query: CallbackQuery, callback_data: AdminServerCallback, state: FSMContext -): +@router.callback_query(AdminServerCallback.filter(F.action == "set_limit"), IsAdminFilter()) +async def ask_server_limit(callback_query: CallbackQuery, callback_data: AdminServerCallback, state: FSMContext): server_name = callback_data.data await state.set_state(ServerLimitState.waiting_for_limit) await state.update_data(server_name=server_name) @@ -334,9 +279,7 @@ async def ask_server_limit( @router.message(ServerLimitState.waiting_for_limit, IsAdminFilter()) -async def save_server_limit( - message: types.Message, state: FSMContext, session: AsyncSession -): +async def save_server_limit(message: types.Message, state: FSMContext, session: AsyncSession): try: limit = int(message.text.strip()) if limit < 0: @@ -347,21 +290,12 @@ async def save_server_limit( new_value = limit if limit > 0 else None - await session.execute( - update(Server) - .where(Server.server_name == server_name) - .values(max_keys=new_value) - ) + await session.execute(update(Server).where(Server.server_name == server_name).values(max_keys=new_value)) await session.commit() servers = await get_servers(session=session, include_enabled=True) cluster_name, server = next( - ( - (c, s) - for c, cs in servers.items() - for s in cs - if s["server_name"] == server_name - ), + ((c, s) for c, cs in servers.items() for s in cs if s["server_name"] == server_name), (None, None), ) @@ -383,9 +317,7 @@ async def save_server_limit( await message.answer( text, - reply_markup=build_manage_server_kb( - server_name, cluster_name, enabled=server.get("enabled", True) - ), + reply_markup=build_manage_server_kb(server_name, cluster_name, enabled=server.get("enabled", True)), ) await state.clear() @@ -398,69 +330,69 @@ async def start_edit_server(callback: CallbackQuery, state: FSMContext, session: server_name = callback.data.split("|")[1] await state.clear() - + server_data = await get_server_by_name(session, server_name) if not server_data: await callback.message.edit_text("❌ Сервер не найден.") return - + await callback.message.edit_text( - f"✏️ Редактирование сервера: {server_name}\n\n" - "Выберите поле для редактирования:", - reply_markup=build_edit_server_fields_kb(server_name, server_data) + f"✏️ Редактирование сервера: {server_name}\n\nВыберите поле для редактирования:", + reply_markup=build_edit_server_fields_kb(server_name, server_data), ) @router.callback_query(F.data.startswith("edit_server_field|"), IsAdminFilter()) async def ask_new_field_value(callback: CallbackQuery, state: FSMContext, session: AsyncSession): _, server_name, field = callback.data.split("|") - + if field == "cluster_name": clusters = await get_available_clusters(session) await callback.message.edit_text( f"🗂 Выберите кластер для сервера {server_name}:", - reply_markup=build_cluster_selection_kb(server_name, clusters) + reply_markup=build_cluster_selection_kb(server_name, clusters), ) return - + await state.update_data(server_name=server_name, field=field) await state.set_state(ServerEditState.editing_value) - + field_names = { "server_name": "имя сервера", "api_url": "API URL", "subscription_url": "Subscription URL", "inbound_id": "Inbound ID", } - + await callback.message.edit_text( f"✏️ Введите новое значение для {field_names.get(field, field)}:", - reply_markup=build_cancel_edit_kb(server_name) + reply_markup=build_cancel_edit_kb(server_name), ) @router.callback_query(F.data.startswith("select_panel_type|"), IsAdminFilter()) async def select_panel_type(callback: CallbackQuery): server_name = callback.data.split("|")[1] - + await callback.message.edit_text( f"⚙️ Выберите тип панели для сервера {server_name}:", - reply_markup=build_panel_type_selection_kb(server_name) + reply_markup=build_panel_type_selection_kb(server_name), ) @router.callback_query(F.data.startswith("set_panel_type|"), IsAdminFilter()) async def set_panel_type(callback: CallbackQuery, session: AsyncSession): _, server_name, panel_type = callback.data.split("|") - + success = await update_server_field(session, server_name, "panel_type", panel_type) if success: await callback.message.edit_text( f"✅ Тип панели сервера {server_name} изменен на {panel_type}", - reply_markup=InlineKeyboardBuilder().button( - text="⬅️ Назад к серверу", - callback_data=AdminServerCallback(action="manage", data=server_name).pack() - ).as_markup() + reply_markup=InlineKeyboardBuilder() + .button( + text="⬅️ Назад к серверу", callback_data=AdminServerCallback(action="manage", data=server_name).pack() + ) + .as_markup(), ) else: await callback.message.edit_text("❌ Ошибка при изменении типа панели") @@ -469,15 +401,16 @@ async def set_panel_type(callback: CallbackQuery, session: AsyncSession): @router.callback_query(F.data.startswith("set_cluster|"), IsAdminFilter()) async def set_cluster(callback: CallbackQuery, session: AsyncSession): _, server_name, new_cluster = callback.data.split("|") - + success = await update_server_cluster(session, server_name, new_cluster) if success: await callback.message.edit_text( f"✅ Кластер сервера {server_name} изменен на {new_cluster}", - reply_markup=InlineKeyboardBuilder().button( - text="⬅️ Назад к серверу", - callback_data=AdminServerCallback(action="manage", data=server_name).pack() - ).as_markup() + reply_markup=InlineKeyboardBuilder() + .button( + text="⬅️ Назад к серверу", callback_data=AdminServerCallback(action="manage", data=server_name).pack() + ) + .as_markup(), ) else: await callback.message.edit_text("❌ Ошибка при изменении кластера") @@ -499,7 +432,7 @@ async def apply_field_edit(message: types.Message, state: FSMContext, session: A return else: success = await update_server_field(session, server_name, field, value) - + if success: field_names = { "server_name": "имя сервера", @@ -507,15 +440,16 @@ async def apply_field_edit(message: types.Message, state: FSMContext, session: A "subscription_url": "Subscription URL", "inbound_id": "Inbound ID", } - + await message.answer( f"✅ {field_names.get(field, field).capitalize()} изменено", - reply_markup=InlineKeyboardBuilder().button( - text="⬅️ Назад к серверу", - callback_data=AdminServerCallback(action="manage", data=server_name).pack() - ).as_markup() + reply_markup=InlineKeyboardBuilder() + .button( + text="⬅️ Назад к серверу", callback_data=AdminServerCallback(action="manage", data=server_name).pack() + ) + .as_markup(), ) else: await message.answer("❌ Ошибка при изменении поля") - + await state.clear() diff --git a/handlers/admin/stats/keyboard.py b/handlers/admin/stats/keyboard.py index 9da49fbb..06480040 100644 --- a/handlers/admin/stats/keyboard.py +++ b/handlers/admin/stats/keyboard.py @@ -6,9 +6,7 @@ from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn def build_stats_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="🔄 Обновить", callback_data=AdminPanelCallback(action="stats").pack() - ) + builder.button(text="🔄 Обновить", callback_data=AdminPanelCallback(action="stats").pack()) builder.button( text="📥 Выгрузить пользователей в CSV", callback_data=AdminPanelCallback(action="stats_export_users_csv").pack(), diff --git a/handlers/admin/stats/stats_handler.py b/handlers/admin/stats/stats_handler.py index c37deb5c..cf932f33 100644 --- a/handlers/admin/stats/stats_handler.py +++ b/handlers/admin/stats/stats_handler.py @@ -1,17 +1,18 @@ +from collections import Counter from datetime import datetime, timedelta import pytz + from aiogram import F, Router from aiogram.exceptions import TelegramBadRequest from aiogram.types import CallbackQuery, Message from sqlalchemy.ext.asyncio import AsyncSession -from collections import Counter - from bot import bot from config import ADMIN_ID from database import ( count_active_keys, + count_hot_leads, count_total_keys, count_total_referrals, count_total_users, @@ -20,13 +21,12 @@ from database import ( count_users_registered_since, count_users_updated_today, get_tariff_distribution, - get_tariff_names, - get_tariff_groups, get_tariff_durations, + get_tariff_groups, + get_tariff_names, sum_payments_between, sum_payments_since, sum_total_payments, - count_hot_leads ) from filters.admin import IsAdminFilter from logger import logger @@ -40,6 +40,7 @@ from utils.csv_export import ( from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from .keyboard import build_stats_kb + router = Router() @@ -53,39 +54,37 @@ async def handle_stats(callback_query: CallbackQuery, session: AsyncSession): total_users = await count_total_users(session) today_start = moscow_tz.localize(datetime.combine(today, datetime.min.time())) today_start_utc = today_start.astimezone(pytz.UTC).replace(tzinfo=None) - + users_updated_today = await count_users_updated_today(session, today_start_utc) registrations_today = await count_users_registered_since(session, today_start_utc) - + yesterday_date = today - timedelta(days=1) yesterday_start = moscow_tz.localize(datetime.combine(yesterday_date, datetime.min.time())) yesterday_end = moscow_tz.localize(datetime.combine(today, datetime.min.time())) yesterday_start_utc = yesterday_start.astimezone(pytz.UTC).replace(tzinfo=None) yesterday_end_utc = yesterday_end.astimezone(pytz.UTC).replace(tzinfo=None) - - registrations_yesterday = await count_users_registered_between( - session, yesterday_start_utc, yesterday_end_utc - ) + + registrations_yesterday = await count_users_registered_between(session, yesterday_start_utc, yesterday_end_utc) week_start_date = today - timedelta(days=today.weekday()) week_start = moscow_tz.localize(datetime.combine(week_start_date, datetime.min.time())) week_start_utc = week_start.astimezone(pytz.UTC).replace(tzinfo=None) - + month_start_date = today.replace(day=1) month_start = moscow_tz.localize(datetime.combine(month_start_date, datetime.min.time())) month_start_utc = month_start.astimezone(pytz.UTC).replace(tzinfo=None) - + registrations_week = await count_users_registered_since(session, week_start_utc) registrations_month = await count_users_registered_since(session, month_start_utc) - + last_month_start_date = (today.replace(day=1) - timedelta(days=1)).replace(day=1) this_month_start_date = today.replace(day=1) - + last_month_start = moscow_tz.localize(datetime.combine(last_month_start_date, datetime.min.time())) last_month_end = moscow_tz.localize(datetime.combine(this_month_start_date, datetime.min.time())) last_month_start_utc = last_month_start.astimezone(pytz.UTC).replace(tzinfo=None) last_month_end_utc = last_month_end.astimezone(pytz.UTC).replace(tzinfo=None) - + registrations_last_month = await count_users_registered_between( session, last_month_start_utc, last_month_end_utc ) @@ -130,43 +129,31 @@ async def handle_stats(callback_query: CallbackQuery, session: AsyncSession): "Без тарифа: 3 мес": 2, "Без тарифа: 6 мес": 3, "Без тарифа: 12 мес": 4, - "Без тарифа: прочее": 5 + "Без тарифа: прочее": 5, } - sorted_buckets = sorted( - duration_buckets.items(), - key=lambda x: bucket_order.get(x[0], 999) - ) + sorted_buckets = sorted(duration_buckets.items(), key=lambda x: bucket_order.get(x[0], 999)) for name, count in sorted_buckets: tariff_stats_text += f"├ {name}: {count}\n" for group, tariffs in grouped_tariffs.items(): tariff_stats_text += f"Тариф {group}\n" - sorted_tariffs = sorted( - tariffs, - key=lambda x: tariff_durations.get(x[0], 0) - ) + sorted_tariffs = sorted(tariffs, key=lambda x: tariff_durations.get(x[0], 0)) for tid, count in sorted_tariffs: name = tariff_names.get(tid, f"ID {tid}") tariff_stats_text += f" ├ {name}: {count}\n" tariff_stats_text = ( - "└ По тарифам и срокам:\n" + tariff_stats_text - if tariff_stats_text - else "└ Нет данных по тарифам\n" + "└ По тарифам и срокам:\n" + tariff_stats_text if tariff_stats_text else "└ Нет данных по тарифам\n" ) total_referrals = await count_total_referrals(session) total_payments_today = await sum_payments_since(session, today_start_utc) - total_payments_yesterday = await sum_payments_between( - session, yesterday_start_utc, yesterday_end_utc - ) + total_payments_yesterday = await sum_payments_between(session, yesterday_start_utc, yesterday_end_utc) total_payments_week = await sum_payments_since(session, week_start_utc) total_payments_month = await sum_payments_since(session, month_start_utc) - total_payments_last_month = await sum_payments_between( - session, last_month_start_utc, last_month_end_utc - ) + total_payments_last_month = await sum_payments_between(session, last_month_start_utc, last_month_end_utc) total_payments_all_time = await sum_total_payments(session) hot_leads_count = await count_hot_leads(session) @@ -208,79 +195,55 @@ async def handle_stats(callback_query: CallbackQuery, session: AsyncSession): f"⏱️ Последнее обновление: {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")