feat: new endpoints for users and updated models
This commit is contained in:
21
app/api/server.py
Normal file
21
app/api/server.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/api/server",
|
||||||
|
tags=["Server Management"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/events")
|
||||||
|
async def receive_server_event(event_data: dict):
|
||||||
|
return await EventService().process_event(event_data)
|
||||||
|
|
||||||
|
@router.post("/commands")
|
||||||
|
async def add_server_command(command_data: ServerCommand):
|
||||||
|
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)
|
@ -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
|
||||||
|
7
app/models/server/command.py
Normal file
7
app/models/server/command.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class ServerCommand(BaseModel):
|
||||||
|
command: str
|
||||||
|
server_ip: str
|
||||||
|
require_online_player: Optional[bool] = False
|
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 # последнее время начисления монет
|
@ -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()
|
||||||
|
|
||||||
|
102
app/services/coins.py
Normal file
102
app/services/coins.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from app.db.database import users_collection, sessions_collection
|
||||||
|
|
||||||
|
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}с"
|
||||||
|
}
|
||||||
|
}
|
41
app/services/server/command.py
Normal file
41
app/services/server/command.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
# Глобальное хранилище команд (в реальном проекте используйте БД)
|
||||||
|
pending_commands: Dict[str, Dict] = {}
|
||||||
|
|
||||||
|
class CommandService:
|
||||||
|
async def add_command(self, command_data):
|
||||||
|
try:
|
||||||
|
command_id = str(uuid.uuid4())
|
||||||
|
pending_commands[command_id] = {
|
||||||
|
"command": command_data.command,
|
||||||
|
"server_ip": command_data.server_ip,
|
||||||
|
"require_online_player": command_data.require_online_player,
|
||||||
|
"created_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
print(f"[{datetime.now()}] Добавлена команда: {command_data.command} "
|
||||||
|
f"для сервера {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 = [
|
||||||
|
{"id": cmd_id, "command": cmd["command"], "require_online_player": cmd["require_online_player"]}
|
||||||
|
for cmd_id, cmd in pending_commands.items()
|
||||||
|
if cmd["server_ip"] == server_ip
|
||||||
|
]
|
||||||
|
|
||||||
|
# Удаляем полученные команды (чтобы не выполнять их повторно)
|
||||||
|
for cmd_id in list(pending_commands.keys()):
|
||||||
|
if pending_commands[cmd_id]["server_ip"] == server_ip:
|
||||||
|
del pending_commands[cmd_id]
|
||||||
|
|
||||||
|
return {"status": "success", "commands": commands}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
70
app/services/server/event.py
Normal file
70
app/services/server/event.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from fastapi import HTTPException
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
from app.services.coins import CoinsService
|
||||||
|
from app.models.server.event import PlayerEvent, OnlinePlayersUpdate
|
||||||
|
|
||||||
|
class EventService:
|
||||||
|
def __init__(self):
|
||||||
|
self.coins_service = CoinsService()
|
||||||
|
|
||||||
|
async def process_event(self, event_data: dict):
|
||||||
|
try:
|
||||||
|
event_type = event_data.get("event_type")
|
||||||
|
server_ip = event_data.get("server_ip", "unknown")
|
||||||
|
|
||||||
|
if event_type == "player_join":
|
||||||
|
player_name = event_data["player_name"]
|
||||||
|
player_id = event_data["player_id"]
|
||||||
|
print(f"[{datetime.now()}] Игрок вошел: {player_name} (ID: {player_id}) "
|
||||||
|
f"IP сервера: {server_ip}")
|
||||||
|
|
||||||
|
elif event_type == "player_quit":
|
||||||
|
player_name = event_data["player_name"]
|
||||||
|
player_id = event_data["player_id"]
|
||||||
|
print(f"[{datetime.now()}] Игрок вышел: {player_name} (ID: {player_id}) "
|
||||||
|
f"IP сервера: {server_ip}")
|
||||||
|
|
||||||
|
elif event_type == "player_session":
|
||||||
|
player_name = event_data["player_name"]
|
||||||
|
player_id = event_data["player_id"]
|
||||||
|
duration = event_data["duration"]
|
||||||
|
|
||||||
|
# Обновляем монеты через выделенный сервис
|
||||||
|
await self.coins_service.update_player_coins(player_id, player_name, duration, server_ip)
|
||||||
|
|
||||||
|
print(f"[{datetime.now()}] Игрок {player_name} провел на сервере: {duration} секунд "
|
||||||
|
f"IP сервера: {server_ip}")
|
||||||
|
|
||||||
|
elif event_type == "online_players_update":
|
||||||
|
players = event_data["players"]
|
||||||
|
print(f"\n[{datetime.now()}] Текущие онлайн-игроки ({len(players)}): "
|
||||||
|
f"IP сервера: {server_ip}")
|
||||||
|
|
||||||
|
# Обрабатываем каждого игрока
|
||||||
|
for player in players:
|
||||||
|
player_id = player["player_id"]
|
||||||
|
player_name = player["player_name"]
|
||||||
|
online_time = player["online_time"]
|
||||||
|
|
||||||
|
# Обновляем монеты через выделенный сервис
|
||||||
|
await self.coins_service.update_player_coins(
|
||||||
|
player_id, player_name, online_time, server_ip
|
||||||
|
)
|
||||||
|
|
||||||
|
hours, remainder = divmod(online_time, 3600)
|
||||||
|
minutes, seconds = divmod(remainder, 60)
|
||||||
|
print(f" - {player_name} (ID: {player_id}) "
|
||||||
|
f"Онлайн: {hours}ч {minutes}м {seconds}с")
|
||||||
|
print()
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"[{datetime.now()}] Неизвестное событие: {json.dumps(event_data, indent=2)}")
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid event type")
|
||||||
|
|
||||||
|
return {"status": "success", "message": "Event processed"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{datetime.now()}] Ошибка обработки события: {str(e)}")
|
||||||
|
print(f"Полученные данные: {json.dumps(event_data, indent=2)}")
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
3
main.py
3
main.py
@ -1,6 +1,6 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from app.api import users, skins, capes, meta
|
from app.api import users, skins, capes, meta, server
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@ -9,6 +9,7 @@ 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.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")
|
app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")
|
||||||
|
Reference in New Issue
Block a user