API/ remnawave 1.6.12/ admins in database and more
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
from typing import AsyncGenerator
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException, Header, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from database import async_session_maker
|
||||||
|
from database.models import Admin
|
||||||
|
|
||||||
|
|
||||||
|
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
|
||||||
|
def hash_token(token: str) -> str:
|
||||||
|
return hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_admin_token(
|
||||||
|
admin_id: int = Query(..., alias="tg_id"),
|
||||||
|
token: str = Header(..., alias="X-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)
|
||||||
|
)
|
||||||
|
admin = result.scalar_one_or_none()
|
||||||
|
if not admin:
|
||||||
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
return admin
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from api.routes import users, keys, coupons, servers, tariffs
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="SoloBot API (preAlpha)",
|
||||||
|
version="0.1",
|
||||||
|
docs_url="/api/docs",
|
||||||
|
redoc_url="/api/redoc",
|
||||||
|
openapi_url="/api/openapi.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
app.include_router(users.router, prefix="/api/users", tags=["Users"])
|
||||||
|
app.include_router(keys.router, prefix="/api/keys", tags=["Keys"])
|
||||||
|
app.include_router(coupons.router, prefix="/api/coupons", tags=["Coupons"])
|
||||||
|
app.include_router(servers.router, prefix="/api/servers", tags=["Servers"])
|
||||||
|
app.include_router(tariffs.router, prefix="/api/tariffs", tags=["Tariffs"])
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api", include_in_schema=False)
|
||||||
|
async def root():
|
||||||
|
return {"message": "Welcome to SoloBot API. Docs: /api/docs"}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query, Path
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
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]):
|
||||||
|
column_type = type(field.property.columns[0].type).__name__
|
||||||
|
if column_type in ("Integer", "BigInteger"):
|
||||||
|
return int(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
extra_get_by_email: bool = False,
|
||||||
|
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),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
result = await session.execute(select(model))
|
||||||
|
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(...),
|
||||||
|
admin: Admin = Depends(verify_admin_token),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
result = await session.execute(select(model).where(model.email == email))
|
||||||
|
obj = result.scalar_one_or_none()
|
||||||
|
if not obj:
|
||||||
|
raise HTTPException(status_code=404, detail="Not found by email")
|
||||||
|
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),
|
||||||
|
admin: Admin = Depends(verify_admin_token),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
field = getattr(model, identifier_field)
|
||||||
|
casted = _cast_identifier_type(field, value)
|
||||||
|
result = await session.execute(select(model).where(field == casted))
|
||||||
|
obj = result.scalar_one_or_none()
|
||||||
|
if not obj:
|
||||||
|
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
|
||||||
|
return obj
|
||||||
|
|
||||||
|
if "create" in enabled_methods:
|
||||||
|
@router.post("/", response_model=schema_response)
|
||||||
|
async def create(
|
||||||
|
payload: schema_create, # type: ignore
|
||||||
|
admin: Admin = Depends(verify_admin_token),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
data = payload.dict(exclude_unset=True)
|
||||||
|
if "days" in data and data["days"] == 0:
|
||||||
|
data["days"] = None
|
||||||
|
obj = model(**data)
|
||||||
|
session.add(obj)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(obj)
|
||||||
|
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),
|
||||||
|
admin: Admin = Depends(verify_admin_token),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
field = getattr(model, identifier_field)
|
||||||
|
casted = _cast_identifier_type(field, value)
|
||||||
|
result = await session.execute(select(model).where(field == casted))
|
||||||
|
obj = result.scalar_one_or_none()
|
||||||
|
if not obj:
|
||||||
|
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
|
||||||
|
|
||||||
|
for k, v in payload.dict(exclude_unset=True).items():
|
||||||
|
setattr(obj, k, v)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(obj)
|
||||||
|
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),
|
||||||
|
admin: Admin = Depends(verify_admin_token),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
field = getattr(model, identifier_field)
|
||||||
|
casted = _cast_identifier_type(field, value)
|
||||||
|
result = await session.execute(select(model).where(field == casted))
|
||||||
|
obj = result.scalar_one_or_none()
|
||||||
|
if not obj:
|
||||||
|
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
|
||||||
|
|
||||||
|
await session.delete(obj)
|
||||||
|
await session.commit()
|
||||||
|
return {"detail": f"{model.__name__} deleted"}
|
||||||
|
|
||||||
|
return router
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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,
|
||||||
|
schema_create=CouponBase,
|
||||||
|
schema_update=CouponUpdate,
|
||||||
|
identifier_field="code",
|
||||||
|
parameter_name="code",
|
||||||
|
enabled_methods=["get_all", "get_one", "create", "update", "delete"]
|
||||||
|
)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from fastapi import Depends, HTTPException, Path
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from database.models import Key, Admin
|
||||||
|
from api.schemas.keys import KeyBase, KeyResponse, KeyUpdate
|
||||||
|
from api.routes.base_crud import generate_crud_router
|
||||||
|
from api.depends import get_session, verify_admin_token
|
||||||
|
from handlers.keys.key_utils import delete_key_from_cluster
|
||||||
|
from logger import logger
|
||||||
|
|
||||||
|
router = generate_crud_router(
|
||||||
|
model=Key,
|
||||||
|
schema_response=KeyResponse,
|
||||||
|
schema_create=KeyBase,
|
||||||
|
schema_update=KeyUpdate,
|
||||||
|
identifier_field="tg_id",
|
||||||
|
extra_get_by_email=True,
|
||||||
|
enabled_methods=["get_all", "get_one", "get_by_email"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/by_email/{email}", response_model=dict)
|
||||||
|
async def delete_key_by_email(
|
||||||
|
email: str = Path(..., description="Email клиента"),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
admin: Admin = Depends(verify_admin_token),
|
||||||
|
):
|
||||||
|
result = await session.execute(select(Key).where(Key.email == email))
|
||||||
|
db_key = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not db_key:
|
||||||
|
raise HTTPException(status_code=404, detail="Ключ не найден")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await delete_key_from_cluster(
|
||||||
|
session=session,
|
||||||
|
email=db_key.email,
|
||||||
|
client_id=db_key.client_id,
|
||||||
|
cluster_id=db_key.server_id,
|
||||||
|
)
|
||||||
|
await session.delete(db_key)
|
||||||
|
await session.commit()
|
||||||
|
logger.info(f"[API] Ключ удалён: {db_key.client_id}")
|
||||||
|
return {"message": "Ключ успешно удалён"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[API] Ошибка при удалении ключа: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Ошибка при удалении ключа")
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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,
|
||||||
|
schema_create=ServerBase,
|
||||||
|
schema_update=ServerUpdate,
|
||||||
|
identifier_field="server_name",
|
||||||
|
parameter_name="server_name",
|
||||||
|
enabled_methods=["get_all", "get_one", "create", "update", "delete"]
|
||||||
|
)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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,
|
||||||
|
schema_create=TariffBase,
|
||||||
|
schema_update=TariffUpdate,
|
||||||
|
identifier_field="name",
|
||||||
|
parameter_name="name",
|
||||||
|
enabled_methods=["get_all", "get_one", "create", "update", "delete"]
|
||||||
|
)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from fastapi import Depends, HTTPException, Path
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
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 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(
|
||||||
|
model=User,
|
||||||
|
schema_response=UserResponse,
|
||||||
|
schema_create=UserBase,
|
||||||
|
schema_update=UserUpdate,
|
||||||
|
identifier_field="tg_id",
|
||||||
|
enabled_methods=["get_all", "get_one", "get_by_email", "create", "update"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{tg_id}", response_model=dict)
|
||||||
|
async def delete_user(
|
||||||
|
tg_id: int = Path(..., description="Telegram ID пользователя"),
|
||||||
|
admin=Depends(verify_admin_token),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
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():
|
||||||
|
try:
|
||||||
|
servers = await get_servers(session=session)
|
||||||
|
tasks = []
|
||||||
|
for email, client_id in key_records:
|
||||||
|
for cluster_id in servers:
|
||||||
|
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"[DELETE] Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}")
|
||||||
|
|
||||||
|
await delete_keys_from_servers()
|
||||||
|
await delete_user_data(session, tg_id)
|
||||||
|
|
||||||
|
return {"detail": f"Пользователь {tg_id} и его ключи успешно удалены."}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[DELETE] Ошибка при удалении пользователя {tg_id}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Ошибка при удалении пользователя")
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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 .referrals import ReferralResponse
|
||||||
|
from .gifts import GiftResponse, GiftUsageResponse
|
||||||
|
from .misc import ManualBanResponse, TemporaryDataResponse, BlockedUserResponse, TrackingSourceResponse, PaymentResponse, NotificationResponse
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from pydantic import BaseModel, Field, model_validator
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class CouponBase(BaseModel):
|
||||||
|
code: str
|
||||||
|
amount: Optional[int] = Field(default=None)
|
||||||
|
usage_limit: int
|
||||||
|
usage_count: int = 0
|
||||||
|
is_used: bool = False
|
||||||
|
days: Optional[int] = Field(default=None)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def check_exactly_one_of_amount_or_days(self) -> "CouponBase":
|
||||||
|
has_amount = getattr(self, "amount", None) is not None
|
||||||
|
has_days = getattr(self, "days", None) is not None
|
||||||
|
|
||||||
|
if has_amount == has_days:
|
||||||
|
raise ValueError("Coupon must have exactly one of: 'amount' or 'days'")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class CouponResponse(CouponBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_amount_or_days(self) -> "CouponUpdate":
|
||||||
|
if self.amount is None and self.days is None:
|
||||||
|
return self
|
||||||
|
if self.amount is not None and self.days is not None:
|
||||||
|
raise ValueError("Specify only one of: 'amount' or 'days'")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class CouponUsageResponse(BaseModel):
|
||||||
|
coupon_id: int
|
||||||
|
user_id: int
|
||||||
|
used_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class GiftBase(BaseModel):
|
||||||
|
sender_tg_id: int
|
||||||
|
recipient_tg_id: Optional[int] = 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
|
||||||
|
|
||||||
|
|
||||||
|
class GiftResponse(GiftBase):
|
||||||
|
gift_id: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class GiftUsageResponse(BaseModel):
|
||||||
|
gift_id: str
|
||||||
|
tg_id: int
|
||||||
|
used_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class KeyBase(BaseModel):
|
||||||
|
tg_id: int
|
||||||
|
client_id: str
|
||||||
|
email: Optional[str] = None
|
||||||
|
created_at: Optional[int] = 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
|
||||||
|
|
||||||
|
|
||||||
|
class KeyResponse(KeyBase):
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class KeyDetailsResponse(BaseModel):
|
||||||
|
key: Optional[str]
|
||||||
|
remnawave_link: Optional[str]
|
||||||
|
server_id: Optional[str]
|
||||||
|
created_at: Optional[int]
|
||||||
|
expiry_time: Optional[int]
|
||||||
|
client_id: str
|
||||||
|
tg_id: int
|
||||||
|
email: Optional[str]
|
||||||
|
is_frozen: bool
|
||||||
|
balance: float
|
||||||
|
alias: Optional[str]
|
||||||
|
expiry_date: str
|
||||||
|
days_left_message: str
|
||||||
|
link: Optional[str]
|
||||||
|
cluster_name: Optional[str]
|
||||||
|
location_name: Optional[str]
|
||||||
|
tariff_id: Optional[int]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class KeyCreateRequest(BaseModel):
|
||||||
|
tg_id: int
|
||||||
|
cluster_id: str
|
||||||
|
tariff_id: int
|
||||||
|
email: Optional[str] = None
|
||||||
|
alias: Optional[str] = None
|
||||||
|
remnawave_link: Optional[str] = None
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, Union, Literal
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentBase(BaseModel):
|
||||||
|
tg_id: int
|
||||||
|
amount: float
|
||||||
|
payment_system: str
|
||||||
|
status: Literal["success", "pending", "failed"]
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentResponse(PaymentBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class ReferralResponse(BaseModel):
|
||||||
|
referred_tg_id: int
|
||||||
|
referrer_tg_id: int
|
||||||
|
reward_issued: bool = False
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationResponse(BaseModel):
|
||||||
|
tg_id: int
|
||||||
|
notification_type: str
|
||||||
|
last_notification_time: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class GiftBase(BaseModel):
|
||||||
|
sender_tg_id: int
|
||||||
|
recipient_tg_id: Optional[int] = 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
|
||||||
|
|
||||||
|
|
||||||
|
class GiftResponse(GiftBase):
|
||||||
|
gift_id: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class GiftUsageResponse(BaseModel):
|
||||||
|
gift_id: str
|
||||||
|
tg_id: int
|
||||||
|
used_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class ManualBanResponse(BaseModel):
|
||||||
|
tg_id: int
|
||||||
|
banned_at: datetime
|
||||||
|
reason: str
|
||||||
|
banned_by: int
|
||||||
|
until: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class TemporaryDataResponse(BaseModel):
|
||||||
|
tg_id: int
|
||||||
|
state: str
|
||||||
|
data: dict
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class BlockedUserResponse(BaseModel):
|
||||||
|
tg_id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class TrackingSourceResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
type: str
|
||||||
|
created_by: int
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ReferralResponse(BaseModel):
|
||||||
|
referred_tg_id: int
|
||||||
|
referrer_tg_id: int
|
||||||
|
reward_issued: bool = False
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class ServerBase(BaseModel):
|
||||||
|
cluster_name: str
|
||||||
|
server_name: str
|
||||||
|
api_url: str
|
||||||
|
subscription_url: Optional[str] = None
|
||||||
|
inbound_id: str
|
||||||
|
panel_type: str
|
||||||
|
max_keys: Optional[int] = None
|
||||||
|
tariff_group: str
|
||||||
|
enabled: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class ServerResponse(ServerBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TariffBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
group_code: str
|
||||||
|
duration_days: int
|
||||||
|
price_rub: int
|
||||||
|
traffic_limit: Optional[int] = None
|
||||||
|
device_limit: Optional[int] = None
|
||||||
|
is_active: bool = True
|
||||||
|
subgroup_title: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class TariffResponse(TariffBase):
|
||||||
|
id: int
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = 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
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
balance: float = 0.0
|
||||||
|
trial: int = 0
|
||||||
|
source_code: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserResponse(UserBase):
|
||||||
|
created_at: Optional[datetime]
|
||||||
|
updated_at: Optional[datetime]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
+17
-3
@@ -1,7 +1,21 @@
|
|||||||
from database.db import engine
|
from database.db import engine, async_session_maker
|
||||||
from database.models import Base
|
from database.models import Base, Admin
|
||||||
|
from sqlalchemy import select
|
||||||
|
from config import ADMIN_ID
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
async def init_db():
|
async def init_db():
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
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()
|
||||||
|
))
|
||||||
|
await session.commit()
|
||||||
|
|||||||
+13
-8
@@ -12,6 +12,7 @@ from sqlalchemy import (
|
|||||||
String,
|
String,
|
||||||
Text,
|
Text,
|
||||||
)
|
)
|
||||||
|
import secrets
|
||||||
from sqlalchemy.orm import Mapped, declarative_base, mapped_column
|
from sqlalchemy.orm import Mapped, declarative_base, mapped_column
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
@@ -57,7 +58,7 @@ class Key(DictLikeMixin, Base):
|
|||||||
key = Column(String)
|
key = Column(String)
|
||||||
server_id = Column(String)
|
server_id = Column(String)
|
||||||
remnawave_link = Column(String)
|
remnawave_link = Column(String)
|
||||||
tariff_id = Column(Integer, ForeignKey("tariffs.id"))
|
tariff_id = Column(Integer, ForeignKey("tariffs.id", ondelete="SET NULL"))
|
||||||
is_frozen = Column(Boolean, default=False)
|
is_frozen = Column(Boolean, default=False)
|
||||||
alias = Column(String)
|
alias = Column(String)
|
||||||
notified = Column(Boolean, default=False)
|
notified = Column(Boolean, default=False)
|
||||||
@@ -202,11 +203,15 @@ class TrackingSource(DictLikeMixin, Base):
|
|||||||
created_at = Column(DateTime, default=datetime.utcnow)
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
class Log(DictLikeMixin, Base):
|
class Admin(Base):
|
||||||
__tablename__ = "logs"
|
__tablename__ = "admins"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
tg_id = Column(BigInteger, primary_key=True)
|
||||||
event_type = Column(String)
|
token = Column(String, unique=True, nullable=True)
|
||||||
tg_id = Column(BigInteger)
|
description = Column(String, nullable=True)
|
||||||
details = Column(JSON)
|
role = Column(String, nullable=False, default="admin")
|
||||||
created_at = Column(DateTime, default=datetime.utcnow)
|
added_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_token() -> str:
|
||||||
|
return secrets.token_urlsafe(32)
|
||||||
+9
-3
@@ -1,12 +1,18 @@
|
|||||||
from aiogram.filters import BaseFilter
|
from aiogram.filters import BaseFilter
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
from sqlalchemy import select
|
||||||
|
from database.models import Admin
|
||||||
|
|
||||||
from config import ADMIN_ID
|
from database.db import async_session_maker
|
||||||
|
|
||||||
|
|
||||||
class IsAdminFilter(BaseFilter):
|
class IsAdminFilter(BaseFilter):
|
||||||
async def __call__(self, message: Message) -> bool:
|
async def __call__(self, message: Message) -> bool:
|
||||||
try:
|
try:
|
||||||
return message.from_user.id in ADMIN_ID
|
async with async_session_maker() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(Admin).where(Admin.tg_id == message.from_user.id)
|
||||||
|
)
|
||||||
|
admin = result.scalar_one_or_none()
|
||||||
|
return admin is not None
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|||||||
from ..panel.keyboard import AdminPanelCallback
|
from ..panel.keyboard import AdminPanelCallback
|
||||||
from .keyboard import build_admin_gifts_kb, build_gifts_list_kb
|
from .keyboard import build_admin_gifts_kb, build_gifts_list_kb
|
||||||
from database.models import Tariff, Gift, GiftUsage
|
from database.models import Tariff, Gift, GiftUsage
|
||||||
from handlers.payments.gift import finalize_gift
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ async def handle_tariff_selection(callback: CallbackQuery, state: FSMContext):
|
|||||||
|
|
||||||
@router.callback_query(F.data == "gift_limit_unlimited")
|
@router.callback_query(F.data == "gift_limit_unlimited")
|
||||||
async def handle_unlimited_gift(callback: CallbackQuery, state: FSMContext, bot: Bot):
|
async def handle_unlimited_gift(callback: CallbackQuery, state: FSMContext, bot: Bot):
|
||||||
|
from handlers.payments.gift import finalize_gift
|
||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
session: AsyncSession = callback.bot["session"]
|
session: AsyncSession = callback.bot["session"]
|
||||||
await state.clear()
|
await state.clear()
|
||||||
@@ -74,6 +75,7 @@ async def handle_unlimited_gift(callback: CallbackQuery, state: FSMContext, bot:
|
|||||||
|
|
||||||
@router.message(GiftCreationState.waiting_for_limit_input_or_unlimited)
|
@router.message(GiftCreationState.waiting_for_limit_input_or_unlimited)
|
||||||
async def handle_limited_gift_input(message: types.Message, session: AsyncSession, state: FSMContext, bot: Bot):
|
async def handle_limited_gift_input(message: types.Message, session: AsyncSession, state: FSMContext, bot: Bot):
|
||||||
|
from handlers.payments.gift import finalize_gift
|
||||||
try:
|
try:
|
||||||
max_usages = int(message.text.strip())
|
max_usages = int(message.text.strip())
|
||||||
if max_usages <= 0:
|
if max_usages <= 0:
|
||||||
|
|||||||
@@ -6,8 +6,15 @@ from middlewares import maintenance
|
|||||||
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
|
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
|
||||||
|
|
||||||
|
|
||||||
def build_management_kb() -> InlineKeyboardMarkup:
|
def build_management_kb(admin_role: str) -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
|
if admin_role == "superadmin":
|
||||||
|
builder.button(
|
||||||
|
text="👑 Управление админами",
|
||||||
|
callback_data=AdminPanelCallback(action="admins").pack(),
|
||||||
|
)
|
||||||
|
|
||||||
builder.button(
|
builder.button(
|
||||||
text="💾 Создать резервную копию",
|
text="💾 Создать резервную копию",
|
||||||
callback_data=AdminPanelCallback(action="backups").pack(),
|
callback_data=AdminPanelCallback(action="backups").pack(),
|
||||||
@@ -41,3 +48,91 @@ def build_management_kb() -> InlineKeyboardMarkup:
|
|||||||
builder.row(build_admin_back_btn())
|
builder.row(build_admin_back_btn())
|
||||||
builder.adjust(1)
|
builder.adjust(1)
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def build_admins_kb(admins: list[tuple[int, str]]) -> InlineKeyboardMarkup:
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
|
for tg_id, role in admins:
|
||||||
|
builder.button(
|
||||||
|
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.row(build_admin_back_btn())
|
||||||
|
builder.adjust(1)
|
||||||
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
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"delete_admin|{tg_id}").pack()
|
||||||
|
)
|
||||||
|
|
||||||
|
if role == "superadmin":
|
||||||
|
builder.button(
|
||||||
|
text="🎟 Выпустить токен",
|
||||||
|
callback_data=AdminPanelCallback(action=f"generate_token|{tg_id}").pack()
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.button(
|
||||||
|
text="🔙 Назад",
|
||||||
|
callback_data=AdminPanelCallback(action="admins").pack()
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.adjust(1)
|
||||||
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
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.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.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.adjust(1)
|
||||||
|
return builder.as_markup()
|
||||||
@@ -2,16 +2,19 @@ from aiogram import F, Router
|
|||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.fsm.state import State, StatesGroup
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import CallbackQuery, Message
|
||||||
from sqlalchemy import func, select, update
|
from sqlalchemy import func, select, update, delete
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from database.models import Key
|
from database.models import Key, Admin
|
||||||
from filters.admin import IsAdminFilter
|
from filters.admin import IsAdminFilter
|
||||||
from logger import logger
|
from logger import logger
|
||||||
from middlewares import maintenance
|
from middlewares import maintenance
|
||||||
|
|
||||||
from ..panel.keyboard import build_admin_back_kb
|
from ..panel.keyboard import build_admin_back_kb
|
||||||
from .keyboard import AdminPanelCallback, build_management_kb
|
from .keyboard import AdminPanelCallback, build_management_kb, build_admins_kb, build_single_admin_menu, build_role_selection_kb, build_admin_back_kb_to_admins, build_token_result_kb
|
||||||
|
from asyncio import sleep
|
||||||
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -20,13 +23,26 @@ class AdminManagementStates(StatesGroup):
|
|||||||
waiting_for_new_domain = State()
|
waiting_for_new_domain = State()
|
||||||
|
|
||||||
|
|
||||||
|
class AdminState(StatesGroup):
|
||||||
|
waiting_for_tg_id = State()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(
|
@router.callback_query(
|
||||||
AdminPanelCallback.filter(F.action == "management"), IsAdminFilter()
|
AdminPanelCallback.filter(F.action == "management"), IsAdminFilter()
|
||||||
)
|
)
|
||||||
async def handle_management(callback_query: CallbackQuery):
|
async def handle_management(callback_query: CallbackQuery, session: AsyncSession):
|
||||||
|
tg_id = callback_query.from_user.id
|
||||||
|
|
||||||
|
result = await session.execute(select(Admin).where(Admin.tg_id == tg_id))
|
||||||
|
admin = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not admin:
|
||||||
|
await callback_query.message.edit_text("❌ Вы не зарегистрированы как администратор.")
|
||||||
|
return
|
||||||
|
|
||||||
await callback_query.message.edit_text(
|
await callback_query.message.edit_text(
|
||||||
text="🤖 Управление ботом",
|
text="🤖 Управление ботом",
|
||||||
reply_markup=build_management_kb(),
|
reply_markup=build_management_kb(admin.role),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -94,10 +110,153 @@ async def process_new_domain(
|
|||||||
|
|
||||||
|
|
||||||
@router.callback_query(AdminPanelCallback.filter(F.action == "toggle_maintenance"))
|
@router.callback_query(AdminPanelCallback.filter(F.action == "toggle_maintenance"))
|
||||||
async def toggle_maintenance_mode(callback: CallbackQuery):
|
async def toggle_maintenance_mode(callback: CallbackQuery, session: AsyncSession):
|
||||||
maintenance.maintenance_mode = not maintenance.maintenance_mode
|
tg_id = callback.from_user.id
|
||||||
|
|
||||||
|
result = await session.execute(select(Admin).where(Admin.tg_id == tg_id))
|
||||||
|
admin = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not admin:
|
||||||
|
await callback.answer("❌ Админ не найден.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
maintenance.maintenance_mode = not maintenance.maintenance_mode
|
||||||
new_status = "включён" if maintenance.maintenance_mode else "выключен"
|
new_status = "включён" if maintenance.maintenance_mode else "выключен"
|
||||||
await callback.answer(f"🛠️ Режим обслуживания {new_status}.", show_alert=True)
|
await callback.answer(f"🛠️ Режим обслуживания {new_status}.", show_alert=True)
|
||||||
|
|
||||||
await callback.message.edit_reply_markup(reply_markup=build_management_kb())
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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> нового админа:")
|
||||||
|
await state.set_state(AdminState.waiting_for_tg_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(AdminState.waiting_for_tg_id)
|
||||||
|
async def save_new_admin(message: Message, session: AsyncSession, state: FSMContext):
|
||||||
|
try:
|
||||||
|
tg_id = int(message.text.strip())
|
||||||
|
except ValueError:
|
||||||
|
await message.answer("❌ Неверный формат. Введите числовой <code>tg_id</code>.")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = await session.execute(select(Admin).where(Admin.tg_id == tg_id))
|
||||||
|
if result.scalar_one_or_none():
|
||||||
|
await message.answer("⚠️ Такой админ уже существует.")
|
||||||
|
else:
|
||||||
|
session.add(Admin(
|
||||||
|
tg_id=tg_id,
|
||||||
|
role="admin",
|
||||||
|
description="Добавлен вручную"
|
||||||
|
))
|
||||||
|
await session.commit()
|
||||||
|
await message.answer(
|
||||||
|
f"✅ Админ <code>{tg_id}</code> добавлен.",
|
||||||
|
reply_markup=build_admin_back_kb_to_admins()
|
||||||
|
)
|
||||||
|
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(AdminPanelCallback.filter(F.action.startswith("admin_menu|")))
|
||||||
|
async def open_admin_menu(callback: CallbackQuery, callback_data: AdminPanelCallback, session: AsyncSession):
|
||||||
|
tg_id = int(callback_data.action.split("|")[1])
|
||||||
|
|
||||||
|
result = await session.execute(select(Admin.role).where(Admin.tg_id == tg_id))
|
||||||
|
admin = result.scalar_one_or_none()
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(AdminPanelCallback.filter(F.action.startswith("generate_token|")))
|
||||||
|
async def generate_token(callback: CallbackQuery, callback_data: AdminPanelCallback, session: AsyncSession):
|
||||||
|
tg_id = int(callback_data.action.split("|")[1])
|
||||||
|
|
||||||
|
result = await session.execute(select(Admin).where(Admin.tg_id == tg_id))
|
||||||
|
admin = result.scalar_one_or_none()
|
||||||
|
if not admin:
|
||||||
|
await callback.message.edit_text("❌ Админ не найден.")
|
||||||
|
return
|
||||||
|
|
||||||
|
token = Admin.generate_token()
|
||||||
|
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
admin.token = token_hash
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
msg = await callback.message.edit_text(
|
||||||
|
f"🎟 <b>Новый токен для</b> <code>{tg_id}</code>:\n\n"
|
||||||
|
f"<code>{token}</code>\n\n"
|
||||||
|
f"⚠️ Это сообщение исчезнет через 5 минут.",
|
||||||
|
reply_markup=build_token_result_kb(token)
|
||||||
|
)
|
||||||
|
|
||||||
|
await sleep(300)
|
||||||
|
try:
|
||||||
|
await msg.delete()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(AdminPanelCallback.filter(F.action.startswith("edit_role|")))
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(AdminPanelCallback.filter(F.action.startswith("set_role|")))
|
||||||
|
async def set_admin_role(callback: CallbackQuery, callback_data: AdminPanelCallback, session: AsyncSession):
|
||||||
|
try:
|
||||||
|
_, tg_id_str, role = callback_data.action.split("|")
|
||||||
|
tg_id = int(tg_id_str)
|
||||||
|
if role not in ("superadmin", "moderator"):
|
||||||
|
raise ValueError
|
||||||
|
except Exception:
|
||||||
|
await callback.message.edit_text("❌ Неверный формат.")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = await session.execute(select(Admin).where(Admin.tg_id == tg_id))
|
||||||
|
admin = result.scalar_one_or_none()
|
||||||
|
if not admin:
|
||||||
|
await callback.message.edit_text("❌ Админ не найден.")
|
||||||
|
return
|
||||||
|
|
||||||
|
admin.role = role
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
f"✅ Роль админа <code>{tg_id}</code> изменена на <b>{role}</b>.",
|
||||||
|
reply_markup=build_single_admin_menu(tg_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(AdminPanelCallback.filter(F.action.startswith("delete_admin|")))
|
||||||
|
async def delete_admin(callback: CallbackQuery, callback_data: AdminPanelCallback, session: AsyncSession):
|
||||||
|
tg_id = int(callback_data.action.split("|")[1])
|
||||||
|
|
||||||
|
await session.execute(delete(Admin).where(Admin.tg_id == tg_id))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
f"🗑 Админ <code>{tg_id}</code> удалён.",
|
||||||
|
reply_markup=build_admin_back_kb_to_admins()
|
||||||
|
)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user