formatting/3x-ui db import/cosmetic fixes
This commit is contained in:
+5
-6
@@ -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
|
||||
return admin
|
||||
|
||||
+25
-17
@@ -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),
|
||||
):
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
+7
-6
@@ -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"])
|
||||
|
||||
+13
-17
@@ -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="Ошибка при создании ключа")
|
||||
raise HTTPException(status_code=500, detail="Ошибка при создании ключа")
|
||||
|
||||
+36
-24
@@ -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)],
|
||||
)
|
||||
|
||||
+10
-11
@@ -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"}
|
||||
return {"status": "deleted_one"}
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
+9
-9
@@ -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():
|
||||
|
||||
+14
-7
@@ -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
|
||||
|
||||
+12
-11
@@ -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
|
||||
from_attributes = True
|
||||
|
||||
+17
-16
@@ -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
|
||||
from_attributes = True
|
||||
|
||||
+40
-39
@@ -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="Флаг триального ключа")
|
||||
|
||||
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="Флаг триального ключа")
|
||||
|
||||
+7
-6
@@ -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
|
||||
|
||||
@@ -7,4 +7,4 @@ class ReferralResponse(BaseModel):
|
||||
reward_issued: bool = False
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
from_attributes = True
|
||||
|
||||
+15
-14
@@ -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
|
||||
from_attributes = True
|
||||
|
||||
+16
-16
@@ -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
|
||||
from_attributes = True
|
||||
|
||||
+19
-18
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <a href=\"{repo_url}/commit/{local_hash}\">"
|
||||
f'\n(commit <a href="{repo_url}/commit/{local_hash}">'
|
||||
f"#{local_number}</a> / actual commit "
|
||||
f"<a href=\"{repo_url}/commit/{remote_hash}\">#{remote_number}</a>)"
|
||||
f'<a href="{repo_url}/commit/{remote_hash}">#{remote_number}</a>)'
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return f"\n(Требуется обновление через CLI, команда <code>sudo solobot</code>)"
|
||||
return "\n(Требуется обновление через CLI, команда <code>sudo solobot</code>)"
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
|
||||
+26
-56
@@ -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":
|
||||
|
||||
+1
-5
@@ -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()
|
||||
|
||||
+6
-18
@@ -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()
|
||||
|
||||
+2
-3
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
+23
-19
@@ -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()
|
||||
|
||||
+11
-39
@@ -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}")
|
||||
|
||||
+7
-11
@@ -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)
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
+10
-22
@@ -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}")
|
||||
|
||||
+5
-14
@@ -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}")
|
||||
|
||||
+15
-52
@@ -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()]
|
||||
|
||||
+32
-56
@@ -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:
|
||||
|
||||
+20
-56
@@ -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()
|
||||
return result.scalar()
|
||||
|
||||
+15
-31
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
+9
-30
@@ -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)
|
||||
|
||||
+4
-5
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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="📊 <b>Аналитика рекламы:</b>", reply_markup=build_ads_kb()
|
||||
)
|
||||
await callback_query.message.edit_text(text="📊 <b>Аналитика рекламы:</b>", 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"🔗 Введите <b>код ссылки</b> для: <code>{name}</code>.\n\n"
|
||||
f"💡 <b>Правила:</b> только латинские буквы и цифры",
|
||||
f"🔗 Введите <b>код ссылки</b> для: <code>{name}</code>.\n\n💡 <b>Правила:</b> только латинские буквы и цифры",
|
||||
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>{code}</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="📊 <b>Аналитика рекламы:</b>", reply_markup=build_ads_kb()
|
||||
)
|
||||
await callback_query.message.edit_text(text="📊 <b>Аналитика рекламы:</b>", reply_markup=build_ads_kb())
|
||||
|
||||
|
||||
def format_ads_stats(stats: dict, username_bot: str) -> str:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -6,6 +6,7 @@ from filters.admin import IsAdminFilter
|
||||
|
||||
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
@@ -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("❌ Ошибка при удалении вручную забаненных пользователей.")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
"<i>Пример:</i> <code>cluster1</code> или <code>us_east_1</code>"
|
||||
)
|
||||
|
||||
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"🌍 <b>{prefix} {server_name}</b> - {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"🌍 <b>{prefix} {server_name}</b> - {online_remna_users} онлайн\n"
|
||||
)
|
||||
result_text += f"🌍 <b>{prefix} {server_name}</b> - {online_remna_users} онлайн\n"
|
||||
|
||||
except Exception as e:
|
||||
error_text = str(e) or "Сервер недоступен"
|
||||
result_text += f"❌ <b>{prefix} {server_name}</b> - ошибка: {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"✅ Время подписки продлено на <b>{days} дней</b> всем пользователям в кластере <b>{cluster_name}</b>."
|
||||
@@ -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(
|
||||
"❌ Произошла ошибка при установке тарифной группы."
|
||||
)
|
||||
await callback.message.edit_text("❌ Произошла ошибка при установке тарифной группы.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = (
|
||||
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
|
||||
"📝 <i>код</i> 💰 <i>сумма</i> 🔢 <i>лимит</i>\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 = (
|
||||
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
|
||||
"📝 <i>код</i> ⏳ <i>дни</i> 🔢 <i>лимит</i>\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(),
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
__all__ = ("router",)
|
||||
|
||||
from .gifts_handler import router
|
||||
from .gifts_handler import router
|
||||
|
||||
@@ -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="🎁 <b>Подарки</b>\nВыберите, что хотите сделать:",
|
||||
reply_markup=build_admin_gifts_kb()
|
||||
text="🎁 <b>Подарки</b>\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"🎁 <b>Список подарков</b>\nСтраница {page}:",
|
||||
reply_markup=builder.as_markup()
|
||||
)
|
||||
await callback.message.edit_text(f"🎁 <b>Список подарков</b>\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"🎁 <b>Список подарков</b>\nСтраница {page}:",
|
||||
reply_markup=builder.as_markup()
|
||||
)
|
||||
await callback.message.edit_text(f"🎁 <b>Список подарков</b>\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())
|
||||
|
||||
@@ -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()
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -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()
|
||||
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()
|
||||
|
||||
@@ -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(
|
||||
"👑 <b>Список админов</b>",
|
||||
reply_markup=build_admins_kb(admins)
|
||||
)
|
||||
await callback.message.edit_text("👑 <b>Список админов</b>", 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(
|
||||
"Введите <code>tg_id</code> нового админа:",
|
||||
reply_markup=build_admin_back_kb_to_admins()
|
||||
"Введите <code>tg_id</code> нового админа:", 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"✅ Админ <code>{tg_id}</code> добавлен.",
|
||||
reply_markup=build_admin_back_kb_to_admins()
|
||||
)
|
||||
await message.answer(f"✅ Админ <code>{tg_id}</code> добавлен.", 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"👤 <b>Управление админом</b> <code>{tg_id}</code>",
|
||||
reply_markup=build_single_admin_menu(tg_id, role)
|
||||
f"👤 <b>Управление админом</b> <code>{tg_id}</code>", reply_markup=build_single_admin_menu(tg_id, role)
|
||||
)
|
||||
|
||||
|
||||
@@ -229,7 +226,7 @@ async def generate_token(callback: CallbackQuery, callback_data: AdminPanelCallb
|
||||
f"🎟 <b>Новый токен для</b> <code>{tg_id}</code>:\n\n"
|
||||
f"<code>{token}</code>\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"✏ <b>Выберите новую роль для</b> <code>{tg_id}</code>:",
|
||||
reply_markup=build_role_selection_kb(tg_id)
|
||||
f"✏ <b>Выберите новую роль для</b> <code>{tg_id}</code>:", 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(
|
||||
"🚫 <b>Нельзя изменить свою собственную роль!</b>",
|
||||
reply_markup=build_single_admin_menu(tg_id)
|
||||
"🚫 <b>Нельзя изменить свою собственную роль!</b>", 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"✅ Роль админа <code>{tg_id}</code> изменена на <b>{role}</b>.",
|
||||
reply_markup=build_single_admin_menu(tg_id)
|
||||
f"✅ Роль админа <code>{tg_id}</code> изменена на <b>{role}</b>.", 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"🗑 Админ <code>{tg_id}</code> удалён.",
|
||||
reply_markup=build_admin_back_kb_to_admins()
|
||||
f"🗑 Админ <code>{tg_id}</code> удалён.", 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
|
||||
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(
|
||||
"📂 Пришлите файл базы данных <code>x-ui.db</code> для восстановления подписок и клиентов.\n\n"
|
||||
"Формат: SQLite-файл с таблицей <code>inbounds</code>.\n\n"
|
||||
"<b>⚠️ Важно!</b> Убедитесь, что у всех подписок в панели прописан <code>telegram_id</code>.\n"
|
||||
"После восстановления обязательно выполните <b>синхронизацию</b> с текущими серверами!",
|
||||
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"🔐 Импортировано подписок: <b>{imported}</b>\n"
|
||||
f"⏭ Пропущено (уже есть): <b>{skipped}</b>",
|
||||
reply_markup=build_post_import_kb(),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Import 3x-ui] Ошибка: {e}")
|
||||
await processing_message.edit_text(
|
||||
"❌ Произошла ошибка при импорте. Убедись, что это валидный файл <code>x-ui.db</code>",
|
||||
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✅ Успешно: <b>{success}</b>\n❌ Ошибки: <b>{failed}</b>",
|
||||
parse_mode="HTML",
|
||||
reply_markup=build_back_to_db_menu(),
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<blockquote>{version}</blockquote>"
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'<tg-emoji emoji-id="[^"]*">([^<]*)</tg-emoji>', r'\1', line)
|
||||
|
||||
cleaned_line = re.sub(r'<tg-emoji emoji-id="[^"]*">([^<]*)</tg-emoji>', 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"Максимум: <b>{max_len}</b> символов, сейчас: <b>{len(clean_text)}</b>.",
|
||||
f"⚠️ Сообщение слишком длинное.\nМаксимум: <b>{max_len}</b> символов, сейчас: <b>{len(clean_text)}</b>.",
|
||||
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"📤 <b>Рассылка начата!</b>\n👥 Количество получателей: {total_users}"
|
||||
)
|
||||
await callback_query.message.edit_text(f"📤 <b>Рассылка начата!</b>\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()
|
||||
await state.clear()
|
||||
|
||||
@@ -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}")]]
|
||||
)
|
||||
|
||||
|
||||
@@ -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"<b>✏️ Редактирование сервера: {server_name}</b>\n\n"
|
||||
"Выберите поле для редактирования:",
|
||||
reply_markup=build_edit_server_fields_kb(server_name, server_data)
|
||||
f"<b>✏️ Редактирование сервера: {server_name}</b>\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"<b>🗂 Выберите кластер для сервера {server_name}:</b>",
|
||||
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"✏️ Введите новое значение для <b>{field_names.get(field, field)}</b>:",
|
||||
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"<b>⚙️ Выберите тип панели для сервера {server_name}:</b>",
|
||||
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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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}: <b>{count}</b>\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}: <b>{count}</b>\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"⏱️ <i>Последнее обновление:</i> <code>{update_time}</code>"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
)]
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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 = (
|
||||
"<b>💸 Управление тарифами</b>\n\n"
|
||||
"Вы можете выполнить следующие действия:\n\n"
|
||||
"<b>🆕 Создать тариф</b>\n"
|
||||
"• Установите длительность (в днях)\n"
|
||||
"<blockquote>• Установите длительность (в днях)\n"
|
||||
"• Задайте цену (в рублях)\n"
|
||||
"• Задайте лимит устройств (hwid/ip_limit)\n"
|
||||
"• Укажите лимит трафика (в ГБ)\n\n"
|
||||
"• Укажите лимит трафика (в ГБ)</blockquote>\n\n"
|
||||
"<b>📋 Редактировать тарифы</b>\n"
|
||||
"• Просматривайте список текущих тарифов\n"
|
||||
"• Изменяйте параметры или удаляйте при необходимости"
|
||||
)
|
||||
await callback_query.message.edit_text(
|
||||
text=text, reply_markup=build_tariff_menu_kb()
|
||||
"<blockquote>• Просматривайте список текущих тарифов\n"
|
||||
"• Изменяйте параметры или удаляйте при необходимости</blockquote>"
|
||||
)
|
||||
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(
|
||||
"📝 Введите <b>название следующего тарифа</b>:", reply_markup=build_cancel_kb()
|
||||
)
|
||||
await callback.message.edit_text("📝 Введите <b>название следующего тарифа</b>:", 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(
|
||||
"⚠️ Вы уверены, что хотите <b>удалить</b> этот тариф?",
|
||||
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"<b>📄 Тариф: {tariff.name}</b>\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)} тарифов сгруппированы в подгруппу: <b>{title}</b>.", reply_markup=build_tariff_menu_kb())
|
||||
await message.answer(
|
||||
f"✅ {len(selected_ids)} тарифов сгруппированы в подгруппу: <b>{title}</b>.",
|
||||
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"✅ Подгруппа <b>{old_title}</b> переименована в <b>{new_title}</b>.",
|
||||
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"✅ Подгруппа <b>{subgroup_title}</b> удалена.",
|
||||
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"✏️ <b>Редактирование подгруппы: {subgroup_title}</b>\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"✅ Подгруппа <b>{subgroup_title}</b> была расформирована.",
|
||||
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"✅ <b>Подгруппа обновлена: {subgroup_title}</b>\n\n"
|
||||
f"{chr(10).join(changes_text)}",
|
||||
f"✅ <b>Подгруппа обновлена: {subgroup_title}</b>\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}"
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"└ 🔄 <b>Обновлено:</b> {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"Максимум: <b>{max_len}</b> символов, сейчас: <b>{len(text_message)}</b>.",
|
||||
f"⚠️ Сообщение слишком длинное.\nМаксимум: <b>{max_len}</b> символов, сейчас: <b>{len(text_message)}</b>.",
|
||||
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 <i>🚫 Отсутствуют</i>"
|
||||
|
||||
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 += "</blockquote>"
|
||||
|
||||
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"📡 Выберите кластер, на котором пересоздать ключ <b>{email}</b>:",
|
||||
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"🌐 <b>Кластер:</b> <code>{cluster_id}</code>\n"
|
||||
f"⚠️ <b>Статус:</b> Нет привязанного тарифа\n</blockquote>"
|
||||
f"💡 <b>Привяжите тариф к кластеру</b>",
|
||||
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"🌐 <b>Кластер:</b> <code>{cluster_id}</code>\n"
|
||||
f"⚠️ <b>Статус:</b> Нет привязанного тарифа\n</blockquote>"
|
||||
f"💡 <b>Привяжите тариф к кластеру</b>",
|
||||
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🔢 <b>Общий трафик:</b> {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"✅ Пользователь <code>{callback_data.tg_id}</code> разблокирован. Нажмите кнопку ниже для возврата в профиль."
|
||||
|
||||
await callback.message.edit_text(
|
||||
text=text, reply_markup=build_editor_kb(callback_data.tg_id, edit=True)
|
||||
text = (
|
||||
f"✅ Пользователь <code>{callback_data.tg_id}</code> разблокирован. Нажмите кнопку ниже для возврата в профиль."
|
||||
)
|
||||
|
||||
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,
|
||||
|
||||
+5
-9
@@ -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]
|
||||
|
||||
+22
-45
@@ -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"• <b>{key_display}</b> ({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)
|
||||
|
||||
+2
-8
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from aiogram import Router
|
||||
|
||||
from .instructions import router as instructions_router
|
||||
|
||||
|
||||
router = Router(name="instructions_main_router")
|
||||
|
||||
router.include_routers(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
+18
-39
@@ -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}")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"🚫 <b>Невозможно создать подписку</b>\n\n"
|
||||
f"📊 <b>Информация о кластере:</b>\n<blockquote>"
|
||||
@@ -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,
|
||||
|
||||
@@ -14,6 +14,7 @@ from logger import logger
|
||||
|
||||
from .key_create import select_tariff_plan
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
+36
-83
@@ -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}")
|
||||
logger.error(f"[Error] Ошибка в complete_key_renewal: {e}")
|
||||
|
||||
+73
-198
@@ -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
|
||||
logger.error(f"[Reset Traffic] Ошибка при сбросе трафика клиента {email} в кластере {cluster_id}: {e}")
|
||||
raise
|
||||
|
||||
+47
-97
@@ -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"• <b>{key_display}</b> ({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"⏳ Осталось: <b>{format_days(days)}</b>, <b>{format_hours(hours)}</b>, <b>{format_minutes(minutes)}</b>"
|
||||
days_left_message = (
|
||||
f"⏳ Осталось: <b>{format_days(days)}</b>, <b>{format_hours(hours)}</b>, <b>{format_minutes(minutes)}</b>"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
+9
-26
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
return web.Response(text=f"❌ Ошибка сервера: {e}", status=500)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
+15
-17
@@ -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")
|
||||
|
||||
+3
-4
@@ -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(
|
||||
|
||||
+16
-31
@@ -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))
|
||||
|
||||
|
||||
+18
-41
@@ -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:
|
||||
|
||||
+16
-44
@@ -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"
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user