Compare commits

..

12 Commits

Author SHA1 Message Date
2bd081fe7a add: action
Some checks failed
Build and Deploy / build (push) Has been cancelled
2025-07-21 00:09:53 +05:00
e59669f66a add: dockerfile 2025-07-20 23:24:00 +05:00
c7f6baac5d add: new capes in store 2025-07-20 22:16:55 +05:00
75d7e29f6e fix: another player see your skin! 2025-07-20 09:12:37 +05:00
39cd14f1d7 feat: full woriking markertplace 2025-07-19 04:39:51 +05:00
6b8f116608 wokring marketplace without enchancts and durability on item 2025-07-19 04:13:04 +05:00
44e12723ad add: getting player inventory 2025-07-19 01:20:26 +05:00
259e3c373b feat: auto delete server if it is inactive for more than 5 minutes, minor fix 2025-07-18 20:34:11 +05:00
7e4e2c0bad add: store pranks 2025-07-18 18:05:45 +05:00
d52d4dbf75 add: store cape 2025-07-18 03:39:21 +05:00
ff65e4a333 required acess token and client token in set, delete skin and capes 2025-07-18 03:13:52 +05:00
2e59d03784 feat: new endpoints for users and updated models 2025-07-18 02:58:22 +05:00
37 changed files with 2205 additions and 62 deletions

View 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
View File

@ -3,3 +3,4 @@ __pycache__
.env .env
skins skins
capes capes
mongodb

View File

@ -1,12 +1,32 @@
from fastapi import APIRouter, UploadFile, File from fastapi import APIRouter, UploadFile, File, HTTPException, Form
from app.services.cape import CapeService from app.services.cape import CapeService
from app.services.auth import AuthService
router = APIRouter(tags=["Capes"]) router = APIRouter(tags=["Capes"])
@router.post("/user/{username}/cape") @router.post("/user/{username}/cape")
async def set_cape(username: str, cape_file: UploadFile = File(...)): async def set_cape(
username: str,
cape_file: UploadFile = File(...),
accessToken: str = Form(...),
clientToken: str = Form(...)
):
# Validate the token
is_valid = await AuthService().validate(accessToken, clientToken)
if not is_valid:
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
return await CapeService().set_cape(username, cape_file) return await CapeService().set_cape(username, cape_file)
@router.delete("/user/{username}/cape") @router.delete("/user/{username}/cape")
async def remove_cape(username: str): async def remove_cape(
username: str,
accessToken: str,
clientToken: str
):
# Validate the token
is_valid = await AuthService().validate(accessToken, clientToken)
if not is_valid:
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
return await CapeService().remove_cape(username) return await CapeService().remove_cape(username)

63
app/api/marketplace.py Normal file
View 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"])

View File

@ -4,15 +4,29 @@ router = APIRouter(tags=["Meta"])
@router.get("/") @router.get("/")
def api_root(): def api_root():
return { # Читаем публичный ключ из файла
"meta": { public_key_path = "app/keys/public_key.pem"
"serverName": "Your Auth Server",
"implementationName": "FastAPI", try:
"implementationVersion": "1.0.0", with open(public_key_path, "r") as f:
"links": { public_key = f.read().strip()
"homepage": "https://your-server.com"
return {
"meta": {
"serverName": "Popa Auth Server",
"implementationName": "FastAPI",
"implementationVersion": "1.0.0",
"links": {
"homepage": "https://popa-popa.ru"
}
}, },
}, "skinDomains": ["147.78.65.214"],
"skinDomains": ["147.78.65.214"], "capeDomains": ["147.78.65.214"],
"capeDomains": ["147.78.65.214"] # Важно - возвращаем ключ как есть, без дополнительной обработки
} "signaturePublickey": public_key
}
except Exception as e:
return {
"error": str(e),
"traceback": str(e)
}

55
app/api/pranks.py Normal file
View 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
View 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
)

View File

@ -1,12 +1,33 @@
from fastapi import APIRouter, UploadFile, File, Form from fastapi import APIRouter, UploadFile, File, Form, HTTPException
from app.services.skin import SkinService from app.services.skin import SkinService
from app.services.auth import AuthService
router = APIRouter(tags=["Skins"]) router = APIRouter(tags=["Skins"])
@router.post("/user/{username}/skin") @router.post("/user/{username}/skin")
async def set_skin(username: str, skin_file: UploadFile = File(...), skin_model: str = Form("classic")): async def set_skin(
username: str,
skin_file: UploadFile = File(...),
skin_model: str = Form("classic"),
accessToken: str = Form(...),
clientToken: str = Form(...)
):
# Validate the token
is_valid = await AuthService().validate(accessToken, clientToken)
if not is_valid:
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
return await SkinService().set_skin(username, skin_file, skin_model) return await SkinService().set_skin(username, skin_file, skin_model)
@router.delete("/user/{username}/skin") @router.delete("/user/{username}/skin")
async def remove_skin(username: str): async def remove_skin(
username: str,
accessToken: str,
clientToken: str
):
# Validate the token
is_valid = await AuthService().validate(accessToken, clientToken)
if not is_valid:
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
return await SkinService().remove_skin(username) return await SkinService().remove_skin(username)

61
app/api/store.py Normal file
View 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)

View File

@ -2,6 +2,16 @@ from fastapi import APIRouter, HTTPException, Body, Response
from app.models.user import UserCreate, UserLogin from app.models.user import UserCreate, UserLogin
from app.models.request import ValidateRequest from app.models.request import ValidateRequest
from app.services.auth import AuthService from app.services.auth import AuthService
from app.db.database import users_collection, sessions_collection
from datetime import datetime
import json
from fastapi import HTTPException
from datetime import datetime, timedelta
from app.models.server.event import PlayerEvent, OnlinePlayersUpdate
from app.models.server.playtime import PlayerSession, PlayerPlaytime
from app.services.coins import CoinsService
coins_service = CoinsService()
router = APIRouter( router = APIRouter(
tags=["Users"] tags=["Users"]
@ -45,3 +55,65 @@ async def join_server(request_data: dict = Body(...)):
@router.get("/sessionserver/session/minecraft/hasJoined") @router.get("/sessionserver/session/minecraft/hasJoined")
async def has_joined(username: str, serverId: str): async def has_joined(username: str, serverId: str):
return await AuthService().has_joined(username, serverId) return await AuthService().has_joined(username, serverId)
@router.get("/users/{username}/coins")
async def get_user_coins(username: str):
coins_data = await coins_service.get_player_coins(username)
if not coins_data:
raise HTTPException(status_code=404, detail="User not found")
return coins_data
@router.get("/users")
async def get_users():
"""Получение списка всех пользователей"""
users = await users_collection.find().to_list(1000)
# Исключаем чувствительные данные перед отправкой
safe_users = []
for user in users:
safe_users.append({
"username": user["username"],
"uuid": user["uuid"],
"skin_url": user.get("skin_url"),
"cloak_url": user.get("cloak_url"),
"coins": user.get("coins", 0),
"total_time_played": user.get("total_time_played", 0),
"is_active": user.get("is_active", True)
})
return {"users": safe_users, "count": len(safe_users)}
@router.get("/users/{uuid}")
async def get_user_by_uuid(uuid: str):
"""Получение пользователя по UUID"""
user = await users_collection.find_one({"uuid": uuid})
if not user:
# Пробуем разные форматы UUID
if '-' in uuid:
user = await users_collection.find_one({"uuid": uuid.replace('-', '')})
else:
formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
user = await users_collection.find_one({"uuid": formatted_uuid})
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Исключаем чувствительные данные
safe_user = {
"username": user["username"],
"uuid": user["uuid"],
"skin_url": user.get("skin_url"),
"cloak_url": user.get("cloak_url"),
"coins": user.get("coins", 0),
"total_time_played": user.get("total_time_played", 0),
"is_active": user.get("is_active", True),
"created_at": user.get("created_at")
}
if "total_time_played" in safe_user:
total_time = safe_user["total_time_played"]
hours, remainder = divmod(total_time, 3600)
minutes, seconds = divmod(remainder, 60)
safe_user["total_time_formatted"] = f"{hours}ч {minutes}м {seconds}с"
return safe_user

View File

@ -2,7 +2,7 @@ from motor.motor_asyncio import AsyncIOMotorClient
from app.core.config import MONGO_URI from app.core.config import MONGO_URI
client = AsyncIOMotorClient(MONGO_URI) client = AsyncIOMotorClient(MONGO_URI)
db = client["minecraft_auth"] db = client["minecraft-api"]
users_collection = db["users"] users_collection = db["users"]
sessions_collection = db["sessions"] sessions_collection = db["sessions"]

View File

@ -1,4 +1,25 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional
class CapeUpdate(BaseModel): class CapeUpdate(BaseModel):
cape_url: str cape_url: str
class CapeStore(BaseModel):
id: str
name: str
description: str
price: int
file_name: str
class CapeStoreCreate(BaseModel):
name: str
description: str
price: int
class CapeStoreUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
price: Optional[int] = None
class CapePurchase(BaseModel):
cape_id: str

20
app/models/marketplace.py Normal file
View 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

View 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

View 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

View 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 # последнее время начисления монет

View 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

View File

@ -19,6 +19,8 @@ class UserInDB(BaseModel):
skin_url: Optional[str] = None skin_url: Optional[str] = None
skin_model: Optional[str] = "classic" skin_model: Optional[str] = "classic"
cloak_url: Optional[str] = None cloak_url: Optional[str] = None
coins: int = 0 # Новое поле для монет
total_time_played: int = 0 # Общее время игры в секундах
is_active: bool = True is_active: bool = True
created_at: datetime = datetime.utcnow() created_at: datetime = datetime.utcnow()

View File

@ -108,7 +108,7 @@ class AuthService:
textures = { textures = {
"timestamp": int(datetime.now().timestamp() * 1000), "timestamp": int(datetime.now().timestamp() * 1000),
"profileId": user["uuid"], # UUID с дефисами "profileId": user["uuid"].replace("-", ""),
"profileName": user["username"], "profileName": user["username"],
"textures": {} "textures": {}
} }
@ -125,28 +125,66 @@ class AuthService:
textures_json = json.dumps(textures).encode() textures_json = json.dumps(textures).encode()
base64_textures = base64.b64encode(textures_json).decode() base64_textures = base64.b64encode(textures_json).decode()
# Подписываем текстуры try:
with open("private_key.pem", "rb") as key_file: # Подписываем текстуры
private_key = serialization.load_pem_private_key( private_key_path = "app/keys/private_key.pem"
key_file.read(), with open(private_key_path, "rb") as key_file:
password=None private_key = serialization.load_pem_private_key(
key_file.read(),
password=None
)
signature = private_key.sign(
base64.b64encode(textures_json),
padding.PKCS1v15(),
hashes.SHA1()
) )
signature = private_key.sign( signature_base64 = base64.b64encode(signature).decode()
textures_json,
padding.PKCS1v15(),
hashes.SHA1()
)
return JSONResponse({ return {
"id": user["uuid"].replace("-", ""), # Уберите дефисы "id": user["uuid"].replace("-", ""),
"name": user["username"], "name": user["username"],
"properties": [{ "properties": [{
"name": "textures", "name": "textures",
"value": base64_textures, "value": base64_textures,
"signature": base64.b64encode(signature).decode() "signature": signature_base64
}] }]
}) }
except Exception as e:
print(f"Error signing textures: {e}")
# В случае ошибки возвращаем текстуры без подписи
return {
"id": user["uuid"].replace("-", ""),
"name": user["username"],
"properties": [{
"name": "textures",
"value": base64_textures
}]
}
# # Подписываем текстуры
# with open("private_key.pem", "rb") as key_file:
# private_key = serialization.load_pem_private_key(
# key_file.read(),
# password=None
# )
# signature = private_key.sign(
# textures_json,
# padding.PKCS1v15(),
# hashes.SHA1()
# )
# return JSONResponse({
# "id": user["uuid"].replace("-", ""), # Уберите дефисы
# "name": user["username"],
# "properties": [{
# "name": "textures",
# "value": base64_textures,
# # "signature": base64.b64encode(signature).decode()
# }]
# })
async def join_server(self, request_data: dict): async def join_server(self, request_data: dict):
access_token = request_data.get("accessToken") access_token = request_data.get("accessToken")
@ -186,24 +224,59 @@ class AuthService:
if not session: if not session:
raise HTTPException(status_code=403, detail="Not joined this server") raise HTTPException(status_code=403, detail="Not joined this server")
textures = {} textures = {
if user.get("skin_url"): "timestamp": int(datetime.now().timestamp() * 1000),
textures["SKIN"] = {"url": user["skin_url"]} "profileId": user["uuid"].replace("-", ""),
if user.get("cloak_url"): "profileName": user["username"],
textures["CAPE"] = {"url": user["cloak_url"]} "textures": {}
textures_value = base64.b64encode(json.dumps({
"timestamp": int(datetime.now().timestamp()),
"profileId": user["uuid"].replace("-", ""), # UUID без дефисов
"profileName": username,
"textures": textures
}).encode()).decode()
return {
"id": user["uuid"].replace("-", ""), # UUID без дефисов
"name": username,
"properties": [{
"name": "textures",
"value": textures_value
}] if textures else []
} }
if user.get("skin_url"):
textures["textures"]["SKIN"] = {
"url": user["skin_url"],
"metadata": {"model": user.get("skin_model", "classic")}
}
if user.get("cloak_url"):
textures["textures"]["CAPE"] = {"url": user["cloak_url"]}
textures_json = json.dumps(textures).encode()
base64_textures = base64.b64encode(textures_json).decode()
try:
# Подписываем текстуры
private_key_path = "app/keys/private_key.pem"
with open(private_key_path, "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None
)
signature = private_key.sign(
base64.b64encode(textures_json),
padding.PKCS1v15(),
hashes.SHA1()
)
signature_base64 = base64.b64encode(signature).decode()
return {
"id": user["uuid"].replace("-", ""),
"name": user["username"],
"properties": [{
"name": "textures",
"value": base64_textures,
"signature": signature_base64
}]
}
except Exception as e:
print(f"Error signing textures: {e}")
# В случае ошибки возвращаем текстуры без подписи
return {
"id": user["uuid"].replace("-", ""),
"name": user["username"],
"properties": [{
"name": "textures",
"value": base64_textures
}]
}

142
app/services/coins.py Normal file
View 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
View 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": "Покупка в обработке. Предмет будет добавлен в ваш инвентарь."
}

View 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 # Не создаем новый сервер, только обновляем существующий
)

View 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} сек.")

View File

@ -0,0 +1,288 @@
from fastapi import HTTPException
from app.db.database import db, users_collection
from app.models.server.prank import PrankCommand, PrankCommandUpdate
from datetime import datetime, timedelta
import uuid
from app.services.server.command import CommandService
# Создаем коллекции для хранения пакостей и серверов
prank_commands_collection = db.prank_commands
game_servers_collection = db.game_servers
online_players_collection = db.online_players
class PrankService:
async def add_prank_command(self, command_data):
"""Добавление новой команды-пакости"""
# Проверяем корректность шаблона команды
if "{targetPlayer}" not in command_data.command_template:
raise HTTPException(status_code=400,
detail="Шаблон команды должен содержать {targetPlayer} для подстановки имени цели")
prank_id = str(uuid.uuid4())
# Создаем новую команду в БД
prank_command = {
"id": prank_id,
"name": command_data.name,
"description": command_data.description,
"price": command_data.price,
"command_template": command_data.command_template,
"server_ids": command_data.server_ids,
"targetDescription": command_data.targetDescription,
"globalDescription": command_data.globalDescription, # Добавить это поле
"created_at": datetime.utcnow()
}
await prank_commands_collection.insert_one(prank_command)
return {"status": "success", "id": prank_id}
async def get_all_prank_commands(self):
"""Получение списка всех команд-пакостей"""
commands = await prank_commands_collection.find().to_list(1000)
result = []
for cmd in commands:
result.append({
"id": cmd["id"],
"name": cmd["name"],
"description": cmd["description"],
"price": cmd["price"],
"command_template": cmd["command_template"],
"server_ids": cmd.get("server_ids", []),
"targetDescription": cmd.get("targetDescription"),
"globalDescription": cmd.get("globalDescription") # Добавить это поле
})
return result
async def get_prank_command(self, command_id: str):
"""Получение конкретной команды по ID"""
command = await prank_commands_collection.find_one({"id": command_id})
if not command:
raise HTTPException(status_code=404, detail="Команда не найдена")
return {
"id": command["id"],
"name": command["name"],
"description": command["description"],
"price": command["price"],
"command_template": command["command_template"],
"server_ids": command.get("server_ids", []),
"targetDescription": command.get("targetDescription"),
"globalDescription": command.get("globalDescription") # Добавить это поле
}
async def update_prank_command(self, command_id: str, update_data: PrankCommandUpdate):
"""Обновление команды-пакости"""
command = await prank_commands_collection.find_one({"id": command_id})
if not command:
raise HTTPException(status_code=404, detail="Команда не найдена")
# Готовим данные для обновления
update = {}
if update_data.name is not None:
update["name"] = update_data.name
if update_data.description is not None:
update["description"] = update_data.description
if update_data.price is not None:
update["price"] = update_data.price
if update_data.command_template is not None:
if "{targetPlayer}" not in update_data.command_template:
raise HTTPException(status_code=400,
detail="Шаблон команды должен содержать {targetPlayer} для подстановки имени цели")
update["command_template"] = update_data.command_template
if update_data.server_ids is not None:
update["server_ids"] = update_data.server_ids
if update_data.targetDescription is not None:
update["targetDescription"] = update_data.targetDescription
if update_data.globalDescription is not None: # Добавить эту проверку
update["globalDescription"] = update_data.globalDescription
if update:
result = await prank_commands_collection.update_one(
{"id": command_id},
{"$set": update}
)
if result.modified_count == 0:
raise HTTPException(status_code=500, detail="Ошибка при обновлении команды")
return {"status": "success"}
async def delete_prank_command(self, command_id: str):
"""Удаление команды-пакости"""
command = await prank_commands_collection.find_one({"id": command_id})
if not command:
raise HTTPException(status_code=404, detail="Команда не найдена")
result = await prank_commands_collection.delete_one({"id": command_id})
if result.deleted_count == 0:
raise HTTPException(status_code=500, detail="Ошибка при удалении команды")
return {"status": "success"}
async def get_all_servers(self):
"""Получение списка всех доступных серверов"""
# Проверяем и удаляем неактивные серверы (более 5 минут без данных)
current_time = datetime.utcnow()
inactive_threshold = 5 * 60 # 5 минут в секундах
# Находим серверы, которые не отправляли данные больше 5 минут
# Учитываем, что у некоторых серверов может не быть поля last_activity
inactive_servers = await game_servers_collection.find({
"last_activity": {
"$exists": True,
"$lt": current_time - timedelta(seconds=inactive_threshold)
}
}).to_list(100)
# Удаляем неактивные серверы
if inactive_servers:
server_ids = [server["id"] for server in inactive_servers]
await game_servers_collection.delete_many({"id": {"$in": server_ids}})
# Опционально: логирование удаленных серверов
for server in inactive_servers:
print(f"Удален неактивный сервер: {server['name']} (ID: {server['id']})")
# Получаем актуальный список серверов
servers = await game_servers_collection.find().to_list(100)
# Если нет зарегистрированных серверов, вернем пустой список
if not servers:
return []
result = []
for server in servers:
# Получаем количество онлайн игроков
online_count = await online_players_collection.count_documents(
{"server_id": server["id"], "is_online": True}
)
result.append({
"id": server["id"],
"name": server["name"],
"ip": server.get("ip"),
"port": server.get("port"),
"description": server.get("description", ""),
"online_players": online_count,
"max_players": server.get("max_players", 0),
"last_activity": server.get("last_activity")
})
return result
async def get_server_online_players(self, server_id: str):
"""Получение списка онлайн игроков на конкретном сервере"""
server = await game_servers_collection.find_one({"id": server_id})
if not server:
raise HTTPException(status_code=404, detail="Сервер не найден")
players = await online_players_collection.find(
{"server_id": server_id, "is_online": True}
).to_list(1000)
result = []
for player in players:
result.append({
"username": player["username"],
"uuid": player.get("uuid", ""),
"online_since": player.get("login_time")
})
return {
"server": {
"id": server["id"],
"name": server["name"]
},
"online_players": result,
"count": len(result)
}
async def execute_prank(self, username: str, command_id: str, target_player: str, server_id: str):
"""Выполнение пакости (покупка и выполнение команды)"""
# Проверяем пользователя
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Проверяем команду
command = await prank_commands_collection.find_one({"id": command_id})
if not command:
raise HTTPException(status_code=404, detail="Команда не найдена")
# Проверяем сервер
server = await game_servers_collection.find_one({"id": server_id})
if not server:
raise HTTPException(status_code=404, detail="Сервер не найден")
# Проверяем, доступна ли команда на данном сервере
if (command.get("server_ids") and
"*" not in command.get("server_ids", []) and
server_id not in command.get("server_ids", [])):
raise HTTPException(status_code=400, detail="Команда недоступна на выбранном сервере")
# Проверяем, онлайн ли целевой игрок
target_online = await online_players_collection.find_one({
"username": target_player,
"server_id": server_id,
"is_online": True
})
if not target_online:
raise HTTPException(status_code=400, detail=f"Игрок {target_player} не в сети на этом сервере")
# Проверяем достаточно ли монет
user_coins = user.get("coins", 0)
if user_coins < command["price"]:
raise HTTPException(status_code=400,
detail=f"Недостаточно монет. Требуется: {command['price']}, имеется: {user_coins}")
# Формируем команду для выполнения
actual_command = command["command_template"].replace("{targetPlayer}", target_player)
# Обрабатываем оба типа сообщений
target_desc = None
global_desc = None
if command.get("targetDescription"):
target_desc = command.get("targetDescription").replace("{username}", username).replace("{targetPlayer}", target_player)
if command.get("globalDescription"):
global_desc = command.get("globalDescription").replace("{username}", username).replace("{targetPlayer}", target_player)
# Отправляем команду с обоими сообщениями
command_service = CommandService()
from app.models.server.command import ServerCommand
server_command = ServerCommand(
command=actual_command,
server_ip=server.get("ip", ""),
require_online_player=True,
target_message=target_desc, # Сообщение для цели
global_message=global_desc # Сообщение для всех остальных
)
command_result = await command_service.add_command(server_command)
# Логируем выполнение пакости
log_entry = {
"user_id": user["_id"],
"username": username,
"target_player": target_player,
"command_id": command_id,
"command_name": command["name"],
"server_id": server_id,
"price": command["price"],
"executed_command": actual_command,
"executed_at": datetime.utcnow()
}
await db.prank_executions.insert_one(log_entry)
return {
"status": "success",
"message": f"Команда '{command['name']}' успешно выполнена на игроке {target_player}",
"remaining_coins": user_coins - command["price"]
}

314
app/services/store_cape.py Normal file
View File

@ -0,0 +1,314 @@
from fastapi import HTTPException, UploadFile
from app.db.database import users_collection
from app.core.config import FILES_URL
from datetime import datetime
import uuid
from pathlib import Path
import os
import shutil
from app.models.cape import CapeStore, CapeStoreUpdate
# Создаем коллекцию для плащей в БД
from app.db.database import db
store_capes_collection = db.store_capes
class StoreCapeService:
async def add_cape(self, name: str, description: str, price: int, cape_file: UploadFile):
"""Добавление нового плаща в магазин"""
# Проверка типа файла
if not cape_file.content_type.startswith('image/'):
raise HTTPException(status_code=400, detail="Файл должен быть изображением")
# Определяем расширение
ext = None
if cape_file.content_type == "image/png":
ext = "png"
elif cape_file.content_type == "image/gif":
ext = "gif"
else:
raise HTTPException(status_code=400, detail="Поддерживаются только PNG и GIF плащи")
# Проверка размера файла (максимум 2MB)
max_size = 2 * 1024 * 1024 # 2MB
contents = await cape_file.read()
if len(contents) > max_size:
raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 2MB)")
# Создаем папку для плащей магазина, если ее нет
cape_dir = Path("app/static/capes_store")
cape_dir.mkdir(parents=True, exist_ok=True)
# Генерируем ID и имя файла
cape_id = str(uuid.uuid4())
cape_filename = f"store_cape_{cape_id}.{ext}"
cape_path = cape_dir / cape_filename
# Сохраняем файл
with open(cape_path, "wb") as f:
f.write(contents)
# Создаем запись в БД
cape_data = {
"id": cape_id,
"name": name,
"description": description,
"price": price,
"file_name": cape_filename,
"created_at": datetime.utcnow()
}
await store_capes_collection.insert_one(cape_data)
return {"id": cape_id, "status": "success"}
async def get_all_capes(self):
"""Получение всех плащей из магазина"""
capes = await store_capes_collection.find().to_list(1000)
result = []
for cape in capes:
result.append({
"id": cape["id"],
"name": cape["name"],
"description": cape["description"],
"price": cape["price"],
"image_url": f"{FILES_URL}/capes_store/{cape['file_name']}"
})
return result
async def get_cape_by_id(self, cape_id: str):
"""Получение плаща по ID"""
cape = await store_capes_collection.find_one({"id": cape_id})
if not cape:
raise HTTPException(status_code=404, detail="Плащ не найден")
return {
"id": cape["id"],
"name": cape["name"],
"description": cape["description"],
"price": cape["price"],
"image_url": f"{FILES_URL}/capes_store/{cape['file_name']}"
}
async def update_cape(self, cape_id: str, update_data: CapeStoreUpdate):
"""Обновление информации о плаще"""
cape = await store_capes_collection.find_one({"id": cape_id})
if not cape:
raise HTTPException(status_code=404, detail="Плащ не найден")
# Готовим данные для обновления
update = {}
if update_data.name:
update["name"] = update_data.name
if update_data.description:
update["description"] = update_data.description
if update_data.price is not None:
update["price"] = update_data.price
if update:
result = await store_capes_collection.update_one(
{"id": cape_id},
{"$set": update}
)
if result.modified_count == 0:
raise HTTPException(status_code=500, detail="Ошибка при обновлении")
return {"status": "success"}
async def delete_cape(self, cape_id: str):
"""Удаление плаща из магазина"""
cape = await store_capes_collection.find_one({"id": cape_id})
if not cape:
raise HTTPException(status_code=404, detail="Плащ не найден")
# Удаляем файл
cape_path = Path(f"app/static/capes_store/{cape['file_name']}")
if cape_path.exists():
try:
cape_path.unlink()
except Exception as e:
print(f"Ошибка при удалении файла: {e}")
# Удаляем из БД плащей магазина
result = await store_capes_collection.delete_one({"id": cape_id})
if result.deleted_count == 0:
raise HTTPException(status_code=500, detail="Ошибка при удалении из БД")
# Удаляем из БД купленных плащей
purchases_collection = db.purchases
purchases = await purchases_collection.find_one({"cape_id": cape_id})
if purchases:
await purchases_collection.delete_one({"cape_id": cape_id})
# Удаляем плащ из массива purchased_capes всех пользователей
users_collection = db.users
await users_collection.update_many(
{"purchased_capes.cape_id": cape_id},
{"$pull": {"purchased_capes": {"cape_id": cape_id}}}
)
return {"status": "success"}
async def purchase_cape(self, username: str, cape_id: str):
"""Покупка плаща пользователем"""
# Находим пользователя
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Находим плащ
cape = await store_capes_collection.find_one({"id": cape_id})
if not cape:
raise HTTPException(status_code=404, detail="Плащ не найден")
# Проверяем достаточно ли монет
user_coins = user.get("coins", 0)
if user_coins < cape["price"]:
raise HTTPException(status_code=400,
detail=f"Недостаточно монет. Требуется: {cape['price']}, имеется: {user_coins}")
# Копируем плащ из хранилища магазина в персональную папку пользователя
cape_store_path = Path(f"app/static/capes_store/{cape['file_name']}")
# Создаем папку для плащей пользователя
cape_dir = Path("app/static/capes")
cape_dir.mkdir(parents=True, exist_ok=True)
# Генерируем имя файла для персонального плаща
filename_parts = cape['file_name'].split('.')
ext = filename_parts[-1]
cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"
cape_path = cape_dir / cape_filename
# Копируем файл
try:
shutil.copy(cape_store_path, cape_path)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при копировании файла: {e}")
# Обновляем данные пользователя
# 1. Списываем монеты
# 2. Устанавливаем новый плащ
# 3. Добавляем плащ в список приобретенных
result = await users_collection.update_one(
{"username": username},
{"$set": {
"coins": user_coins - cape["price"],
"cloak_url": f"{FILES_URL}/capes/{cape_filename}"
},
"$push": {
"purchased_capes": {
"cape_id": cape_id,
"cape_name": cape["name"],
"cape_description": cape["description"],
"file_name": cape_filename,
"purchased_at": datetime.utcnow()
}
}}
)
if result.modified_count == 0:
# Если обновление не удалось, удаляем файл плаща
if os.path.exists(cape_path):
os.remove(cape_path)
raise HTTPException(status_code=500, detail="Ошибка при обновлении данных пользователя")
# Логируем покупку в БД
purchase_data = {
"username": username,
"user_id": user["_id"],
"cape_id": cape_id,
"cape_name": cape["name"],
"price": cape["price"],
"purchase_date": datetime.utcnow()
}
from app.db.database import db
await db.purchases.insert_one(purchase_data)
return {
"status": "success",
"message": f"Плащ '{cape['name']}' успешно приобретен",
"remaining_coins": user_coins - cape["price"]
}
async def get_user_purchased_capes(self, username: str):
"""Получение всех плащей, приобретенных пользователем"""
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
purchased_capes = user.get("purchased_capes", [])
result = []
for cape in purchased_capes:
result.append({
"cape_id": cape.get("cape_id"),
"cape_name": cape.get("cape_name"),
"cape_description": cape.get("cape_description"),
"image_url": f"{FILES_URL}/capes/{cape.get('file_name')}",
"purchased_at": cape.get("purchased_at"),
"is_active": user.get("cloak_url") == f"{FILES_URL}/capes/{cape.get('file_name')}"
})
return result
async def activate_purchased_cape(self, username: str, cape_id: str):
"""Активация приобретенного плаща"""
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Проверяем, что плащ был приобретен
purchased_capes = user.get("purchased_capes", [])
selected_cape = None
for cape in purchased_capes:
if cape.get("cape_id") == cape_id:
selected_cape = cape
break
if not selected_cape:
raise HTTPException(status_code=404, detail="Плащ не найден среди приобретенных")
# Устанавливаем выбранный плащ
await users_collection.update_one(
{"username": username},
{"$set": {"cloak_url": f"{FILES_URL}/capes/{selected_cape.get('file_name')}"}}
)
return {
"status": "success",
"message": f"Плащ '{selected_cape.get('cape_name')}' активирован"
}
async def deactivate_purchased_cape(self, username: str, cape_id: str):
"""Деактивация приобретенного плаща"""
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Проверяем, что плащ был приобретен
purchased_capes = user.get("purchased_capes", [])
selected_cape = None
for cape in purchased_capes:
if cape.get("cape_id") == cape_id:
selected_cape = cape
break
if not selected_cape:
raise HTTPException(status_code=404, detail="Плащ не найден среди приобретенных")
# Устанавливаем выбранный плащ
await users_collection.update_one(
{"username": username},
{"$set": {"cloak_url": None}}
)
return {
"status": "success",
"message": f"Плащ '{selected_cape.get('cape_name')}' деактивирован"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

29
docker-compose.yml Normal file
View 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
View 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"]

View File

@ -1,6 +1,6 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from app.api import users, skins, capes, meta from app.api import users, skins, capes, meta, server, store, pranks, marketplace
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
app = FastAPI() app = FastAPI()
@ -9,13 +9,17 @@ app.include_router(meta.router)
app.include_router(users.router) app.include_router(users.router)
app.include_router(skins.router) app.include_router(skins.router)
app.include_router(capes.router) app.include_router(capes.router)
app.include_router(server.router)
app.include_router(store.router)
app.include_router(pranks.router)
app.include_router(marketplace.router)
# Монтируем статику # Монтируем статику
app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins") app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")
app.mount("/capes", StaticFiles(directory="app/static/capes"), name="capes") app.mount("/capes", StaticFiles(directory="app/static/capes"), name="capes")
app.mount("/capes_store", StaticFiles(directory="app/static/capes_store"), name="capes_store")
# CORS, middleware и т.д. # CORS, middleware и т.д.
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],

View File

@ -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