From 2e59d037849b053da1e75fbbf9befa0733b43610 Mon Sep 17 00:00:00 2001 From: DIKER0K Date: Fri, 18 Jul 2025 02:58:22 +0500 Subject: [PATCH] feat: new endpoints for users and updated models --- app/api/server.py | 21 +++++++ app/api/users.py | 72 +++++++++++++++++++++++ app/models/server/command.py | 7 +++ app/models/server/event.py | 17 ++++++ app/models/server/playtime.py | 17 ++++++ app/models/user.py | 2 + app/services/coins.py | 102 +++++++++++++++++++++++++++++++++ app/services/server/command.py | 41 +++++++++++++ app/services/server/event.py | 70 ++++++++++++++++++++++ main.py | 3 +- 10 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 app/api/server.py create mode 100644 app/models/server/command.py create mode 100644 app/models/server/event.py create mode 100644 app/models/server/playtime.py create mode 100644 app/services/coins.py create mode 100644 app/services/server/command.py create mode 100644 app/services/server/event.py diff --git a/app/api/server.py b/app/api/server.py new file mode 100644 index 0000000..b50229b --- /dev/null +++ b/app/api/server.py @@ -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) diff --git a/app/api/users.py b/app/api/users.py index c1564da..ff0757b 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -2,6 +2,16 @@ from fastapi import APIRouter, HTTPException, Body, Response from app.models.user import UserCreate, UserLogin from app.models.request import ValidateRequest from app.services.auth import AuthService +from app.db.database import users_collection, sessions_collection +from datetime import datetime +import json +from fastapi import HTTPException +from datetime import datetime, timedelta +from app.models.server.event import PlayerEvent, OnlinePlayersUpdate +from app.models.server.playtime import PlayerSession, PlayerPlaytime +from app.services.coins import CoinsService + +coins_service = CoinsService() router = APIRouter( tags=["Users"] @@ -45,3 +55,65 @@ async def join_server(request_data: dict = Body(...)): @router.get("/sessionserver/session/minecraft/hasJoined") async def has_joined(username: str, serverId: str): return await AuthService().has_joined(username, serverId) + +@router.get("/users/{username}/coins") +async def get_user_coins(username: str): + coins_data = await coins_service.get_player_coins(username) + if not coins_data: + raise HTTPException(status_code=404, detail="User not found") + return coins_data + +@router.get("/users") +async def get_users(): + """Получение списка всех пользователей""" + users = await users_collection.find().to_list(1000) + + # Исключаем чувствительные данные перед отправкой + safe_users = [] + for user in users: + safe_users.append({ + "username": user["username"], + "uuid": user["uuid"], + "skin_url": user.get("skin_url"), + "cloak_url": user.get("cloak_url"), + "coins": user.get("coins", 0), + "total_time_played": user.get("total_time_played", 0), + "is_active": user.get("is_active", True) + }) + + return {"users": safe_users, "count": len(safe_users)} + +@router.get("/users/{uuid}") +async def get_user_by_uuid(uuid: str): + """Получение пользователя по UUID""" + user = await users_collection.find_one({"uuid": uuid}) + if not user: + # Пробуем разные форматы UUID + if '-' in uuid: + user = await users_collection.find_one({"uuid": uuid.replace('-', '')}) + else: + formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}" + user = await users_collection.find_one({"uuid": formatted_uuid}) + + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Исключаем чувствительные данные + safe_user = { + "username": user["username"], + "uuid": user["uuid"], + "skin_url": user.get("skin_url"), + "cloak_url": user.get("cloak_url"), + "coins": user.get("coins", 0), + "total_time_played": user.get("total_time_played", 0), + "is_active": user.get("is_active", True), + "created_at": user.get("created_at") + } + + if "total_time_played" in safe_user: + total_time = safe_user["total_time_played"] + hours, remainder = divmod(total_time, 3600) + minutes, seconds = divmod(remainder, 60) + safe_user["total_time_formatted"] = f"{hours}ч {minutes}м {seconds}с" + + return safe_user diff --git a/app/models/server/command.py b/app/models/server/command.py new file mode 100644 index 0000000..d4e0f90 --- /dev/null +++ b/app/models/server/command.py @@ -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 diff --git a/app/models/server/event.py b/app/models/server/event.py new file mode 100644 index 0000000..77288f1 --- /dev/null +++ b/app/models/server/event.py @@ -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 diff --git a/app/models/server/playtime.py b/app/models/server/playtime.py new file mode 100644 index 0000000..09e35f2 --- /dev/null +++ b/app/models/server/playtime.py @@ -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 # последнее время начисления монет diff --git a/app/models/user.py b/app/models/user.py index 7283cba..f041fb3 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -19,6 +19,8 @@ class UserInDB(BaseModel): skin_url: Optional[str] = None skin_model: Optional[str] = "classic" cloak_url: Optional[str] = None + coins: int = 0 # Новое поле для монет + total_time_played: int = 0 # Общее время игры в секундах is_active: bool = True created_at: datetime = datetime.utcnow() diff --git a/app/services/coins.py b/app/services/coins.py new file mode 100644 index 0000000..54e6fb8 --- /dev/null +++ b/app/services/coins.py @@ -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}с" + } + } diff --git a/app/services/server/command.py b/app/services/server/command.py new file mode 100644 index 0000000..1debedb --- /dev/null +++ b/app/services/server/command.py @@ -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)) diff --git a/app/services/server/event.py b/app/services/server/event.py new file mode 100644 index 0000000..0f46e7d --- /dev/null +++ b/app/services/server/event.py @@ -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)) diff --git a/main.py b/main.py index 1f4035a..3f5a7fd 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.staticfiles import StaticFiles -from app.api import users, skins, capes, meta +from app.api import users, skins, capes, meta, server from fastapi.middleware.cors import CORSMiddleware app = FastAPI() @@ -9,6 +9,7 @@ app.include_router(meta.router) app.include_router(users.router) app.include_router(skins.router) app.include_router(capes.router) +app.include_router(server.router) # Монтируем статику app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")