Compare commits
12 Commits
733977f56e
...
refactor-b
Author | SHA1 | Date | |
---|---|---|---|
2bd081fe7a | |||
e59669f66a | |||
c7f6baac5d | |||
75d7e29f6e | |||
39cd14f1d7 | |||
6b8f116608 | |||
44e12723ad | |||
259e3c373b | |||
7e4e2c0bad | |||
d52d4dbf75 | |||
ff65e4a333 | |||
2e59d03784 |
41
.gitea/workflows/build.yaml
Normal file
41
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,41 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: git.popa-popa.ru
|
||||
username: ${{ secrets.USERNAME }}
|
||||
password: ${{ secrets.PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: git.popa-popa.ru/DIKER/minecraft-api:latest # Замените username на ваше имя пользователя
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.SSH_HOST }}
|
||||
username: ${{ secrets.SSH_USERNAME }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
cd /home/server/minecraft-api/
|
||||
docker pull git.popa-popa.ru/DIKER/minecraft-api:latest
|
||||
docker-compose up -d
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ __pycache__
|
||||
.env
|
||||
skins
|
||||
capes
|
||||
mongodb
|
||||
|
@ -1,12 +1,32 @@
|
||||
from fastapi import APIRouter, UploadFile, File
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException, Form
|
||||
from app.services.cape import CapeService
|
||||
from app.services.auth import AuthService
|
||||
|
||||
router = APIRouter(tags=["Capes"])
|
||||
|
||||
@router.post("/user/{username}/cape")
|
||||
async def set_cape(username: str, cape_file: UploadFile = File(...)):
|
||||
async def set_cape(
|
||||
username: str,
|
||||
cape_file: UploadFile = File(...),
|
||||
accessToken: str = Form(...),
|
||||
clientToken: str = Form(...)
|
||||
):
|
||||
# Validate the token
|
||||
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||
|
||||
return await CapeService().set_cape(username, cape_file)
|
||||
|
||||
@router.delete("/user/{username}/cape")
|
||||
async def remove_cape(username: str):
|
||||
async def remove_cape(
|
||||
username: str,
|
||||
accessToken: str,
|
||||
clientToken: str
|
||||
):
|
||||
# Validate the token
|
||||
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||
|
||||
return await CapeService().remove_cape(username)
|
||||
|
63
app/api/marketplace.py
Normal file
63
app/api/marketplace.py
Normal file
@ -0,0 +1,63 @@
|
||||
from fastapi import APIRouter, Query, Body
|
||||
from typing import Optional
|
||||
from app.models.marketplace import BuyItemRequest
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/marketplace",
|
||||
tags=["Marketplace"]
|
||||
)
|
||||
|
||||
@router.get("/items")
|
||||
async def get_marketplace_items(
|
||||
server_ip: Optional[str] = None,
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100)
|
||||
):
|
||||
"""Получить список предметов на торговой площадке"""
|
||||
from app.services.marketplace import MarketplaceService
|
||||
return await MarketplaceService().list_items(server_ip, page, limit)
|
||||
|
||||
@router.get("/items/{item_id}")
|
||||
async def get_marketplace_item(item_id: str):
|
||||
"""Получить информацию о конкретном предмете"""
|
||||
from app.services.marketplace import MarketplaceService
|
||||
return await MarketplaceService().get_item(item_id)
|
||||
|
||||
@router.post("/items/sell")
|
||||
async def sell_item(
|
||||
username: str = Body(...),
|
||||
slot_index: int = Body(...),
|
||||
amount: int = Body(...),
|
||||
price: int = Body(...),
|
||||
server_ip: str = Body(...)
|
||||
):
|
||||
"""Выставить предмет на продажу"""
|
||||
from app.services.marketplace import MarketplaceService
|
||||
return await MarketplaceService().add_item(username, slot_index, amount, price, server_ip)
|
||||
|
||||
@router.post("/items/buy/{item_id}")
|
||||
async def buy_item(
|
||||
item_id: str,
|
||||
request: BuyItemRequest
|
||||
):
|
||||
"""Купить предмет"""
|
||||
from app.services.marketplace import MarketplaceService
|
||||
return await MarketplaceService().buy_item(request.username, item_id)
|
||||
|
||||
@router.get("/operations")
|
||||
async def get_marketplace_operations(server_ip: str):
|
||||
"""Получить список операций для выполнения на сервере"""
|
||||
from app.services.marketplace import MarketplaceService
|
||||
return await MarketplaceService().get_pending_operations(server_ip)
|
||||
|
||||
@router.post("/operations/confirm")
|
||||
async def confirm_marketplace_operation(data: dict):
|
||||
"""Подтвердить выполнение операции"""
|
||||
from app.services.marketplace import MarketplaceService
|
||||
return await MarketplaceService().confirm_operation(data["operation_id"], data.get("status"), data.get("error"))
|
||||
|
||||
@router.post("/items/details")
|
||||
async def submit_item_details(data: dict):
|
||||
"""Получить подробные данные о предмете"""
|
||||
from app.services.marketplace import MarketplaceService
|
||||
return await MarketplaceService().update_item_details(data["operation_id"], data["item_data"])
|
@ -4,15 +4,29 @@ router = APIRouter(tags=["Meta"])
|
||||
|
||||
@router.get("/")
|
||||
def api_root():
|
||||
return {
|
||||
"meta": {
|
||||
"serverName": "Your Auth Server",
|
||||
"implementationName": "FastAPI",
|
||||
"implementationVersion": "1.0.0",
|
||||
"links": {
|
||||
"homepage": "https://your-server.com"
|
||||
# Читаем публичный ключ из файла
|
||||
public_key_path = "app/keys/public_key.pem"
|
||||
|
||||
try:
|
||||
with open(public_key_path, "r") as f:
|
||||
public_key = f.read().strip()
|
||||
|
||||
return {
|
||||
"meta": {
|
||||
"serverName": "Popa Auth Server",
|
||||
"implementationName": "FastAPI",
|
||||
"implementationVersion": "1.0.0",
|
||||
"links": {
|
||||
"homepage": "https://popa-popa.ru"
|
||||
}
|
||||
},
|
||||
},
|
||||
"skinDomains": ["147.78.65.214"],
|
||||
"capeDomains": ["147.78.65.214"]
|
||||
}
|
||||
"skinDomains": ["147.78.65.214"],
|
||||
"capeDomains": ["147.78.65.214"],
|
||||
# Важно - возвращаем ключ как есть, без дополнительной обработки
|
||||
"signaturePublickey": public_key
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": str(e),
|
||||
"traceback": str(e)
|
||||
}
|
||||
|
55
app/api/pranks.py
Normal file
55
app/api/pranks.py
Normal file
@ -0,0 +1,55 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from app.services.server.prank import PrankService
|
||||
from app.models.server.prank import PrankCommandCreate, PrankCommandUpdate, PrankExecute
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/pranks",
|
||||
tags=["Pranks"]
|
||||
)
|
||||
|
||||
prank_service = PrankService()
|
||||
|
||||
@router.get("/commands")
|
||||
async def get_all_prank_commands():
|
||||
"""Получение всех доступных команд-пакостей"""
|
||||
return await prank_service.get_all_prank_commands()
|
||||
|
||||
@router.get("/commands/{command_id}")
|
||||
async def get_prank_command(command_id: str):
|
||||
"""Получение команды-пакости по ID"""
|
||||
return await prank_service.get_prank_command(command_id)
|
||||
|
||||
@router.post("/commands")
|
||||
async def add_prank_command(command: PrankCommandCreate):
|
||||
"""Добавление новой команды-пакости"""
|
||||
return await prank_service.add_prank_command(command)
|
||||
|
||||
@router.put("/commands/{command_id}")
|
||||
async def update_prank_command(command_id: str, update_data: PrankCommandUpdate):
|
||||
"""Обновление команды-пакости"""
|
||||
return await prank_service.update_prank_command(command_id, update_data)
|
||||
|
||||
@router.delete("/commands/{command_id}")
|
||||
async def delete_prank_command(command_id: str):
|
||||
"""Удаление команды-пакости"""
|
||||
return await prank_service.delete_prank_command(command_id)
|
||||
|
||||
@router.get("/servers")
|
||||
async def get_all_servers():
|
||||
"""Получение списка всех доступных серверов"""
|
||||
return await prank_service.get_all_servers()
|
||||
|
||||
@router.get("/servers/{server_id}/players")
|
||||
async def get_server_online_players(server_id: str):
|
||||
"""Получение списка онлайн игроков на сервере"""
|
||||
return await prank_service.get_server_online_players(server_id)
|
||||
|
||||
@router.post("/execute")
|
||||
async def execute_prank(username: str, prank_data: PrankExecute):
|
||||
"""Выполнение пакости (списание монет и выполнение команды)"""
|
||||
return await prank_service.execute_prank(
|
||||
username,
|
||||
prank_data.command_id,
|
||||
prank_data.target_player,
|
||||
prank_data.server_id
|
||||
)
|
60
app/api/server.py
Normal file
60
app/api/server.py
Normal file
@ -0,0 +1,60 @@
|
||||
from fastapi import APIRouter
|
||||
from app.services.server.command import CommandService
|
||||
from app.services.server.event import EventService
|
||||
from app.models.server.command import ServerCommand, InventoryRequest
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/server",
|
||||
tags=["Server Management"]
|
||||
)
|
||||
|
||||
@router.post("/events")
|
||||
async def receive_server_event(event_data: dict):
|
||||
# Обновляем активность сервера
|
||||
server_ip = event_data.get("server_ip")
|
||||
if server_ip:
|
||||
await update_server_activity(server_ip)
|
||||
return await EventService().process_event(event_data)
|
||||
|
||||
@router.post("/commands")
|
||||
async def add_server_command(command_data: ServerCommand):
|
||||
# Обновляем last_activity для сервера
|
||||
await CommandService()._update_server_activity(command_data.server_ip)
|
||||
return await CommandService().add_command(command_data)
|
||||
|
||||
@router.get("/commands")
|
||||
async def get_server_commands(server_ip: str):
|
||||
return await CommandService().get_commands(server_ip)
|
||||
|
||||
@router.post("/inventory")
|
||||
async def request_player_inventory(inventory_request: InventoryRequest):
|
||||
"""Создаёт запрос на получение инвентаря игрока"""
|
||||
return await CommandService().request_inventory(inventory_request)
|
||||
|
||||
@router.get("/inventory/requests")
|
||||
async def get_inventory_requests(server_ip: str):
|
||||
"""Получает список запросов инвентаря для сервера"""
|
||||
return await CommandService().get_inventory_requests(server_ip)
|
||||
|
||||
@router.post("/inventory/submit")
|
||||
async def submit_inventory(inventory_data: dict):
|
||||
"""Принимает данные инвентаря от сервера"""
|
||||
return await CommandService().submit_inventory(inventory_data)
|
||||
|
||||
@router.get("/inventory/{request_id}")
|
||||
async def get_inventory_result(request_id: str):
|
||||
"""Получает результаты запроса инвентаря"""
|
||||
return await CommandService().get_inventory_result(request_id)
|
||||
|
||||
async def update_server_activity(server_ip):
|
||||
"""Обновляет время последней активности сервера"""
|
||||
from app.db.database import db
|
||||
game_servers_collection = db.game_servers
|
||||
|
||||
await game_servers_collection.update_one(
|
||||
{"ip": server_ip},
|
||||
{"$set": {"last_activity": datetime.utcnow()}},
|
||||
upsert=False
|
||||
)
|
@ -1,12 +1,33 @@
|
||||
from fastapi import APIRouter, UploadFile, File, Form
|
||||
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
|
||||
from app.services.skin import SkinService
|
||||
from app.services.auth import AuthService
|
||||
|
||||
router = APIRouter(tags=["Skins"])
|
||||
|
||||
@router.post("/user/{username}/skin")
|
||||
async def set_skin(username: str, skin_file: UploadFile = File(...), skin_model: str = Form("classic")):
|
||||
async def set_skin(
|
||||
username: str,
|
||||
skin_file: UploadFile = File(...),
|
||||
skin_model: str = Form("classic"),
|
||||
accessToken: str = Form(...),
|
||||
clientToken: str = Form(...)
|
||||
):
|
||||
# Validate the token
|
||||
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||
|
||||
return await SkinService().set_skin(username, skin_file, skin_model)
|
||||
|
||||
@router.delete("/user/{username}/skin")
|
||||
async def remove_skin(username: str):
|
||||
async def remove_skin(
|
||||
username: str,
|
||||
accessToken: str,
|
||||
clientToken: str
|
||||
):
|
||||
# Validate the token
|
||||
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||
|
||||
return await SkinService().remove_skin(username)
|
||||
|
61
app/api/store.py
Normal file
61
app/api/store.py
Normal file
@ -0,0 +1,61 @@
|
||||
from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException
|
||||
from app.services.store_cape import StoreCapeService
|
||||
from app.models.cape import CapeStoreUpdate, CapePurchase
|
||||
from typing import Optional
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/store",
|
||||
tags=["Store"]
|
||||
)
|
||||
|
||||
store_cape_service = StoreCapeService()
|
||||
|
||||
@router.get("/capes")
|
||||
async def get_all_capes():
|
||||
"""Получение списка всех плащей в магазине"""
|
||||
return await store_cape_service.get_all_capes()
|
||||
|
||||
@router.get("/capes/{cape_id}")
|
||||
async def get_cape_by_id(cape_id: str):
|
||||
"""Получение плаща по ID"""
|
||||
return await store_cape_service.get_cape_by_id(cape_id)
|
||||
|
||||
@router.post("/capes")
|
||||
async def add_cape(
|
||||
name: str = Form(...),
|
||||
description: str = Form(...),
|
||||
price: int = Form(...),
|
||||
cape_file: UploadFile = File(...)
|
||||
):
|
||||
"""Добавление нового плаща в магазин"""
|
||||
return await store_cape_service.add_cape(name, description, price, cape_file)
|
||||
|
||||
@router.put("/capes/{cape_id}")
|
||||
async def update_cape(cape_id: str, update_data: CapeStoreUpdate):
|
||||
"""Обновление информации о плаще"""
|
||||
return await store_cape_service.update_cape(cape_id, update_data)
|
||||
|
||||
@router.delete("/capes/{cape_id}")
|
||||
async def delete_cape(cape_id: str):
|
||||
"""Удаление плаща из магазина"""
|
||||
return await store_cape_service.delete_cape(cape_id)
|
||||
|
||||
@router.post("/purchase/cape")
|
||||
async def purchase_cape(username: str, cape_id: str):
|
||||
"""Покупка плаща пользователем"""
|
||||
return await store_cape_service.purchase_cape(username, cape_id)
|
||||
|
||||
@router.get("/user/{username}/capes")
|
||||
async def get_user_purchased_capes(username: str):
|
||||
"""Получение всех приобретенных плащей пользователя"""
|
||||
return await store_cape_service.get_user_purchased_capes(username)
|
||||
|
||||
@router.post("/user/{username}/capes/activate/{cape_id}")
|
||||
async def activate_purchased_cape(username: str, cape_id: str):
|
||||
"""Активация приобретенного плаща"""
|
||||
return await store_cape_service.activate_purchased_cape(username, cape_id)
|
||||
|
||||
@router.post("/user/{username}/capes/deactivate/{cape_id}")
|
||||
async def deactivate_purchased_cape(username: str, cape_id: str):
|
||||
"""Деактивация приобретенного плаща"""
|
||||
return await store_cape_service.deactivate_purchased_cape(username, cape_id)
|
@ -2,6 +2,16 @@ from fastapi import APIRouter, HTTPException, Body, Response
|
||||
from app.models.user import UserCreate, UserLogin
|
||||
from app.models.request import ValidateRequest
|
||||
from app.services.auth import AuthService
|
||||
from app.db.database import users_collection, sessions_collection
|
||||
from datetime import datetime
|
||||
import json
|
||||
from fastapi import HTTPException
|
||||
from datetime import datetime, timedelta
|
||||
from app.models.server.event import PlayerEvent, OnlinePlayersUpdate
|
||||
from app.models.server.playtime import PlayerSession, PlayerPlaytime
|
||||
from app.services.coins import CoinsService
|
||||
|
||||
coins_service = CoinsService()
|
||||
|
||||
router = APIRouter(
|
||||
tags=["Users"]
|
||||
@ -45,3 +55,65 @@ async def join_server(request_data: dict = Body(...)):
|
||||
@router.get("/sessionserver/session/minecraft/hasJoined")
|
||||
async def has_joined(username: str, serverId: str):
|
||||
return await AuthService().has_joined(username, serverId)
|
||||
|
||||
@router.get("/users/{username}/coins")
|
||||
async def get_user_coins(username: str):
|
||||
coins_data = await coins_service.get_player_coins(username)
|
||||
if not coins_data:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return coins_data
|
||||
|
||||
@router.get("/users")
|
||||
async def get_users():
|
||||
"""Получение списка всех пользователей"""
|
||||
users = await users_collection.find().to_list(1000)
|
||||
|
||||
# Исключаем чувствительные данные перед отправкой
|
||||
safe_users = []
|
||||
for user in users:
|
||||
safe_users.append({
|
||||
"username": user["username"],
|
||||
"uuid": user["uuid"],
|
||||
"skin_url": user.get("skin_url"),
|
||||
"cloak_url": user.get("cloak_url"),
|
||||
"coins": user.get("coins", 0),
|
||||
"total_time_played": user.get("total_time_played", 0),
|
||||
"is_active": user.get("is_active", True)
|
||||
})
|
||||
|
||||
return {"users": safe_users, "count": len(safe_users)}
|
||||
|
||||
@router.get("/users/{uuid}")
|
||||
async def get_user_by_uuid(uuid: str):
|
||||
"""Получение пользователя по UUID"""
|
||||
user = await users_collection.find_one({"uuid": uuid})
|
||||
if not user:
|
||||
# Пробуем разные форматы UUID
|
||||
if '-' in uuid:
|
||||
user = await users_collection.find_one({"uuid": uuid.replace('-', '')})
|
||||
else:
|
||||
formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
|
||||
user = await users_collection.find_one({"uuid": formatted_uuid})
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Исключаем чувствительные данные
|
||||
safe_user = {
|
||||
"username": user["username"],
|
||||
"uuid": user["uuid"],
|
||||
"skin_url": user.get("skin_url"),
|
||||
"cloak_url": user.get("cloak_url"),
|
||||
"coins": user.get("coins", 0),
|
||||
"total_time_played": user.get("total_time_played", 0),
|
||||
"is_active": user.get("is_active", True),
|
||||
"created_at": user.get("created_at")
|
||||
}
|
||||
|
||||
if "total_time_played" in safe_user:
|
||||
total_time = safe_user["total_time_played"]
|
||||
hours, remainder = divmod(total_time, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
safe_user["total_time_formatted"] = f"{hours}ч {minutes}м {seconds}с"
|
||||
|
||||
return safe_user
|
||||
|
@ -2,7 +2,7 @@ from motor.motor_asyncio import AsyncIOMotorClient
|
||||
from app.core.config import MONGO_URI
|
||||
|
||||
client = AsyncIOMotorClient(MONGO_URI)
|
||||
db = client["minecraft_auth"]
|
||||
db = client["minecraft-api"]
|
||||
|
||||
users_collection = db["users"]
|
||||
sessions_collection = db["sessions"]
|
||||
|
@ -1,4 +1,25 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class CapeUpdate(BaseModel):
|
||||
cape_url: str
|
||||
|
||||
class CapeStore(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
price: int
|
||||
file_name: str
|
||||
|
||||
class CapeStoreCreate(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
price: int
|
||||
|
||||
class CapeStoreUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
price: Optional[int] = None
|
||||
|
||||
class CapePurchase(BaseModel):
|
||||
cape_id: str
|
||||
|
20
app/models/marketplace.py
Normal file
20
app/models/marketplace.py
Normal file
@ -0,0 +1,20 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
class MarketplaceItemBase(BaseModel):
|
||||
material: str
|
||||
amount: int
|
||||
price: int
|
||||
seller_name: str
|
||||
server_ip: str
|
||||
display_name: Optional[str] = None
|
||||
lore: Optional[List[str]] = None
|
||||
enchants: Optional[Dict[str, int]] = None
|
||||
item_data: Optional[Dict[str, Any]] = None # Дополнительные данные предмета
|
||||
|
||||
class MarketplaceItem(MarketplaceItemBase):
|
||||
id: str
|
||||
created_at: str
|
||||
|
||||
class BuyItemRequest(BaseModel):
|
||||
username: str
|
13
app/models/server/command.py
Normal file
13
app/models/server/command.py
Normal file
@ -0,0 +1,13 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class ServerCommand(BaseModel):
|
||||
command: str
|
||||
server_ip: str
|
||||
require_online_player: Optional[bool] = False
|
||||
target_message: Optional[str] = None # Сообщение для цели
|
||||
global_message: Optional[str] = None # Сообщение для остальных
|
||||
|
||||
class InventoryRequest(BaseModel):
|
||||
server_ip: str
|
||||
player_name: str
|
17
app/models/server/event.py
Normal file
17
app/models/server/event.py
Normal file
@ -0,0 +1,17 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
|
||||
class PlayerEvent(BaseModel):
|
||||
event_type: str
|
||||
player_id: Optional[str] = None
|
||||
player_name: str
|
||||
duration: Optional[int] = None # в секундах
|
||||
timestamp: Optional[int] = None # UNIX timestamp в миллисекундах
|
||||
server_ip: str
|
||||
|
||||
class OnlinePlayersUpdate(BaseModel):
|
||||
event_type: str = "online_players_update"
|
||||
players: List[Dict]
|
||||
timestamp: int
|
||||
server_ip: str
|
17
app/models/server/playtime.py
Normal file
17
app/models/server/playtime.py
Normal file
@ -0,0 +1,17 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
class PlayerSession(BaseModel):
|
||||
player_id: str
|
||||
player_name: str
|
||||
server_ip: str
|
||||
start_time: datetime
|
||||
end_time: Optional[datetime] = None
|
||||
duration: Optional[int] = None # в секундах
|
||||
|
||||
class PlayerPlaytime(BaseModel):
|
||||
player_id: str
|
||||
player_name: str
|
||||
total_time: int # общее время в секундах
|
||||
last_coins_update: datetime # последнее время начисления монет
|
36
app/models/server/prank.py
Normal file
36
app/models/server/prank.py
Normal file
@ -0,0 +1,36 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
|
||||
class PrankCommandCreate(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
price: int
|
||||
command_template: str
|
||||
server_ids: List[str] = Field(
|
||||
default=[],
|
||||
description='Список серверов, где доступна команда. Использование ["*"] означает доступность на всех серверах'
|
||||
)
|
||||
targetDescription: Optional[str] = None # Сообщение для целевого игрока
|
||||
globalDescription: Optional[str] = None # Сообщение для всех остальных
|
||||
|
||||
class PrankCommandUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
price: Optional[int] = None
|
||||
command_template: Optional[str] = None
|
||||
server_ids: Optional[List[str]] = None
|
||||
targetDescription: Optional[str] = None
|
||||
globalDescription: Optional[str] = None
|
||||
|
||||
class PrankCommand(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
price: int
|
||||
command_template: str
|
||||
server_ids: List[str] = []
|
||||
|
||||
class PrankExecute(BaseModel):
|
||||
command_id: str
|
||||
target_player: str
|
||||
server_id: str
|
@ -19,6 +19,8 @@ class UserInDB(BaseModel):
|
||||
skin_url: Optional[str] = None
|
||||
skin_model: Optional[str] = "classic"
|
||||
cloak_url: Optional[str] = None
|
||||
coins: int = 0 # Новое поле для монет
|
||||
total_time_played: int = 0 # Общее время игры в секундах
|
||||
is_active: bool = True
|
||||
created_at: datetime = datetime.utcnow()
|
||||
|
||||
|
@ -108,7 +108,7 @@ class AuthService:
|
||||
|
||||
textures = {
|
||||
"timestamp": int(datetime.now().timestamp() * 1000),
|
||||
"profileId": user["uuid"], # UUID с дефисами
|
||||
"profileId": user["uuid"].replace("-", ""),
|
||||
"profileName": user["username"],
|
||||
"textures": {}
|
||||
}
|
||||
@ -125,28 +125,66 @@ class AuthService:
|
||||
textures_json = json.dumps(textures).encode()
|
||||
base64_textures = base64.b64encode(textures_json).decode()
|
||||
|
||||
# Подписываем текстуры
|
||||
with open("private_key.pem", "rb") as key_file:
|
||||
private_key = serialization.load_pem_private_key(
|
||||
key_file.read(),
|
||||
password=None
|
||||
try:
|
||||
# Подписываем текстуры
|
||||
private_key_path = "app/keys/private_key.pem"
|
||||
with open(private_key_path, "rb") as key_file:
|
||||
private_key = serialization.load_pem_private_key(
|
||||
key_file.read(),
|
||||
password=None
|
||||
)
|
||||
|
||||
signature = private_key.sign(
|
||||
base64.b64encode(textures_json),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA1()
|
||||
)
|
||||
|
||||
signature = private_key.sign(
|
||||
textures_json,
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA1()
|
||||
)
|
||||
signature_base64 = base64.b64encode(signature).decode()
|
||||
|
||||
return JSONResponse({
|
||||
"id": user["uuid"].replace("-", ""), # Уберите дефисы
|
||||
"name": user["username"],
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": base64_textures,
|
||||
"signature": base64.b64encode(signature).decode()
|
||||
}]
|
||||
})
|
||||
return {
|
||||
"id": user["uuid"].replace("-", ""),
|
||||
"name": user["username"],
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": base64_textures,
|
||||
"signature": signature_base64
|
||||
}]
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error signing textures: {e}")
|
||||
# В случае ошибки возвращаем текстуры без подписи
|
||||
return {
|
||||
"id": user["uuid"].replace("-", ""),
|
||||
"name": user["username"],
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": base64_textures
|
||||
}]
|
||||
}
|
||||
|
||||
# # Подписываем текстуры
|
||||
# with open("private_key.pem", "rb") as key_file:
|
||||
# private_key = serialization.load_pem_private_key(
|
||||
# key_file.read(),
|
||||
# password=None
|
||||
# )
|
||||
|
||||
# signature = private_key.sign(
|
||||
# textures_json,
|
||||
# padding.PKCS1v15(),
|
||||
# hashes.SHA1()
|
||||
# )
|
||||
|
||||
# return JSONResponse({
|
||||
# "id": user["uuid"].replace("-", ""), # Уберите дефисы
|
||||
# "name": user["username"],
|
||||
# "properties": [{
|
||||
# "name": "textures",
|
||||
# "value": base64_textures,
|
||||
# # "signature": base64.b64encode(signature).decode()
|
||||
# }]
|
||||
# })
|
||||
|
||||
async def join_server(self, request_data: dict):
|
||||
access_token = request_data.get("accessToken")
|
||||
@ -186,24 +224,59 @@ class AuthService:
|
||||
if not session:
|
||||
raise HTTPException(status_code=403, detail="Not joined this server")
|
||||
|
||||
textures = {}
|
||||
if user.get("skin_url"):
|
||||
textures["SKIN"] = {"url": user["skin_url"]}
|
||||
if user.get("cloak_url"):
|
||||
textures["CAPE"] = {"url": user["cloak_url"]}
|
||||
|
||||
textures_value = base64.b64encode(json.dumps({
|
||||
"timestamp": int(datetime.now().timestamp()),
|
||||
"profileId": user["uuid"].replace("-", ""), # UUID без дефисов
|
||||
"profileName": username,
|
||||
"textures": textures
|
||||
}).encode()).decode()
|
||||
|
||||
return {
|
||||
"id": user["uuid"].replace("-", ""), # UUID без дефисов
|
||||
"name": username,
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": textures_value
|
||||
}] if textures else []
|
||||
textures = {
|
||||
"timestamp": int(datetime.now().timestamp() * 1000),
|
||||
"profileId": user["uuid"].replace("-", ""),
|
||||
"profileName": user["username"],
|
||||
"textures": {}
|
||||
}
|
||||
|
||||
if user.get("skin_url"):
|
||||
textures["textures"]["SKIN"] = {
|
||||
"url": user["skin_url"],
|
||||
"metadata": {"model": user.get("skin_model", "classic")}
|
||||
}
|
||||
|
||||
if user.get("cloak_url"):
|
||||
textures["textures"]["CAPE"] = {"url": user["cloak_url"]}
|
||||
|
||||
textures_json = json.dumps(textures).encode()
|
||||
base64_textures = base64.b64encode(textures_json).decode()
|
||||
|
||||
try:
|
||||
# Подписываем текстуры
|
||||
private_key_path = "app/keys/private_key.pem"
|
||||
with open(private_key_path, "rb") as key_file:
|
||||
private_key = serialization.load_pem_private_key(
|
||||
key_file.read(),
|
||||
password=None
|
||||
)
|
||||
|
||||
signature = private_key.sign(
|
||||
base64.b64encode(textures_json),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA1()
|
||||
)
|
||||
|
||||
signature_base64 = base64.b64encode(signature).decode()
|
||||
|
||||
return {
|
||||
"id": user["uuid"].replace("-", ""),
|
||||
"name": user["username"],
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": base64_textures,
|
||||
"signature": signature_base64
|
||||
}]
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error signing textures: {e}")
|
||||
# В случае ошибки возвращаем текстуры без подписи
|
||||
return {
|
||||
"id": user["uuid"].replace("-", ""),
|
||||
"name": user["username"],
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": base64_textures
|
||||
}]
|
||||
}
|
||||
|
142
app/services/coins.py
Normal file
142
app/services/coins.py
Normal file
@ -0,0 +1,142 @@
|
||||
from datetime import datetime
|
||||
from app.db.database import users_collection, sessions_collection
|
||||
from fastapi import HTTPException
|
||||
|
||||
class CoinsService:
|
||||
async def update_player_coins(self, player_id: str, player_name: str, online_time: int, server_ip: str):
|
||||
"""Обновляет монеты игрока на основе времени онлайн"""
|
||||
|
||||
# Находим пользователя
|
||||
user = await self._find_user_by_uuid(player_id)
|
||||
if not user:
|
||||
return # Пользователь не найден
|
||||
|
||||
# Находим последнее обновление монет
|
||||
last_update = await sessions_collection.find_one({
|
||||
"player_id": player_id,
|
||||
"server_ip": server_ip,
|
||||
"update_type": "coins_update"
|
||||
}, sort=[("timestamp", -1)])
|
||||
|
||||
now = datetime.now()
|
||||
current_coins = user.get("coins", 0)
|
||||
current_total_time = user.get("total_time_played", 0)
|
||||
|
||||
if last_update:
|
||||
# Время с последнего начисления
|
||||
last_timestamp = last_update["timestamp"]
|
||||
seconds_since_update = int((now - last_timestamp).total_seconds())
|
||||
|
||||
# Начисляем монеты только за полные минуты
|
||||
minutes_to_reward = seconds_since_update // 60
|
||||
|
||||
# Если прошло меньше минуты, пропускаем
|
||||
if minutes_to_reward < 1:
|
||||
return
|
||||
else:
|
||||
# Первое обновление (ограничиваем для безопасности)
|
||||
minutes_to_reward = min(online_time // 60, 5)
|
||||
|
||||
if minutes_to_reward > 0:
|
||||
# Обновляем монеты и время
|
||||
new_coins = current_coins + minutes_to_reward
|
||||
new_total_time = current_total_time + (minutes_to_reward * 60)
|
||||
|
||||
# Сохраняем в БД
|
||||
await users_collection.update_one(
|
||||
{"_id": user["_id"]},
|
||||
{"$set": {
|
||||
"coins": new_coins,
|
||||
"total_time_played": new_total_time
|
||||
}}
|
||||
)
|
||||
|
||||
# Сохраняем запись о начислении
|
||||
await sessions_collection.insert_one({
|
||||
"player_id": player_id,
|
||||
"player_name": player_name,
|
||||
"server_ip": server_ip,
|
||||
"update_type": "coins_update",
|
||||
"timestamp": now,
|
||||
"minutes_added": minutes_to_reward,
|
||||
"coins_added": minutes_to_reward
|
||||
})
|
||||
|
||||
print(f"[{now}] Игроку {user.get('username')} начислено {minutes_to_reward} монет. "
|
||||
f"Всего монет: {new_coins}")
|
||||
|
||||
async def _find_user_by_uuid(self, player_id: str):
|
||||
"""Находит пользователя по UUID с поддержкой разных форматов"""
|
||||
|
||||
# Пробуем найти как есть
|
||||
user = await users_collection.find_one({"uuid": player_id})
|
||||
if user:
|
||||
return user
|
||||
|
||||
# Пробуем разные форматы UUID
|
||||
if '-' in player_id:
|
||||
user = await users_collection.find_one({"uuid": player_id.replace('-', '')})
|
||||
else:
|
||||
formatted_uuid = f"{player_id[:8]}-{player_id[8:12]}-{player_id[12:16]}-{player_id[16:20]}-{player_id[20:]}"
|
||||
user = await users_collection.find_one({"uuid": formatted_uuid})
|
||||
|
||||
return user
|
||||
|
||||
async def get_player_coins(self, username: str):
|
||||
"""Возвращает информацию о монетах и времени игрока"""
|
||||
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
return None
|
||||
|
||||
total_time = user.get("total_time_played", 0)
|
||||
hours, remainder = divmod(total_time, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
return {
|
||||
"username": username,
|
||||
"coins": user.get("coins", 0),
|
||||
"total_time_played": {
|
||||
"seconds": total_time,
|
||||
"formatted": f"{hours}ч {minutes}м {seconds}с"
|
||||
}
|
||||
}
|
||||
|
||||
async def get_balance(self, username: str) -> int:
|
||||
"""Получить текущий баланс пользователя"""
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден")
|
||||
return user.get("coins", 0)
|
||||
|
||||
async def increase_balance(self, username: str, amount: int) -> int:
|
||||
"""Увеличить баланс пользователя"""
|
||||
if amount <= 0:
|
||||
raise ValueError("Сумма должна быть положительной")
|
||||
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$inc": {"coins": amount}}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден")
|
||||
|
||||
user = await users_collection.find_one({"username": username})
|
||||
return user.get("coins", 0)
|
||||
|
||||
async def decrease_balance(self, username: str, amount: int) -> int:
|
||||
"""Уменьшить баланс пользователя"""
|
||||
if amount <= 0:
|
||||
raise ValueError("Сумма должна быть положительной")
|
||||
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$inc": {"coins": -amount}} # Уменьшаем на отрицательное значение
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден")
|
||||
|
||||
user = await users_collection.find_one({"username": username})
|
||||
return user.get("coins", 0)
|
213
app/services/marketplace.py
Normal file
213
app/services/marketplace.py
Normal file
@ -0,0 +1,213 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from fastapi import HTTPException
|
||||
from app.db.database import db
|
||||
from app.services.coins import CoinsService
|
||||
from app.services.server.command import CommandService
|
||||
|
||||
# Коллекция для хранения товаров на торговой площадке
|
||||
marketplace_collection = db.marketplace_items
|
||||
|
||||
# Добавьте новую коллекцию для операций
|
||||
marketplace_operations = db.marketplace_operations
|
||||
|
||||
# Добавьте эту функцию
|
||||
def _serialize_mongodb_doc(doc):
|
||||
"""Преобразует MongoDB документ для JSON сериализации"""
|
||||
if doc is None:
|
||||
return None
|
||||
|
||||
# Добавить проверку на список
|
||||
if isinstance(doc, list):
|
||||
return [_serialize_mongodb_doc(item) for item in doc]
|
||||
|
||||
result = {}
|
||||
for key, value in doc.items():
|
||||
# Обработка ObjectId
|
||||
if key == "_id":
|
||||
result["_id"] = str(value)
|
||||
continue
|
||||
|
||||
# Обработка ISODate
|
||||
if isinstance(value, datetime):
|
||||
result[key] = value.isoformat()
|
||||
# Обработка вложенных словарей
|
||||
elif isinstance(value, dict):
|
||||
if "$date" in value:
|
||||
# Это ISODate
|
||||
result[key] = datetime.fromisoformat(value["$date"].replace("Z", "+00:00")).isoformat()
|
||||
else:
|
||||
result[key] = _serialize_mongodb_doc(value)
|
||||
# Обработка списков
|
||||
elif isinstance(value, list):
|
||||
result[key] = [_serialize_mongodb_doc(item) if isinstance(item, dict) else item for item in value]
|
||||
else:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
class MarketplaceService:
|
||||
async def list_items(self, server_ip: str = None, page: int = 1, limit: int = 20):
|
||||
"""Получить список предметов на торговой площадке"""
|
||||
query = {}
|
||||
if server_ip:
|
||||
query["server_ip"] = server_ip
|
||||
|
||||
total = await marketplace_collection.count_documents(query)
|
||||
|
||||
items_cursor = marketplace_collection.find(query) \
|
||||
.sort("created_at", -1) \
|
||||
.skip((page - 1) * limit) \
|
||||
.limit(limit)
|
||||
|
||||
items = await items_cursor.to_list(limit)
|
||||
|
||||
# Преобразуем каждый документ
|
||||
serialized_items = [_serialize_mongodb_doc(item) for item in items]
|
||||
|
||||
return {
|
||||
"items": serialized_items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pages": (total + limit - 1) // limit
|
||||
}
|
||||
|
||||
async def get_item(self, item_id: str):
|
||||
"""Получить информацию о конкретном предмете"""
|
||||
item = await marketplace_collection.find_one({"id": item_id})
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Предмет не найден")
|
||||
return _serialize_mongodb_doc(item)
|
||||
|
||||
async def add_item(self, username: str, slot_index: int, amount: int, price: int, server_ip: str):
|
||||
"""Выставить предмет на продажу"""
|
||||
# Создаем операцию продажи
|
||||
operation_id = str(uuid.uuid4())
|
||||
|
||||
operation = {
|
||||
"id": operation_id,
|
||||
"type": "sell",
|
||||
"player_name": username,
|
||||
"slot_index": slot_index,
|
||||
"amount": amount,
|
||||
"price": price,
|
||||
"server_ip": server_ip,
|
||||
"status": "pending",
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await marketplace_operations.insert_one(operation)
|
||||
|
||||
return {"status": "pending", "operation_id": operation_id}
|
||||
|
||||
async def get_pending_operations(self, server_ip: str):
|
||||
"""Получить список операций для выполнения на сервере"""
|
||||
operations = await marketplace_operations.find({
|
||||
"server_ip": server_ip,
|
||||
"status": "pending"
|
||||
}).to_list(100)
|
||||
|
||||
return {
|
||||
"operations": _serialize_mongodb_doc(operations)
|
||||
}
|
||||
|
||||
async def confirm_operation(self, operation_id: str, status: str = "success", error: str = None):
|
||||
"""Подтвердить выполнение операции"""
|
||||
update = {
|
||||
"status": status
|
||||
}
|
||||
|
||||
if error:
|
||||
update["error"] = error
|
||||
|
||||
result = await marketplace_operations.update_one(
|
||||
{"id": operation_id},
|
||||
{"$set": update}
|
||||
)
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
async def update_item_details(self, operation_id: str, item_data: dict):
|
||||
"""Обновить детальную информацию о предмете"""
|
||||
operation = await marketplace_operations.find_one({"id": operation_id})
|
||||
|
||||
if not operation:
|
||||
return {"status": "error", "message": "Операция не найдена"}
|
||||
|
||||
# Создаем запись о предмете на торговой площадке
|
||||
item_id = str(uuid.uuid4())
|
||||
marketplace_item = {
|
||||
"id": item_id,
|
||||
"material": item_data.get("material"),
|
||||
"amount": item_data.get("amount"),
|
||||
"price": operation.get("price"),
|
||||
"seller_name": operation.get("player_name"),
|
||||
"server_ip": operation.get("server_ip"),
|
||||
"display_name": item_data.get("meta", {}).get("display_name"),
|
||||
"lore": item_data.get("meta", {}).get("lore"),
|
||||
"enchants": item_data.get("meta", {}).get("enchants"),
|
||||
"durability": item_data.get("meta", {}).get("durability"),
|
||||
"item_data": item_data,
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await marketplace_collection.insert_one(marketplace_item)
|
||||
|
||||
# Обновляем операцию
|
||||
await marketplace_operations.update_one(
|
||||
{"id": operation_id},
|
||||
{"$set": {"item_id": item_id, "status": "completed"}}
|
||||
)
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
async def buy_item(self, buyer_username: str, item_id: str):
|
||||
"""Купить предмет с торговой площадки"""
|
||||
# 1. Находим предмет
|
||||
item = await marketplace_collection.find_one({"id": item_id})
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Предмет не найден")
|
||||
|
||||
# 2. Проверяем, что покупатель не является продавцом
|
||||
if item["seller_name"] == buyer_username:
|
||||
raise HTTPException(status_code=400, detail="Вы не можете купить свой же предмет")
|
||||
|
||||
# 3. Проверяем баланс покупателя
|
||||
coins_service = CoinsService()
|
||||
buyer_balance = await coins_service.get_balance(buyer_username)
|
||||
|
||||
if buyer_balance < item["price"]:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Недостаточно монет. Требуется: {item['price']}, имеется: {buyer_balance}")
|
||||
|
||||
# 4. Создаем операцию покупки
|
||||
operation_id = str(uuid.uuid4())
|
||||
|
||||
operation = {
|
||||
"id": operation_id,
|
||||
"type": "buy",
|
||||
"player_name": buyer_username,
|
||||
"item_id": item_id,
|
||||
"item_data": item["item_data"],
|
||||
"price": item["price"],
|
||||
"server_ip": item["server_ip"],
|
||||
"status": "pending",
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await marketplace_operations.insert_one(operation)
|
||||
|
||||
# 5. Списываем деньги с покупателя
|
||||
await coins_service.decrease_balance(buyer_username, item["price"])
|
||||
|
||||
# 6. Начисляем деньги продавцу
|
||||
await coins_service.increase_balance(item["seller_name"], item["price"])
|
||||
|
||||
# 7. Удаляем предмет с торговой площадки
|
||||
await marketplace_collection.delete_one({"id": item_id})
|
||||
|
||||
return {
|
||||
"status": "pending",
|
||||
"operation_id": operation_id,
|
||||
"message": "Покупка в обработке. Предмет будет добавлен в ваш инвентарь."
|
||||
}
|
234
app/services/server/command.py
Normal file
234
app/services/server/command.py
Normal file
@ -0,0 +1,234 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from fastapi import HTTPException
|
||||
from typing import Dict
|
||||
from app.db.database import db
|
||||
import asyncio
|
||||
|
||||
# Создаем коллекции для хранения команд и инвентаря
|
||||
pending_commands_collection = db.pending_commands
|
||||
inventory_requests_collection = db.inventory_requests
|
||||
inventory_collection = db.inventory
|
||||
game_servers_collection = db.game_servers
|
||||
|
||||
class CommandService:
|
||||
async def add_command(self, command_data):
|
||||
try:
|
||||
command_id = str(uuid.uuid4())
|
||||
command_doc = {
|
||||
"id": command_id,
|
||||
"command": command_data.command,
|
||||
"server_ip": command_data.server_ip,
|
||||
"require_online_player": command_data.require_online_player,
|
||||
"target_message": command_data.target_message if hasattr(command_data, 'target_message') else None,
|
||||
"global_message": command_data.global_message if hasattr(command_data, 'global_message') else None,
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await pending_commands_collection.insert_one(command_doc)
|
||||
print(f"[{datetime.now()}] Добавлена команда: {command_data.command} "
|
||||
f"для сервера {command_data.server_ip}")
|
||||
|
||||
# Обновляем last_activity для сервера
|
||||
await self._update_server_activity(command_data.server_ip)
|
||||
|
||||
return {"status": "success", "command_id": command_id}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def get_commands(self, server_ip: str):
|
||||
try:
|
||||
# Получаем команды для указанного сервера
|
||||
commands_cursor = pending_commands_collection.find({"server_ip": server_ip})
|
||||
commands = await commands_cursor.to_list(1000)
|
||||
|
||||
result_commands = [
|
||||
{
|
||||
"id": cmd["id"],
|
||||
"command": cmd["command"],
|
||||
"require_online_player": cmd["require_online_player"],
|
||||
"target_message": cmd.get("target_message"),
|
||||
"global_message": cmd.get("global_message")
|
||||
}
|
||||
for cmd in commands
|
||||
]
|
||||
|
||||
# Удаляем полученные команды (чтобы не выполнять их повторно)
|
||||
await pending_commands_collection.delete_many({"server_ip": server_ip})
|
||||
|
||||
return {"status": "success", "commands": result_commands}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def request_inventory(self, inventory_request):
|
||||
"""Создаёт запрос на получение инвентаря игрока"""
|
||||
try:
|
||||
request_id = str(uuid.uuid4())
|
||||
inventory_request_doc = {
|
||||
"id": request_id,
|
||||
"server_ip": inventory_request.server_ip,
|
||||
"player_name": inventory_request.player_name,
|
||||
"created_at": datetime.utcnow(),
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
await inventory_requests_collection.insert_one(inventory_request_doc)
|
||||
|
||||
print(f"[{datetime.now()}] Запрос инвентаря игрока {inventory_request.player_name} "
|
||||
f"с сервера {inventory_request.server_ip}")
|
||||
|
||||
# Обновляем last_activity для сервера
|
||||
await self._update_server_activity(inventory_request.server_ip)
|
||||
|
||||
return {"status": "pending", "request_id": request_id}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def get_inventory_requests(self, server_ip: str):
|
||||
"""Получает запросы на инвентарь для указанного сервера"""
|
||||
try:
|
||||
requests_cursor = inventory_requests_collection.find(
|
||||
{"server_ip": server_ip, "status": "pending"}
|
||||
)
|
||||
requests = await requests_cursor.to_list(1000)
|
||||
|
||||
result_requests = [
|
||||
{
|
||||
"id": req["id"],
|
||||
"player_name": req["player_name"]
|
||||
}
|
||||
for req in requests
|
||||
]
|
||||
|
||||
# Помечаем запросы как обработанные
|
||||
for req in result_requests:
|
||||
await inventory_requests_collection.update_one(
|
||||
{"id": req["id"]},
|
||||
{"$set": {"status": "processing"}}
|
||||
)
|
||||
|
||||
return {"status": "success", "inventory_requests": result_requests}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def submit_inventory(self, inventory_data: dict):
|
||||
"""Принимает данные инвентаря от сервера"""
|
||||
try:
|
||||
request_id = inventory_data.get("request_id")
|
||||
request = await inventory_requests_collection.find_one({"id": request_id})
|
||||
|
||||
if not request:
|
||||
raise HTTPException(status_code=404, detail="Запрос не найден")
|
||||
|
||||
player_name = request["player_name"]
|
||||
server_ip = request["server_ip"]
|
||||
|
||||
# Обновляем или создаем запись инвентаря
|
||||
await inventory_collection.update_one(
|
||||
{
|
||||
"player_name": player_name,
|
||||
"server_ip": server_ip
|
||||
},
|
||||
{
|
||||
"$set": {
|
||||
"inventory_data": inventory_data.get("inventory", []),
|
||||
"updated_at": datetime.utcnow()
|
||||
}
|
||||
},
|
||||
upsert=True # Создает новую запись, если не найдена существующая
|
||||
)
|
||||
|
||||
# Помечаем запрос как выполненный
|
||||
await inventory_requests_collection.update_one(
|
||||
{"id": request_id},
|
||||
{"$set": {"status": "completed"}}
|
||||
)
|
||||
|
||||
return {"status": "success"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def get_inventory_result(self, request_id: str):
|
||||
"""Получает результаты запроса инвентаря"""
|
||||
request = await inventory_requests_collection.find_one({"id": request_id})
|
||||
if not request:
|
||||
raise HTTPException(status_code=404, detail="Запрос не найден")
|
||||
|
||||
if request["status"] != "completed":
|
||||
return {"status": request["status"]}
|
||||
|
||||
# Получаем инвентарь из коллекции inventory
|
||||
inventory = await inventory_collection.find_one({
|
||||
"player_name": request["player_name"],
|
||||
"server_ip": request["server_ip"]
|
||||
})
|
||||
|
||||
if not inventory:
|
||||
raise HTTPException(status_code=404, detail="Инвентарь не найден")
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"result": {
|
||||
"player_name": inventory["player_name"],
|
||||
"server_ip": inventory["server_ip"],
|
||||
"inventory_data": inventory["inventory_data"],
|
||||
"updated_at": inventory["updated_at"]
|
||||
}
|
||||
}
|
||||
|
||||
async def get_player_inventory(self, player_name: str, server_ip: str, timeout: int = 10):
|
||||
"""Запрашивает и ждет получения инвентаря игрока"""
|
||||
try:
|
||||
# Проверяем, есть ли уже актуальный инвентарь
|
||||
existing_inventory = await inventory_collection.find_one({
|
||||
"player_name": player_name,
|
||||
"server_ip": server_ip
|
||||
})
|
||||
|
||||
# Если инвентарь уже есть и он достаточно свежий (не старше 1 минуты)
|
||||
if existing_inventory and "updated_at" in existing_inventory:
|
||||
if (datetime.utcnow() - existing_inventory["updated_at"]).total_seconds() < 60:
|
||||
return {
|
||||
"status": "success",
|
||||
"player_name": existing_inventory["player_name"],
|
||||
"server_ip": existing_inventory["server_ip"],
|
||||
"inventory": existing_inventory["inventory_data"],
|
||||
"updated_at": existing_inventory["updated_at"]
|
||||
}
|
||||
|
||||
# Запрашиваем новый инвентарь
|
||||
request_id = str(uuid.uuid4())
|
||||
inventory_request_doc = {
|
||||
"id": request_id,
|
||||
"server_ip": server_ip,
|
||||
"player_name": player_name,
|
||||
"created_at": datetime.utcnow(),
|
||||
"status": "pending"
|
||||
}
|
||||
await inventory_requests_collection.insert_one(inventory_request_doc)
|
||||
|
||||
print(f"[{datetime.now()}] Запрос инвентаря игрока {player_name} "
|
||||
f"с сервера {server_ip}")
|
||||
|
||||
# Обновляем last_activity для сервера
|
||||
await self._update_server_activity(server_ip)
|
||||
|
||||
# Ждем ответа от сервера
|
||||
start_time = datetime.utcnow()
|
||||
while (datetime.utcnow() - start_time).total_seconds() < timeout:
|
||||
result = await self.get_inventory_result(request_id)
|
||||
if result["status"] == "completed":
|
||||
return result
|
||||
await asyncio.sleep(1) # Ждем 1 секунду перед следующей проверкой
|
||||
|
||||
raise HTTPException(status_code=504, detail="Timeout waiting for inventory")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def _update_server_activity(self, server_ip):
|
||||
"""Обновляет время последней активности для сервера"""
|
||||
await game_servers_collection.update_one(
|
||||
{"ip": server_ip},
|
||||
{"$set": {"last_activity": datetime.utcnow()}},
|
||||
upsert=False # Не создаем новый сервер, только обновляем существующий
|
||||
)
|
290
app/services/server/event.py
Normal file
290
app/services/server/event.py
Normal file
@ -0,0 +1,290 @@
|
||||
from fastapi import HTTPException
|
||||
from datetime import datetime
|
||||
import json
|
||||
from app.services.coins import CoinsService
|
||||
from app.models.server.event import PlayerEvent, OnlinePlayersUpdate
|
||||
import uuid
|
||||
|
||||
class EventService:
|
||||
def __init__(self):
|
||||
self.coins_service = CoinsService()
|
||||
|
||||
async def process_event(self, event_data):
|
||||
"""Обработка событий от сервера Minecraft"""
|
||||
try:
|
||||
# Проверяем формат ваших событий (event_type вместо type)
|
||||
event_type = event_data.get("event_type")
|
||||
if not event_type:
|
||||
# Для совместимости со старым форматом
|
||||
event_type = event_data.get("type")
|
||||
|
||||
if not event_type:
|
||||
raise HTTPException(status_code=400, detail="Missing event type")
|
||||
|
||||
server_ip = event_data.get("server_ip")
|
||||
if not server_ip:
|
||||
raise HTTPException(status_code=400, detail="Missing server IP")
|
||||
|
||||
# Преобразуем ваши типы событий в нужные форматы
|
||||
if event_type == "online_players_update":
|
||||
# Регистрируем сервер, если его нет
|
||||
await self._register_server(server_ip, event_data)
|
||||
|
||||
# Обновляем данные об онлайн игроках
|
||||
players = event_data.get("players", [])
|
||||
await self._update_online_players(server_ip, players)
|
||||
return {"status": "success"}
|
||||
|
||||
elif event_type == "player_join":
|
||||
player_id = event_data.get("player_id")
|
||||
player_name = event_data.get("player_name")
|
||||
|
||||
if not player_id or not player_name:
|
||||
raise HTTPException(status_code=400, detail="Missing player data")
|
||||
|
||||
# Регистрируем вход игрока
|
||||
await self._register_player_login(server_ip, player_id, player_name)
|
||||
return {"status": "success"}
|
||||
|
||||
elif event_type == "player_quit":
|
||||
player_id = event_data.get("player_id")
|
||||
player_name = event_data.get("player_name")
|
||||
|
||||
if not player_id or not player_name:
|
||||
raise HTTPException(status_code=400, detail="Missing player data")
|
||||
|
||||
# Регистрируем выход игрока
|
||||
await self._register_player_logout(server_ip, player_id, player_name)
|
||||
return {"status": "success"}
|
||||
|
||||
elif event_type == "player_session":
|
||||
player_id = event_data.get("player_id")
|
||||
player_name = event_data.get("player_name")
|
||||
duration = event_data.get("duration", 0)
|
||||
|
||||
if not player_id or not player_name:
|
||||
raise HTTPException(status_code=400, detail="Missing player data")
|
||||
|
||||
# Обрабатываем информацию о сессии
|
||||
await self._process_player_session(server_ip, player_id, player_name, duration)
|
||||
return {"status": "success"}
|
||||
|
||||
# Если тип события не распознан
|
||||
print(f"[{datetime.now()}] Неизвестное событие: {event_data}")
|
||||
raise HTTPException(status_code=400, detail="Invalid event type")
|
||||
|
||||
except HTTPException as e:
|
||||
print(f"[{datetime.now()}] Ошибка обработки события: {e.status_code}: {e.detail}")
|
||||
print(f"Полученные данные: {event_data}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[{datetime.now()}] Необработанная ошибка: {str(e)}")
|
||||
print(f"Полученные данные: {event_data}")
|
||||
raise HTTPException(status_code=500, detail=f"Server error: {str(e)}")
|
||||
|
||||
async def _register_server(self, server_ip, event_data):
|
||||
"""Регистрирует сервер, если его нет в базе"""
|
||||
from app.db.database import db
|
||||
import uuid
|
||||
|
||||
game_servers_collection = db.game_servers
|
||||
|
||||
# Проверяем, есть ли уже такой сервер
|
||||
existing_server = await game_servers_collection.find_one({"ip": server_ip})
|
||||
|
||||
if not existing_server:
|
||||
# Создаем новую запись сервера
|
||||
server_data = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": f"Server {server_ip}", # Можно улучшить название
|
||||
"ip": server_ip,
|
||||
"port": 25565, # Стандартный порт Minecraft
|
||||
"description": f"Minecraft server {server_ip}",
|
||||
"max_players": 100,
|
||||
"registered_at": datetime.utcnow(),
|
||||
"last_activity": datetime.utcnow() # Добавляем поле last_activity
|
||||
}
|
||||
|
||||
await game_servers_collection.insert_one(server_data)
|
||||
print(f"[{datetime.utcnow()}] Зарегистрирован новый сервер: {server_ip}")
|
||||
else:
|
||||
# Обновляем активность существующего сервера
|
||||
await game_servers_collection.update_one(
|
||||
{"ip": server_ip},
|
||||
{"$set": {"last_activity": datetime.utcnow()}}
|
||||
)
|
||||
|
||||
return existing_server or await game_servers_collection.find_one({"ip": server_ip})
|
||||
|
||||
async def _update_online_players(self, server_ip, players_data):
|
||||
"""Обновляет информацию об онлайн игроках"""
|
||||
from app.db.database import db
|
||||
|
||||
online_players_collection = db.online_players
|
||||
game_servers_collection = db.game_servers
|
||||
|
||||
# Получаем ID сервера
|
||||
server = await self._register_server(server_ip, {})
|
||||
server_id = server["id"]
|
||||
|
||||
# Обновляем время активности сервера
|
||||
await game_servers_collection.update_one(
|
||||
{"id": server_id},
|
||||
{"$set": {"last_activity": datetime.utcnow()}}
|
||||
)
|
||||
|
||||
# Помечаем всех игроков как оффлайн на этом сервере
|
||||
await online_players_collection.update_many(
|
||||
{"server_id": server_id},
|
||||
{"$set": {"is_online": False}}
|
||||
)
|
||||
|
||||
# Обновляем данные для каждого онлайн игрока
|
||||
now = datetime.utcnow()
|
||||
for player in players_data:
|
||||
player_id = player.get("player_id")
|
||||
player_name = player.get("player_name")
|
||||
online_time = player.get("online_time", 0)
|
||||
|
||||
if not player_id or not player_name:
|
||||
continue
|
||||
|
||||
# Проверяем, существует ли уже запись
|
||||
existing_player = await online_players_collection.find_one({
|
||||
"uuid": player_id,
|
||||
"server_id": server_id
|
||||
})
|
||||
|
||||
if existing_player:
|
||||
# Обновляем существующую запись
|
||||
await online_players_collection.update_one(
|
||||
{"_id": existing_player["_id"]},
|
||||
{"$set": {
|
||||
"username": player_name,
|
||||
"is_online": True,
|
||||
"last_seen": now,
|
||||
"online_duration": online_time
|
||||
}}
|
||||
)
|
||||
else:
|
||||
# Создаем новую запись
|
||||
await online_players_collection.insert_one({
|
||||
"uuid": player_id,
|
||||
"username": player_name,
|
||||
"server_id": server_id,
|
||||
"is_online": True,
|
||||
"login_time": now,
|
||||
"last_seen": now,
|
||||
"online_duration": online_time
|
||||
})
|
||||
|
||||
online_count = len(players_data)
|
||||
print(f"[{now}] Обновлена информация о {online_count} игроках на сервере {server_ip}")
|
||||
|
||||
# Также обновляем информацию о коинах для каждого игрока
|
||||
if players_data:
|
||||
from app.services.coins import CoinsService
|
||||
coins_service = CoinsService()
|
||||
|
||||
for player in players_data:
|
||||
player_id = player.get("player_id")
|
||||
player_name = player.get("player_name")
|
||||
online_time = player.get("online_time", 0)
|
||||
|
||||
if player_id and player_name:
|
||||
await coins_service.update_player_coins(player_id, player_name, online_time, server_ip)
|
||||
|
||||
async def _register_player_login(self, server_ip, player_id, player_name):
|
||||
"""Регистрирует вход игрока на сервер"""
|
||||
from app.db.database import db
|
||||
|
||||
online_players_collection = db.online_players
|
||||
|
||||
server = await self._register_server(server_ip, {})
|
||||
server_id = server["id"]
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Проверяем, есть ли уже запись для этого игрока
|
||||
existing_player = await online_players_collection.find_one({
|
||||
"uuid": player_id,
|
||||
"server_id": server_id
|
||||
})
|
||||
|
||||
if existing_player:
|
||||
# Обновляем запись
|
||||
await online_players_collection.update_one(
|
||||
{"_id": existing_player["_id"]},
|
||||
{"$set": {
|
||||
"username": player_name,
|
||||
"is_online": True,
|
||||
"login_time": now,
|
||||
"last_seen": now
|
||||
}}
|
||||
)
|
||||
else:
|
||||
# Создаем новую запись
|
||||
await online_players_collection.insert_one({
|
||||
"uuid": player_id,
|
||||
"username": player_name,
|
||||
"server_id": server_id,
|
||||
"is_online": True,
|
||||
"login_time": now,
|
||||
"last_seen": now,
|
||||
"online_duration": 0
|
||||
})
|
||||
|
||||
print(f"[{now}] Игрок {player_name} зашел на сервер {server_ip}")
|
||||
|
||||
async def _register_player_logout(self, server_ip, player_id, player_name):
|
||||
"""Регистрирует выход игрока с сервера"""
|
||||
from app.db.database import db
|
||||
|
||||
online_players_collection = db.online_players
|
||||
|
||||
server = await self._register_server(server_ip, {})
|
||||
server_id = server["id"]
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Ищем запись игрока
|
||||
player = await online_players_collection.find_one({
|
||||
"uuid": player_id,
|
||||
"server_id": server_id
|
||||
})
|
||||
|
||||
if player:
|
||||
# Обновляем запись
|
||||
await online_players_collection.update_one(
|
||||
{"_id": player["_id"]},
|
||||
{"$set": {
|
||||
"is_online": False,
|
||||
"last_seen": now
|
||||
}}
|
||||
)
|
||||
|
||||
print(f"[{now}] Игрок {player_name} вышел с сервера {server_ip}")
|
||||
|
||||
async def _process_player_session(self, server_ip, player_id, player_name, duration):
|
||||
"""Обрабатывает информацию о завершенной сессии игрока"""
|
||||
from app.db.database import db
|
||||
from app.services.coins import CoinsService
|
||||
|
||||
server = await self._register_server(server_ip, {})
|
||||
server_id = server["id"]
|
||||
|
||||
# Обновляем статистику времени игры
|
||||
await db.player_sessions.insert_one({
|
||||
"uuid": player_id,
|
||||
"username": player_name,
|
||||
"server_id": server_id,
|
||||
"server_ip": server_ip,
|
||||
"duration": duration,
|
||||
"session_end": datetime.utcnow()
|
||||
})
|
||||
|
||||
# Начисляем коины за время игры
|
||||
coins_service = CoinsService()
|
||||
await coins_service.update_player_coins(player_id, player_name, duration, server_ip)
|
||||
|
||||
print(f"[{datetime.now()}] Сессия игрока {player_name} завершена, длительность: {duration} сек.")
|
288
app/services/server/prank.py
Normal file
288
app/services/server/prank.py
Normal file
@ -0,0 +1,288 @@
|
||||
from fastapi import HTTPException
|
||||
from app.db.database import db, users_collection
|
||||
from app.models.server.prank import PrankCommand, PrankCommandUpdate
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
from app.services.server.command import CommandService
|
||||
|
||||
# Создаем коллекции для хранения пакостей и серверов
|
||||
prank_commands_collection = db.prank_commands
|
||||
game_servers_collection = db.game_servers
|
||||
online_players_collection = db.online_players
|
||||
|
||||
class PrankService:
|
||||
async def add_prank_command(self, command_data):
|
||||
"""Добавление новой команды-пакости"""
|
||||
# Проверяем корректность шаблона команды
|
||||
if "{targetPlayer}" not in command_data.command_template:
|
||||
raise HTTPException(status_code=400,
|
||||
detail="Шаблон команды должен содержать {targetPlayer} для подстановки имени цели")
|
||||
|
||||
prank_id = str(uuid.uuid4())
|
||||
|
||||
# Создаем новую команду в БД
|
||||
prank_command = {
|
||||
"id": prank_id,
|
||||
"name": command_data.name,
|
||||
"description": command_data.description,
|
||||
"price": command_data.price,
|
||||
"command_template": command_data.command_template,
|
||||
"server_ids": command_data.server_ids,
|
||||
"targetDescription": command_data.targetDescription,
|
||||
"globalDescription": command_data.globalDescription, # Добавить это поле
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await prank_commands_collection.insert_one(prank_command)
|
||||
|
||||
return {"status": "success", "id": prank_id}
|
||||
|
||||
async def get_all_prank_commands(self):
|
||||
"""Получение списка всех команд-пакостей"""
|
||||
commands = await prank_commands_collection.find().to_list(1000)
|
||||
result = []
|
||||
|
||||
for cmd in commands:
|
||||
result.append({
|
||||
"id": cmd["id"],
|
||||
"name": cmd["name"],
|
||||
"description": cmd["description"],
|
||||
"price": cmd["price"],
|
||||
"command_template": cmd["command_template"],
|
||||
"server_ids": cmd.get("server_ids", []),
|
||||
"targetDescription": cmd.get("targetDescription"),
|
||||
"globalDescription": cmd.get("globalDescription") # Добавить это поле
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
async def get_prank_command(self, command_id: str):
|
||||
"""Получение конкретной команды по ID"""
|
||||
command = await prank_commands_collection.find_one({"id": command_id})
|
||||
if not command:
|
||||
raise HTTPException(status_code=404, detail="Команда не найдена")
|
||||
|
||||
return {
|
||||
"id": command["id"],
|
||||
"name": command["name"],
|
||||
"description": command["description"],
|
||||
"price": command["price"],
|
||||
"command_template": command["command_template"],
|
||||
"server_ids": command.get("server_ids", []),
|
||||
"targetDescription": command.get("targetDescription"),
|
||||
"globalDescription": command.get("globalDescription") # Добавить это поле
|
||||
}
|
||||
|
||||
async def update_prank_command(self, command_id: str, update_data: PrankCommandUpdate):
|
||||
"""Обновление команды-пакости"""
|
||||
command = await prank_commands_collection.find_one({"id": command_id})
|
||||
if not command:
|
||||
raise HTTPException(status_code=404, detail="Команда не найдена")
|
||||
|
||||
# Готовим данные для обновления
|
||||
update = {}
|
||||
if update_data.name is not None:
|
||||
update["name"] = update_data.name
|
||||
if update_data.description is not None:
|
||||
update["description"] = update_data.description
|
||||
if update_data.price is not None:
|
||||
update["price"] = update_data.price
|
||||
if update_data.command_template is not None:
|
||||
if "{targetPlayer}" not in update_data.command_template:
|
||||
raise HTTPException(status_code=400,
|
||||
detail="Шаблон команды должен содержать {targetPlayer} для подстановки имени цели")
|
||||
update["command_template"] = update_data.command_template
|
||||
if update_data.server_ids is not None:
|
||||
update["server_ids"] = update_data.server_ids
|
||||
if update_data.targetDescription is not None:
|
||||
update["targetDescription"] = update_data.targetDescription
|
||||
if update_data.globalDescription is not None: # Добавить эту проверку
|
||||
update["globalDescription"] = update_data.globalDescription
|
||||
|
||||
if update:
|
||||
result = await prank_commands_collection.update_one(
|
||||
{"id": command_id},
|
||||
{"$set": update}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=500, detail="Ошибка при обновлении команды")
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
async def delete_prank_command(self, command_id: str):
|
||||
"""Удаление команды-пакости"""
|
||||
command = await prank_commands_collection.find_one({"id": command_id})
|
||||
if not command:
|
||||
raise HTTPException(status_code=404, detail="Команда не найдена")
|
||||
|
||||
result = await prank_commands_collection.delete_one({"id": command_id})
|
||||
if result.deleted_count == 0:
|
||||
raise HTTPException(status_code=500, detail="Ошибка при удалении команды")
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
async def get_all_servers(self):
|
||||
"""Получение списка всех доступных серверов"""
|
||||
# Проверяем и удаляем неактивные серверы (более 5 минут без данных)
|
||||
current_time = datetime.utcnow()
|
||||
inactive_threshold = 5 * 60 # 5 минут в секундах
|
||||
|
||||
# Находим серверы, которые не отправляли данные больше 5 минут
|
||||
# Учитываем, что у некоторых серверов может не быть поля last_activity
|
||||
inactive_servers = await game_servers_collection.find({
|
||||
"last_activity": {
|
||||
"$exists": True,
|
||||
"$lt": current_time - timedelta(seconds=inactive_threshold)
|
||||
}
|
||||
}).to_list(100)
|
||||
|
||||
# Удаляем неактивные серверы
|
||||
if inactive_servers:
|
||||
server_ids = [server["id"] for server in inactive_servers]
|
||||
await game_servers_collection.delete_many({"id": {"$in": server_ids}})
|
||||
|
||||
# Опционально: логирование удаленных серверов
|
||||
for server in inactive_servers:
|
||||
print(f"Удален неактивный сервер: {server['name']} (ID: {server['id']})")
|
||||
|
||||
# Получаем актуальный список серверов
|
||||
servers = await game_servers_collection.find().to_list(100)
|
||||
|
||||
# Если нет зарегистрированных серверов, вернем пустой список
|
||||
if not servers:
|
||||
return []
|
||||
|
||||
result = []
|
||||
for server in servers:
|
||||
# Получаем количество онлайн игроков
|
||||
online_count = await online_players_collection.count_documents(
|
||||
{"server_id": server["id"], "is_online": True}
|
||||
)
|
||||
|
||||
result.append({
|
||||
"id": server["id"],
|
||||
"name": server["name"],
|
||||
"ip": server.get("ip"),
|
||||
"port": server.get("port"),
|
||||
"description": server.get("description", ""),
|
||||
"online_players": online_count,
|
||||
"max_players": server.get("max_players", 0),
|
||||
"last_activity": server.get("last_activity")
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
async def get_server_online_players(self, server_id: str):
|
||||
"""Получение списка онлайн игроков на конкретном сервере"""
|
||||
server = await game_servers_collection.find_one({"id": server_id})
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Сервер не найден")
|
||||
|
||||
players = await online_players_collection.find(
|
||||
{"server_id": server_id, "is_online": True}
|
||||
).to_list(1000)
|
||||
|
||||
result = []
|
||||
for player in players:
|
||||
result.append({
|
||||
"username": player["username"],
|
||||
"uuid": player.get("uuid", ""),
|
||||
"online_since": player.get("login_time")
|
||||
})
|
||||
|
||||
return {
|
||||
"server": {
|
||||
"id": server["id"],
|
||||
"name": server["name"]
|
||||
},
|
||||
"online_players": result,
|
||||
"count": len(result)
|
||||
}
|
||||
|
||||
async def execute_prank(self, username: str, command_id: str, target_player: str, server_id: str):
|
||||
"""Выполнение пакости (покупка и выполнение команды)"""
|
||||
# Проверяем пользователя
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Проверяем команду
|
||||
command = await prank_commands_collection.find_one({"id": command_id})
|
||||
if not command:
|
||||
raise HTTPException(status_code=404, detail="Команда не найдена")
|
||||
|
||||
# Проверяем сервер
|
||||
server = await game_servers_collection.find_one({"id": server_id})
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Сервер не найден")
|
||||
|
||||
# Проверяем, доступна ли команда на данном сервере
|
||||
if (command.get("server_ids") and
|
||||
"*" not in command.get("server_ids", []) and
|
||||
server_id not in command.get("server_ids", [])):
|
||||
raise HTTPException(status_code=400, detail="Команда недоступна на выбранном сервере")
|
||||
|
||||
# Проверяем, онлайн ли целевой игрок
|
||||
target_online = await online_players_collection.find_one({
|
||||
"username": target_player,
|
||||
"server_id": server_id,
|
||||
"is_online": True
|
||||
})
|
||||
|
||||
if not target_online:
|
||||
raise HTTPException(status_code=400, detail=f"Игрок {target_player} не в сети на этом сервере")
|
||||
|
||||
# Проверяем достаточно ли монет
|
||||
user_coins = user.get("coins", 0)
|
||||
if user_coins < command["price"]:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Недостаточно монет. Требуется: {command['price']}, имеется: {user_coins}")
|
||||
|
||||
# Формируем команду для выполнения
|
||||
actual_command = command["command_template"].replace("{targetPlayer}", target_player)
|
||||
|
||||
# Обрабатываем оба типа сообщений
|
||||
target_desc = None
|
||||
global_desc = None
|
||||
|
||||
if command.get("targetDescription"):
|
||||
target_desc = command.get("targetDescription").replace("{username}", username).replace("{targetPlayer}", target_player)
|
||||
|
||||
if command.get("globalDescription"):
|
||||
global_desc = command.get("globalDescription").replace("{username}", username).replace("{targetPlayer}", target_player)
|
||||
|
||||
# Отправляем команду с обоими сообщениями
|
||||
command_service = CommandService()
|
||||
from app.models.server.command import ServerCommand
|
||||
|
||||
server_command = ServerCommand(
|
||||
command=actual_command,
|
||||
server_ip=server.get("ip", ""),
|
||||
require_online_player=True,
|
||||
target_message=target_desc, # Сообщение для цели
|
||||
global_message=global_desc # Сообщение для всех остальных
|
||||
)
|
||||
|
||||
command_result = await command_service.add_command(server_command)
|
||||
|
||||
# Логируем выполнение пакости
|
||||
log_entry = {
|
||||
"user_id": user["_id"],
|
||||
"username": username,
|
||||
"target_player": target_player,
|
||||
"command_id": command_id,
|
||||
"command_name": command["name"],
|
||||
"server_id": server_id,
|
||||
"price": command["price"],
|
||||
"executed_command": actual_command,
|
||||
"executed_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await db.prank_executions.insert_one(log_entry)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Команда '{command['name']}' успешно выполнена на игроке {target_player}",
|
||||
"remaining_coins": user_coins - command["price"]
|
||||
}
|
314
app/services/store_cape.py
Normal file
314
app/services/store_cape.py
Normal file
@ -0,0 +1,314 @@
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from app.db.database import users_collection
|
||||
from app.core.config import FILES_URL
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
from app.models.cape import CapeStore, CapeStoreUpdate
|
||||
|
||||
# Создаем коллекцию для плащей в БД
|
||||
from app.db.database import db
|
||||
store_capes_collection = db.store_capes
|
||||
|
||||
class StoreCapeService:
|
||||
async def add_cape(self, name: str, description: str, price: int, cape_file: UploadFile):
|
||||
"""Добавление нового плаща в магазин"""
|
||||
# Проверка типа файла
|
||||
if not cape_file.content_type.startswith('image/'):
|
||||
raise HTTPException(status_code=400, detail="Файл должен быть изображением")
|
||||
|
||||
# Определяем расширение
|
||||
ext = None
|
||||
if cape_file.content_type == "image/png":
|
||||
ext = "png"
|
||||
elif cape_file.content_type == "image/gif":
|
||||
ext = "gif"
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Поддерживаются только PNG и GIF плащи")
|
||||
|
||||
# Проверка размера файла (максимум 2MB)
|
||||
max_size = 2 * 1024 * 1024 # 2MB
|
||||
contents = await cape_file.read()
|
||||
if len(contents) > max_size:
|
||||
raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 2MB)")
|
||||
|
||||
# Создаем папку для плащей магазина, если ее нет
|
||||
cape_dir = Path("app/static/capes_store")
|
||||
cape_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Генерируем ID и имя файла
|
||||
cape_id = str(uuid.uuid4())
|
||||
cape_filename = f"store_cape_{cape_id}.{ext}"
|
||||
cape_path = cape_dir / cape_filename
|
||||
|
||||
# Сохраняем файл
|
||||
with open(cape_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
# Создаем запись в БД
|
||||
cape_data = {
|
||||
"id": cape_id,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"price": price,
|
||||
"file_name": cape_filename,
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await store_capes_collection.insert_one(cape_data)
|
||||
|
||||
return {"id": cape_id, "status": "success"}
|
||||
|
||||
async def get_all_capes(self):
|
||||
"""Получение всех плащей из магазина"""
|
||||
capes = await store_capes_collection.find().to_list(1000)
|
||||
|
||||
result = []
|
||||
for cape in capes:
|
||||
result.append({
|
||||
"id": cape["id"],
|
||||
"name": cape["name"],
|
||||
"description": cape["description"],
|
||||
"price": cape["price"],
|
||||
"image_url": f"{FILES_URL}/capes_store/{cape['file_name']}"
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
async def get_cape_by_id(self, cape_id: str):
|
||||
"""Получение плаща по ID"""
|
||||
cape = await store_capes_collection.find_one({"id": cape_id})
|
||||
if not cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден")
|
||||
|
||||
return {
|
||||
"id": cape["id"],
|
||||
"name": cape["name"],
|
||||
"description": cape["description"],
|
||||
"price": cape["price"],
|
||||
"image_url": f"{FILES_URL}/capes_store/{cape['file_name']}"
|
||||
}
|
||||
|
||||
async def update_cape(self, cape_id: str, update_data: CapeStoreUpdate):
|
||||
"""Обновление информации о плаще"""
|
||||
cape = await store_capes_collection.find_one({"id": cape_id})
|
||||
if not cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден")
|
||||
|
||||
# Готовим данные для обновления
|
||||
update = {}
|
||||
if update_data.name:
|
||||
update["name"] = update_data.name
|
||||
if update_data.description:
|
||||
update["description"] = update_data.description
|
||||
if update_data.price is not None:
|
||||
update["price"] = update_data.price
|
||||
|
||||
if update:
|
||||
result = await store_capes_collection.update_one(
|
||||
{"id": cape_id},
|
||||
{"$set": update}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=500, detail="Ошибка при обновлении")
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
async def delete_cape(self, cape_id: str):
|
||||
"""Удаление плаща из магазина"""
|
||||
cape = await store_capes_collection.find_one({"id": cape_id})
|
||||
if not cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден")
|
||||
|
||||
# Удаляем файл
|
||||
cape_path = Path(f"app/static/capes_store/{cape['file_name']}")
|
||||
if cape_path.exists():
|
||||
try:
|
||||
cape_path.unlink()
|
||||
except Exception as e:
|
||||
print(f"Ошибка при удалении файла: {e}")
|
||||
|
||||
# Удаляем из БД плащей магазина
|
||||
result = await store_capes_collection.delete_one({"id": cape_id})
|
||||
if result.deleted_count == 0:
|
||||
raise HTTPException(status_code=500, detail="Ошибка при удалении из БД")
|
||||
|
||||
# Удаляем из БД купленных плащей
|
||||
purchases_collection = db.purchases
|
||||
purchases = await purchases_collection.find_one({"cape_id": cape_id})
|
||||
if purchases:
|
||||
await purchases_collection.delete_one({"cape_id": cape_id})
|
||||
|
||||
# Удаляем плащ из массива purchased_capes всех пользователей
|
||||
users_collection = db.users
|
||||
await users_collection.update_many(
|
||||
{"purchased_capes.cape_id": cape_id},
|
||||
{"$pull": {"purchased_capes": {"cape_id": cape_id}}}
|
||||
)
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
async def purchase_cape(self, username: str, cape_id: str):
|
||||
"""Покупка плаща пользователем"""
|
||||
# Находим пользователя
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Находим плащ
|
||||
cape = await store_capes_collection.find_one({"id": cape_id})
|
||||
if not cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден")
|
||||
|
||||
# Проверяем достаточно ли монет
|
||||
user_coins = user.get("coins", 0)
|
||||
if user_coins < cape["price"]:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Недостаточно монет. Требуется: {cape['price']}, имеется: {user_coins}")
|
||||
|
||||
# Копируем плащ из хранилища магазина в персональную папку пользователя
|
||||
cape_store_path = Path(f"app/static/capes_store/{cape['file_name']}")
|
||||
|
||||
# Создаем папку для плащей пользователя
|
||||
cape_dir = Path("app/static/capes")
|
||||
cape_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Генерируем имя файла для персонального плаща
|
||||
filename_parts = cape['file_name'].split('.')
|
||||
ext = filename_parts[-1]
|
||||
cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"
|
||||
cape_path = cape_dir / cape_filename
|
||||
|
||||
# Копируем файл
|
||||
try:
|
||||
shutil.copy(cape_store_path, cape_path)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка при копировании файла: {e}")
|
||||
|
||||
# Обновляем данные пользователя
|
||||
# 1. Списываем монеты
|
||||
# 2. Устанавливаем новый плащ
|
||||
# 3. Добавляем плащ в список приобретенных
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$set": {
|
||||
"coins": user_coins - cape["price"],
|
||||
"cloak_url": f"{FILES_URL}/capes/{cape_filename}"
|
||||
},
|
||||
"$push": {
|
||||
"purchased_capes": {
|
||||
"cape_id": cape_id,
|
||||
"cape_name": cape["name"],
|
||||
"cape_description": cape["description"],
|
||||
"file_name": cape_filename,
|
||||
"purchased_at": datetime.utcnow()
|
||||
}
|
||||
}}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
# Если обновление не удалось, удаляем файл плаща
|
||||
if os.path.exists(cape_path):
|
||||
os.remove(cape_path)
|
||||
raise HTTPException(status_code=500, detail="Ошибка при обновлении данных пользователя")
|
||||
|
||||
# Логируем покупку в БД
|
||||
purchase_data = {
|
||||
"username": username,
|
||||
"user_id": user["_id"],
|
||||
"cape_id": cape_id,
|
||||
"cape_name": cape["name"],
|
||||
"price": cape["price"],
|
||||
"purchase_date": datetime.utcnow()
|
||||
}
|
||||
|
||||
from app.db.database import db
|
||||
await db.purchases.insert_one(purchase_data)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Плащ '{cape['name']}' успешно приобретен",
|
||||
"remaining_coins": user_coins - cape["price"]
|
||||
}
|
||||
|
||||
async def get_user_purchased_capes(self, username: str):
|
||||
"""Получение всех плащей, приобретенных пользователем"""
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
purchased_capes = user.get("purchased_capes", [])
|
||||
result = []
|
||||
|
||||
for cape in purchased_capes:
|
||||
result.append({
|
||||
"cape_id": cape.get("cape_id"),
|
||||
"cape_name": cape.get("cape_name"),
|
||||
"cape_description": cape.get("cape_description"),
|
||||
"image_url": f"{FILES_URL}/capes/{cape.get('file_name')}",
|
||||
"purchased_at": cape.get("purchased_at"),
|
||||
"is_active": user.get("cloak_url") == f"{FILES_URL}/capes/{cape.get('file_name')}"
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
async def activate_purchased_cape(self, username: str, cape_id: str):
|
||||
"""Активация приобретенного плаща"""
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Проверяем, что плащ был приобретен
|
||||
purchased_capes = user.get("purchased_capes", [])
|
||||
selected_cape = None
|
||||
|
||||
for cape in purchased_capes:
|
||||
if cape.get("cape_id") == cape_id:
|
||||
selected_cape = cape
|
||||
break
|
||||
|
||||
if not selected_cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден среди приобретенных")
|
||||
|
||||
# Устанавливаем выбранный плащ
|
||||
await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$set": {"cloak_url": f"{FILES_URL}/capes/{selected_cape.get('file_name')}"}}
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Плащ '{selected_cape.get('cape_name')}' активирован"
|
||||
}
|
||||
|
||||
async def deactivate_purchased_cape(self, username: str, cape_id: str):
|
||||
"""Деактивация приобретенного плаща"""
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Проверяем, что плащ был приобретен
|
||||
purchased_capes = user.get("purchased_capes", [])
|
||||
selected_cape = None
|
||||
|
||||
for cape in purchased_capes:
|
||||
if cape.get("cape_id") == cape_id:
|
||||
selected_cape = cape
|
||||
break
|
||||
|
||||
if not selected_cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден среди приобретенных")
|
||||
|
||||
# Устанавливаем выбранный плащ
|
||||
await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$set": {"cloak_url": None}}
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Плащ '{selected_cape.get('cape_name')}' деактивирован"
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
After Width: | Height: | Size: 310 B |
Binary file not shown.
After Width: | Height: | Size: 738 B |
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 122 KiB |
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@ -0,0 +1,29 @@
|
||||
services:
|
||||
app:
|
||||
container_name: minecraft-api
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3000"
|
||||
volumes:
|
||||
- ./app/static:/app/static
|
||||
environment:
|
||||
- MONGO_URI=mongodb://mongodb:27017/minecraft-api
|
||||
- SECRET_KEY=your-secret-key
|
||||
- ALGORITHM=HS256
|
||||
- ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
depends_on:
|
||||
- mongodb
|
||||
|
||||
mongodb:
|
||||
container_name: mongodb
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./mongodb:/data/db
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=popa
|
||||
- MONGO_INITDB_ROOT_PASSWORD=2006sit_
|
||||
restart: always
|
16
dockerfile
Normal file
16
dockerfile
Normal file
@ -0,0 +1,16 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
VOLUME /app/static
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"]
|
||||
|
8
main.py
8
main.py
@ -1,6 +1,6 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from app.api import users, skins, capes, meta
|
||||
from app.api import users, skins, capes, meta, server, store, pranks, marketplace
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
@ -9,13 +9,17 @@ app.include_router(meta.router)
|
||||
app.include_router(users.router)
|
||||
app.include_router(skins.router)
|
||||
app.include_router(capes.router)
|
||||
app.include_router(server.router)
|
||||
app.include_router(store.router)
|
||||
app.include_router(pranks.router)
|
||||
app.include_router(marketplace.router)
|
||||
|
||||
# Монтируем статику
|
||||
app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")
|
||||
app.mount("/capes", StaticFiles(directory="app/static/capes"), name="capes")
|
||||
app.mount("/capes_store", StaticFiles(directory="app/static/capes_store"), name="capes_store")
|
||||
|
||||
# CORS, middleware и т.д.
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
|
@ -5,3 +5,9 @@ python-jose>=3.3.0
|
||||
passlib>=1.7.4
|
||||
bcrypt>=4.0.1
|
||||
python-multipart>=0.0.9
|
||||
mongoengine>=0.24.2
|
||||
python-dotenv>=1.0.0
|
||||
pydantic>=2.0.0
|
||||
pydantic[email]>=2.0.0
|
||||
cryptography>=43.0.0
|
||||
|
||||
|
Reference in New Issue
Block a user