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
|
.env
|
||||||
skins
|
skins
|
||||||
capes
|
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.cape import CapeService
|
||||||
|
from app.services.auth import AuthService
|
||||||
|
|
||||||
router = APIRouter(tags=["Capes"])
|
router = APIRouter(tags=["Capes"])
|
||||||
|
|
||||||
@router.post("/user/{username}/cape")
|
@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)
|
return await CapeService().set_cape(username, cape_file)
|
||||||
|
|
||||||
@router.delete("/user/{username}/cape")
|
@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)
|
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("/")
|
@router.get("/")
|
||||||
def api_root():
|
def api_root():
|
||||||
return {
|
# Читаем публичный ключ из файла
|
||||||
"meta": {
|
public_key_path = "app/keys/public_key.pem"
|
||||||
"serverName": "Your Auth Server",
|
|
||||||
"implementationName": "FastAPI",
|
try:
|
||||||
"implementationVersion": "1.0.0",
|
with open(public_key_path, "r") as f:
|
||||||
"links": {
|
public_key = f.read().strip()
|
||||||
"homepage": "https://your-server.com"
|
|
||||||
|
return {
|
||||||
|
"meta": {
|
||||||
|
"serverName": "Popa Auth Server",
|
||||||
|
"implementationName": "FastAPI",
|
||||||
|
"implementationVersion": "1.0.0",
|
||||||
|
"links": {
|
||||||
|
"homepage": "https://popa-popa.ru"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
"skinDomains": ["147.78.65.214"],
|
||||||
"skinDomains": ["147.78.65.214"],
|
"capeDomains": ["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.skin import SkinService
|
||||||
|
from app.services.auth import AuthService
|
||||||
|
|
||||||
router = APIRouter(tags=["Skins"])
|
router = APIRouter(tags=["Skins"])
|
||||||
|
|
||||||
@router.post("/user/{username}/skin")
|
@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)
|
return await SkinService().set_skin(username, skin_file, skin_model)
|
||||||
|
|
||||||
@router.delete("/user/{username}/skin")
|
@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)
|
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.user import UserCreate, UserLogin
|
||||||
from app.models.request import ValidateRequest
|
from app.models.request import ValidateRequest
|
||||||
from app.services.auth import AuthService
|
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(
|
router = APIRouter(
|
||||||
tags=["Users"]
|
tags=["Users"]
|
||||||
@ -45,3 +55,65 @@ async def join_server(request_data: dict = Body(...)):
|
|||||||
@router.get("/sessionserver/session/minecraft/hasJoined")
|
@router.get("/sessionserver/session/minecraft/hasJoined")
|
||||||
async def has_joined(username: str, serverId: str):
|
async def has_joined(username: str, serverId: str):
|
||||||
return await AuthService().has_joined(username, serverId)
|
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
|
from app.core.config import MONGO_URI
|
||||||
|
|
||||||
client = AsyncIOMotorClient(MONGO_URI)
|
client = AsyncIOMotorClient(MONGO_URI)
|
||||||
db = client["minecraft_auth"]
|
db = client["minecraft-api"]
|
||||||
|
|
||||||
users_collection = db["users"]
|
users_collection = db["users"]
|
||||||
sessions_collection = db["sessions"]
|
sessions_collection = db["sessions"]
|
||||||
|
@ -1,4 +1,25 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
class CapeUpdate(BaseModel):
|
class CapeUpdate(BaseModel):
|
||||||
cape_url: str
|
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_url: Optional[str] = None
|
||||||
skin_model: Optional[str] = "classic"
|
skin_model: Optional[str] = "classic"
|
||||||
cloak_url: Optional[str] = None
|
cloak_url: Optional[str] = None
|
||||||
|
coins: int = 0 # Новое поле для монет
|
||||||
|
total_time_played: int = 0 # Общее время игры в секундах
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
created_at: datetime = datetime.utcnow()
|
created_at: datetime = datetime.utcnow()
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ class AuthService:
|
|||||||
|
|
||||||
textures = {
|
textures = {
|
||||||
"timestamp": int(datetime.now().timestamp() * 1000),
|
"timestamp": int(datetime.now().timestamp() * 1000),
|
||||||
"profileId": user["uuid"], # UUID с дефисами
|
"profileId": user["uuid"].replace("-", ""),
|
||||||
"profileName": user["username"],
|
"profileName": user["username"],
|
||||||
"textures": {}
|
"textures": {}
|
||||||
}
|
}
|
||||||
@ -125,28 +125,66 @@ class AuthService:
|
|||||||
textures_json = json.dumps(textures).encode()
|
textures_json = json.dumps(textures).encode()
|
||||||
base64_textures = base64.b64encode(textures_json).decode()
|
base64_textures = base64.b64encode(textures_json).decode()
|
||||||
|
|
||||||
# Подписываем текстуры
|
try:
|
||||||
with open("private_key.pem", "rb") as key_file:
|
# Подписываем текстуры
|
||||||
private_key = serialization.load_pem_private_key(
|
private_key_path = "app/keys/private_key.pem"
|
||||||
key_file.read(),
|
with open(private_key_path, "rb") as key_file:
|
||||||
password=None
|
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(
|
signature_base64 = base64.b64encode(signature).decode()
|
||||||
textures_json,
|
|
||||||
padding.PKCS1v15(),
|
|
||||||
hashes.SHA1()
|
|
||||||
)
|
|
||||||
|
|
||||||
return JSONResponse({
|
return {
|
||||||
"id": user["uuid"].replace("-", ""), # Уберите дефисы
|
"id": user["uuid"].replace("-", ""),
|
||||||
"name": user["username"],
|
"name": user["username"],
|
||||||
"properties": [{
|
"properties": [{
|
||||||
"name": "textures",
|
"name": "textures",
|
||||||
"value": base64_textures,
|
"value": base64_textures,
|
||||||
"signature": base64.b64encode(signature).decode()
|
"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):
|
async def join_server(self, request_data: dict):
|
||||||
access_token = request_data.get("accessToken")
|
access_token = request_data.get("accessToken")
|
||||||
@ -186,24 +224,59 @@ class AuthService:
|
|||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(status_code=403, detail="Not joined this server")
|
raise HTTPException(status_code=403, detail="Not joined this server")
|
||||||
|
|
||||||
textures = {}
|
textures = {
|
||||||
if user.get("skin_url"):
|
"timestamp": int(datetime.now().timestamp() * 1000),
|
||||||
textures["SKIN"] = {"url": user["skin_url"]}
|
"profileId": user["uuid"].replace("-", ""),
|
||||||
if user.get("cloak_url"):
|
"profileName": user["username"],
|
||||||
textures["CAPE"] = {"url": user["cloak_url"]}
|
"textures": {}
|
||||||
|
|
||||||
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 []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 import FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
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
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@ -9,13 +9,17 @@ app.include_router(meta.router)
|
|||||||
app.include_router(users.router)
|
app.include_router(users.router)
|
||||||
app.include_router(skins.router)
|
app.include_router(skins.router)
|
||||||
app.include_router(capes.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("/skins", StaticFiles(directory="app/static/skins"), name="skins")
|
||||||
app.mount("/capes", StaticFiles(directory="app/static/capes"), name="capes")
|
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 и т.д.
|
# CORS, middleware и т.д.
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=["*"],
|
||||||
|
@ -5,3 +5,9 @@ python-jose>=3.3.0
|
|||||||
passlib>=1.7.4
|
passlib>=1.7.4
|
||||||
bcrypt>=4.0.1
|
bcrypt>=4.0.1
|
||||||
python-multipart>=0.0.9
|
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