Compare commits
13 Commits
b79b0ae69f
...
refactor-b
Author | SHA1 | Date | |
---|---|---|---|
2bd081fe7a | |||
e59669f66a | |||
c7f6baac5d | |||
75d7e29f6e | |||
39cd14f1d7 | |||
6b8f116608 | |||
44e12723ad | |||
259e3c373b | |||
7e4e2c0bad | |||
d52d4dbf75 | |||
ff65e4a333 | |||
2e59d03784 | |||
733977f56e |
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
|
||||||
|
0
app/api/__init__.py
Normal file
0
app/api/__init__.py
Normal file
32
app/api/capes.py
Normal file
32
app/api/capes.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from fastapi import APIRouter, UploadFile, File, HTTPException, Form
|
||||||
|
from app.services.cape import CapeService
|
||||||
|
from app.services.auth import AuthService
|
||||||
|
|
||||||
|
router = APIRouter(tags=["Capes"])
|
||||||
|
|
||||||
|
@router.post("/user/{username}/cape")
|
||||||
|
async def set_cape(
|
||||||
|
username: str,
|
||||||
|
cape_file: UploadFile = File(...),
|
||||||
|
accessToken: str = Form(...),
|
||||||
|
clientToken: str = Form(...)
|
||||||
|
):
|
||||||
|
# Validate the token
|
||||||
|
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||||
|
if not is_valid:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||||
|
|
||||||
|
return await CapeService().set_cape(username, cape_file)
|
||||||
|
|
||||||
|
@router.delete("/user/{username}/cape")
|
||||||
|
async def remove_cape(
|
||||||
|
username: str,
|
||||||
|
accessToken: str,
|
||||||
|
clientToken: str
|
||||||
|
):
|
||||||
|
# Validate the token
|
||||||
|
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||||
|
if not is_valid:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||||
|
|
||||||
|
return await CapeService().remove_cape(username)
|
63
app/api/marketplace.py
Normal file
63
app/api/marketplace.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from fastapi import APIRouter, Query, Body
|
||||||
|
from typing import Optional
|
||||||
|
from app.models.marketplace import BuyItemRequest
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/api/marketplace",
|
||||||
|
tags=["Marketplace"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/items")
|
||||||
|
async def get_marketplace_items(
|
||||||
|
server_ip: Optional[str] = None,
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
limit: int = Query(20, ge=1, le=100)
|
||||||
|
):
|
||||||
|
"""Получить список предметов на торговой площадке"""
|
||||||
|
from app.services.marketplace import MarketplaceService
|
||||||
|
return await MarketplaceService().list_items(server_ip, page, limit)
|
||||||
|
|
||||||
|
@router.get("/items/{item_id}")
|
||||||
|
async def get_marketplace_item(item_id: str):
|
||||||
|
"""Получить информацию о конкретном предмете"""
|
||||||
|
from app.services.marketplace import MarketplaceService
|
||||||
|
return await MarketplaceService().get_item(item_id)
|
||||||
|
|
||||||
|
@router.post("/items/sell")
|
||||||
|
async def sell_item(
|
||||||
|
username: str = Body(...),
|
||||||
|
slot_index: int = Body(...),
|
||||||
|
amount: int = Body(...),
|
||||||
|
price: int = Body(...),
|
||||||
|
server_ip: str = Body(...)
|
||||||
|
):
|
||||||
|
"""Выставить предмет на продажу"""
|
||||||
|
from app.services.marketplace import MarketplaceService
|
||||||
|
return await MarketplaceService().add_item(username, slot_index, amount, price, server_ip)
|
||||||
|
|
||||||
|
@router.post("/items/buy/{item_id}")
|
||||||
|
async def buy_item(
|
||||||
|
item_id: str,
|
||||||
|
request: BuyItemRequest
|
||||||
|
):
|
||||||
|
"""Купить предмет"""
|
||||||
|
from app.services.marketplace import MarketplaceService
|
||||||
|
return await MarketplaceService().buy_item(request.username, item_id)
|
||||||
|
|
||||||
|
@router.get("/operations")
|
||||||
|
async def get_marketplace_operations(server_ip: str):
|
||||||
|
"""Получить список операций для выполнения на сервере"""
|
||||||
|
from app.services.marketplace import MarketplaceService
|
||||||
|
return await MarketplaceService().get_pending_operations(server_ip)
|
||||||
|
|
||||||
|
@router.post("/operations/confirm")
|
||||||
|
async def confirm_marketplace_operation(data: dict):
|
||||||
|
"""Подтвердить выполнение операции"""
|
||||||
|
from app.services.marketplace import MarketplaceService
|
||||||
|
return await MarketplaceService().confirm_operation(data["operation_id"], data.get("status"), data.get("error"))
|
||||||
|
|
||||||
|
@router.post("/items/details")
|
||||||
|
async def submit_item_details(data: dict):
|
||||||
|
"""Получить подробные данные о предмете"""
|
||||||
|
from app.services.marketplace import MarketplaceService
|
||||||
|
return await MarketplaceService().update_item_details(data["operation_id"], data["item_data"])
|
32
app/api/meta.py
Normal file
32
app/api/meta.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter(tags=["Meta"])
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
def api_root():
|
||||||
|
# Читаем публичный ключ из файла
|
||||||
|
public_key_path = "app/keys/public_key.pem"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(public_key_path, "r") as f:
|
||||||
|
public_key = f.read().strip()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"meta": {
|
||||||
|
"serverName": "Popa Auth Server",
|
||||||
|
"implementationName": "FastAPI",
|
||||||
|
"implementationVersion": "1.0.0",
|
||||||
|
"links": {
|
||||||
|
"homepage": "https://popa-popa.ru"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skinDomains": ["147.78.65.214"],
|
||||||
|
"capeDomains": ["147.78.65.214"],
|
||||||
|
# Важно - возвращаем ключ как есть, без дополнительной обработки
|
||||||
|
"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
|
||||||
|
)
|
33
app/api/skins.py
Normal file
33
app/api/skins.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
|
||||||
|
from app.services.skin import SkinService
|
||||||
|
from app.services.auth import AuthService
|
||||||
|
|
||||||
|
router = APIRouter(tags=["Skins"])
|
||||||
|
|
||||||
|
@router.post("/user/{username}/skin")
|
||||||
|
async def set_skin(
|
||||||
|
username: str,
|
||||||
|
skin_file: UploadFile = File(...),
|
||||||
|
skin_model: str = Form("classic"),
|
||||||
|
accessToken: str = Form(...),
|
||||||
|
clientToken: str = Form(...)
|
||||||
|
):
|
||||||
|
# Validate the token
|
||||||
|
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||||
|
if not is_valid:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||||
|
|
||||||
|
return await SkinService().set_skin(username, skin_file, skin_model)
|
||||||
|
|
||||||
|
@router.delete("/user/{username}/skin")
|
||||||
|
async def remove_skin(
|
||||||
|
username: str,
|
||||||
|
accessToken: str,
|
||||||
|
clientToken: str
|
||||||
|
):
|
||||||
|
# Validate the token
|
||||||
|
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||||
|
if not is_valid:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||||
|
|
||||||
|
return await SkinService().remove_skin(username)
|
61
app/api/store.py
Normal file
61
app/api/store.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException
|
||||||
|
from app.services.store_cape import StoreCapeService
|
||||||
|
from app.models.cape import CapeStoreUpdate, CapePurchase
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/store",
|
||||||
|
tags=["Store"]
|
||||||
|
)
|
||||||
|
|
||||||
|
store_cape_service = StoreCapeService()
|
||||||
|
|
||||||
|
@router.get("/capes")
|
||||||
|
async def get_all_capes():
|
||||||
|
"""Получение списка всех плащей в магазине"""
|
||||||
|
return await store_cape_service.get_all_capes()
|
||||||
|
|
||||||
|
@router.get("/capes/{cape_id}")
|
||||||
|
async def get_cape_by_id(cape_id: str):
|
||||||
|
"""Получение плаща по ID"""
|
||||||
|
return await store_cape_service.get_cape_by_id(cape_id)
|
||||||
|
|
||||||
|
@router.post("/capes")
|
||||||
|
async def add_cape(
|
||||||
|
name: str = Form(...),
|
||||||
|
description: str = Form(...),
|
||||||
|
price: int = Form(...),
|
||||||
|
cape_file: UploadFile = File(...)
|
||||||
|
):
|
||||||
|
"""Добавление нового плаща в магазин"""
|
||||||
|
return await store_cape_service.add_cape(name, description, price, cape_file)
|
||||||
|
|
||||||
|
@router.put("/capes/{cape_id}")
|
||||||
|
async def update_cape(cape_id: str, update_data: CapeStoreUpdate):
|
||||||
|
"""Обновление информации о плаще"""
|
||||||
|
return await store_cape_service.update_cape(cape_id, update_data)
|
||||||
|
|
||||||
|
@router.delete("/capes/{cape_id}")
|
||||||
|
async def delete_cape(cape_id: str):
|
||||||
|
"""Удаление плаща из магазина"""
|
||||||
|
return await store_cape_service.delete_cape(cape_id)
|
||||||
|
|
||||||
|
@router.post("/purchase/cape")
|
||||||
|
async def purchase_cape(username: str, cape_id: str):
|
||||||
|
"""Покупка плаща пользователем"""
|
||||||
|
return await store_cape_service.purchase_cape(username, cape_id)
|
||||||
|
|
||||||
|
@router.get("/user/{username}/capes")
|
||||||
|
async def get_user_purchased_capes(username: str):
|
||||||
|
"""Получение всех приобретенных плащей пользователя"""
|
||||||
|
return await store_cape_service.get_user_purchased_capes(username)
|
||||||
|
|
||||||
|
@router.post("/user/{username}/capes/activate/{cape_id}")
|
||||||
|
async def activate_purchased_cape(username: str, cape_id: str):
|
||||||
|
"""Активация приобретенного плаща"""
|
||||||
|
return await store_cape_service.activate_purchased_cape(username, cape_id)
|
||||||
|
|
||||||
|
@router.post("/user/{username}/capes/deactivate/{cape_id}")
|
||||||
|
async def deactivate_purchased_cape(username: str, cape_id: str):
|
||||||
|
"""Деактивация приобретенного плаща"""
|
||||||
|
return await store_cape_service.deactivate_purchased_cape(username, cape_id)
|
119
app/api/users.py
Normal file
119
app/api/users.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, Body, Response
|
||||||
|
from app.models.user import UserCreate, UserLogin
|
||||||
|
from app.models.request import ValidateRequest
|
||||||
|
from app.services.auth import AuthService
|
||||||
|
from app.db.database import users_collection, sessions_collection
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from app.models.server.event import PlayerEvent, OnlinePlayersUpdate
|
||||||
|
from app.models.server.playtime import PlayerSession, PlayerPlaytime
|
||||||
|
from app.services.coins import CoinsService
|
||||||
|
|
||||||
|
coins_service = CoinsService()
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
tags=["Users"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/auth/register")
|
||||||
|
async def register(user: UserCreate):
|
||||||
|
"""Регистрация нового пользователя"""
|
||||||
|
return await AuthService().register(user)
|
||||||
|
|
||||||
|
@router.post("/auth/authenticate")
|
||||||
|
async def authenticate(credentials: UserLogin):
|
||||||
|
"""Аутентификация пользователя"""
|
||||||
|
return await AuthService().login(credentials)
|
||||||
|
|
||||||
|
@router.post("/auth/validate")
|
||||||
|
async def validate_token(request: ValidateRequest):
|
||||||
|
is_valid = await AuthService().validate(request.accessToken, request.clientToken)
|
||||||
|
return {"valid": is_valid}
|
||||||
|
|
||||||
|
@router.post("/auth/refresh")
|
||||||
|
async def refresh_token(access_token: str, client_token: str):
|
||||||
|
result = await AuthService().refresh(access_token, client_token)
|
||||||
|
if not result:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid tokens")
|
||||||
|
return result
|
||||||
|
|
||||||
|
@router.get("/sessionserver/session/minecraft/profile/{uuid}")
|
||||||
|
async def get_minecraft_profile(uuid: str, unsigned: bool = False):
|
||||||
|
return await AuthService().get_minecraft_profile(uuid)
|
||||||
|
|
||||||
|
@router.post("/sessionserver/session/minecraft/join")
|
||||||
|
async def join_server(request_data: dict = Body(...)):
|
||||||
|
try:
|
||||||
|
await AuthService().join_server(request_data)
|
||||||
|
return Response(status_code=204)
|
||||||
|
except Exception as e:
|
||||||
|
print("Error in join_server:", str(e))
|
||||||
|
raise
|
||||||
|
|
||||||
|
@router.get("/sessionserver/session/minecraft/hasJoined")
|
||||||
|
async def has_joined(username: str, serverId: str):
|
||||||
|
return await AuthService().has_joined(username, serverId)
|
||||||
|
|
||||||
|
@router.get("/users/{username}/coins")
|
||||||
|
async def get_user_coins(username: str):
|
||||||
|
coins_data = await coins_service.get_player_coins(username)
|
||||||
|
if not coins_data:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
return coins_data
|
||||||
|
|
||||||
|
@router.get("/users")
|
||||||
|
async def get_users():
|
||||||
|
"""Получение списка всех пользователей"""
|
||||||
|
users = await users_collection.find().to_list(1000)
|
||||||
|
|
||||||
|
# Исключаем чувствительные данные перед отправкой
|
||||||
|
safe_users = []
|
||||||
|
for user in users:
|
||||||
|
safe_users.append({
|
||||||
|
"username": user["username"],
|
||||||
|
"uuid": user["uuid"],
|
||||||
|
"skin_url": user.get("skin_url"),
|
||||||
|
"cloak_url": user.get("cloak_url"),
|
||||||
|
"coins": user.get("coins", 0),
|
||||||
|
"total_time_played": user.get("total_time_played", 0),
|
||||||
|
"is_active": user.get("is_active", True)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"users": safe_users, "count": len(safe_users)}
|
||||||
|
|
||||||
|
@router.get("/users/{uuid}")
|
||||||
|
async def get_user_by_uuid(uuid: str):
|
||||||
|
"""Получение пользователя по UUID"""
|
||||||
|
user = await users_collection.find_one({"uuid": uuid})
|
||||||
|
if not user:
|
||||||
|
# Пробуем разные форматы UUID
|
||||||
|
if '-' in uuid:
|
||||||
|
user = await users_collection.find_one({"uuid": uuid.replace('-', '')})
|
||||||
|
else:
|
||||||
|
formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
|
||||||
|
user = await users_collection.find_one({"uuid": formatted_uuid})
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
# Исключаем чувствительные данные
|
||||||
|
safe_user = {
|
||||||
|
"username": user["username"],
|
||||||
|
"uuid": user["uuid"],
|
||||||
|
"skin_url": user.get("skin_url"),
|
||||||
|
"cloak_url": user.get("cloak_url"),
|
||||||
|
"coins": user.get("coins", 0),
|
||||||
|
"total_time_played": user.get("total_time_played", 0),
|
||||||
|
"is_active": user.get("is_active", True),
|
||||||
|
"created_at": user.get("created_at")
|
||||||
|
}
|
||||||
|
|
||||||
|
if "total_time_played" in safe_user:
|
||||||
|
total_time = safe_user["total_time_played"]
|
||||||
|
hours, remainder = divmod(total_time, 3600)
|
||||||
|
minutes, seconds = divmod(remainder, 60)
|
||||||
|
safe_user["total_time_formatted"] = f"{hours}ч {minutes}м {seconds}с"
|
||||||
|
|
||||||
|
return safe_user
|
0
app/core/__init__.py
Normal file
0
app/core/__init__.py
Normal file
11
app/core/config.py
Normal file
11
app/core/config.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
load_dotenv(dotenv_path=Path(__file__).parent.parent.parent / ".env")
|
||||||
|
|
||||||
|
FILES_URL = os.getenv("FILES_URL")
|
||||||
|
MONGO_URI = os.getenv("MONGO_URI")
|
||||||
|
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 часа
|
0
app/db/__init__.py
Normal file
0
app/db/__init__.py
Normal file
8
app/db/database.py
Normal file
8
app/db/database.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
from app.core.config import MONGO_URI
|
||||||
|
|
||||||
|
client = AsyncIOMotorClient(MONGO_URI)
|
||||||
|
db = client["minecraft-api"]
|
||||||
|
|
||||||
|
users_collection = db["users"]
|
||||||
|
sessions_collection = db["sessions"]
|
0
app/models/__init__.py
Normal file
0
app/models/__init__.py
Normal file
25
app/models/cape.py
Normal file
25
app/models/cape.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class CapeUpdate(BaseModel):
|
||||||
|
cape_url: str
|
||||||
|
|
||||||
|
class CapeStore(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
price: int
|
||||||
|
file_name: str
|
||||||
|
|
||||||
|
class CapeStoreCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
price: int
|
||||||
|
|
||||||
|
class CapeStoreUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
price: Optional[int] = None
|
||||||
|
|
||||||
|
class CapePurchase(BaseModel):
|
||||||
|
cape_id: str
|
20
app/models/marketplace.py
Normal file
20
app/models/marketplace.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
class MarketplaceItemBase(BaseModel):
|
||||||
|
material: str
|
||||||
|
amount: int
|
||||||
|
price: int
|
||||||
|
seller_name: str
|
||||||
|
server_ip: str
|
||||||
|
display_name: Optional[str] = None
|
||||||
|
lore: Optional[List[str]] = None
|
||||||
|
enchants: Optional[Dict[str, int]] = None
|
||||||
|
item_data: Optional[Dict[str, Any]] = None # Дополнительные данные предмета
|
||||||
|
|
||||||
|
class MarketplaceItem(MarketplaceItemBase):
|
||||||
|
id: str
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
class BuyItemRequest(BaseModel):
|
||||||
|
username: str
|
5
app/models/request.py
Normal file
5
app/models/request.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class ValidateRequest(BaseModel):
|
||||||
|
accessToken: str # camelCase
|
||||||
|
clientToken: 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
|
5
app/models/skin.py
Normal file
5
app/models/skin.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class SkinUpdate(BaseModel):
|
||||||
|
skin_model: Optional[str] = "classic"
|
@ -1,8 +1,7 @@
|
|||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel, EmailStr
|
||||||
from typing import Optional
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
# Для запросов
|
|
||||||
class UserCreate(BaseModel):
|
class UserCreate(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
@ -12,31 +11,21 @@ class UserLogin(BaseModel):
|
|||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
# Для MongoDB
|
|
||||||
class UserInDB(BaseModel):
|
class UserInDB(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
hashed_password: str
|
hashed_password: str
|
||||||
uuid: str
|
uuid: str
|
||||||
skin_url: Optional[str] = None
|
skin_url: Optional[str] = None
|
||||||
skin_model: Optional[str] = "classic" # "classic" или "slim"
|
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()
|
||||||
|
|
||||||
class Session(BaseModel):
|
class Session(BaseModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
client_token: str
|
client_token: str
|
||||||
user_uuid: str
|
user_uuid: str
|
||||||
expires_at: datetime
|
expires_at: datetime
|
||||||
|
|
||||||
class ValidateRequest(BaseModel):
|
|
||||||
accessToken: str # camelCase
|
|
||||||
clientToken: str
|
|
||||||
|
|
||||||
class SkinUpdate(BaseModel):
|
|
||||||
skin_model: Optional[str] = "classic" # "classic" или "slim"
|
|
||||||
# Удаляем skin_url и skin_file, так как будем принимать файл напрямую
|
|
||||||
|
|
||||||
class CapeUpdate(BaseModel):
|
|
||||||
cape_url: str
|
|
0
app/services/__init__.py
Normal file
0
app/services/__init__.py
Normal file
282
app/services/auth.py
Normal file
282
app/services/auth.py
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from fastapi import HTTPException, UploadFile
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from app.models.user import UserLogin, UserInDB, UserCreate, Session
|
||||||
|
from app.utils.misc import (
|
||||||
|
verify_password,
|
||||||
|
get_password_hash,
|
||||||
|
create_access_token,
|
||||||
|
decode_token,
|
||||||
|
)
|
||||||
|
from ..db.database import users_collection, sessions_collection
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from cryptography.hazmat.primitives import serialization, hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
env_path = Path(__file__).parent.parent / ".env"
|
||||||
|
load_dotenv(dotenv_path=env_path)
|
||||||
|
FILES_URL = os.getenv("FILES_URL")
|
||||||
|
|
||||||
|
class AuthService:
|
||||||
|
async def register(self, user: UserCreate):
|
||||||
|
# Проверяем, существует ли пользователь
|
||||||
|
if await users_collection.find_one({"username": user.username}):
|
||||||
|
raise HTTPException(status_code=400, detail="Username already taken")
|
||||||
|
|
||||||
|
# Хешируем пароль
|
||||||
|
hashed_password = get_password_hash(user.password)
|
||||||
|
|
||||||
|
# Создаём UUID для Minecraft
|
||||||
|
user_uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Сохраняем в MongoDB
|
||||||
|
new_user = UserInDB(
|
||||||
|
username=user.username,
|
||||||
|
email=user.email,
|
||||||
|
hashed_password=hashed_password,
|
||||||
|
uuid=user_uuid,
|
||||||
|
)
|
||||||
|
await users_collection.insert_one(new_user.dict())
|
||||||
|
return {"status": "success", "uuid": user_uuid}
|
||||||
|
|
||||||
|
async def login(self, credentials: UserLogin):
|
||||||
|
# Ищем пользователя
|
||||||
|
user = await users_collection.find_one({"username": credentials.username})
|
||||||
|
if not user or not verify_password(credentials.password, user["hashed_password"]):
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||||
|
|
||||||
|
# Генерируем токены
|
||||||
|
access_token = create_access_token({"sub": user["username"], "uuid": user["uuid"]})
|
||||||
|
client_token = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Сохраняем сессию
|
||||||
|
session = Session(
|
||||||
|
access_token=access_token,
|
||||||
|
client_token=client_token,
|
||||||
|
user_uuid=user["uuid"],
|
||||||
|
expires_at=datetime.utcnow() + timedelta(minutes=1440),
|
||||||
|
)
|
||||||
|
await sessions_collection.insert_one(session.dict())
|
||||||
|
|
||||||
|
return {
|
||||||
|
"accessToken": access_token,
|
||||||
|
"clientToken": client_token,
|
||||||
|
"selectedProfile": {
|
||||||
|
"id": user["uuid"],
|
||||||
|
"name": user["username"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async def validate(self, access_token: str, client_token: str):
|
||||||
|
print(f"Searching for access_toke and client_token: '{access_token}', '{client_token}")
|
||||||
|
session = await sessions_collection.find_one({
|
||||||
|
"access_token": access_token,
|
||||||
|
"client_token": client_token,
|
||||||
|
})
|
||||||
|
print("Session from DB:", session)
|
||||||
|
if not session or datetime.utcnow() > session["expires_at"]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def refresh(self, access_token: str, client_token: str):
|
||||||
|
if not await self.validate(access_token, client_token):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Обновляем токен
|
||||||
|
new_access_token = create_access_token({"sub": "user", "uuid": "user_uuid"})
|
||||||
|
await sessions_collection.update_one(
|
||||||
|
{"access_token": access_token},
|
||||||
|
{"$set": {"access_token": new_access_token}},
|
||||||
|
)
|
||||||
|
return {"accessToken": new_access_token, "clientToken": client_token}
|
||||||
|
|
||||||
|
async def get_minecraft_profile(self, uuid: str):
|
||||||
|
# Преобразуем UUID без дефисов в формат с дефисами (если нужно)
|
||||||
|
if '-' not in uuid:
|
||||||
|
formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
|
||||||
|
else:
|
||||||
|
formatted_uuid = uuid
|
||||||
|
|
||||||
|
user = await users_collection.find_one({"uuid": formatted_uuid}) # Ищем по UUID с дефисами
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
textures = {
|
||||||
|
"timestamp": int(datetime.now().timestamp() * 1000),
|
||||||
|
"profileId": user["uuid"].replace("-", ""),
|
||||||
|
"profileName": user["username"],
|
||||||
|
"textures": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.get("skin_url"):
|
||||||
|
textures["textures"]["SKIN"] = {
|
||||||
|
"url": user["skin_url"],
|
||||||
|
"metadata": {"model": user.get("skin_model", "classic")}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.get("cloak_url"):
|
||||||
|
textures["textures"]["CAPE"] = {"url": user["cloak_url"]}
|
||||||
|
|
||||||
|
textures_json = json.dumps(textures).encode()
|
||||||
|
base64_textures = base64.b64encode(textures_json).decode()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Подписываем текстуры
|
||||||
|
private_key_path = "app/keys/private_key.pem"
|
||||||
|
with open(private_key_path, "rb") as key_file:
|
||||||
|
private_key = serialization.load_pem_private_key(
|
||||||
|
key_file.read(),
|
||||||
|
password=None
|
||||||
|
)
|
||||||
|
|
||||||
|
signature = private_key.sign(
|
||||||
|
base64.b64encode(textures_json),
|
||||||
|
padding.PKCS1v15(),
|
||||||
|
hashes.SHA1()
|
||||||
|
)
|
||||||
|
|
||||||
|
signature_base64 = base64.b64encode(signature).decode()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": user["uuid"].replace("-", ""),
|
||||||
|
"name": user["username"],
|
||||||
|
"properties": [{
|
||||||
|
"name": "textures",
|
||||||
|
"value": base64_textures,
|
||||||
|
"signature": signature_base64
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error signing textures: {e}")
|
||||||
|
# В случае ошибки возвращаем текстуры без подписи
|
||||||
|
return {
|
||||||
|
"id": user["uuid"].replace("-", ""),
|
||||||
|
"name": user["username"],
|
||||||
|
"properties": [{
|
||||||
|
"name": "textures",
|
||||||
|
"value": base64_textures
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
# # Подписываем текстуры
|
||||||
|
# with open("private_key.pem", "rb") as key_file:
|
||||||
|
# private_key = serialization.load_pem_private_key(
|
||||||
|
# key_file.read(),
|
||||||
|
# password=None
|
||||||
|
# )
|
||||||
|
|
||||||
|
# signature = private_key.sign(
|
||||||
|
# textures_json,
|
||||||
|
# padding.PKCS1v15(),
|
||||||
|
# hashes.SHA1()
|
||||||
|
# )
|
||||||
|
|
||||||
|
# return JSONResponse({
|
||||||
|
# "id": user["uuid"].replace("-", ""), # Уберите дефисы
|
||||||
|
# "name": user["username"],
|
||||||
|
# "properties": [{
|
||||||
|
# "name": "textures",
|
||||||
|
# "value": base64_textures,
|
||||||
|
# # "signature": base64.b64encode(signature).decode()
|
||||||
|
# }]
|
||||||
|
# })
|
||||||
|
|
||||||
|
async def join_server(self, request_data: dict):
|
||||||
|
access_token = request_data.get("accessToken")
|
||||||
|
selected_profile = request_data.get("selectedProfile") # UUID без дефисов
|
||||||
|
server_id = request_data.get("serverId")
|
||||||
|
|
||||||
|
if not all([access_token, selected_profile, server_id]):
|
||||||
|
raise HTTPException(status_code=400, detail="Missing required parameters")
|
||||||
|
|
||||||
|
decoded_token = decode_token(access_token)
|
||||||
|
if not decoded_token:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid access token")
|
||||||
|
|
||||||
|
token_uuid = decoded_token.get("uuid", "").replace("-", "")
|
||||||
|
if token_uuid != selected_profile:
|
||||||
|
raise HTTPException(status_code=403, detail="Token doesn't match selected profile")
|
||||||
|
|
||||||
|
# Сохраняем server_id в сессию
|
||||||
|
await sessions_collection.update_one(
|
||||||
|
{"user_uuid": decoded_token["uuid"]}, # UUID с дефисами
|
||||||
|
{"$set": {"server_id": server_id}},
|
||||||
|
upsert=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def has_joined(self, username: str, server_id: str):
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
# Ищем сессию с этим server_id
|
||||||
|
session = await sessions_collection.find_one({
|
||||||
|
"user_uuid": user["uuid"], # UUID с дефисами
|
||||||
|
"server_id": server_id
|
||||||
|
})
|
||||||
|
if not session:
|
||||||
|
raise HTTPException(status_code=403, detail="Not joined this server")
|
||||||
|
|
||||||
|
textures = {
|
||||||
|
"timestamp": int(datetime.now().timestamp() * 1000),
|
||||||
|
"profileId": user["uuid"].replace("-", ""),
|
||||||
|
"profileName": user["username"],
|
||||||
|
"textures": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.get("skin_url"):
|
||||||
|
textures["textures"]["SKIN"] = {
|
||||||
|
"url": user["skin_url"],
|
||||||
|
"metadata": {"model": user.get("skin_model", "classic")}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.get("cloak_url"):
|
||||||
|
textures["textures"]["CAPE"] = {"url": user["cloak_url"]}
|
||||||
|
|
||||||
|
textures_json = json.dumps(textures).encode()
|
||||||
|
base64_textures = base64.b64encode(textures_json).decode()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Подписываем текстуры
|
||||||
|
private_key_path = "app/keys/private_key.pem"
|
||||||
|
with open(private_key_path, "rb") as key_file:
|
||||||
|
private_key = serialization.load_pem_private_key(
|
||||||
|
key_file.read(),
|
||||||
|
password=None
|
||||||
|
)
|
||||||
|
|
||||||
|
signature = private_key.sign(
|
||||||
|
base64.b64encode(textures_json),
|
||||||
|
padding.PKCS1v15(),
|
||||||
|
hashes.SHA1()
|
||||||
|
)
|
||||||
|
|
||||||
|
signature_base64 = base64.b64encode(signature).decode()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": user["uuid"].replace("-", ""),
|
||||||
|
"name": user["username"],
|
||||||
|
"properties": [{
|
||||||
|
"name": "textures",
|
||||||
|
"value": base64_textures,
|
||||||
|
"signature": signature_base64
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error signing textures: {e}")
|
||||||
|
# В случае ошибки возвращаем текстуры без подписи
|
||||||
|
return {
|
||||||
|
"id": user["uuid"].replace("-", ""),
|
||||||
|
"name": user["username"],
|
||||||
|
"properties": [{
|
||||||
|
"name": "textures",
|
||||||
|
"value": base64_textures
|
||||||
|
}]
|
||||||
|
}
|
70
app/services/cape.py
Normal file
70
app/services/cape.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from app.db.database import users_collection
|
||||||
|
from app.core.config import FILES_URL
|
||||||
|
from fastapi import HTTPException, UploadFile
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class CapeService:
|
||||||
|
async def set_cape(self, username: str, cape_file: UploadFile):
|
||||||
|
"""Установка или замена плаща через загрузку файла (PNG или GIF)"""
|
||||||
|
if not cape_file.content_type.startswith('image/'):
|
||||||
|
raise HTTPException(status_code=400, detail="File must be an image")
|
||||||
|
|
||||||
|
# Определяем расширение
|
||||||
|
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="Only PNG and GIF capes are supported")
|
||||||
|
|
||||||
|
max_size = 2 * 1024 * 1024 # 2MB
|
||||||
|
contents = await cape_file.read()
|
||||||
|
if len(contents) > max_size:
|
||||||
|
raise HTTPException(status_code=400, detail="File too large (max 2MB)")
|
||||||
|
|
||||||
|
# Удаляем старый плащ, если есть
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if user and user.get("cloak_url"):
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
import os
|
||||||
|
old_url = user["cloak_url"]
|
||||||
|
old_filename = os.path.basename(urlparse(old_url).path)
|
||||||
|
old_path = os.path.join("app/static/capes", old_filename)
|
||||||
|
if os.path.exists(old_path):
|
||||||
|
try:
|
||||||
|
os.remove(old_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Создаем папку для плащей, если ее нет
|
||||||
|
from pathlib import Path
|
||||||
|
cape_dir = Path("app/static/capes")
|
||||||
|
cape_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"
|
||||||
|
cape_path = cape_dir / cape_filename
|
||||||
|
|
||||||
|
with open(cape_path, "wb") as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
result = await users_collection.update_one(
|
||||||
|
{"username": username},
|
||||||
|
{"$set": {
|
||||||
|
"cloak_url": f"{FILES_URL}/capes/{cape_filename}"
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.modified_count == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
async def remove_cape(self, username: str):
|
||||||
|
"""Удаление плаща"""
|
||||||
|
result = await users_collection.update_one(
|
||||||
|
{"username": username},
|
||||||
|
{"$unset": {"cloak_url": ""}}
|
||||||
|
)
|
||||||
|
if result.modified_count == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
return {"status": "success"}
|
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"]
|
||||||
|
}
|
82
app/services/skin.py
Normal file
82
app/services/skin.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from fastapi import HTTPException, UploadFile
|
||||||
|
from datetime import datetime
|
||||||
|
from app.db.database import users_collection
|
||||||
|
from app.core.config import FILES_URL
|
||||||
|
|
||||||
|
class SkinService:
|
||||||
|
async def set_skin(self, username: str, skin_file: UploadFile, skin_model: str = "classic"):
|
||||||
|
"""Установка или замена скина через загрузку файла"""
|
||||||
|
# Проверяем тип файла
|
||||||
|
if not skin_file.content_type.startswith('image/'):
|
||||||
|
raise HTTPException(status_code=400, detail="File must be an image")
|
||||||
|
|
||||||
|
# Проверяем размер файла (максимум 2MB)
|
||||||
|
max_size = 2 * 1024 * 1024 # 2MB
|
||||||
|
contents = await skin_file.read()
|
||||||
|
if len(contents) > max_size:
|
||||||
|
raise HTTPException(status_code=400, detail="File too large (max 2MB)")
|
||||||
|
|
||||||
|
# Удаляем старый скин, если есть
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if user and user.get("skin_url"):
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
import os
|
||||||
|
old_url = user["skin_url"]
|
||||||
|
# Получаем имя файла из url
|
||||||
|
old_filename = os.path.basename(urlparse(old_url).path)
|
||||||
|
old_path = os.path.join("app/static/skins", old_filename)
|
||||||
|
print(f"Trying to delete old skin at: {old_path}")
|
||||||
|
if os.path.exists(old_path):
|
||||||
|
try:
|
||||||
|
os.remove(old_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Создаем папку для скинов, если ее нет
|
||||||
|
from pathlib import Path
|
||||||
|
skin_dir = Path("app/static/skins")
|
||||||
|
skin_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Генерируем имя файла
|
||||||
|
skin_filename = f"{username}_{int(datetime.now().timestamp())}.png"
|
||||||
|
skin_path = skin_dir / skin_filename
|
||||||
|
|
||||||
|
# Сохраняем файл
|
||||||
|
with open(skin_path, "wb") as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
# Обновляем запись пользователя
|
||||||
|
result = await users_collection.update_one(
|
||||||
|
{"username": username},
|
||||||
|
{"$set": {
|
||||||
|
"skin_url": f"{FILES_URL}/skins/{skin_filename}",
|
||||||
|
"skin_model": skin_model
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.modified_count == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
async def remove_skin(self, username: str):
|
||||||
|
"""Удаление скина"""
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
# Удаляем файл скина, если он существует
|
||||||
|
if user.get("skin_url") and user["skin_url"].startswith("/skins/"):
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
os.remove(f"skins/{user['skin_url'].split('/')[-1]}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
result = await users_collection.update_one(
|
||||||
|
{"username": username},
|
||||||
|
{"$unset": {
|
||||||
|
"skin_url": "",
|
||||||
|
"skin_model": ""
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
return {"status": "success"}
|
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 |
0
app/utils/__init__.py
Normal file
0
app/utils/__init__.py
Normal file
@ -1,16 +1,7 @@
|
|||||||
from jose import jwt, JWTError
|
from jose import jwt, JWTError
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import os
|
from app.core.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
from pathlib import Path
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
env_path = Path(__file__).parent.parent / ".env"
|
|
||||||
load_dotenv(dotenv_path=env_path)
|
|
||||||
|
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
|
||||||
ALGORITHM = "HS256"
|
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 часа
|
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
349
auth/app/auth.py
349
auth/app/auth.py
@ -1,349 +0,0 @@
|
|||||||
import base64
|
|
||||||
import json
|
|
||||||
from fastapi import HTTPException, UploadFile
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from .models import UserLogin, UserInDB, Session, UserCreate, SkinUpdate, CapeUpdate
|
|
||||||
from .utils import (
|
|
||||||
verify_password,
|
|
||||||
get_password_hash,
|
|
||||||
create_access_token,
|
|
||||||
decode_token,
|
|
||||||
)
|
|
||||||
from .database import users_collection, sessions_collection
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from cryptography.hazmat.primitives import serialization, hashes
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import padding
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
env_path = Path(__file__).parent.parent / ".env"
|
|
||||||
load_dotenv(dotenv_path=env_path)
|
|
||||||
FILES_URL = os.getenv("FILES_URL")
|
|
||||||
|
|
||||||
class AuthService:
|
|
||||||
async def register(self, user: UserCreate):
|
|
||||||
# Проверяем, существует ли пользователь
|
|
||||||
if await users_collection.find_one({"username": user.username}):
|
|
||||||
raise HTTPException(status_code=400, detail="Username already taken")
|
|
||||||
|
|
||||||
# Хешируем пароль
|
|
||||||
hashed_password = get_password_hash(user.password)
|
|
||||||
|
|
||||||
# Создаём UUID для Minecraft
|
|
||||||
user_uuid = str(uuid.uuid4())
|
|
||||||
|
|
||||||
# Сохраняем в MongoDB
|
|
||||||
new_user = UserInDB(
|
|
||||||
username=user.username,
|
|
||||||
email=user.email,
|
|
||||||
hashed_password=hashed_password,
|
|
||||||
uuid=user_uuid,
|
|
||||||
)
|
|
||||||
await users_collection.insert_one(new_user.dict())
|
|
||||||
return {"status": "success", "uuid": user_uuid}
|
|
||||||
|
|
||||||
async def login(self, credentials: UserLogin):
|
|
||||||
# Ищем пользователя
|
|
||||||
user = await users_collection.find_one({"username": credentials.username})
|
|
||||||
if not user or not verify_password(credentials.password, user["hashed_password"]):
|
|
||||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
||||||
|
|
||||||
# Генерируем токены
|
|
||||||
access_token = create_access_token({"sub": user["username"], "uuid": user["uuid"]})
|
|
||||||
client_token = str(uuid.uuid4())
|
|
||||||
|
|
||||||
# Сохраняем сессию
|
|
||||||
session = Session(
|
|
||||||
access_token=access_token,
|
|
||||||
client_token=client_token,
|
|
||||||
user_uuid=user["uuid"],
|
|
||||||
expires_at=datetime.utcnow() + timedelta(minutes=1440),
|
|
||||||
)
|
|
||||||
await sessions_collection.insert_one(session.dict())
|
|
||||||
|
|
||||||
return {
|
|
||||||
"accessToken": access_token,
|
|
||||||
"clientToken": client_token,
|
|
||||||
"selectedProfile": {
|
|
||||||
"id": user["uuid"],
|
|
||||||
"name": user["username"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
async def validate(self, access_token: str, client_token: str):
|
|
||||||
print(f"Searching for access_toke and client_token: '{access_token}', '{client_token}")
|
|
||||||
session = await sessions_collection.find_one({
|
|
||||||
"access_token": access_token,
|
|
||||||
"client_token": client_token,
|
|
||||||
})
|
|
||||||
print("Session from DB:", session)
|
|
||||||
if not session or datetime.utcnow() > session["expires_at"]:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def refresh(self, access_token: str, client_token: str):
|
|
||||||
if not await self.validate(access_token, client_token):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Обновляем токен
|
|
||||||
new_access_token = create_access_token({"sub": "user", "uuid": "user_uuid"})
|
|
||||||
await sessions_collection.update_one(
|
|
||||||
{"access_token": access_token},
|
|
||||||
{"$set": {"access_token": new_access_token}},
|
|
||||||
)
|
|
||||||
return {"accessToken": new_access_token, "clientToken": client_token}
|
|
||||||
|
|
||||||
async def get_minecraft_profile(self, uuid: str):
|
|
||||||
# Преобразуем UUID без дефисов в формат с дефисами (если нужно)
|
|
||||||
if '-' not in uuid:
|
|
||||||
formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
|
|
||||||
else:
|
|
||||||
formatted_uuid = uuid
|
|
||||||
|
|
||||||
user = await users_collection.find_one({"uuid": formatted_uuid}) # Ищем по UUID с дефисами
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
|
||||||
|
|
||||||
textures = {
|
|
||||||
"timestamp": int(datetime.now().timestamp() * 1000),
|
|
||||||
"profileId": user["uuid"], # UUID с дефисами
|
|
||||||
"profileName": user["username"],
|
|
||||||
"textures": {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.get("skin_url"):
|
|
||||||
textures["textures"]["SKIN"] = {
|
|
||||||
"url": user["skin_url"],
|
|
||||||
"metadata": {"model": user.get("skin_model", "classic")}
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.get("cloak_url"):
|
|
||||||
textures["textures"]["CAPE"] = {"url": user["cloak_url"]}
|
|
||||||
|
|
||||||
textures_json = json.dumps(textures).encode()
|
|
||||||
base64_textures = base64.b64encode(textures_json).decode()
|
|
||||||
|
|
||||||
# Подписываем текстуры
|
|
||||||
with open("private_key.pem", "rb") as key_file:
|
|
||||||
private_key = serialization.load_pem_private_key(
|
|
||||||
key_file.read(),
|
|
||||||
password=None
|
|
||||||
)
|
|
||||||
|
|
||||||
signature = private_key.sign(
|
|
||||||
textures_json,
|
|
||||||
padding.PKCS1v15(),
|
|
||||||
hashes.SHA1()
|
|
||||||
)
|
|
||||||
|
|
||||||
return JSONResponse({
|
|
||||||
"id": user["uuid"].replace("-", ""), # Уберите дефисы
|
|
||||||
"name": user["username"],
|
|
||||||
"properties": [{
|
|
||||||
"name": "textures",
|
|
||||||
"value": base64_textures,
|
|
||||||
"signature": base64.b64encode(signature).decode()
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
|
|
||||||
async def join_server(self, request_data: dict):
|
|
||||||
access_token = request_data.get("accessToken")
|
|
||||||
selected_profile = request_data.get("selectedProfile") # UUID без дефисов
|
|
||||||
server_id = request_data.get("serverId")
|
|
||||||
|
|
||||||
if not all([access_token, selected_profile, server_id]):
|
|
||||||
raise HTTPException(status_code=400, detail="Missing required parameters")
|
|
||||||
|
|
||||||
decoded_token = decode_token(access_token)
|
|
||||||
if not decoded_token:
|
|
||||||
raise HTTPException(status_code=401, detail="Invalid access token")
|
|
||||||
|
|
||||||
token_uuid = decoded_token.get("uuid", "").replace("-", "")
|
|
||||||
if token_uuid != selected_profile:
|
|
||||||
raise HTTPException(status_code=403, detail="Token doesn't match selected profile")
|
|
||||||
|
|
||||||
# Сохраняем server_id в сессию
|
|
||||||
await sessions_collection.update_one(
|
|
||||||
{"user_uuid": decoded_token["uuid"]}, # UUID с дефисами
|
|
||||||
{"$set": {"server_id": server_id}},
|
|
||||||
upsert=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def has_joined(self, username: str, server_id: str):
|
|
||||||
user = await users_collection.find_one({"username": username})
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
|
||||||
|
|
||||||
# Ищем сессию с этим server_id
|
|
||||||
session = await sessions_collection.find_one({
|
|
||||||
"user_uuid": user["uuid"], # UUID с дефисами
|
|
||||||
"server_id": server_id
|
|
||||||
})
|
|
||||||
if not session:
|
|
||||||
raise HTTPException(status_code=403, detail="Not joined this server")
|
|
||||||
|
|
||||||
textures = {}
|
|
||||||
if user.get("skin_url"):
|
|
||||||
textures["SKIN"] = {"url": user["skin_url"]}
|
|
||||||
if user.get("cloak_url"):
|
|
||||||
textures["CAPE"] = {"url": user["cloak_url"]}
|
|
||||||
|
|
||||||
textures_value = base64.b64encode(json.dumps({
|
|
||||||
"timestamp": int(datetime.now().timestamp()),
|
|
||||||
"profileId": user["uuid"].replace("-", ""), # UUID без дефисов
|
|
||||||
"profileName": username,
|
|
||||||
"textures": textures
|
|
||||||
}).encode()).decode()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": user["uuid"].replace("-", ""), # UUID без дефисов
|
|
||||||
"name": username,
|
|
||||||
"properties": [{
|
|
||||||
"name": "textures",
|
|
||||||
"value": textures_value
|
|
||||||
}] if textures else []
|
|
||||||
}
|
|
||||||
|
|
||||||
async def set_skin(self, username: str, skin_file: UploadFile, skin_model: str = "classic"):
|
|
||||||
"""Установка или замена скина через загрузку файла"""
|
|
||||||
# Проверяем тип файла
|
|
||||||
if not skin_file.content_type.startswith('image/'):
|
|
||||||
raise HTTPException(status_code=400, detail="File must be an image")
|
|
||||||
|
|
||||||
# Проверяем размер файла (максимум 2MB)
|
|
||||||
max_size = 2 * 1024 * 1024 # 2MB
|
|
||||||
contents = await skin_file.read()
|
|
||||||
if len(contents) > max_size:
|
|
||||||
raise HTTPException(status_code=400, detail="File too large (max 2MB)")
|
|
||||||
|
|
||||||
# Удаляем старый скин, если есть
|
|
||||||
user = await users_collection.find_one({"username": username})
|
|
||||||
if user and user.get("skin_url"):
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
import os
|
|
||||||
old_url = user["skin_url"]
|
|
||||||
# Получаем имя файла из url
|
|
||||||
old_filename = os.path.basename(urlparse(old_url).path)
|
|
||||||
old_path = os.path.join("skins", old_filename)
|
|
||||||
if os.path.exists(old_path):
|
|
||||||
try:
|
|
||||||
os.remove(old_path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Создаем папку для скинов, если ее нет
|
|
||||||
from pathlib import Path
|
|
||||||
skin_dir = Path("skins")
|
|
||||||
skin_dir.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
# Генерируем имя файла
|
|
||||||
skin_filename = f"{username}_{int(datetime.now().timestamp())}.png"
|
|
||||||
skin_path = skin_dir / skin_filename
|
|
||||||
|
|
||||||
# Сохраняем файл
|
|
||||||
with open(skin_path, "wb") as f:
|
|
||||||
f.write(contents)
|
|
||||||
|
|
||||||
# Обновляем запись пользователя
|
|
||||||
result = await users_collection.update_one(
|
|
||||||
{"username": username},
|
|
||||||
{"$set": {
|
|
||||||
"skin_url": f"{FILES_URL}/skins/{skin_filename}",
|
|
||||||
"skin_model": skin_model
|
|
||||||
}}
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.modified_count == 0:
|
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
|
||||||
return {"status": "success"}
|
|
||||||
|
|
||||||
async def remove_skin(self, username: str):
|
|
||||||
"""Удаление скина"""
|
|
||||||
user = await users_collection.find_one({"username": username})
|
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
|
||||||
|
|
||||||
# Удаляем файл скина, если он существует
|
|
||||||
if user.get("skin_url") and user["skin_url"].startswith("/skins/"):
|
|
||||||
import os
|
|
||||||
try:
|
|
||||||
os.remove(f"skins/{user['skin_url'].split('/')[-1]}")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
result = await users_collection.update_one(
|
|
||||||
{"username": username},
|
|
||||||
{"$unset": {
|
|
||||||
"skin_url": "",
|
|
||||||
"skin_model": ""
|
|
||||||
}}
|
|
||||||
)
|
|
||||||
return {"status": "success"}
|
|
||||||
|
|
||||||
async def set_cape(self, username: str, cape_file: UploadFile):
|
|
||||||
"""Установка или замена плаща через загрузку файла (PNG или GIF)"""
|
|
||||||
if not cape_file.content_type.startswith('image/'):
|
|
||||||
raise HTTPException(status_code=400, detail="File must be an image")
|
|
||||||
|
|
||||||
# Определяем расширение
|
|
||||||
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="Only PNG and GIF capes are supported")
|
|
||||||
|
|
||||||
max_size = 2 * 1024 * 1024 # 2MB
|
|
||||||
contents = await cape_file.read()
|
|
||||||
if len(contents) > max_size:
|
|
||||||
raise HTTPException(status_code=400, detail="File too large (max 2MB)")
|
|
||||||
|
|
||||||
# Удаляем старый плащ, если есть
|
|
||||||
user = await users_collection.find_one({"username": username})
|
|
||||||
if user and user.get("cloak_url"):
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
import os
|
|
||||||
old_url = user["cloak_url"]
|
|
||||||
old_filename = os.path.basename(urlparse(old_url).path)
|
|
||||||
old_path = os.path.join("capes", old_filename)
|
|
||||||
if os.path.exists(old_path):
|
|
||||||
try:
|
|
||||||
os.remove(old_path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
cape_dir = Path("capes")
|
|
||||||
cape_dir.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"
|
|
||||||
cape_path = cape_dir / cape_filename
|
|
||||||
|
|
||||||
with open(cape_path, "wb") as f:
|
|
||||||
f.write(contents)
|
|
||||||
|
|
||||||
result = await users_collection.update_one(
|
|
||||||
{"username": username},
|
|
||||||
{"$set": {
|
|
||||||
"cloak_url": f"{FILES_URL}/capes/{cape_filename}"
|
|
||||||
}}
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.modified_count == 0:
|
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
|
||||||
return {"status": "success"}
|
|
||||||
|
|
||||||
async def remove_cape(self, username: str):
|
|
||||||
"""Удаление плаща"""
|
|
||||||
result = await users_collection.update_one(
|
|
||||||
{"username": username},
|
|
||||||
{"$unset": {"cloak_url": ""}}
|
|
||||||
)
|
|
||||||
if result.modified_count == 0:
|
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
|
||||||
return {"status": "success"}
|
|
@ -1,17 +0,0 @@
|
|||||||
from motor.motor_asyncio import AsyncIOMotorClient
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
env_path = Path(__file__).parent.parent / ".env"
|
|
||||||
load_dotenv(dotenv_path=env_path)
|
|
||||||
|
|
||||||
MONGO_URI = os.getenv("MONGO_URI")
|
|
||||||
DB_NAME = "minecraft_auth"
|
|
||||||
|
|
||||||
client = AsyncIOMotorClient(MONGO_URI)
|
|
||||||
db = client[DB_NAME]
|
|
||||||
|
|
||||||
# Коллекции
|
|
||||||
users_collection = db["users"]
|
|
||||||
sessions_collection = db["sessions"]
|
|
@ -1,19 +0,0 @@
|
|||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
|
|
||||||
# Генерация ключа
|
|
||||||
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
||||||
|
|
||||||
# Сохранение в PEM-формат
|
|
||||||
with open("private_key.pem", "wb") as f:
|
|
||||||
f.write(private_key.private_bytes(
|
|
||||||
encoding=serialization.Encoding.PEM,
|
|
||||||
format=serialization.PrivateFormat.PKCS8,
|
|
||||||
encryption_algorithm=serialization.NoEncryption()
|
|
||||||
))
|
|
||||||
|
|
||||||
with open("public_key.pem", "wb") as f:
|
|
||||||
f.write(private_key.public_key().public_bytes(
|
|
||||||
encoding=serialization.Encoding.PEM,
|
|
||||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
))
|
|
124
auth/app/main.py
124
auth/app/main.py
@ -1,124 +0,0 @@
|
|||||||
import base64
|
|
||||||
from datetime import datetime
|
|
||||||
import json
|
|
||||||
from fastapi import FastAPI, Depends, File, Form, HTTPException, Body, Request, Response, UploadFile
|
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from .models import UserCreate, UserLogin, ValidateRequest, SkinUpdate, CapeUpdate
|
|
||||||
from .auth import AuthService
|
|
||||||
from .database import users_collection
|
|
||||||
from .utils import decode_token
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Union
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
import logging
|
|
||||||
# logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
auth_service = AuthService()
|
|
||||||
|
|
||||||
skin_dir = Path("skins")
|
|
||||||
skin_dir.mkdir(exist_ok=True)
|
|
||||||
app.mount("/skins", StaticFiles(directory="skins"), name="skins")
|
|
||||||
|
|
||||||
cape_dir = Path("capes")
|
|
||||||
cape_dir.mkdir(exist_ok=True)
|
|
||||||
app.mount("/capes", StaticFiles(directory="capes"), name="capes")
|
|
||||||
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"], # Разрешить все домены
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
def api_root():
|
|
||||||
return {
|
|
||||||
"meta": {
|
|
||||||
"serverName": "Your Auth Server",
|
|
||||||
"implementationName": "FastAPI",
|
|
||||||
"implementationVersion": "1.0.0",
|
|
||||||
"links": {
|
|
||||||
"homepage": "https://your-server.com"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"skinDomains": ["147.78.65.214"],
|
|
||||||
"capeDomains": ["147.78.65.214"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Эндпоинты Mojang-like API
|
|
||||||
@app.post("/auth/register")
|
|
||||||
async def register(user: UserCreate):
|
|
||||||
return await auth_service.register(user)
|
|
||||||
|
|
||||||
@app.post("/auth/authenticate")
|
|
||||||
async def authenticate(credentials: UserLogin):
|
|
||||||
return await auth_service.login(credentials)
|
|
||||||
|
|
||||||
@app.post("/auth/validate")
|
|
||||||
async def validate_token(request: ValidateRequest):
|
|
||||||
is_valid = await auth_service.validate(request.accessToken, request.clientToken)
|
|
||||||
return {"valid": is_valid}
|
|
||||||
|
|
||||||
@app.post("/auth/refresh")
|
|
||||||
async def refresh_token(access_token: str, client_token: str):
|
|
||||||
result = await auth_service.refresh(access_token, client_token)
|
|
||||||
if not result:
|
|
||||||
raise HTTPException(status_code=401, detail="Invalid tokens")
|
|
||||||
return result
|
|
||||||
|
|
||||||
@app.get("/sessionserver/session/minecraft/profile/{uuid}")
|
|
||||||
async def get_minecraft_profile(uuid: str, unsigned: bool = False):
|
|
||||||
return await auth_service.get_minecraft_profile(uuid)
|
|
||||||
|
|
||||||
@app.post("/sessionserver/session/minecraft/join")
|
|
||||||
async def join_server(request_data: dict = Body(...)):
|
|
||||||
try:
|
|
||||||
await auth_service.join_server(request_data)
|
|
||||||
return Response(status_code=204)
|
|
||||||
except Exception as e:
|
|
||||||
print("Error in join_server:", str(e))
|
|
||||||
raise
|
|
||||||
|
|
||||||
@app.get("/sessionserver/session/minecraft/hasJoined")
|
|
||||||
async def has_joined(username: str, serverId: str):
|
|
||||||
return await auth_service.has_joined(username, serverId)
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/user/{username}/skin")
|
|
||||||
async def set_skin(
|
|
||||||
username: str,
|
|
||||||
skin_file: UploadFile = File(...),
|
|
||||||
skin_model: str = Form("classic")
|
|
||||||
):
|
|
||||||
return await auth_service.set_skin(username, skin_file, skin_model)
|
|
||||||
|
|
||||||
@app.delete("/user/{username}/skin")
|
|
||||||
async def remove_skin(username: str):
|
|
||||||
return await auth_service.remove_skin(username)
|
|
||||||
|
|
||||||
@app.post("/user/{username}/cape")
|
|
||||||
async def set_cape(
|
|
||||||
username: str,
|
|
||||||
cape_file: UploadFile = File(...)
|
|
||||||
):
|
|
||||||
return await auth_service.set_cape(username, cape_file)
|
|
||||||
|
|
||||||
@app.delete("/user/{username}/cape")
|
|
||||||
async def remove_cape(username: str):
|
|
||||||
return await auth_service.remove_cape(username)
|
|
||||||
|
|
||||||
@app.get("/debug/profile/{uuid}")
|
|
||||||
async def debug_profile(uuid: str):
|
|
||||||
profile = await auth_service.get_minecraft_profile(uuid)
|
|
||||||
textures = base64.b64decode(profile['properties'][0]['value']).decode()
|
|
||||||
return {
|
|
||||||
"profile": profile,
|
|
||||||
"textures_decoded": json.loads(textures)
|
|
||||||
}
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
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"]
|
||||||
|
|
59
main.py
59
main.py
@ -1,40 +1,29 @@
|
|||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI
|
||||||
from aiomcrcon import Client, RCONConnectionError, IncorrectPasswordError
|
from fastapi.staticfiles import StaticFiles
|
||||||
import asyncio
|
from app.api import users, skins, capes, meta, server, store, pranks, marketplace
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
# Конфигурация RCON (замените на свои данные)
|
app.include_router(meta.router)
|
||||||
RCON_CONFIG = {
|
app.include_router(users.router)
|
||||||
"hub": {"host": "minecraft.hub.popa-popa.ru", "port": 29001, "password": "2006siT_"},
|
app.include_router(skins.router)
|
||||||
"survival": {"host": "minecraft.survival.popa-popa.ru", "port": 25575, "password": "пароль_survival"},
|
app.include_router(capes.router)
|
||||||
"pillars": {"host": "minecraft.pillars.popa-popa.ru", "port": 29003, "password": "2006siT_"},
|
app.include_router(server.router)
|
||||||
"velocity": {"host": "minecraft.velocity.popa-popa.ru", "port": 25575, "password": "пароль_velocity"}
|
app.include_router(store.router)
|
||||||
}
|
app.include_router(pranks.router)
|
||||||
|
app.include_router(marketplace.router)
|
||||||
|
|
||||||
async def send_rcon_command(server_type: str, command: str) -> str:
|
# Монтируем статику
|
||||||
"""Отправляет RCON-команду на указанный сервер."""
|
app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")
|
||||||
config = RCON_CONFIG.get(server_type)
|
app.mount("/capes", StaticFiles(directory="app/static/capes"), name="capes")
|
||||||
if not config:
|
app.mount("/capes_store", StaticFiles(directory="app/static/capes_store"), name="capes_store")
|
||||||
raise HTTPException(status_code=400, detail="Неверный тип сервера")
|
|
||||||
|
|
||||||
try:
|
# CORS, middleware и т.д.
|
||||||
async with Client(config["host"], config["port"], config["password"]) as client:
|
app.add_middleware(
|
||||||
response = await client.send_cmd(command)
|
CORSMiddleware,
|
||||||
return response
|
allow_origins=["*"],
|
||||||
except RCONConnectionError:
|
allow_credentials=True,
|
||||||
raise HTTPException(status_code=503, detail="Не удалось подключиться к серверу")
|
allow_methods=["*"],
|
||||||
except IncorrectPasswordError:
|
allow_headers=["*"],
|
||||||
raise HTTPException(status_code=403, detail="Неверный пароль RCON")
|
)
|
||||||
|
|
||||||
@app.get("/rcon/")
|
|
||||||
async def execute_rcon(server_type: str, command: str):
|
|
||||||
"""Выполняет RCON-команду на указанном сервере."""
|
|
||||||
result = await send_rcon_command(server_type, command)
|
|
||||||
return {"server": server_type, "command": command, "response": result}
|
|
||||||
|
|
||||||
@app.get("/players/online/")
|
|
||||||
async def get_online_players(server_type: str):
|
|
||||||
"""Возвращает список игроков онлайн на сервере."""
|
|
||||||
players = await send_rcon_command(server_type, "list")
|
|
||||||
return {"server": server_type, "online_players": players}
|
|
||||||
|
@ -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