Compare commits
40 Commits
b79b0ae69f
...
dev
Author | SHA1 | Date | |
---|---|---|---|
fa9611cc99 | |||
8c4db146c9 | |||
1ae08de28b | |||
6e2742bc09 | |||
7ab955dbb4 | |||
8a57fdad7a | |||
a404377108 | |||
c8d8c65251 | |||
dd71c19c6b | |||
56eaaa4103 | |||
91e54bb4e0 | |||
176320154f | |||
25b0ec0809 | |||
cddd20e203 | |||
b5369ed060 | |||
49dbc664b3 | |||
4bf266e2ba | |||
7131f6613e | |||
d2084e73ee | |||
860b73554c | |||
ac0f58fe68 | |||
b505448f36 | |||
06ac3c01a2 | |||
2d377088b0 | |||
ee8bd8c052 | |||
b851a049b8 | |||
409295358c | |||
2bd081fe7a | |||
e59669f66a | |||
c7f6baac5d | |||
75d7e29f6e | |||
39cd14f1d7 | |||
6b8f116608 | |||
44e12723ad | |||
259e3c373b | |||
7e4e2c0bad | |||
d52d4dbf75 | |||
ff65e4a333 | |||
2e59d03784 | |||
733977f56e |
31
.gitea/workflows/build.yaml
Normal file
31
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: docker
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Create .env file
|
||||
run: |
|
||||
echo "MONGO_URI=${{ secrets.MONGO_URI }}" > /home/server/popa_minecraft_launcher_api/.env
|
||||
echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> /home/server/popa_minecraft_launcher_api/.env
|
||||
echo "FILES_URL=${{ secrets.FILES_URL }}" >> /home/server/popa_minecraft_launcher_api/.env
|
||||
echo "TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }}" >> /home/server/popa_minecraft_launcher_api/.env
|
||||
echo "API_URL=${{ secrets.API_URL }}" >> /home/server/popa_minecraft_launcher_api/.env
|
||||
|
||||
- name: Build and deploy
|
||||
run: |
|
||||
cd /home/server/popa_minecraft_launcher_api
|
||||
git reset --hard HEAD
|
||||
git checkout main
|
||||
git pull
|
||||
docker-compose down -v
|
||||
docker-compose build
|
||||
docker-compose up -d
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ __pycache__
|
||||
.env
|
||||
skins
|
||||
capes
|
||||
mongodb
|
||||
|
0
app/api/__init__.py
Normal file
0
app/api/__init__.py
Normal file
41
app/api/bonuses.py
Normal file
41
app/api/bonuses.py
Normal file
@ -0,0 +1,41 @@
|
||||
from fastapi import APIRouter, Query, Body
|
||||
from fastapi import HTTPException
|
||||
from datetime import datetime, timedelta
|
||||
from app.models.bonus import PurchaseBonus
|
||||
import uuid
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/bonuses",
|
||||
tags=["Bonuses"]
|
||||
)
|
||||
|
||||
@router.get("/effects")
|
||||
async def get_user_effects(username: str):
|
||||
"""Получить активные эффекты пользователя для плагина"""
|
||||
from app.services.bonus import BonusService
|
||||
return await BonusService().get_user_active_effects(username)
|
||||
|
||||
@router.get("/types")
|
||||
async def get_bonus_types():
|
||||
"""Получить доступные типы бонусов"""
|
||||
from app.services.bonus import BonusService
|
||||
return await BonusService().list_available_bonuses()
|
||||
|
||||
@router.get("/user/{username}")
|
||||
async def get_user_bonuses(username: str):
|
||||
"""Получить активные бонусы пользователя"""
|
||||
from app.services.bonus import BonusService
|
||||
return await BonusService().get_user_bonuses(username)
|
||||
|
||||
@router.post("/purchase")
|
||||
async def purchase_bonus(purchase_bonus: PurchaseBonus):
|
||||
"""Купить бонус"""
|
||||
from app.services.bonus import BonusService
|
||||
return await BonusService().purchase_bonus(purchase_bonus.username, purchase_bonus.bonus_type_id)
|
||||
|
||||
@router.post("/upgrade")
|
||||
async def upgrade_user_bonus(username: str = Body(...), bonus_id: str = Body(...)):
|
||||
"""Улучшить существующий бонус"""
|
||||
from app.services.bonus import BonusService
|
||||
return await BonusService().upgrade_bonus(username, bonus_id)
|
||||
|
32
app/api/capes.py
Normal file
32
app/api/capes.py
Normal file
@ -0,0 +1,32 @@
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException, Form
|
||||
from app.services.cape import CapeService
|
||||
from app.services.auth import AuthService
|
||||
|
||||
router = APIRouter(tags=["Capes"])
|
||||
|
||||
@router.post("/user/{username}/cape")
|
||||
async def set_cape(
|
||||
username: str,
|
||||
cape_file: UploadFile = File(...),
|
||||
accessToken: str = Form(...),
|
||||
clientToken: str = Form(...)
|
||||
):
|
||||
# Validate the token
|
||||
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||
|
||||
return await CapeService().set_cape(username, cape_file)
|
||||
|
||||
@router.delete("/user/{username}/cape")
|
||||
async def remove_cape(
|
||||
username: str,
|
||||
accessToken: str,
|
||||
clientToken: str
|
||||
):
|
||||
# Validate the token
|
||||
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||
|
||||
return await CapeService().remove_cape(username)
|
82
app/api/marketplace.py
Normal file
82
app/api/marketplace.py
Normal file
@ -0,0 +1,82 @@
|
||||
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"])
|
||||
|
||||
@router.delete("/items/{item_id}")
|
||||
async def cancel_item_sale(
|
||||
item_id: str,
|
||||
username: str = Query(...)
|
||||
):
|
||||
"""Снять предмет с продажи"""
|
||||
from app.services.marketplace import MarketplaceService
|
||||
return await MarketplaceService().cancel_item_sale(username, item_id)
|
||||
|
||||
@router.put("/items/{item_id}/price")
|
||||
async def update_item_price(
|
||||
item_id: str,
|
||||
new_price: int = Body(..., gt=0),
|
||||
username: str = Body(...)
|
||||
):
|
||||
"""Обновить цену предмета на торговой площадке"""
|
||||
from app.services.marketplace import MarketplaceService
|
||||
return await MarketplaceService().update_item_price(username, item_id, new_price)
|
32
app/api/meta.py
Normal file
32
app/api/meta.py
Normal file
@ -0,0 +1,32 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(tags=["Meta"])
|
||||
|
||||
@router.get("/")
|
||||
def api_root():
|
||||
# Читаем публичный ключ из файла
|
||||
public_key_path = "app/keys/public_key.pem"
|
||||
|
||||
try:
|
||||
with open(public_key_path, "r") as f:
|
||||
public_key = f.read().strip()
|
||||
|
||||
return {
|
||||
"meta": {
|
||||
"serverName": "Popa Auth Server",
|
||||
"implementationName": "FastAPI",
|
||||
"implementationVersion": "1.0.0",
|
||||
"links": {
|
||||
"homepage": "https://popa-popa.ru"
|
||||
}
|
||||
},
|
||||
"skinDomains": ["147.78.65.214", "minecraft.api.popa-popa.ru"],
|
||||
"capeDomains": ["147.78.65.214", "minecraft.api.popa-popa.ru"],
|
||||
# Важно - возвращаем ключ как есть, без дополнительной обработки
|
||||
"signaturePublickey": public_key
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": str(e),
|
||||
"traceback": str(e)
|
||||
}
|
55
app/api/pranks.py
Normal file
55
app/api/pranks.py
Normal file
@ -0,0 +1,55 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from app.services.server.prank import PrankService
|
||||
from app.models.server.prank import PrankCommandCreate, PrankCommandUpdate, PrankExecute
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/pranks",
|
||||
tags=["Pranks"]
|
||||
)
|
||||
|
||||
prank_service = PrankService()
|
||||
|
||||
@router.get("/commands")
|
||||
async def get_all_prank_commands():
|
||||
"""Получение всех доступных команд-пакостей"""
|
||||
return await prank_service.get_all_prank_commands()
|
||||
|
||||
@router.get("/commands/{command_id}")
|
||||
async def get_prank_command(command_id: str):
|
||||
"""Получение команды-пакости по ID"""
|
||||
return await prank_service.get_prank_command(command_id)
|
||||
|
||||
@router.post("/commands")
|
||||
async def add_prank_command(command: PrankCommandCreate):
|
||||
"""Добавление новой команды-пакости"""
|
||||
return await prank_service.add_prank_command(command)
|
||||
|
||||
@router.put("/commands/{command_id}")
|
||||
async def update_prank_command(command_id: str, update_data: PrankCommandUpdate):
|
||||
"""Обновление команды-пакости"""
|
||||
return await prank_service.update_prank_command(command_id, update_data)
|
||||
|
||||
@router.delete("/commands/{command_id}")
|
||||
async def delete_prank_command(command_id: str):
|
||||
"""Удаление команды-пакости"""
|
||||
return await prank_service.delete_prank_command(command_id)
|
||||
|
||||
@router.get("/servers")
|
||||
async def get_all_servers():
|
||||
"""Получение списка всех доступных серверов"""
|
||||
return await prank_service.get_all_servers()
|
||||
|
||||
@router.get("/servers/{server_id}/players")
|
||||
async def get_server_online_players(server_id: str):
|
||||
"""Получение списка онлайн игроков на сервере"""
|
||||
return await prank_service.get_server_online_players(server_id)
|
||||
|
||||
@router.post("/execute")
|
||||
async def execute_prank(username: str, prank_data: PrankExecute):
|
||||
"""Выполнение пакости (списание монет и выполнение команды)"""
|
||||
return await prank_service.execute_prank(
|
||||
username,
|
||||
prank_data.command_id,
|
||||
prank_data.target_player,
|
||||
prank_data.server_id
|
||||
)
|
60
app/api/server.py
Normal file
60
app/api/server.py
Normal file
@ -0,0 +1,60 @@
|
||||
from fastapi import APIRouter
|
||||
from app.services.server.command import CommandService
|
||||
from app.services.server.event import EventService
|
||||
from app.models.server.command import ServerCommand, InventoryRequest
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/server",
|
||||
tags=["Server Management"]
|
||||
)
|
||||
|
||||
@router.post("/events")
|
||||
async def receive_server_event(event_data: dict):
|
||||
# Обновляем активность сервера
|
||||
server_ip = event_data.get("server_ip")
|
||||
if server_ip:
|
||||
await update_server_activity(server_ip)
|
||||
return await EventService().process_event(event_data)
|
||||
|
||||
@router.post("/commands")
|
||||
async def add_server_command(command_data: ServerCommand):
|
||||
# Обновляем last_activity для сервера
|
||||
await CommandService()._update_server_activity(command_data.server_ip)
|
||||
return await CommandService().add_command(command_data)
|
||||
|
||||
@router.get("/commands")
|
||||
async def get_server_commands(server_ip: str):
|
||||
return await CommandService().get_commands(server_ip)
|
||||
|
||||
@router.post("/inventory")
|
||||
async def request_player_inventory(inventory_request: InventoryRequest):
|
||||
"""Создаёт запрос на получение инвентаря игрока"""
|
||||
return await CommandService().request_inventory(inventory_request)
|
||||
|
||||
@router.get("/inventory/requests")
|
||||
async def get_inventory_requests(server_ip: str):
|
||||
"""Получает список запросов инвентаря для сервера"""
|
||||
return await CommandService().get_inventory_requests(server_ip)
|
||||
|
||||
@router.post("/inventory/submit")
|
||||
async def submit_inventory(inventory_data: dict):
|
||||
"""Принимает данные инвентаря от сервера"""
|
||||
return await CommandService().submit_inventory(inventory_data)
|
||||
|
||||
@router.get("/inventory/{request_id}")
|
||||
async def get_inventory_result(request_id: str):
|
||||
"""Получает результаты запроса инвентаря"""
|
||||
return await CommandService().get_inventory_result(request_id)
|
||||
|
||||
async def update_server_activity(server_ip):
|
||||
"""Обновляет время последней активности сервера"""
|
||||
from app.db.database import db
|
||||
game_servers_collection = db.game_servers
|
||||
|
||||
await game_servers_collection.update_one(
|
||||
{"ip": server_ip},
|
||||
{"$set": {"last_activity": datetime.utcnow()}},
|
||||
upsert=False
|
||||
)
|
33
app/api/skins.py
Normal file
33
app/api/skins.py
Normal file
@ -0,0 +1,33 @@
|
||||
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
|
||||
from app.services.skin import SkinService
|
||||
from app.services.auth import AuthService
|
||||
|
||||
router = APIRouter(tags=["Skins"])
|
||||
|
||||
@router.post("/user/{username}/skin")
|
||||
async def set_skin(
|
||||
username: str,
|
||||
skin_file: UploadFile = File(...),
|
||||
skin_model: str = Form("classic"),
|
||||
accessToken: str = Form(...),
|
||||
clientToken: str = Form(...)
|
||||
):
|
||||
# Validate the token
|
||||
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||
|
||||
return await SkinService().set_skin(username, skin_file, skin_model)
|
||||
|
||||
@router.delete("/user/{username}/skin")
|
||||
async def remove_skin(
|
||||
username: str,
|
||||
accessToken: str,
|
||||
clientToken: str
|
||||
):
|
||||
# Validate the token
|
||||
is_valid = await AuthService().validate(accessToken, clientToken)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
|
||||
|
||||
return await SkinService().remove_skin(username)
|
61
app/api/store.py
Normal file
61
app/api/store.py
Normal file
@ -0,0 +1,61 @@
|
||||
from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException
|
||||
from app.services.store_cape import StoreCapeService
|
||||
from app.models.cape import CapeStoreUpdate, CapePurchase
|
||||
from typing import Optional
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/store",
|
||||
tags=["Store"]
|
||||
)
|
||||
|
||||
store_cape_service = StoreCapeService()
|
||||
|
||||
@router.get("/capes")
|
||||
async def get_all_capes():
|
||||
"""Получение списка всех плащей в магазине"""
|
||||
return await store_cape_service.get_all_capes()
|
||||
|
||||
@router.get("/capes/{cape_id}")
|
||||
async def get_cape_by_id(cape_id: str):
|
||||
"""Получение плаща по ID"""
|
||||
return await store_cape_service.get_cape_by_id(cape_id)
|
||||
|
||||
@router.post("/capes")
|
||||
async def add_cape(
|
||||
name: str = Form(...),
|
||||
description: str = Form(...),
|
||||
price: int = Form(...),
|
||||
cape_file: UploadFile = File(...)
|
||||
):
|
||||
"""Добавление нового плаща в магазин"""
|
||||
return await store_cape_service.add_cape(name, description, price, cape_file)
|
||||
|
||||
@router.put("/capes/{cape_id}")
|
||||
async def update_cape(cape_id: str, update_data: CapeStoreUpdate):
|
||||
"""Обновление информации о плаще"""
|
||||
return await store_cape_service.update_cape(cape_id, update_data)
|
||||
|
||||
@router.delete("/capes/{cape_id}")
|
||||
async def delete_cape(cape_id: str):
|
||||
"""Удаление плаща из магазина"""
|
||||
return await store_cape_service.delete_cape(cape_id)
|
||||
|
||||
@router.post("/purchase/cape")
|
||||
async def purchase_cape(username: str, cape_id: str):
|
||||
"""Покупка плаща пользователем"""
|
||||
return await store_cape_service.purchase_cape(username, cape_id)
|
||||
|
||||
@router.get("/user/{username}/capes")
|
||||
async def get_user_purchased_capes(username: str):
|
||||
"""Получение всех приобретенных плащей пользователя"""
|
||||
return await store_cape_service.get_user_purchased_capes(username)
|
||||
|
||||
@router.post("/user/{username}/capes/activate/{cape_id}")
|
||||
async def activate_purchased_cape(username: str, cape_id: str):
|
||||
"""Активация приобретенного плаща"""
|
||||
return await store_cape_service.activate_purchased_cape(username, cape_id)
|
||||
|
||||
@router.post("/user/{username}/capes/deactivate/{cape_id}")
|
||||
async def deactivate_purchased_cape(username: str, cape_id: str):
|
||||
"""Деактивация приобретенного плаща"""
|
||||
return await store_cape_service.deactivate_purchased_cape(username, cape_id)
|
131
app/api/users.py
Normal file
131
app/api/users.py
Normal file
@ -0,0 +1,131 @@
|
||||
from fastapi import APIRouter, HTTPException, Body, Response
|
||||
from app.models.user import UserCreate, UserLogin, VerifyCode
|
||||
from app.models.request import ValidateRequest
|
||||
from app.services.auth import AuthService
|
||||
from app.db.database import users_collection, sessions_collection
|
||||
from datetime import datetime
|
||||
import json
|
||||
from fastapi import HTTPException
|
||||
from datetime import datetime, timedelta
|
||||
from app.models.server.event import PlayerEvent, OnlinePlayersUpdate
|
||||
from app.models.server.playtime import PlayerSession, PlayerPlaytime
|
||||
from app.services.coins import CoinsService
|
||||
|
||||
coins_service = CoinsService()
|
||||
|
||||
router = APIRouter(
|
||||
tags=["Users"]
|
||||
)
|
||||
|
||||
@router.post("/auth/register")
|
||||
async def register(user: UserCreate):
|
||||
"""Регистрация нового пользователя"""
|
||||
return await AuthService().register(user)
|
||||
|
||||
@router.post("/auth/authenticate")
|
||||
async def authenticate(credentials: UserLogin):
|
||||
"""Аутентификация пользователя"""
|
||||
return await AuthService().login(credentials)
|
||||
|
||||
@router.post("/auth/validate")
|
||||
async def validate_token(request: ValidateRequest):
|
||||
is_valid = await AuthService().validate(request.accessToken, request.clientToken)
|
||||
return {"valid": is_valid}
|
||||
|
||||
@router.post("/auth/refresh")
|
||||
async def refresh_token(access_token: str, client_token: str):
|
||||
result = await AuthService().refresh(access_token, client_token)
|
||||
if not result:
|
||||
raise HTTPException(status_code=401, detail="Invalid tokens")
|
||||
return result
|
||||
|
||||
@router.get("/sessionserver/session/minecraft/profile/{uuid}")
|
||||
async def get_minecraft_profile(uuid: str, unsigned: bool = False):
|
||||
return await AuthService().get_minecraft_profile(uuid)
|
||||
|
||||
@router.post("/sessionserver/session/minecraft/join")
|
||||
async def join_server(request_data: dict = Body(...)):
|
||||
try:
|
||||
await AuthService().join_server(request_data)
|
||||
return Response(status_code=204)
|
||||
except Exception as e:
|
||||
print("Error in join_server:", str(e))
|
||||
raise
|
||||
|
||||
@router.get("/sessionserver/session/minecraft/hasJoined")
|
||||
async def has_joined(username: str, serverId: str):
|
||||
return await AuthService().has_joined(username, serverId)
|
||||
|
||||
@router.get("/users/{username}/coins")
|
||||
async def get_user_coins(username: str):
|
||||
coins_data = await coins_service.get_player_coins(username)
|
||||
if not coins_data:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return coins_data
|
||||
|
||||
@router.get("/users")
|
||||
async def get_users():
|
||||
"""Получение списка всех пользователей"""
|
||||
users = await users_collection.find().to_list(1000)
|
||||
|
||||
# Исключаем чувствительные данные перед отправкой
|
||||
safe_users = []
|
||||
for user in users:
|
||||
safe_users.append({
|
||||
"username": user["username"],
|
||||
"uuid": user["uuid"],
|
||||
"skin_url": user.get("skin_url"),
|
||||
"cloak_url": user.get("cloak_url"),
|
||||
"coins": user.get("coins", 0),
|
||||
"total_time_played": user.get("total_time_played", 0),
|
||||
"is_active": user.get("is_active", True)
|
||||
})
|
||||
|
||||
return {"users": safe_users, "count": len(safe_users)}
|
||||
|
||||
@router.get("/users/{uuid}")
|
||||
async def get_user_by_uuid(uuid: str):
|
||||
"""Получение пользователя по UUID"""
|
||||
user = await users_collection.find_one({"uuid": uuid})
|
||||
if not user:
|
||||
# Пробуем разные форматы UUID
|
||||
if '-' in uuid:
|
||||
user = await users_collection.find_one({"uuid": uuid.replace('-', '')})
|
||||
else:
|
||||
formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
|
||||
user = await users_collection.find_one({"uuid": formatted_uuid})
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Исключаем чувствительные данные
|
||||
safe_user = {
|
||||
"username": user["username"],
|
||||
"uuid": user["uuid"],
|
||||
"skin_url": user.get("skin_url"),
|
||||
"cloak_url": user.get("cloak_url"),
|
||||
"coins": user.get("coins", 0),
|
||||
"total_time_played": user.get("total_time_played", 0),
|
||||
"is_active": user.get("is_active", True),
|
||||
"created_at": user.get("created_at")
|
||||
}
|
||||
|
||||
if "total_time_played" in safe_user:
|
||||
total_time = safe_user["total_time_played"]
|
||||
hours, remainder = divmod(total_time, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
safe_user["total_time_formatted"] = f"{hours}ч {minutes}м {seconds}с"
|
||||
|
||||
return safe_user
|
||||
|
||||
@router.post("/auth/verify_code")
|
||||
async def verify_code(verify_code: VerifyCode):
|
||||
return await AuthService().verify_code(verify_code.username, verify_code.code, verify_code.telegram_chat_id)
|
||||
|
||||
@router.post("/auth/generate_code")
|
||||
async def generate_code(username: str):
|
||||
return await AuthService().generate_code(username)
|
||||
|
||||
@router.get("/auth/verification_status/{username}")
|
||||
async def get_verification_status(username: str):
|
||||
return await AuthService().get_verification_status(username)
|
0
app/core/__init__.py
Normal file
0
app/core/__init__.py
Normal file
11
app/core/config.py
Normal file
11
app/core/config.py
Normal file
@ -0,0 +1,11 @@
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
load_dotenv(dotenv_path=Path(__file__).parent.parent.parent / ".env")
|
||||
|
||||
FILES_URL = os.getenv("FILES_URL")
|
||||
MONGO_URI = os.getenv("MONGO_URI")
|
||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 часа
|
0
app/db/__init__.py
Normal file
0
app/db/__init__.py
Normal file
9
app/db/database.py
Normal file
9
app/db/database.py
Normal file
@ -0,0 +1,9 @@
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
from app.core.config import MONGO_URI
|
||||
|
||||
client = AsyncIOMotorClient(MONGO_URI)
|
||||
print(MONGO_URI)
|
||||
db = client["minecraft-api"]
|
||||
|
||||
users_collection = db["users"]
|
||||
sessions_collection = db["sessions"]
|
0
app/models/__init__.py
Normal file
0
app/models/__init__.py
Normal file
48
app/models/bonus.py
Normal file
48
app/models/bonus.py
Normal file
@ -0,0 +1,48 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
class PurchaseBonus(BaseModel):
|
||||
username: str
|
||||
bonus_type_id: str
|
||||
|
||||
class BonusEffect(BaseModel):
|
||||
effect_type: str
|
||||
effect_value: float
|
||||
expires_at: Optional[datetime] = None
|
||||
|
||||
class BonusType(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
effect_type: str # "experience", "strength", "speed", etc.
|
||||
base_effect_value: float # Базовое значение эффекта (например, 1.0 для +100%)
|
||||
effect_increment: float # Прирост эффекта за уровень (например, 0.1 для +10%)
|
||||
price: int # Базовая цена
|
||||
upgrade_price: int # Цена улучшения за уровень
|
||||
duration: int # Длительность в секундах (0 для бесконечных)
|
||||
max_level: int = 0 # 0 = без ограничения уровней
|
||||
|
||||
class UserTypeBonus(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
effect_type: str
|
||||
effect_value: float
|
||||
level: int
|
||||
purchased_at: datetime
|
||||
can_upgrade: bool
|
||||
upgrade_price: int
|
||||
expires_at: Optional[datetime] = None
|
||||
is_active: bool = True
|
||||
is_permanent: bool
|
||||
|
||||
class UserBonus(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
username: str
|
||||
bonus_type_id: str
|
||||
level: int
|
||||
purchased_at: datetime
|
||||
expires_at: Optional[datetime] = None
|
||||
is_active: bool
|
25
app/models/cape.py
Normal file
25
app/models/cape.py
Normal file
@ -0,0 +1,25 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class CapeUpdate(BaseModel):
|
||||
cape_url: str
|
||||
|
||||
class CapeStore(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
price: int
|
||||
file_name: str
|
||||
|
||||
class CapeStoreCreate(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
price: int
|
||||
|
||||
class CapeStoreUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
price: Optional[int] = None
|
||||
|
||||
class CapePurchase(BaseModel):
|
||||
cape_id: str
|
20
app/models/marketplace.py
Normal file
20
app/models/marketplace.py
Normal file
@ -0,0 +1,20 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
class MarketplaceItemBase(BaseModel):
|
||||
material: str
|
||||
amount: int
|
||||
price: int
|
||||
seller_name: str
|
||||
server_ip: str
|
||||
display_name: Optional[str] = None
|
||||
lore: Optional[List[str]] = None
|
||||
enchants: Optional[Dict[str, int]] = None
|
||||
item_data: Optional[Dict[str, Any]] = None # Дополнительные данные предмета
|
||||
|
||||
class MarketplaceItem(MarketplaceItemBase):
|
||||
id: str
|
||||
created_at: str
|
||||
|
||||
class BuyItemRequest(BaseModel):
|
||||
username: str
|
5
app/models/request.py
Normal file
5
app/models/request.py
Normal file
@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ValidateRequest(BaseModel):
|
||||
accessToken: str # camelCase
|
||||
clientToken: str
|
13
app/models/server/command.py
Normal file
13
app/models/server/command.py
Normal file
@ -0,0 +1,13 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class ServerCommand(BaseModel):
|
||||
command: str
|
||||
server_ip: str
|
||||
require_online_player: Optional[bool] = False
|
||||
target_message: Optional[str] = None # Сообщение для цели
|
||||
global_message: Optional[str] = None # Сообщение для остальных
|
||||
|
||||
class InventoryRequest(BaseModel):
|
||||
server_ip: str
|
||||
player_name: str
|
17
app/models/server/event.py
Normal file
17
app/models/server/event.py
Normal file
@ -0,0 +1,17 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
|
||||
class PlayerEvent(BaseModel):
|
||||
event_type: str
|
||||
player_id: Optional[str] = None
|
||||
player_name: str
|
||||
duration: Optional[int] = None # в секундах
|
||||
timestamp: Optional[int] = None # UNIX timestamp в миллисекундах
|
||||
server_ip: str
|
||||
|
||||
class OnlinePlayersUpdate(BaseModel):
|
||||
event_type: str = "online_players_update"
|
||||
players: List[Dict]
|
||||
timestamp: int
|
||||
server_ip: str
|
17
app/models/server/playtime.py
Normal file
17
app/models/server/playtime.py
Normal file
@ -0,0 +1,17 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
class PlayerSession(BaseModel):
|
||||
player_id: str
|
||||
player_name: str
|
||||
server_ip: str
|
||||
start_time: datetime
|
||||
end_time: Optional[datetime] = None
|
||||
duration: Optional[int] = None # в секундах
|
||||
|
||||
class PlayerPlaytime(BaseModel):
|
||||
player_id: str
|
||||
player_name: str
|
||||
total_time: int # общее время в секундах
|
||||
last_coins_update: datetime # последнее время начисления монет
|
36
app/models/server/prank.py
Normal file
36
app/models/server/prank.py
Normal file
@ -0,0 +1,36 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
|
||||
class PrankCommandCreate(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
price: int
|
||||
command_template: str
|
||||
server_ids: List[str] = Field(
|
||||
default=[],
|
||||
description='Список серверов, где доступна команда. Использование ["*"] означает доступность на всех серверах'
|
||||
)
|
||||
targetDescription: Optional[str] = None # Сообщение для целевого игрока
|
||||
globalDescription: Optional[str] = None # Сообщение для всех остальных
|
||||
|
||||
class PrankCommandUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
price: Optional[int] = None
|
||||
command_template: Optional[str] = None
|
||||
server_ids: Optional[List[str]] = None
|
||||
targetDescription: Optional[str] = None
|
||||
globalDescription: Optional[str] = None
|
||||
|
||||
class PrankCommand(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
price: int
|
||||
command_template: str
|
||||
server_ids: List[str] = []
|
||||
|
||||
class PrankExecute(BaseModel):
|
||||
command_id: str
|
||||
target_player: str
|
||||
server_id: str
|
5
app/models/skin.py
Normal file
5
app/models/skin.py
Normal file
@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class SkinUpdate(BaseModel):
|
||||
skin_model: Optional[str] = "classic"
|
37
app/models/user.py
Normal file
37
app/models/user.py
Normal file
@ -0,0 +1,37 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class UserInDB(BaseModel):
|
||||
username: str
|
||||
hashed_password: str
|
||||
uuid: str
|
||||
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()
|
||||
code: Optional[str] = None
|
||||
telegram_id: Optional[str] = None
|
||||
is_verified: bool = False
|
||||
code_expires_at: Optional[datetime] = None
|
||||
class Session(BaseModel):
|
||||
access_token: str
|
||||
client_token: str
|
||||
user_uuid: str
|
||||
expires_at: datetime
|
||||
|
||||
class VerifyCode(BaseModel):
|
||||
username: str
|
||||
code: str
|
||||
telegram_chat_id: int
|
0
app/services/__init__.py
Normal file
0
app/services/__init__.py
Normal file
330
app/services/auth.py
Normal file
330
app/services/auth.py
Normal file
@ -0,0 +1,330 @@
|
||||
import base64
|
||||
import json
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from app.models.user import UserLogin, UserInDB, UserCreate, Session
|
||||
from app.utils.misc import (
|
||||
verify_password,
|
||||
get_password_hash,
|
||||
create_access_token,
|
||||
decode_token,
|
||||
)
|
||||
from ..db.database import users_collection, sessions_collection
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
|
||||
env_path = Path(__file__).parent.parent / ".env"
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
FILES_URL = os.getenv("FILES_URL")
|
||||
|
||||
class AuthService:
|
||||
async def register(self, user: UserCreate):
|
||||
# Проверяем, существует ли пользователь
|
||||
if await users_collection.find_one({"username": user.username}):
|
||||
raise HTTPException(status_code=400, detail="Username already taken")
|
||||
|
||||
# Хешируем пароль
|
||||
hashed_password = get_password_hash(user.password)
|
||||
|
||||
# Создаём UUID для Minecraft
|
||||
user_uuid = str(uuid.uuid4())
|
||||
|
||||
# Сохраняем в MongoDB
|
||||
new_user = UserInDB(
|
||||
username=user.username,
|
||||
hashed_password=hashed_password,
|
||||
uuid=user_uuid,
|
||||
is_verified=False,
|
||||
code=None,
|
||||
code_expires_at=None
|
||||
)
|
||||
await users_collection.insert_one(new_user.dict())
|
||||
return {"status": "success", "uuid": user_uuid}
|
||||
|
||||
async def generate_code(self, username: str):
|
||||
if await users_collection.find_one({"username": username}):
|
||||
if await users_collection.find_one({"username": username, "is_verified": True}):
|
||||
raise HTTPException(400, "User already verified")
|
||||
code = secrets.token_hex(3).upper()
|
||||
await users_collection.update_one({"username": username}, {"$set": {"code": code, "code_expires_at": datetime.utcnow() + timedelta(minutes=10)}})
|
||||
return {"status": "success", "code": code}
|
||||
else:
|
||||
raise HTTPException(404, "User not found")
|
||||
|
||||
async def verify_code(self, username: str, code: str, telegram_chat_id: int):
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(404, "User not found")
|
||||
|
||||
if user["is_verified"]:
|
||||
raise HTTPException(400, "User already verified")
|
||||
|
||||
# Проверяем код и привязку к Telegram
|
||||
if user.get("telegram_chat_id") and user["telegram_chat_id"] != telegram_chat_id:
|
||||
raise HTTPException(403, "This account is linked to another Telegram")
|
||||
|
||||
if user.get("code") != code:
|
||||
raise HTTPException(400, "Invalid code")
|
||||
|
||||
# Обновляем chat_id при первом подтверждении
|
||||
await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$set": {
|
||||
"is_verified": True,
|
||||
"telegram_chat_id": telegram_chat_id,
|
||||
"code": None
|
||||
}}
|
||||
)
|
||||
return {"status": "success"}
|
||||
|
||||
async def get_verification_status(self, username: str):
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(404, "User not found")
|
||||
return {"is_verified": user["is_verified"]}
|
||||
|
||||
async def login(self, credentials: UserLogin):
|
||||
# Ищем пользователя
|
||||
user = await users_collection.find_one({"username": credentials.username})
|
||||
if not user or not verify_password(credentials.password, user["hashed_password"]):
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
if not user["is_verified"]:
|
||||
raise HTTPException(status_code=401, detail="User not verified")
|
||||
|
||||
# Генерируем токены
|
||||
access_token = create_access_token({"sub": user["username"], "uuid": user["uuid"]})
|
||||
client_token = str(uuid.uuid4())
|
||||
|
||||
# Сохраняем сессию
|
||||
session = Session(
|
||||
access_token=access_token,
|
||||
client_token=client_token,
|
||||
user_uuid=user["uuid"],
|
||||
expires_at=datetime.utcnow() + timedelta(minutes=1440),
|
||||
)
|
||||
await sessions_collection.insert_one(session.dict())
|
||||
|
||||
return {
|
||||
"accessToken": access_token,
|
||||
"clientToken": client_token,
|
||||
"selectedProfile": {
|
||||
"id": user["uuid"],
|
||||
"name": user["username"],
|
||||
},
|
||||
}
|
||||
|
||||
async def validate(self, access_token: str, client_token: str):
|
||||
print(f"Searching for access_toke and client_token: '{access_token}', '{client_token}")
|
||||
session = await sessions_collection.find_one({
|
||||
"access_token": access_token,
|
||||
"client_token": client_token,
|
||||
})
|
||||
print("Session from DB:", session)
|
||||
if not session or datetime.utcnow() > session["expires_at"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def refresh(self, access_token: str, client_token: str):
|
||||
if not await self.validate(access_token, client_token):
|
||||
return None
|
||||
|
||||
# Обновляем токен
|
||||
new_access_token = create_access_token({"sub": "user", "uuid": "user_uuid"})
|
||||
await sessions_collection.update_one(
|
||||
{"access_token": access_token},
|
||||
{"$set": {"access_token": new_access_token}},
|
||||
)
|
||||
return {"accessToken": new_access_token, "clientToken": client_token}
|
||||
|
||||
async def get_minecraft_profile(self, uuid: str):
|
||||
# Преобразуем UUID без дефисов в формат с дефисами (если нужно)
|
||||
if '-' not in uuid:
|
||||
formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
|
||||
else:
|
||||
formatted_uuid = uuid
|
||||
|
||||
user = await users_collection.find_one({"uuid": formatted_uuid}) # Ищем по UUID с дефисами
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
textures = {
|
||||
"timestamp": int(datetime.now().timestamp() * 1000),
|
||||
"profileId": user["uuid"].replace("-", ""),
|
||||
"profileName": user["username"],
|
||||
"textures": {}
|
||||
}
|
||||
|
||||
if user.get("skin_url"):
|
||||
textures["textures"]["SKIN"] = {
|
||||
"url": user["skin_url"],
|
||||
"metadata": {"model": user.get("skin_model", "classic")}
|
||||
}
|
||||
|
||||
if user.get("cloak_url"):
|
||||
textures["textures"]["CAPE"] = {"url": user["cloak_url"]}
|
||||
|
||||
textures_json = json.dumps(textures).encode()
|
||||
base64_textures = base64.b64encode(textures_json).decode()
|
||||
|
||||
try:
|
||||
# Подписываем текстуры
|
||||
private_key_path = "app/keys/private_key.pem"
|
||||
with open(private_key_path, "rb") as key_file:
|
||||
private_key = serialization.load_pem_private_key(
|
||||
key_file.read(),
|
||||
password=None
|
||||
)
|
||||
|
||||
signature = private_key.sign(
|
||||
base64.b64encode(textures_json),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA1()
|
||||
)
|
||||
|
||||
signature_base64 = base64.b64encode(signature).decode()
|
||||
|
||||
return {
|
||||
"id": user["uuid"].replace("-", ""),
|
||||
"name": user["username"],
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": base64_textures,
|
||||
"signature": signature_base64
|
||||
}]
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error signing textures: {e}")
|
||||
# В случае ошибки возвращаем текстуры без подписи
|
||||
return {
|
||||
"id": user["uuid"].replace("-", ""),
|
||||
"name": user["username"],
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": base64_textures
|
||||
}]
|
||||
}
|
||||
|
||||
# # Подписываем текстуры
|
||||
# with open("private_key.pem", "rb") as key_file:
|
||||
# private_key = serialization.load_pem_private_key(
|
||||
# key_file.read(),
|
||||
# password=None
|
||||
# )
|
||||
|
||||
# signature = private_key.sign(
|
||||
# textures_json,
|
||||
# padding.PKCS1v15(),
|
||||
# hashes.SHA1()
|
||||
# )
|
||||
|
||||
# return JSONResponse({
|
||||
# "id": user["uuid"].replace("-", ""), # Уберите дефисы
|
||||
# "name": user["username"],
|
||||
# "properties": [{
|
||||
# "name": "textures",
|
||||
# "value": base64_textures,
|
||||
# # "signature": base64.b64encode(signature).decode()
|
||||
# }]
|
||||
# })
|
||||
|
||||
async def join_server(self, request_data: dict):
|
||||
access_token = request_data.get("accessToken")
|
||||
selected_profile = request_data.get("selectedProfile") # UUID без дефисов
|
||||
server_id = request_data.get("serverId")
|
||||
|
||||
if not all([access_token, selected_profile, server_id]):
|
||||
raise HTTPException(status_code=400, detail="Missing required parameters")
|
||||
|
||||
decoded_token = decode_token(access_token)
|
||||
if not decoded_token:
|
||||
raise HTTPException(status_code=401, detail="Invalid access token")
|
||||
|
||||
token_uuid = decoded_token.get("uuid", "").replace("-", "")
|
||||
if token_uuid != selected_profile:
|
||||
raise HTTPException(status_code=403, detail="Token doesn't match selected profile")
|
||||
|
||||
# Сохраняем server_id в сессию
|
||||
await sessions_collection.update_one(
|
||||
{"user_uuid": decoded_token["uuid"]}, # UUID с дефисами
|
||||
{"$set": {"server_id": server_id}},
|
||||
upsert=True
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
async def has_joined(self, username: str, server_id: str):
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Ищем сессию с этим server_id
|
||||
session = await sessions_collection.find_one({
|
||||
"user_uuid": user["uuid"], # UUID с дефисами
|
||||
"server_id": server_id
|
||||
})
|
||||
if not session:
|
||||
raise HTTPException(status_code=403, detail="Not joined this server")
|
||||
|
||||
textures = {
|
||||
"timestamp": int(datetime.now().timestamp() * 1000),
|
||||
"profileId": user["uuid"].replace("-", ""),
|
||||
"profileName": user["username"],
|
||||
"textures": {}
|
||||
}
|
||||
|
||||
if user.get("skin_url"):
|
||||
textures["textures"]["SKIN"] = {
|
||||
"url": user["skin_url"],
|
||||
"metadata": {"model": user.get("skin_model", "classic")}
|
||||
}
|
||||
|
||||
if user.get("cloak_url"):
|
||||
textures["textures"]["CAPE"] = {"url": user["cloak_url"]}
|
||||
|
||||
textures_json = json.dumps(textures).encode()
|
||||
base64_textures = base64.b64encode(textures_json).decode()
|
||||
|
||||
try:
|
||||
# Подписываем текстуры
|
||||
private_key_path = "app/keys/private_key.pem"
|
||||
with open(private_key_path, "rb") as key_file:
|
||||
private_key = serialization.load_pem_private_key(
|
||||
key_file.read(),
|
||||
password=None
|
||||
)
|
||||
|
||||
signature = private_key.sign(
|
||||
base64.b64encode(textures_json),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA1()
|
||||
)
|
||||
|
||||
signature_base64 = base64.b64encode(signature).decode()
|
||||
|
||||
return {
|
||||
"id": user["uuid"].replace("-", ""),
|
||||
"name": user["username"],
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": base64_textures,
|
||||
"signature": signature_base64
|
||||
}]
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error signing textures: {e}")
|
||||
# В случае ошибки возвращаем текстуры без подписи
|
||||
return {
|
||||
"id": user["uuid"].replace("-", ""),
|
||||
"name": user["username"],
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": base64_textures
|
||||
}]
|
||||
}
|
226
app/services/bonus.py
Normal file
226
app/services/bonus.py
Normal file
@ -0,0 +1,226 @@
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import HTTPException
|
||||
from app.db.database import db
|
||||
from app.services.coins import CoinsService
|
||||
from app.models.bonus import BonusType
|
||||
|
||||
# Коллекции для бонусов
|
||||
bonus_types_collection = db.bonus_types
|
||||
user_bonuses_collection = db.user_bonuses
|
||||
|
||||
class BonusService:
|
||||
async def get_user_active_effects(self, username: str):
|
||||
"""Получить активные эффекты пользователя для плагина"""
|
||||
from app.db.database import users_collection
|
||||
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
return {"effects": []}
|
||||
|
||||
# Находим активные бонусы с учетом бесконечных (expires_at = null) или действующих
|
||||
active_bonuses = await user_bonuses_collection.find({
|
||||
"user_id": str(user["_id"]),
|
||||
"is_active": True,
|
||||
}).to_list(50)
|
||||
|
||||
effects = []
|
||||
for bonus in active_bonuses:
|
||||
bonus_type = await bonus_types_collection.find_one({"id": bonus["bonus_type_id"]})
|
||||
if bonus_type:
|
||||
# Рассчитываем итоговое значение эффекта с учетом уровня
|
||||
level = bonus.get("level", 1)
|
||||
effect_value = bonus_type["base_effect_value"] + (level - 1) * bonus_type["effect_increment"]
|
||||
|
||||
effect = {
|
||||
"effect_type": bonus_type["effect_type"],
|
||||
"effect_value": effect_value
|
||||
}
|
||||
|
||||
# Для временных бонусов добавляем срок
|
||||
if bonus.get("expires_at"):
|
||||
effect["expires_at"] = bonus["expires_at"].isoformat()
|
||||
|
||||
effects.append(effect)
|
||||
|
||||
return {"effects": effects}
|
||||
|
||||
async def list_available_bonuses(self):
|
||||
"""Получить список доступных типов бонусов"""
|
||||
bonuses = await bonus_types_collection.find().to_list(50)
|
||||
return {"bonuses": [BonusType(**bonus) for bonus in bonuses]}
|
||||
|
||||
async def get_user_bonuses(self, username: str):
|
||||
"""Получить активные бонусы пользователя"""
|
||||
from app.db.database import users_collection
|
||||
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Находим активные бонусы с учетом бесконечных (expires_at = null) или действующих
|
||||
active_bonuses = await user_bonuses_collection.find({
|
||||
"user_id": str(user["_id"]),
|
||||
"is_active": True,
|
||||
}).to_list(50)
|
||||
|
||||
result = []
|
||||
for bonus in active_bonuses:
|
||||
bonus_type = await bonus_types_collection.find_one({"id": bonus["bonus_type_id"]})
|
||||
if bonus_type:
|
||||
# Рассчитываем итоговое значение эффекта с учетом уровня
|
||||
level = bonus.get("level", 1)
|
||||
effect_value = bonus_type["base_effect_value"] + (level - 1) * bonus_type["effect_increment"]
|
||||
|
||||
bonus_data = {
|
||||
"id": bonus["id"],
|
||||
"name": bonus_type["name"],
|
||||
"description": bonus_type["description"],
|
||||
"effect_type": bonus_type["effect_type"],
|
||||
"effect_value": effect_value,
|
||||
"level": level,
|
||||
"purchased_at": bonus["purchased_at"].isoformat(),
|
||||
"can_upgrade": bonus_type["max_level"] == 0 or level < bonus_type["max_level"],
|
||||
"upgrade_price": bonus_type["upgrade_price"]
|
||||
}
|
||||
|
||||
# Для временных бонусов добавляем срок
|
||||
if bonus.get("expires_at"):
|
||||
bonus_data["expires_at"] = bonus["expires_at"].isoformat()
|
||||
bonus_data["time_left"] = (bonus["expires_at"] - datetime.utcnow()).total_seconds()
|
||||
else:
|
||||
bonus_data["is_permanent"] = True
|
||||
|
||||
result.append(bonus_data)
|
||||
|
||||
return {"bonuses": result}
|
||||
|
||||
async def purchase_bonus(self, username: str, bonus_type_id: str):
|
||||
"""Покупка базового бонуса пользователем"""
|
||||
from app.db.database import users_collection
|
||||
|
||||
# Находим пользователя
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Находим тип бонуса
|
||||
bonus_type = await bonus_types_collection.find_one({"id": bonus_type_id})
|
||||
if not bonus_type:
|
||||
raise HTTPException(status_code=404, detail="Бонус не найден")
|
||||
|
||||
# Проверяем, есть ли уже такой бонус у пользователя
|
||||
existing_bonus = await user_bonuses_collection.find_one({
|
||||
"user_id": str(user["_id"]),
|
||||
"bonus_type_id": bonus_type_id,
|
||||
"is_active": True
|
||||
})
|
||||
|
||||
if existing_bonus:
|
||||
raise HTTPException(status_code=400, detail="Этот бонус уже приобретен. Вы можете улучшить его.")
|
||||
|
||||
# Проверяем достаточно ли монет
|
||||
coins_service = CoinsService()
|
||||
user_coins = await coins_service.get_balance(username)
|
||||
|
||||
if user_coins < bonus_type["price"]:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Недостаточно монет. Требуется: {bonus_type['price']}, имеется: {user_coins}")
|
||||
|
||||
# Создаем запись о бонусе для пользователя
|
||||
bonus_id = str(uuid.uuid4())
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Если бонус имеет длительность
|
||||
expires_at = None
|
||||
if bonus_type["duration"] > 0:
|
||||
expires_at = now + timedelta(seconds=bonus_type["duration"])
|
||||
|
||||
user_bonus = {
|
||||
"id": bonus_id,
|
||||
"user_id": str(user["_id"]),
|
||||
"username": username,
|
||||
"bonus_type_id": bonus_type_id,
|
||||
"level": 1, # Начальный уровень
|
||||
"purchased_at": now,
|
||||
"expires_at": expires_at,
|
||||
"is_active": True
|
||||
}
|
||||
|
||||
# Сохраняем бонус в БД
|
||||
await user_bonuses_collection.insert_one(user_bonus)
|
||||
|
||||
# Списываем монеты
|
||||
await coins_service.decrease_balance(username, bonus_type["price"])
|
||||
|
||||
# Формируем текст сообщения
|
||||
duration_text = "навсегда" if bonus_type["duration"] == 0 else f"на {bonus_type['duration'] // 60} мин."
|
||||
message = f"Бонус '{bonus_type['name']}' успешно приобретен {duration_text}"
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": message,
|
||||
"remaining_coins": user_coins - bonus_type["price"]
|
||||
}
|
||||
|
||||
async def upgrade_bonus(self, username: str, bonus_id: str):
|
||||
"""Улучшение уже купленного бонуса"""
|
||||
from app.db.database import users_collection
|
||||
|
||||
# Находим пользователя
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Находим бонус пользователя
|
||||
user_bonus = await user_bonuses_collection.find_one({
|
||||
"id": bonus_id,
|
||||
"user_id": str(user["_id"]),
|
||||
"is_active": True
|
||||
})
|
||||
|
||||
if not user_bonus:
|
||||
raise HTTPException(status_code=404, detail="Бонус не найден или не принадлежит вам")
|
||||
|
||||
# Находим тип бонуса
|
||||
bonus_type = await bonus_types_collection.find_one({"id": user_bonus["bonus_type_id"]})
|
||||
if not bonus_type:
|
||||
raise HTTPException(status_code=404, detail="Тип бонуса не найден")
|
||||
|
||||
# Проверяем ограничение на максимальный уровень
|
||||
current_level = user_bonus["level"]
|
||||
if bonus_type["max_level"] > 0 and current_level >= bonus_type["max_level"]:
|
||||
raise HTTPException(status_code=400, detail="Достигнут максимальный уровень бонуса")
|
||||
|
||||
# Рассчитываем стоимость улучшения
|
||||
upgrade_price = bonus_type["upgrade_price"]
|
||||
|
||||
# Проверяем достаточно ли монет
|
||||
coins_service = CoinsService()
|
||||
user_coins = await coins_service.get_balance(username)
|
||||
|
||||
if user_coins < upgrade_price:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Недостаточно монет. Требуется: {upgrade_price}, имеется: {user_coins}")
|
||||
|
||||
# Обновляем уровень бонуса
|
||||
new_level = current_level + 1
|
||||
|
||||
await user_bonuses_collection.update_one(
|
||||
{"id": bonus_id},
|
||||
{"$set": {"level": new_level}}
|
||||
)
|
||||
|
||||
# Списываем монеты
|
||||
await coins_service.decrease_balance(username, upgrade_price)
|
||||
|
||||
# Рассчитываем новое значение эффекта
|
||||
new_effect_value = bonus_type["base_effect_value"] + (new_level - 1) * bonus_type["effect_increment"]
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Бонус '{bonus_type['name']}' улучшен до уровня {new_level}",
|
||||
"new_level": new_level,
|
||||
"effect_value": new_effect_value,
|
||||
"remaining_coins": user_coins - upgrade_price
|
||||
}
|
70
app/services/cape.py
Normal file
70
app/services/cape.py
Normal file
@ -0,0 +1,70 @@
|
||||
from app.db.database import users_collection
|
||||
from app.core.config import FILES_URL
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from datetime import datetime
|
||||
|
||||
class CapeService:
|
||||
async def set_cape(self, username: str, cape_file: UploadFile):
|
||||
"""Установка или замена плаща через загрузку файла (PNG или GIF)"""
|
||||
if not cape_file.content_type.startswith('image/'):
|
||||
raise HTTPException(status_code=400, detail="File must be an image")
|
||||
|
||||
# Определяем расширение
|
||||
ext = None
|
||||
if cape_file.content_type == "image/png":
|
||||
ext = "png"
|
||||
elif cape_file.content_type == "image/gif":
|
||||
ext = "gif"
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Only PNG and GIF capes are supported")
|
||||
|
||||
max_size = 2 * 1024 * 1024 # 2MB
|
||||
contents = await cape_file.read()
|
||||
if len(contents) > max_size:
|
||||
raise HTTPException(status_code=400, detail="File too large (max 2MB)")
|
||||
|
||||
# Удаляем старый плащ, если есть
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if user and user.get("cloak_url"):
|
||||
from urllib.parse import urlparse
|
||||
import os
|
||||
old_url = user["cloak_url"]
|
||||
old_filename = os.path.basename(urlparse(old_url).path)
|
||||
old_path = os.path.join("/app/static/capes", old_filename)
|
||||
if os.path.exists(old_path):
|
||||
try:
|
||||
os.remove(old_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Создаем папку для плащей, если ее нет
|
||||
from pathlib import Path
|
||||
cape_dir = Path("/app/static/capes")
|
||||
cape_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"
|
||||
cape_path = cape_dir / cape_filename
|
||||
|
||||
with open(cape_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$set": {
|
||||
"cloak_url": f"{FILES_URL}/capes/{cape_filename}"
|
||||
}}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"status": "success"}
|
||||
|
||||
async def remove_cape(self, username: str):
|
||||
"""Удаление плаща"""
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$unset": {"cloak_url": ""}}
|
||||
)
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"status": "success"}
|
142
app/services/coins.py
Normal file
142
app/services/coins.py
Normal file
@ -0,0 +1,142 @@
|
||||
from datetime import datetime
|
||||
from app.db.database import users_collection, sessions_collection
|
||||
from fastapi import HTTPException
|
||||
|
||||
class CoinsService:
|
||||
async def update_player_coins(self, player_id: str, player_name: str, online_time: int, server_ip: str):
|
||||
"""Обновляет монеты игрока на основе времени онлайн"""
|
||||
|
||||
# Находим пользователя
|
||||
user = await self._find_user_by_uuid(player_id)
|
||||
if not user:
|
||||
return # Пользователь не найден
|
||||
|
||||
# Находим последнее обновление монет
|
||||
last_update = await sessions_collection.find_one({
|
||||
"player_id": player_id,
|
||||
"server_ip": server_ip,
|
||||
"update_type": "coins_update"
|
||||
}, sort=[("timestamp", -1)])
|
||||
|
||||
now = datetime.now()
|
||||
current_coins = user.get("coins", 0)
|
||||
current_total_time = user.get("total_time_played", 0)
|
||||
|
||||
if last_update:
|
||||
# Время с последнего начисления
|
||||
last_timestamp = last_update["timestamp"]
|
||||
seconds_since_update = int((now - last_timestamp).total_seconds())
|
||||
|
||||
# Начисляем монеты только за полные минуты
|
||||
minutes_to_reward = seconds_since_update // 60
|
||||
|
||||
# Если прошло меньше минуты, пропускаем
|
||||
if minutes_to_reward < 1:
|
||||
return
|
||||
else:
|
||||
# Первое обновление (ограничиваем для безопасности)
|
||||
minutes_to_reward = min(online_time // 60, 5)
|
||||
|
||||
if minutes_to_reward > 0:
|
||||
# Обновляем монеты и время
|
||||
new_coins = current_coins + minutes_to_reward
|
||||
new_total_time = current_total_time + (minutes_to_reward * 60)
|
||||
|
||||
# Сохраняем в БД
|
||||
await users_collection.update_one(
|
||||
{"_id": user["_id"]},
|
||||
{"$set": {
|
||||
"coins": new_coins,
|
||||
"total_time_played": new_total_time
|
||||
}}
|
||||
)
|
||||
|
||||
# Сохраняем запись о начислении
|
||||
await sessions_collection.insert_one({
|
||||
"player_id": player_id,
|
||||
"player_name": player_name,
|
||||
"server_ip": server_ip,
|
||||
"update_type": "coins_update",
|
||||
"timestamp": now,
|
||||
"minutes_added": minutes_to_reward,
|
||||
"coins_added": minutes_to_reward
|
||||
})
|
||||
|
||||
print(f"[{now}] Игроку {user.get('username')} начислено {minutes_to_reward} монет. "
|
||||
f"Всего монет: {new_coins}")
|
||||
|
||||
async def _find_user_by_uuid(self, player_id: str):
|
||||
"""Находит пользователя по UUID с поддержкой разных форматов"""
|
||||
|
||||
# Пробуем найти как есть
|
||||
user = await users_collection.find_one({"uuid": player_id})
|
||||
if user:
|
||||
return user
|
||||
|
||||
# Пробуем разные форматы UUID
|
||||
if '-' in player_id:
|
||||
user = await users_collection.find_one({"uuid": player_id.replace('-', '')})
|
||||
else:
|
||||
formatted_uuid = f"{player_id[:8]}-{player_id[8:12]}-{player_id[12:16]}-{player_id[16:20]}-{player_id[20:]}"
|
||||
user = await users_collection.find_one({"uuid": formatted_uuid})
|
||||
|
||||
return user
|
||||
|
||||
async def get_player_coins(self, username: str):
|
||||
"""Возвращает информацию о монетах и времени игрока"""
|
||||
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
return None
|
||||
|
||||
total_time = user.get("total_time_played", 0)
|
||||
hours, remainder = divmod(total_time, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
return {
|
||||
"username": username,
|
||||
"coins": user.get("coins", 0),
|
||||
"total_time_played": {
|
||||
"seconds": total_time,
|
||||
"formatted": f"{hours}ч {minutes}м {seconds}с"
|
||||
}
|
||||
}
|
||||
|
||||
async def get_balance(self, username: str) -> int:
|
||||
"""Получить текущий баланс пользователя"""
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден")
|
||||
return user.get("coins", 0)
|
||||
|
||||
async def increase_balance(self, username: str, amount: int) -> int:
|
||||
"""Увеличить баланс пользователя"""
|
||||
if amount <= 0:
|
||||
raise ValueError("Сумма должна быть положительной")
|
||||
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$inc": {"coins": amount}}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден")
|
||||
|
||||
user = await users_collection.find_one({"username": username})
|
||||
return user.get("coins", 0)
|
||||
|
||||
async def decrease_balance(self, username: str, amount: int) -> int:
|
||||
"""Уменьшить баланс пользователя"""
|
||||
if amount <= 0:
|
||||
raise ValueError("Сумма должна быть положительной")
|
||||
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$inc": {"coins": -amount}} # Уменьшаем на отрицательное значение
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден")
|
||||
|
||||
user = await users_collection.find_one({"username": username})
|
||||
return user.get("coins", 0)
|
278
app/services/marketplace.py
Normal file
278
app/services/marketplace.py
Normal file
@ -0,0 +1,278 @@
|
||||
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": "Покупка в обработке. Предмет будет добавлен в ваш инвентарь."
|
||||
}
|
||||
|
||||
async def cancel_item_sale(self, username: str, item_id: str):
|
||||
"""Снять предмет с продажи"""
|
||||
# Находим предмет
|
||||
item = await marketplace_collection.find_one({"id": item_id})
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Предмет не найден")
|
||||
|
||||
# Проверяем, что пользователь является владельцем предмета
|
||||
if item["seller_name"] != username:
|
||||
raise HTTPException(status_code=403, detail="Вы не можете снять с продажи чужой предмет")
|
||||
|
||||
# Создаем операцию возврата предмета
|
||||
operation_id = str(uuid.uuid4())
|
||||
|
||||
operation = {
|
||||
"id": operation_id,
|
||||
"type": "cancel_sale",
|
||||
"player_name": username,
|
||||
"item_id": item_id,
|
||||
"item_data": item["item_data"],
|
||||
"server_ip": item["server_ip"],
|
||||
"status": "pending",
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await marketplace_operations.insert_one(operation)
|
||||
|
||||
# Удаляем предмет с торговой площадки
|
||||
await marketplace_collection.delete_one({"id": item_id})
|
||||
|
||||
return {
|
||||
"status": "pending",
|
||||
"operation_id": operation_id,
|
||||
"message": "Предмет снят с продажи и будет возвращен в ваш инвентарь"
|
||||
}
|
||||
|
||||
async def update_item_price(self, username: str, item_id: str, new_price: int):
|
||||
"""Обновить цену предмета на торговой площадке"""
|
||||
# Находим предмет
|
||||
item = await marketplace_collection.find_one({"id": item_id})
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Предмет не найден")
|
||||
|
||||
# Проверяем, что пользователь является владельцем предмета
|
||||
if item["seller_name"] != username:
|
||||
raise HTTPException(status_code=403, detail="Вы не можете изменить цену чужого предмета")
|
||||
|
||||
# Валидация новой цены
|
||||
if new_price <= 0:
|
||||
raise HTTPException(status_code=400, detail="Цена должна быть положительным числом")
|
||||
|
||||
# Обновляем цену предмета
|
||||
result = await marketplace_collection.update_one(
|
||||
{"id": item_id},
|
||||
{"$set": {"price": new_price}}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=500, detail="Не удалось обновить цену предмета")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Цена предмета обновлена на {new_price} монет"
|
||||
}
|
234
app/services/server/command.py
Normal file
234
app/services/server/command.py
Normal file
@ -0,0 +1,234 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from fastapi import HTTPException
|
||||
from typing import Dict
|
||||
from app.db.database import db
|
||||
import asyncio
|
||||
|
||||
# Создаем коллекции для хранения команд и инвентаря
|
||||
pending_commands_collection = db.pending_commands
|
||||
inventory_requests_collection = db.inventory_requests
|
||||
inventory_collection = db.inventory
|
||||
game_servers_collection = db.game_servers
|
||||
|
||||
class CommandService:
|
||||
async def add_command(self, command_data):
|
||||
try:
|
||||
command_id = str(uuid.uuid4())
|
||||
command_doc = {
|
||||
"id": command_id,
|
||||
"command": command_data.command,
|
||||
"server_ip": command_data.server_ip,
|
||||
"require_online_player": command_data.require_online_player,
|
||||
"target_message": command_data.target_message if hasattr(command_data, 'target_message') else None,
|
||||
"global_message": command_data.global_message if hasattr(command_data, 'global_message') else None,
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await pending_commands_collection.insert_one(command_doc)
|
||||
print(f"[{datetime.now()}] Добавлена команда: {command_data.command} "
|
||||
f"для сервера {command_data.server_ip}")
|
||||
|
||||
# Обновляем last_activity для сервера
|
||||
await self._update_server_activity(command_data.server_ip)
|
||||
|
||||
return {"status": "success", "command_id": command_id}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def get_commands(self, server_ip: str):
|
||||
try:
|
||||
# Получаем команды для указанного сервера
|
||||
commands_cursor = pending_commands_collection.find({"server_ip": server_ip})
|
||||
commands = await commands_cursor.to_list(1000)
|
||||
|
||||
result_commands = [
|
||||
{
|
||||
"id": cmd["id"],
|
||||
"command": cmd["command"],
|
||||
"require_online_player": cmd["require_online_player"],
|
||||
"target_message": cmd.get("target_message"),
|
||||
"global_message": cmd.get("global_message")
|
||||
}
|
||||
for cmd in commands
|
||||
]
|
||||
|
||||
# Удаляем полученные команды (чтобы не выполнять их повторно)
|
||||
await pending_commands_collection.delete_many({"server_ip": server_ip})
|
||||
|
||||
return {"status": "success", "commands": result_commands}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def request_inventory(self, inventory_request):
|
||||
"""Создаёт запрос на получение инвентаря игрока"""
|
||||
try:
|
||||
request_id = str(uuid.uuid4())
|
||||
inventory_request_doc = {
|
||||
"id": request_id,
|
||||
"server_ip": inventory_request.server_ip,
|
||||
"player_name": inventory_request.player_name,
|
||||
"created_at": datetime.utcnow(),
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
await inventory_requests_collection.insert_one(inventory_request_doc)
|
||||
|
||||
print(f"[{datetime.now()}] Запрос инвентаря игрока {inventory_request.player_name} "
|
||||
f"с сервера {inventory_request.server_ip}")
|
||||
|
||||
# Обновляем last_activity для сервера
|
||||
await self._update_server_activity(inventory_request.server_ip)
|
||||
|
||||
return {"status": "pending", "request_id": request_id}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def get_inventory_requests(self, server_ip: str):
|
||||
"""Получает запросы на инвентарь для указанного сервера"""
|
||||
try:
|
||||
requests_cursor = inventory_requests_collection.find(
|
||||
{"server_ip": server_ip, "status": "pending"}
|
||||
)
|
||||
requests = await requests_cursor.to_list(1000)
|
||||
|
||||
result_requests = [
|
||||
{
|
||||
"id": req["id"],
|
||||
"player_name": req["player_name"]
|
||||
}
|
||||
for req in requests
|
||||
]
|
||||
|
||||
# Помечаем запросы как обработанные
|
||||
for req in result_requests:
|
||||
await inventory_requests_collection.update_one(
|
||||
{"id": req["id"]},
|
||||
{"$set": {"status": "processing"}}
|
||||
)
|
||||
|
||||
return {"status": "success", "inventory_requests": result_requests}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def submit_inventory(self, inventory_data: dict):
|
||||
"""Принимает данные инвентаря от сервера"""
|
||||
try:
|
||||
request_id = inventory_data.get("request_id")
|
||||
request = await inventory_requests_collection.find_one({"id": request_id})
|
||||
|
||||
if not request:
|
||||
raise HTTPException(status_code=404, detail="Запрос не найден")
|
||||
|
||||
player_name = request["player_name"]
|
||||
server_ip = request["server_ip"]
|
||||
|
||||
# Обновляем или создаем запись инвентаря
|
||||
await inventory_collection.update_one(
|
||||
{
|
||||
"player_name": player_name,
|
||||
"server_ip": server_ip
|
||||
},
|
||||
{
|
||||
"$set": {
|
||||
"inventory_data": inventory_data.get("inventory", []),
|
||||
"updated_at": datetime.utcnow()
|
||||
}
|
||||
},
|
||||
upsert=True # Создает новую запись, если не найдена существующая
|
||||
)
|
||||
|
||||
# Помечаем запрос как выполненный
|
||||
await inventory_requests_collection.update_one(
|
||||
{"id": request_id},
|
||||
{"$set": {"status": "completed"}}
|
||||
)
|
||||
|
||||
return {"status": "success"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def get_inventory_result(self, request_id: str):
|
||||
"""Получает результаты запроса инвентаря"""
|
||||
request = await inventory_requests_collection.find_one({"id": request_id})
|
||||
if not request:
|
||||
raise HTTPException(status_code=404, detail="Запрос не найден")
|
||||
|
||||
if request["status"] != "completed":
|
||||
return {"status": request["status"]}
|
||||
|
||||
# Получаем инвентарь из коллекции inventory
|
||||
inventory = await inventory_collection.find_one({
|
||||
"player_name": request["player_name"],
|
||||
"server_ip": request["server_ip"]
|
||||
})
|
||||
|
||||
if not inventory:
|
||||
raise HTTPException(status_code=404, detail="Инвентарь не найден")
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"result": {
|
||||
"player_name": inventory["player_name"],
|
||||
"server_ip": inventory["server_ip"],
|
||||
"inventory_data": inventory["inventory_data"],
|
||||
"updated_at": inventory["updated_at"]
|
||||
}
|
||||
}
|
||||
|
||||
async def get_player_inventory(self, player_name: str, server_ip: str, timeout: int = 10):
|
||||
"""Запрашивает и ждет получения инвентаря игрока"""
|
||||
try:
|
||||
# Проверяем, есть ли уже актуальный инвентарь
|
||||
existing_inventory = await inventory_collection.find_one({
|
||||
"player_name": player_name,
|
||||
"server_ip": server_ip
|
||||
})
|
||||
|
||||
# Если инвентарь уже есть и он достаточно свежий (не старше 1 минуты)
|
||||
if existing_inventory and "updated_at" in existing_inventory:
|
||||
if (datetime.utcnow() - existing_inventory["updated_at"]).total_seconds() < 60:
|
||||
return {
|
||||
"status": "success",
|
||||
"player_name": existing_inventory["player_name"],
|
||||
"server_ip": existing_inventory["server_ip"],
|
||||
"inventory": existing_inventory["inventory_data"],
|
||||
"updated_at": existing_inventory["updated_at"]
|
||||
}
|
||||
|
||||
# Запрашиваем новый инвентарь
|
||||
request_id = str(uuid.uuid4())
|
||||
inventory_request_doc = {
|
||||
"id": request_id,
|
||||
"server_ip": server_ip,
|
||||
"player_name": player_name,
|
||||
"created_at": datetime.utcnow(),
|
||||
"status": "pending"
|
||||
}
|
||||
await inventory_requests_collection.insert_one(inventory_request_doc)
|
||||
|
||||
print(f"[{datetime.now()}] Запрос инвентаря игрока {player_name} "
|
||||
f"с сервера {server_ip}")
|
||||
|
||||
# Обновляем last_activity для сервера
|
||||
await self._update_server_activity(server_ip)
|
||||
|
||||
# Ждем ответа от сервера
|
||||
start_time = datetime.utcnow()
|
||||
while (datetime.utcnow() - start_time).total_seconds() < timeout:
|
||||
result = await self.get_inventory_result(request_id)
|
||||
if result["status"] == "completed":
|
||||
return result
|
||||
await asyncio.sleep(1) # Ждем 1 секунду перед следующей проверкой
|
||||
|
||||
raise HTTPException(status_code=504, detail="Timeout waiting for inventory")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
async def _update_server_activity(self, server_ip):
|
||||
"""Обновляет время последней активности для сервера"""
|
||||
await game_servers_collection.update_one(
|
||||
{"ip": server_ip},
|
||||
{"$set": {"last_activity": datetime.utcnow()}},
|
||||
upsert=False # Не создаем новый сервер, только обновляем существующий
|
||||
)
|
290
app/services/server/event.py
Normal file
290
app/services/server/event.py
Normal file
@ -0,0 +1,290 @@
|
||||
from fastapi import HTTPException
|
||||
from datetime import datetime
|
||||
import json
|
||||
from app.services.coins import CoinsService
|
||||
from app.models.server.event import PlayerEvent, OnlinePlayersUpdate
|
||||
import uuid
|
||||
|
||||
class EventService:
|
||||
def __init__(self):
|
||||
self.coins_service = CoinsService()
|
||||
|
||||
async def process_event(self, event_data):
|
||||
"""Обработка событий от сервера Minecraft"""
|
||||
try:
|
||||
# Проверяем формат ваших событий (event_type вместо type)
|
||||
event_type = event_data.get("event_type")
|
||||
if not event_type:
|
||||
# Для совместимости со старым форматом
|
||||
event_type = event_data.get("type")
|
||||
|
||||
if not event_type:
|
||||
raise HTTPException(status_code=400, detail="Missing event type")
|
||||
|
||||
server_ip = event_data.get("server_ip")
|
||||
if not server_ip:
|
||||
raise HTTPException(status_code=400, detail="Missing server IP")
|
||||
|
||||
# Преобразуем ваши типы событий в нужные форматы
|
||||
if event_type == "online_players_update":
|
||||
# Регистрируем сервер, если его нет
|
||||
await self._register_server(server_ip, event_data)
|
||||
|
||||
# Обновляем данные об онлайн игроках
|
||||
players = event_data.get("players", [])
|
||||
await self._update_online_players(server_ip, players)
|
||||
return {"status": "success"}
|
||||
|
||||
elif event_type == "player_join":
|
||||
player_id = event_data.get("player_id")
|
||||
player_name = event_data.get("player_name")
|
||||
|
||||
if not player_id or not player_name:
|
||||
raise HTTPException(status_code=400, detail="Missing player data")
|
||||
|
||||
# Регистрируем вход игрока
|
||||
await self._register_player_login(server_ip, player_id, player_name)
|
||||
return {"status": "success"}
|
||||
|
||||
elif event_type == "player_quit":
|
||||
player_id = event_data.get("player_id")
|
||||
player_name = event_data.get("player_name")
|
||||
|
||||
if not player_id or not player_name:
|
||||
raise HTTPException(status_code=400, detail="Missing player data")
|
||||
|
||||
# Регистрируем выход игрока
|
||||
await self._register_player_logout(server_ip, player_id, player_name)
|
||||
return {"status": "success"}
|
||||
|
||||
elif event_type == "player_session":
|
||||
player_id = event_data.get("player_id")
|
||||
player_name = event_data.get("player_name")
|
||||
duration = event_data.get("duration", 0)
|
||||
|
||||
if not player_id or not player_name:
|
||||
raise HTTPException(status_code=400, detail="Missing player data")
|
||||
|
||||
# Обрабатываем информацию о сессии
|
||||
await self._process_player_session(server_ip, player_id, player_name, duration)
|
||||
return {"status": "success"}
|
||||
|
||||
# Если тип события не распознан
|
||||
print(f"[{datetime.now()}] Неизвестное событие: {event_data}")
|
||||
raise HTTPException(status_code=400, detail="Invalid event type")
|
||||
|
||||
except HTTPException as e:
|
||||
print(f"[{datetime.now()}] Ошибка обработки события: {e.status_code}: {e.detail}")
|
||||
print(f"Полученные данные: {event_data}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[{datetime.now()}] Необработанная ошибка: {str(e)}")
|
||||
print(f"Полученные данные: {event_data}")
|
||||
raise HTTPException(status_code=500, detail=f"Server error: {str(e)}")
|
||||
|
||||
async def _register_server(self, server_ip, event_data):
|
||||
"""Регистрирует сервер, если его нет в базе"""
|
||||
from app.db.database import db
|
||||
import uuid
|
||||
|
||||
game_servers_collection = db.game_servers
|
||||
|
||||
# Проверяем, есть ли уже такой сервер
|
||||
existing_server = await game_servers_collection.find_one({"ip": server_ip})
|
||||
|
||||
if not existing_server:
|
||||
# Создаем новую запись сервера
|
||||
server_data = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": f"Server {server_ip}", # Можно улучшить название
|
||||
"ip": server_ip,
|
||||
"port": 25565, # Стандартный порт Minecraft
|
||||
"description": f"Minecraft server {server_ip}",
|
||||
"max_players": 100,
|
||||
"registered_at": datetime.utcnow(),
|
||||
"last_activity": datetime.utcnow() # Добавляем поле last_activity
|
||||
}
|
||||
|
||||
await game_servers_collection.insert_one(server_data)
|
||||
print(f"[{datetime.utcnow()}] Зарегистрирован новый сервер: {server_ip}")
|
||||
else:
|
||||
# Обновляем активность существующего сервера
|
||||
await game_servers_collection.update_one(
|
||||
{"ip": server_ip},
|
||||
{"$set": {"last_activity": datetime.utcnow()}}
|
||||
)
|
||||
|
||||
return existing_server or await game_servers_collection.find_one({"ip": server_ip})
|
||||
|
||||
async def _update_online_players(self, server_ip, players_data):
|
||||
"""Обновляет информацию об онлайн игроках"""
|
||||
from app.db.database import db
|
||||
|
||||
online_players_collection = db.online_players
|
||||
game_servers_collection = db.game_servers
|
||||
|
||||
# Получаем ID сервера
|
||||
server = await self._register_server(server_ip, {})
|
||||
server_id = server["id"]
|
||||
|
||||
# Обновляем время активности сервера
|
||||
await game_servers_collection.update_one(
|
||||
{"id": server_id},
|
||||
{"$set": {"last_activity": datetime.utcnow()}}
|
||||
)
|
||||
|
||||
# Помечаем всех игроков как оффлайн на этом сервере
|
||||
await online_players_collection.update_many(
|
||||
{"server_id": server_id},
|
||||
{"$set": {"is_online": False}}
|
||||
)
|
||||
|
||||
# Обновляем данные для каждого онлайн игрока
|
||||
now = datetime.utcnow()
|
||||
for player in players_data:
|
||||
player_id = player.get("player_id")
|
||||
player_name = player.get("player_name")
|
||||
online_time = player.get("online_time", 0)
|
||||
|
||||
if not player_id or not player_name:
|
||||
continue
|
||||
|
||||
# Проверяем, существует ли уже запись
|
||||
existing_player = await online_players_collection.find_one({
|
||||
"uuid": player_id,
|
||||
"server_id": server_id
|
||||
})
|
||||
|
||||
if existing_player:
|
||||
# Обновляем существующую запись
|
||||
await online_players_collection.update_one(
|
||||
{"_id": existing_player["_id"]},
|
||||
{"$set": {
|
||||
"username": player_name,
|
||||
"is_online": True,
|
||||
"last_seen": now,
|
||||
"online_duration": online_time
|
||||
}}
|
||||
)
|
||||
else:
|
||||
# Создаем новую запись
|
||||
await online_players_collection.insert_one({
|
||||
"uuid": player_id,
|
||||
"username": player_name,
|
||||
"server_id": server_id,
|
||||
"is_online": True,
|
||||
"login_time": now,
|
||||
"last_seen": now,
|
||||
"online_duration": online_time
|
||||
})
|
||||
|
||||
online_count = len(players_data)
|
||||
print(f"[{now}] Обновлена информация о {online_count} игроках на сервере {server_ip}")
|
||||
|
||||
# Также обновляем информацию о коинах для каждого игрока
|
||||
if players_data:
|
||||
from app.services.coins import CoinsService
|
||||
coins_service = CoinsService()
|
||||
|
||||
for player in players_data:
|
||||
player_id = player.get("player_id")
|
||||
player_name = player.get("player_name")
|
||||
online_time = player.get("online_time", 0)
|
||||
|
||||
if player_id and player_name:
|
||||
await coins_service.update_player_coins(player_id, player_name, online_time, server_ip)
|
||||
|
||||
async def _register_player_login(self, server_ip, player_id, player_name):
|
||||
"""Регистрирует вход игрока на сервер"""
|
||||
from app.db.database import db
|
||||
|
||||
online_players_collection = db.online_players
|
||||
|
||||
server = await self._register_server(server_ip, {})
|
||||
server_id = server["id"]
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Проверяем, есть ли уже запись для этого игрока
|
||||
existing_player = await online_players_collection.find_one({
|
||||
"uuid": player_id,
|
||||
"server_id": server_id
|
||||
})
|
||||
|
||||
if existing_player:
|
||||
# Обновляем запись
|
||||
await online_players_collection.update_one(
|
||||
{"_id": existing_player["_id"]},
|
||||
{"$set": {
|
||||
"username": player_name,
|
||||
"is_online": True,
|
||||
"login_time": now,
|
||||
"last_seen": now
|
||||
}}
|
||||
)
|
||||
else:
|
||||
# Создаем новую запись
|
||||
await online_players_collection.insert_one({
|
||||
"uuid": player_id,
|
||||
"username": player_name,
|
||||
"server_id": server_id,
|
||||
"is_online": True,
|
||||
"login_time": now,
|
||||
"last_seen": now,
|
||||
"online_duration": 0
|
||||
})
|
||||
|
||||
print(f"[{now}] Игрок {player_name} зашел на сервер {server_ip}")
|
||||
|
||||
async def _register_player_logout(self, server_ip, player_id, player_name):
|
||||
"""Регистрирует выход игрока с сервера"""
|
||||
from app.db.database import db
|
||||
|
||||
online_players_collection = db.online_players
|
||||
|
||||
server = await self._register_server(server_ip, {})
|
||||
server_id = server["id"]
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Ищем запись игрока
|
||||
player = await online_players_collection.find_one({
|
||||
"uuid": player_id,
|
||||
"server_id": server_id
|
||||
})
|
||||
|
||||
if player:
|
||||
# Обновляем запись
|
||||
await online_players_collection.update_one(
|
||||
{"_id": player["_id"]},
|
||||
{"$set": {
|
||||
"is_online": False,
|
||||
"last_seen": now
|
||||
}}
|
||||
)
|
||||
|
||||
print(f"[{now}] Игрок {player_name} вышел с сервера {server_ip}")
|
||||
|
||||
async def _process_player_session(self, server_ip, player_id, player_name, duration):
|
||||
"""Обрабатывает информацию о завершенной сессии игрока"""
|
||||
from app.db.database import db
|
||||
from app.services.coins import CoinsService
|
||||
|
||||
server = await self._register_server(server_ip, {})
|
||||
server_id = server["id"]
|
||||
|
||||
# Обновляем статистику времени игры
|
||||
await db.player_sessions.insert_one({
|
||||
"uuid": player_id,
|
||||
"username": player_name,
|
||||
"server_id": server_id,
|
||||
"server_ip": server_ip,
|
||||
"duration": duration,
|
||||
"session_end": datetime.utcnow()
|
||||
})
|
||||
|
||||
# Начисляем коины за время игры
|
||||
coins_service = CoinsService()
|
||||
await coins_service.update_player_coins(player_id, player_name, duration, server_ip)
|
||||
|
||||
print(f"[{datetime.now()}] Сессия игрока {player_name} завершена, длительность: {duration} сек.")
|
288
app/services/server/prank.py
Normal file
288
app/services/server/prank.py
Normal file
@ -0,0 +1,288 @@
|
||||
from fastapi import HTTPException
|
||||
from app.db.database import db, users_collection
|
||||
from app.models.server.prank import PrankCommand, PrankCommandUpdate
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
from app.services.server.command import CommandService
|
||||
|
||||
# Создаем коллекции для хранения пакостей и серверов
|
||||
prank_commands_collection = db.prank_commands
|
||||
game_servers_collection = db.game_servers
|
||||
online_players_collection = db.online_players
|
||||
|
||||
class PrankService:
|
||||
async def add_prank_command(self, command_data):
|
||||
"""Добавление новой команды-пакости"""
|
||||
# Проверяем корректность шаблона команды
|
||||
if "{targetPlayer}" not in command_data.command_template:
|
||||
raise HTTPException(status_code=400,
|
||||
detail="Шаблон команды должен содержать {targetPlayer} для подстановки имени цели")
|
||||
|
||||
prank_id = str(uuid.uuid4())
|
||||
|
||||
# Создаем новую команду в БД
|
||||
prank_command = {
|
||||
"id": prank_id,
|
||||
"name": command_data.name,
|
||||
"description": command_data.description,
|
||||
"price": command_data.price,
|
||||
"command_template": command_data.command_template,
|
||||
"server_ids": command_data.server_ids,
|
||||
"targetDescription": command_data.targetDescription,
|
||||
"globalDescription": command_data.globalDescription, # Добавить это поле
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await prank_commands_collection.insert_one(prank_command)
|
||||
|
||||
return {"status": "success", "id": prank_id}
|
||||
|
||||
async def get_all_prank_commands(self):
|
||||
"""Получение списка всех команд-пакостей"""
|
||||
commands = await prank_commands_collection.find().to_list(1000)
|
||||
result = []
|
||||
|
||||
for cmd in commands:
|
||||
result.append({
|
||||
"id": cmd["id"],
|
||||
"name": cmd["name"],
|
||||
"description": cmd["description"],
|
||||
"price": cmd["price"],
|
||||
"command_template": cmd["command_template"],
|
||||
"server_ids": cmd.get("server_ids", []),
|
||||
"targetDescription": cmd.get("targetDescription"),
|
||||
"globalDescription": cmd.get("globalDescription") # Добавить это поле
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
async def get_prank_command(self, command_id: str):
|
||||
"""Получение конкретной команды по ID"""
|
||||
command = await prank_commands_collection.find_one({"id": command_id})
|
||||
if not command:
|
||||
raise HTTPException(status_code=404, detail="Команда не найдена")
|
||||
|
||||
return {
|
||||
"id": command["id"],
|
||||
"name": command["name"],
|
||||
"description": command["description"],
|
||||
"price": command["price"],
|
||||
"command_template": command["command_template"],
|
||||
"server_ids": command.get("server_ids", []),
|
||||
"targetDescription": command.get("targetDescription"),
|
||||
"globalDescription": command.get("globalDescription") # Добавить это поле
|
||||
}
|
||||
|
||||
async def update_prank_command(self, command_id: str, update_data: PrankCommandUpdate):
|
||||
"""Обновление команды-пакости"""
|
||||
command = await prank_commands_collection.find_one({"id": command_id})
|
||||
if not command:
|
||||
raise HTTPException(status_code=404, detail="Команда не найдена")
|
||||
|
||||
# Готовим данные для обновления
|
||||
update = {}
|
||||
if update_data.name is not None:
|
||||
update["name"] = update_data.name
|
||||
if update_data.description is not None:
|
||||
update["description"] = update_data.description
|
||||
if update_data.price is not None:
|
||||
update["price"] = update_data.price
|
||||
if update_data.command_template is not None:
|
||||
if "{targetPlayer}" not in update_data.command_template:
|
||||
raise HTTPException(status_code=400,
|
||||
detail="Шаблон команды должен содержать {targetPlayer} для подстановки имени цели")
|
||||
update["command_template"] = update_data.command_template
|
||||
if update_data.server_ids is not None:
|
||||
update["server_ids"] = update_data.server_ids
|
||||
if update_data.targetDescription is not None:
|
||||
update["targetDescription"] = update_data.targetDescription
|
||||
if update_data.globalDescription is not None: # Добавить эту проверку
|
||||
update["globalDescription"] = update_data.globalDescription
|
||||
|
||||
if update:
|
||||
result = await prank_commands_collection.update_one(
|
||||
{"id": command_id},
|
||||
{"$set": update}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=500, detail="Ошибка при обновлении команды")
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
async def delete_prank_command(self, command_id: str):
|
||||
"""Удаление команды-пакости"""
|
||||
command = await prank_commands_collection.find_one({"id": command_id})
|
||||
if not command:
|
||||
raise HTTPException(status_code=404, detail="Команда не найдена")
|
||||
|
||||
result = await prank_commands_collection.delete_one({"id": command_id})
|
||||
if result.deleted_count == 0:
|
||||
raise HTTPException(status_code=500, detail="Ошибка при удалении команды")
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
async def get_all_servers(self):
|
||||
"""Получение списка всех доступных серверов"""
|
||||
# Проверяем и удаляем неактивные серверы (более 5 минут без данных)
|
||||
current_time = datetime.utcnow()
|
||||
inactive_threshold = 5 * 60 # 5 минут в секундах
|
||||
|
||||
# Находим серверы, которые не отправляли данные больше 5 минут
|
||||
# Учитываем, что у некоторых серверов может не быть поля last_activity
|
||||
inactive_servers = await game_servers_collection.find({
|
||||
"last_activity": {
|
||||
"$exists": True,
|
||||
"$lt": current_time - timedelta(seconds=inactive_threshold)
|
||||
}
|
||||
}).to_list(100)
|
||||
|
||||
# Удаляем неактивные серверы
|
||||
if inactive_servers:
|
||||
server_ids = [server["id"] for server in inactive_servers]
|
||||
await game_servers_collection.delete_many({"id": {"$in": server_ids}})
|
||||
|
||||
# Опционально: логирование удаленных серверов
|
||||
for server in inactive_servers:
|
||||
print(f"Удален неактивный сервер: {server['name']} (ID: {server['id']})")
|
||||
|
||||
# Получаем актуальный список серверов
|
||||
servers = await game_servers_collection.find().to_list(100)
|
||||
|
||||
# Если нет зарегистрированных серверов, вернем пустой список
|
||||
if not servers:
|
||||
return []
|
||||
|
||||
result = []
|
||||
for server in servers:
|
||||
# Получаем количество онлайн игроков
|
||||
online_count = await online_players_collection.count_documents(
|
||||
{"server_id": server["id"], "is_online": True}
|
||||
)
|
||||
|
||||
result.append({
|
||||
"id": server["id"],
|
||||
"name": server["name"],
|
||||
"ip": server.get("ip"),
|
||||
"port": server.get("port"),
|
||||
"description": server.get("description", ""),
|
||||
"online_players": online_count,
|
||||
"max_players": server.get("max_players", 0),
|
||||
"last_activity": server.get("last_activity")
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
async def get_server_online_players(self, server_id: str):
|
||||
"""Получение списка онлайн игроков на конкретном сервере"""
|
||||
server = await game_servers_collection.find_one({"id": server_id})
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Сервер не найден")
|
||||
|
||||
players = await online_players_collection.find(
|
||||
{"server_id": server_id, "is_online": True}
|
||||
).to_list(1000)
|
||||
|
||||
result = []
|
||||
for player in players:
|
||||
result.append({
|
||||
"username": player["username"],
|
||||
"uuid": player.get("uuid", ""),
|
||||
"online_since": player.get("login_time")
|
||||
})
|
||||
|
||||
return {
|
||||
"server": {
|
||||
"id": server["id"],
|
||||
"name": server["name"]
|
||||
},
|
||||
"online_players": result,
|
||||
"count": len(result)
|
||||
}
|
||||
|
||||
async def execute_prank(self, username: str, command_id: str, target_player: str, server_id: str):
|
||||
"""Выполнение пакости (покупка и выполнение команды)"""
|
||||
# Проверяем пользователя
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Проверяем команду
|
||||
command = await prank_commands_collection.find_one({"id": command_id})
|
||||
if not command:
|
||||
raise HTTPException(status_code=404, detail="Команда не найдена")
|
||||
|
||||
# Проверяем сервер
|
||||
server = await game_servers_collection.find_one({"id": server_id})
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Сервер не найден")
|
||||
|
||||
# Проверяем, доступна ли команда на данном сервере
|
||||
if (command.get("server_ids") and
|
||||
"*" not in command.get("server_ids", []) and
|
||||
server_id not in command.get("server_ids", [])):
|
||||
raise HTTPException(status_code=400, detail="Команда недоступна на выбранном сервере")
|
||||
|
||||
# Проверяем, онлайн ли целевой игрок
|
||||
target_online = await online_players_collection.find_one({
|
||||
"username": target_player,
|
||||
"server_id": server_id,
|
||||
"is_online": True
|
||||
})
|
||||
|
||||
if not target_online:
|
||||
raise HTTPException(status_code=400, detail=f"Игрок {target_player} не в сети на этом сервере")
|
||||
|
||||
# Проверяем достаточно ли монет
|
||||
user_coins = user.get("coins", 0)
|
||||
if user_coins < command["price"]:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Недостаточно монет. Требуется: {command['price']}, имеется: {user_coins}")
|
||||
|
||||
# Формируем команду для выполнения
|
||||
actual_command = command["command_template"].replace("{targetPlayer}", target_player)
|
||||
|
||||
# Обрабатываем оба типа сообщений
|
||||
target_desc = None
|
||||
global_desc = None
|
||||
|
||||
if command.get("targetDescription"):
|
||||
target_desc = command.get("targetDescription").replace("{username}", username).replace("{targetPlayer}", target_player)
|
||||
|
||||
if command.get("globalDescription"):
|
||||
global_desc = command.get("globalDescription").replace("{username}", username).replace("{targetPlayer}", target_player)
|
||||
|
||||
# Отправляем команду с обоими сообщениями
|
||||
command_service = CommandService()
|
||||
from app.models.server.command import ServerCommand
|
||||
|
||||
server_command = ServerCommand(
|
||||
command=actual_command,
|
||||
server_ip=server.get("ip", ""),
|
||||
require_online_player=True,
|
||||
target_message=target_desc, # Сообщение для цели
|
||||
global_message=global_desc # Сообщение для всех остальных
|
||||
)
|
||||
|
||||
command_result = await command_service.add_command(server_command)
|
||||
|
||||
# Логируем выполнение пакости
|
||||
log_entry = {
|
||||
"user_id": user["_id"],
|
||||
"username": username,
|
||||
"target_player": target_player,
|
||||
"command_id": command_id,
|
||||
"command_name": command["name"],
|
||||
"server_id": server_id,
|
||||
"price": command["price"],
|
||||
"executed_command": actual_command,
|
||||
"executed_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await db.prank_executions.insert_one(log_entry)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Команда '{command['name']}' успешно выполнена на игроке {target_player}",
|
||||
"remaining_coins": user_coins - command["price"]
|
||||
}
|
82
app/services/skin.py
Normal file
82
app/services/skin.py
Normal file
@ -0,0 +1,82 @@
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from datetime import datetime
|
||||
from app.db.database import users_collection
|
||||
from app.core.config import FILES_URL
|
||||
|
||||
class SkinService:
|
||||
async def set_skin(self, username: str, skin_file: UploadFile, skin_model: str = "classic"):
|
||||
"""Установка или замена скина через загрузку файла"""
|
||||
# Проверяем тип файла
|
||||
if not skin_file.content_type.startswith('image/'):
|
||||
raise HTTPException(status_code=400, detail="File must be an image")
|
||||
|
||||
# Проверяем размер файла (максимум 2MB)
|
||||
max_size = 2 * 1024 * 1024 # 2MB
|
||||
contents = await skin_file.read()
|
||||
if len(contents) > max_size:
|
||||
raise HTTPException(status_code=400, detail="File too large (max 2MB)")
|
||||
|
||||
# Удаляем старый скин, если есть
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if user and user.get("skin_url"):
|
||||
from urllib.parse import urlparse
|
||||
import os
|
||||
old_url = user["skin_url"]
|
||||
# Получаем имя файла из url
|
||||
old_filename = os.path.basename(urlparse(old_url).path)
|
||||
old_path = os.path.join("app/static/skins", old_filename)
|
||||
print(f"Trying to delete old skin at: {old_path}")
|
||||
if os.path.exists(old_path):
|
||||
try:
|
||||
os.remove(old_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Создаем папку для скинов, если ее нет
|
||||
from pathlib import Path
|
||||
skin_dir = Path("/app/static/skins")
|
||||
skin_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Генерируем имя файла
|
||||
skin_filename = f"{username}_{int(datetime.now().timestamp())}.png"
|
||||
skin_path = skin_dir / skin_filename
|
||||
|
||||
# Сохраняем файл
|
||||
with open(skin_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
# Обновляем запись пользователя
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$set": {
|
||||
"skin_url": f"{FILES_URL}/skins/{skin_filename}",
|
||||
"skin_model": skin_model
|
||||
}}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"status": "success"}
|
||||
|
||||
async def remove_skin(self, username: str):
|
||||
"""Удаление скина"""
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Удаляем файл скина, если он существует
|
||||
if user.get("skin_url") and user["skin_url"].startswith("/skins/"):
|
||||
import os
|
||||
try:
|
||||
os.remove(f"skins/{user['skin_url'].split('/')[-1]}")
|
||||
except:
|
||||
pass
|
||||
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$unset": {
|
||||
"skin_url": "",
|
||||
"skin_model": ""
|
||||
}}
|
||||
)
|
||||
return {"status": "success"}
|
314
app/services/store_cape.py
Normal file
314
app/services/store_cape.py
Normal file
@ -0,0 +1,314 @@
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from app.db.database import users_collection
|
||||
from app.core.config import FILES_URL
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
from app.models.cape import CapeStore, CapeStoreUpdate
|
||||
|
||||
# Создаем коллекцию для плащей в БД
|
||||
from app.db.database import db
|
||||
store_capes_collection = db.store_capes
|
||||
|
||||
class StoreCapeService:
|
||||
async def add_cape(self, name: str, description: str, price: int, cape_file: UploadFile):
|
||||
"""Добавление нового плаща в магазин"""
|
||||
# Проверка типа файла
|
||||
if not cape_file.content_type.startswith('image/'):
|
||||
raise HTTPException(status_code=400, detail="Файл должен быть изображением")
|
||||
|
||||
# Определяем расширение
|
||||
ext = None
|
||||
if cape_file.content_type == "image/png":
|
||||
ext = "png"
|
||||
elif cape_file.content_type == "image/gif":
|
||||
ext = "gif"
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Поддерживаются только PNG и GIF плащи")
|
||||
|
||||
# Проверка размера файла (максимум 2MB)
|
||||
max_size = 2 * 1024 * 1024 # 2MB
|
||||
contents = await cape_file.read()
|
||||
if len(contents) > max_size:
|
||||
raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 2MB)")
|
||||
|
||||
# Создаем папку для плащей магазина, если ее нет
|
||||
cape_dir = Path("/app/static/capes_store")
|
||||
cape_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Генерируем ID и имя файла
|
||||
cape_id = str(uuid.uuid4())
|
||||
cape_filename = f"store_cape_{cape_id}.{ext}"
|
||||
cape_path = cape_dir / cape_filename
|
||||
|
||||
# Сохраняем файл
|
||||
with open(cape_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
# Создаем запись в БД
|
||||
cape_data = {
|
||||
"id": cape_id,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"price": price,
|
||||
"file_name": cape_filename,
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
await store_capes_collection.insert_one(cape_data)
|
||||
|
||||
return {"id": cape_id, "status": "success"}
|
||||
|
||||
async def get_all_capes(self):
|
||||
"""Получение всех плащей из магазина"""
|
||||
capes = await store_capes_collection.find().to_list(1000)
|
||||
|
||||
result = []
|
||||
for cape in capes:
|
||||
result.append({
|
||||
"id": cape["id"],
|
||||
"name": cape["name"],
|
||||
"description": cape["description"],
|
||||
"price": cape["price"],
|
||||
"image_url": f"{FILES_URL}/capes_store/{cape['file_name']}"
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
async def get_cape_by_id(self, cape_id: str):
|
||||
"""Получение плаща по ID"""
|
||||
cape = await store_capes_collection.find_one({"id": cape_id})
|
||||
if not cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден")
|
||||
|
||||
return {
|
||||
"id": cape["id"],
|
||||
"name": cape["name"],
|
||||
"description": cape["description"],
|
||||
"price": cape["price"],
|
||||
"image_url": f"{FILES_URL}/capes_store/{cape['file_name']}"
|
||||
}
|
||||
|
||||
async def update_cape(self, cape_id: str, update_data: CapeStoreUpdate):
|
||||
"""Обновление информации о плаще"""
|
||||
cape = await store_capes_collection.find_one({"id": cape_id})
|
||||
if not cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден")
|
||||
|
||||
# Готовим данные для обновления
|
||||
update = {}
|
||||
if update_data.name:
|
||||
update["name"] = update_data.name
|
||||
if update_data.description:
|
||||
update["description"] = update_data.description
|
||||
if update_data.price is not None:
|
||||
update["price"] = update_data.price
|
||||
|
||||
if update:
|
||||
result = await store_capes_collection.update_one(
|
||||
{"id": cape_id},
|
||||
{"$set": update}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=500, detail="Ошибка при обновлении")
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
async def delete_cape(self, cape_id: str):
|
||||
"""Удаление плаща из магазина"""
|
||||
cape = await store_capes_collection.find_one({"id": cape_id})
|
||||
if not cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден")
|
||||
|
||||
# Удаляем файл
|
||||
cape_path = Path(f"/app/static/capes_store/{cape['file_name']}")
|
||||
if cape_path.exists():
|
||||
try:
|
||||
cape_path.unlink()
|
||||
except Exception as e:
|
||||
print(f"Ошибка при удалении файла: {e}")
|
||||
|
||||
# Удаляем из БД плащей магазина
|
||||
result = await store_capes_collection.delete_one({"id": cape_id})
|
||||
if result.deleted_count == 0:
|
||||
raise HTTPException(status_code=500, detail="Ошибка при удалении из БД")
|
||||
|
||||
# Удаляем из БД купленных плащей
|
||||
purchases_collection = db.purchases
|
||||
purchases = await purchases_collection.find_one({"cape_id": cape_id})
|
||||
if purchases:
|
||||
await purchases_collection.delete_one({"cape_id": cape_id})
|
||||
|
||||
# Удаляем плащ из массива purchased_capes всех пользователей
|
||||
users_collection = db.users
|
||||
await users_collection.update_many(
|
||||
{"purchased_capes.cape_id": cape_id},
|
||||
{"$pull": {"purchased_capes": {"cape_id": cape_id}}}
|
||||
)
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
async def purchase_cape(self, username: str, cape_id: str):
|
||||
"""Покупка плаща пользователем"""
|
||||
# Находим пользователя
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Находим плащ
|
||||
cape = await store_capes_collection.find_one({"id": cape_id})
|
||||
if not cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден")
|
||||
|
||||
# Проверяем достаточно ли монет
|
||||
user_coins = user.get("coins", 0)
|
||||
if user_coins < cape["price"]:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Недостаточно монет. Требуется: {cape['price']}, имеется: {user_coins}")
|
||||
|
||||
# Копируем плащ из хранилища магазина в персональную папку пользователя
|
||||
cape_store_path = Path(f"/app/static/capes_store/{cape['file_name']}")
|
||||
|
||||
# Создаем папку для плащей пользователя
|
||||
cape_dir = Path("/app/static/capes")
|
||||
cape_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Генерируем имя файла для персонального плаща
|
||||
filename_parts = cape['file_name'].split('.')
|
||||
ext = filename_parts[-1]
|
||||
cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"
|
||||
cape_path = cape_dir / cape_filename
|
||||
|
||||
# Копируем файл
|
||||
try:
|
||||
shutil.copy(cape_store_path, cape_path)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка при копировании файла: {e}")
|
||||
|
||||
# Обновляем данные пользователя
|
||||
# 1. Списываем монеты
|
||||
# 2. Устанавливаем новый плащ
|
||||
# 3. Добавляем плащ в список приобретенных
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$set": {
|
||||
"coins": user_coins - cape["price"],
|
||||
"cloak_url": f"{FILES_URL}/capes/{cape_filename}"
|
||||
},
|
||||
"$push": {
|
||||
"purchased_capes": {
|
||||
"cape_id": cape_id,
|
||||
"cape_name": cape["name"],
|
||||
"cape_description": cape["description"],
|
||||
"file_name": cape_filename,
|
||||
"purchased_at": datetime.utcnow()
|
||||
}
|
||||
}}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
# Если обновление не удалось, удаляем файл плаща
|
||||
if os.path.exists(cape_path):
|
||||
os.remove(cape_path)
|
||||
raise HTTPException(status_code=500, detail="Ошибка при обновлении данных пользователя")
|
||||
|
||||
# Логируем покупку в БД
|
||||
purchase_data = {
|
||||
"username": username,
|
||||
"user_id": user["_id"],
|
||||
"cape_id": cape_id,
|
||||
"cape_name": cape["name"],
|
||||
"price": cape["price"],
|
||||
"purchase_date": datetime.utcnow()
|
||||
}
|
||||
|
||||
from app.db.database import db
|
||||
await db.purchases.insert_one(purchase_data)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Плащ '{cape['name']}' успешно приобретен",
|
||||
"remaining_coins": user_coins - cape["price"]
|
||||
}
|
||||
|
||||
async def get_user_purchased_capes(self, username: str):
|
||||
"""Получение всех плащей, приобретенных пользователем"""
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
purchased_capes = user.get("purchased_capes", [])
|
||||
result = []
|
||||
|
||||
for cape in purchased_capes:
|
||||
result.append({
|
||||
"cape_id": cape.get("cape_id"),
|
||||
"cape_name": cape.get("cape_name"),
|
||||
"cape_description": cape.get("cape_description"),
|
||||
"image_url": f"{FILES_URL}/capes/{cape.get('file_name')}",
|
||||
"purchased_at": cape.get("purchased_at"),
|
||||
"is_active": user.get("cloak_url") == f"{FILES_URL}/capes/{cape.get('file_name')}"
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
async def activate_purchased_cape(self, username: str, cape_id: str):
|
||||
"""Активация приобретенного плаща"""
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Проверяем, что плащ был приобретен
|
||||
purchased_capes = user.get("purchased_capes", [])
|
||||
selected_cape = None
|
||||
|
||||
for cape in purchased_capes:
|
||||
if cape.get("cape_id") == cape_id:
|
||||
selected_cape = cape
|
||||
break
|
||||
|
||||
if not selected_cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден среди приобретенных")
|
||||
|
||||
# Устанавливаем выбранный плащ
|
||||
await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$set": {"cloak_url": f"{FILES_URL}/capes/{selected_cape.get('file_name')}"}}
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Плащ '{selected_cape.get('cape_name')}' активирован"
|
||||
}
|
||||
|
||||
async def deactivate_purchased_cape(self, username: str, cape_id: str):
|
||||
"""Деактивация приобретенного плаща"""
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
|
||||
# Проверяем, что плащ был приобретен
|
||||
purchased_capes = user.get("purchased_capes", [])
|
||||
selected_cape = None
|
||||
|
||||
for cape in purchased_capes:
|
||||
if cape.get("cape_id") == cape_id:
|
||||
selected_cape = cape
|
||||
break
|
||||
|
||||
if not selected_cape:
|
||||
raise HTTPException(status_code=404, detail="Плащ не найден среди приобретенных")
|
||||
|
||||
# Устанавливаем выбранный плащ
|
||||
await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$set": {"cloak_url": None}}
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Плащ '{selected_cape.get('cape_name')}' деактивирован"
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
After Width: | Height: | Size: 310 B |
Binary file not shown.
After Width: | Height: | Size: 738 B |
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 122 KiB |
0
app/utils/__init__.py
Normal file
0
app/utils/__init__.py
Normal file
@ -1,16 +1,7 @@
|
||||
from jose import jwt, JWTError
|
||||
from passlib.context import CryptContext
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
env_path = Path(__file__).parent.parent / ".env"
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
|
||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 часа
|
||||
from app.core.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
349
auth/app/auth.py
349
auth/app/auth.py
@ -1,349 +0,0 @@
|
||||
import base64
|
||||
import json
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from .models import UserLogin, UserInDB, Session, UserCreate, SkinUpdate, CapeUpdate
|
||||
from .utils import (
|
||||
verify_password,
|
||||
get_password_hash,
|
||||
create_access_token,
|
||||
decode_token,
|
||||
)
|
||||
from .database import users_collection, sessions_collection
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
env_path = Path(__file__).parent.parent / ".env"
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
FILES_URL = os.getenv("FILES_URL")
|
||||
|
||||
class AuthService:
|
||||
async def register(self, user: UserCreate):
|
||||
# Проверяем, существует ли пользователь
|
||||
if await users_collection.find_one({"username": user.username}):
|
||||
raise HTTPException(status_code=400, detail="Username already taken")
|
||||
|
||||
# Хешируем пароль
|
||||
hashed_password = get_password_hash(user.password)
|
||||
|
||||
# Создаём UUID для Minecraft
|
||||
user_uuid = str(uuid.uuid4())
|
||||
|
||||
# Сохраняем в MongoDB
|
||||
new_user = UserInDB(
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
hashed_password=hashed_password,
|
||||
uuid=user_uuid,
|
||||
)
|
||||
await users_collection.insert_one(new_user.dict())
|
||||
return {"status": "success", "uuid": user_uuid}
|
||||
|
||||
async def login(self, credentials: UserLogin):
|
||||
# Ищем пользователя
|
||||
user = await users_collection.find_one({"username": credentials.username})
|
||||
if not user or not verify_password(credentials.password, user["hashed_password"]):
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
# Генерируем токены
|
||||
access_token = create_access_token({"sub": user["username"], "uuid": user["uuid"]})
|
||||
client_token = str(uuid.uuid4())
|
||||
|
||||
# Сохраняем сессию
|
||||
session = Session(
|
||||
access_token=access_token,
|
||||
client_token=client_token,
|
||||
user_uuid=user["uuid"],
|
||||
expires_at=datetime.utcnow() + timedelta(minutes=1440),
|
||||
)
|
||||
await sessions_collection.insert_one(session.dict())
|
||||
|
||||
return {
|
||||
"accessToken": access_token,
|
||||
"clientToken": client_token,
|
||||
"selectedProfile": {
|
||||
"id": user["uuid"],
|
||||
"name": user["username"],
|
||||
},
|
||||
}
|
||||
|
||||
async def validate(self, access_token: str, client_token: str):
|
||||
print(f"Searching for access_toke and client_token: '{access_token}', '{client_token}")
|
||||
session = await sessions_collection.find_one({
|
||||
"access_token": access_token,
|
||||
"client_token": client_token,
|
||||
})
|
||||
print("Session from DB:", session)
|
||||
if not session or datetime.utcnow() > session["expires_at"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def refresh(self, access_token: str, client_token: str):
|
||||
if not await self.validate(access_token, client_token):
|
||||
return None
|
||||
|
||||
# Обновляем токен
|
||||
new_access_token = create_access_token({"sub": "user", "uuid": "user_uuid"})
|
||||
await sessions_collection.update_one(
|
||||
{"access_token": access_token},
|
||||
{"$set": {"access_token": new_access_token}},
|
||||
)
|
||||
return {"accessToken": new_access_token, "clientToken": client_token}
|
||||
|
||||
async def get_minecraft_profile(self, uuid: str):
|
||||
# Преобразуем UUID без дефисов в формат с дефисами (если нужно)
|
||||
if '-' not in uuid:
|
||||
formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
|
||||
else:
|
||||
formatted_uuid = uuid
|
||||
|
||||
user = await users_collection.find_one({"uuid": formatted_uuid}) # Ищем по UUID с дефисами
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
textures = {
|
||||
"timestamp": int(datetime.now().timestamp() * 1000),
|
||||
"profileId": user["uuid"], # UUID с дефисами
|
||||
"profileName": user["username"],
|
||||
"textures": {}
|
||||
}
|
||||
|
||||
if user.get("skin_url"):
|
||||
textures["textures"]["SKIN"] = {
|
||||
"url": user["skin_url"],
|
||||
"metadata": {"model": user.get("skin_model", "classic")}
|
||||
}
|
||||
|
||||
if user.get("cloak_url"):
|
||||
textures["textures"]["CAPE"] = {"url": user["cloak_url"]}
|
||||
|
||||
textures_json = json.dumps(textures).encode()
|
||||
base64_textures = base64.b64encode(textures_json).decode()
|
||||
|
||||
# Подписываем текстуры
|
||||
with open("private_key.pem", "rb") as key_file:
|
||||
private_key = serialization.load_pem_private_key(
|
||||
key_file.read(),
|
||||
password=None
|
||||
)
|
||||
|
||||
signature = private_key.sign(
|
||||
textures_json,
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA1()
|
||||
)
|
||||
|
||||
return JSONResponse({
|
||||
"id": user["uuid"].replace("-", ""), # Уберите дефисы
|
||||
"name": user["username"],
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": base64_textures,
|
||||
"signature": base64.b64encode(signature).decode()
|
||||
}]
|
||||
})
|
||||
|
||||
async def join_server(self, request_data: dict):
|
||||
access_token = request_data.get("accessToken")
|
||||
selected_profile = request_data.get("selectedProfile") # UUID без дефисов
|
||||
server_id = request_data.get("serverId")
|
||||
|
||||
if not all([access_token, selected_profile, server_id]):
|
||||
raise HTTPException(status_code=400, detail="Missing required parameters")
|
||||
|
||||
decoded_token = decode_token(access_token)
|
||||
if not decoded_token:
|
||||
raise HTTPException(status_code=401, detail="Invalid access token")
|
||||
|
||||
token_uuid = decoded_token.get("uuid", "").replace("-", "")
|
||||
if token_uuid != selected_profile:
|
||||
raise HTTPException(status_code=403, detail="Token doesn't match selected profile")
|
||||
|
||||
# Сохраняем server_id в сессию
|
||||
await sessions_collection.update_one(
|
||||
{"user_uuid": decoded_token["uuid"]}, # UUID с дефисами
|
||||
{"$set": {"server_id": server_id}},
|
||||
upsert=True
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
async def has_joined(self, username: str, server_id: str):
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Ищем сессию с этим server_id
|
||||
session = await sessions_collection.find_one({
|
||||
"user_uuid": user["uuid"], # UUID с дефисами
|
||||
"server_id": server_id
|
||||
})
|
||||
if not session:
|
||||
raise HTTPException(status_code=403, detail="Not joined this server")
|
||||
|
||||
textures = {}
|
||||
if user.get("skin_url"):
|
||||
textures["SKIN"] = {"url": user["skin_url"]}
|
||||
if user.get("cloak_url"):
|
||||
textures["CAPE"] = {"url": user["cloak_url"]}
|
||||
|
||||
textures_value = base64.b64encode(json.dumps({
|
||||
"timestamp": int(datetime.now().timestamp()),
|
||||
"profileId": user["uuid"].replace("-", ""), # UUID без дефисов
|
||||
"profileName": username,
|
||||
"textures": textures
|
||||
}).encode()).decode()
|
||||
|
||||
return {
|
||||
"id": user["uuid"].replace("-", ""), # UUID без дефисов
|
||||
"name": username,
|
||||
"properties": [{
|
||||
"name": "textures",
|
||||
"value": textures_value
|
||||
}] if textures else []
|
||||
}
|
||||
|
||||
async def set_skin(self, username: str, skin_file: UploadFile, skin_model: str = "classic"):
|
||||
"""Установка или замена скина через загрузку файла"""
|
||||
# Проверяем тип файла
|
||||
if not skin_file.content_type.startswith('image/'):
|
||||
raise HTTPException(status_code=400, detail="File must be an image")
|
||||
|
||||
# Проверяем размер файла (максимум 2MB)
|
||||
max_size = 2 * 1024 * 1024 # 2MB
|
||||
contents = await skin_file.read()
|
||||
if len(contents) > max_size:
|
||||
raise HTTPException(status_code=400, detail="File too large (max 2MB)")
|
||||
|
||||
# Удаляем старый скин, если есть
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if user and user.get("skin_url"):
|
||||
from urllib.parse import urlparse
|
||||
import os
|
||||
old_url = user["skin_url"]
|
||||
# Получаем имя файла из url
|
||||
old_filename = os.path.basename(urlparse(old_url).path)
|
||||
old_path = os.path.join("skins", old_filename)
|
||||
if os.path.exists(old_path):
|
||||
try:
|
||||
os.remove(old_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Создаем папку для скинов, если ее нет
|
||||
from pathlib import Path
|
||||
skin_dir = Path("skins")
|
||||
skin_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Генерируем имя файла
|
||||
skin_filename = f"{username}_{int(datetime.now().timestamp())}.png"
|
||||
skin_path = skin_dir / skin_filename
|
||||
|
||||
# Сохраняем файл
|
||||
with open(skin_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
# Обновляем запись пользователя
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$set": {
|
||||
"skin_url": f"{FILES_URL}/skins/{skin_filename}",
|
||||
"skin_model": skin_model
|
||||
}}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"status": "success"}
|
||||
|
||||
async def remove_skin(self, username: str):
|
||||
"""Удаление скина"""
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Удаляем файл скина, если он существует
|
||||
if user.get("skin_url") and user["skin_url"].startswith("/skins/"):
|
||||
import os
|
||||
try:
|
||||
os.remove(f"skins/{user['skin_url'].split('/')[-1]}")
|
||||
except:
|
||||
pass
|
||||
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$unset": {
|
||||
"skin_url": "",
|
||||
"skin_model": ""
|
||||
}}
|
||||
)
|
||||
return {"status": "success"}
|
||||
|
||||
async def set_cape(self, username: str, cape_file: UploadFile):
|
||||
"""Установка или замена плаща через загрузку файла (PNG или GIF)"""
|
||||
if not cape_file.content_type.startswith('image/'):
|
||||
raise HTTPException(status_code=400, detail="File must be an image")
|
||||
|
||||
# Определяем расширение
|
||||
ext = None
|
||||
if cape_file.content_type == "image/png":
|
||||
ext = "png"
|
||||
elif cape_file.content_type == "image/gif":
|
||||
ext = "gif"
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Only PNG and GIF capes are supported")
|
||||
|
||||
max_size = 2 * 1024 * 1024 # 2MB
|
||||
contents = await cape_file.read()
|
||||
if len(contents) > max_size:
|
||||
raise HTTPException(status_code=400, detail="File too large (max 2MB)")
|
||||
|
||||
# Удаляем старый плащ, если есть
|
||||
user = await users_collection.find_one({"username": username})
|
||||
if user and user.get("cloak_url"):
|
||||
from urllib.parse import urlparse
|
||||
import os
|
||||
old_url = user["cloak_url"]
|
||||
old_filename = os.path.basename(urlparse(old_url).path)
|
||||
old_path = os.path.join("capes", old_filename)
|
||||
if os.path.exists(old_path):
|
||||
try:
|
||||
os.remove(old_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from pathlib import Path
|
||||
cape_dir = Path("capes")
|
||||
cape_dir.mkdir(exist_ok=True)
|
||||
|
||||
cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"
|
||||
cape_path = cape_dir / cape_filename
|
||||
|
||||
with open(cape_path, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$set": {
|
||||
"cloak_url": f"{FILES_URL}/capes/{cape_filename}"
|
||||
}}
|
||||
)
|
||||
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"status": "success"}
|
||||
|
||||
async def remove_cape(self, username: str):
|
||||
"""Удаление плаща"""
|
||||
result = await users_collection.update_one(
|
||||
{"username": username},
|
||||
{"$unset": {"cloak_url": ""}}
|
||||
)
|
||||
if result.modified_count == 0:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return {"status": "success"}
|
@ -1,17 +0,0 @@
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
env_path = Path(__file__).parent.parent / ".env"
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
|
||||
MONGO_URI = os.getenv("MONGO_URI")
|
||||
DB_NAME = "minecraft_auth"
|
||||
|
||||
client = AsyncIOMotorClient(MONGO_URI)
|
||||
db = client[DB_NAME]
|
||||
|
||||
# Коллекции
|
||||
users_collection = db["users"]
|
||||
sessions_collection = db["sessions"]
|
@ -1,19 +0,0 @@
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
# Генерация ключа
|
||||
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
|
||||
# Сохранение в PEM-формат
|
||||
with open("private_key.pem", "wb") as f:
|
||||
f.write(private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
))
|
||||
|
||||
with open("public_key.pem", "wb") as f:
|
||||
f.write(private_key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
))
|
124
auth/app/main.py
124
auth/app/main.py
@ -1,124 +0,0 @@
|
||||
import base64
|
||||
from datetime import datetime
|
||||
import json
|
||||
from fastapi import FastAPI, Depends, File, Form, HTTPException, Body, Request, Response, UploadFile
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from .models import UserCreate, UserLogin, ValidateRequest, SkinUpdate, CapeUpdate
|
||||
from .auth import AuthService
|
||||
from .database import users_collection
|
||||
from .utils import decode_token
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import logging
|
||||
# logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
app = FastAPI()
|
||||
auth_service = AuthService()
|
||||
|
||||
skin_dir = Path("skins")
|
||||
skin_dir.mkdir(exist_ok=True)
|
||||
app.mount("/skins", StaticFiles(directory="skins"), name="skins")
|
||||
|
||||
cape_dir = Path("capes")
|
||||
cape_dir.mkdir(exist_ok=True)
|
||||
app.mount("/capes", StaticFiles(directory="capes"), name="capes")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Разрешить все домены
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/")
|
||||
def api_root():
|
||||
return {
|
||||
"meta": {
|
||||
"serverName": "Your Auth Server",
|
||||
"implementationName": "FastAPI",
|
||||
"implementationVersion": "1.0.0",
|
||||
"links": {
|
||||
"homepage": "https://your-server.com"
|
||||
},
|
||||
},
|
||||
"skinDomains": ["147.78.65.214"],
|
||||
"capeDomains": ["147.78.65.214"]
|
||||
}
|
||||
|
||||
# Эндпоинты Mojang-like API
|
||||
@app.post("/auth/register")
|
||||
async def register(user: UserCreate):
|
||||
return await auth_service.register(user)
|
||||
|
||||
@app.post("/auth/authenticate")
|
||||
async def authenticate(credentials: UserLogin):
|
||||
return await auth_service.login(credentials)
|
||||
|
||||
@app.post("/auth/validate")
|
||||
async def validate_token(request: ValidateRequest):
|
||||
is_valid = await auth_service.validate(request.accessToken, request.clientToken)
|
||||
return {"valid": is_valid}
|
||||
|
||||
@app.post("/auth/refresh")
|
||||
async def refresh_token(access_token: str, client_token: str):
|
||||
result = await auth_service.refresh(access_token, client_token)
|
||||
if not result:
|
||||
raise HTTPException(status_code=401, detail="Invalid tokens")
|
||||
return result
|
||||
|
||||
@app.get("/sessionserver/session/minecraft/profile/{uuid}")
|
||||
async def get_minecraft_profile(uuid: str, unsigned: bool = False):
|
||||
return await auth_service.get_minecraft_profile(uuid)
|
||||
|
||||
@app.post("/sessionserver/session/minecraft/join")
|
||||
async def join_server(request_data: dict = Body(...)):
|
||||
try:
|
||||
await auth_service.join_server(request_data)
|
||||
return Response(status_code=204)
|
||||
except Exception as e:
|
||||
print("Error in join_server:", str(e))
|
||||
raise
|
||||
|
||||
@app.get("/sessionserver/session/minecraft/hasJoined")
|
||||
async def has_joined(username: str, serverId: str):
|
||||
return await auth_service.has_joined(username, serverId)
|
||||
|
||||
|
||||
@app.post("/user/{username}/skin")
|
||||
async def set_skin(
|
||||
username: str,
|
||||
skin_file: UploadFile = File(...),
|
||||
skin_model: str = Form("classic")
|
||||
):
|
||||
return await auth_service.set_skin(username, skin_file, skin_model)
|
||||
|
||||
@app.delete("/user/{username}/skin")
|
||||
async def remove_skin(username: str):
|
||||
return await auth_service.remove_skin(username)
|
||||
|
||||
@app.post("/user/{username}/cape")
|
||||
async def set_cape(
|
||||
username: str,
|
||||
cape_file: UploadFile = File(...)
|
||||
):
|
||||
return await auth_service.set_cape(username, cape_file)
|
||||
|
||||
@app.delete("/user/{username}/cape")
|
||||
async def remove_cape(username: str):
|
||||
return await auth_service.remove_cape(username)
|
||||
|
||||
@app.get("/debug/profile/{uuid}")
|
||||
async def debug_profile(uuid: str):
|
||||
profile = await auth_service.get_minecraft_profile(uuid)
|
||||
textures = base64.b64decode(profile['properties'][0]['value']).decode()
|
||||
return {
|
||||
"profile": profile,
|
||||
"textures_decoded": json.loads(textures)
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
@ -1,42 +0,0 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
# Для запросов
|
||||
class UserCreate(BaseModel):
|
||||
username: str
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
# Для MongoDB
|
||||
class UserInDB(BaseModel):
|
||||
username: str
|
||||
email: EmailStr
|
||||
hashed_password: str
|
||||
uuid: str
|
||||
skin_url: Optional[str] = None
|
||||
skin_model: Optional[str] = "classic" # "classic" или "slim"
|
||||
cloak_url: Optional[str] = None
|
||||
is_active: bool = True
|
||||
created_at: datetime = datetime.utcnow()
|
||||
|
||||
class Session(BaseModel):
|
||||
access_token: str
|
||||
client_token: str
|
||||
user_uuid: str
|
||||
expires_at: datetime
|
||||
|
||||
class ValidateRequest(BaseModel):
|
||||
accessToken: str # camelCase
|
||||
clientToken: str
|
||||
|
||||
class SkinUpdate(BaseModel):
|
||||
skin_model: Optional[str] = "classic" # "classic" или "slim"
|
||||
# Удаляем skin_url и skin_file, так как будем принимать файл напрямую
|
||||
|
||||
class CapeUpdate(BaseModel):
|
||||
cape_url: str
|
40
docker-compose.yml
Normal file
40
docker-compose.yml
Normal file
@ -0,0 +1,40 @@
|
||||
services:
|
||||
app:
|
||||
container_name: minecraft-api
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3000"
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
volumes:
|
||||
- ./app/static:/app/static:rw
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- mongodb
|
||||
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"]
|
||||
|
||||
telegram_bot:
|
||||
container_name: telegram_bot
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
volumes:
|
||||
- ./telegram_bot.py:/app/telegram_bot.py
|
||||
env_file:
|
||||
- .env
|
||||
command: ["python", "telegram_bot.py"]
|
||||
|
||||
mongodb:
|
||||
container_name: mongodb
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- "32768:27017"
|
||||
volumes:
|
||||
- ./mongodb:/data/db
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=popa
|
||||
- MONGO_INITDB_ROOT_PASSWORD=2006sit_
|
||||
restart: always
|
14
dockerfile
Normal file
14
dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p /app/static/skins /app/static/capes /app/static/capes_store && \
|
||||
chown -R 1000:1000 /app/static
|
||||
|
||||
EXPOSE 3000
|
60
main.py
60
main.py
@ -1,40 +1,30 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from aiomcrcon import Client, RCONConnectionError, IncorrectPasswordError
|
||||
import asyncio
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from app.api import users, skins, capes, meta, server, store, pranks, marketplace, bonuses
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Конфигурация RCON (замените на свои данные)
|
||||
RCON_CONFIG = {
|
||||
"hub": {"host": "minecraft.hub.popa-popa.ru", "port": 29001, "password": "2006siT_"},
|
||||
"survival": {"host": "minecraft.survival.popa-popa.ru", "port": 25575, "password": "пароль_survival"},
|
||||
"pillars": {"host": "minecraft.pillars.popa-popa.ru", "port": 29003, "password": "2006siT_"},
|
||||
"velocity": {"host": "minecraft.velocity.popa-popa.ru", "port": 25575, "password": "пароль_velocity"}
|
||||
}
|
||||
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.include_router(store.router)
|
||||
app.include_router(pranks.router)
|
||||
app.include_router(marketplace.router)
|
||||
app.include_router(bonuses.router)
|
||||
|
||||
async def send_rcon_command(server_type: str, command: str) -> str:
|
||||
"""Отправляет RCON-команду на указанный сервер."""
|
||||
config = RCON_CONFIG.get(server_type)
|
||||
if not config:
|
||||
raise HTTPException(status_code=400, detail="Неверный тип сервера")
|
||||
# Монтируем статику
|
||||
app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")
|
||||
app.mount("/capes", StaticFiles(directory="app/static/capes"), name="capes")
|
||||
app.mount("/capes_store", StaticFiles(directory="app/static/capes_store"), name="capes_store")
|
||||
|
||||
try:
|
||||
async with Client(config["host"], config["port"], config["password"]) as client:
|
||||
response = await client.send_cmd(command)
|
||||
return response
|
||||
except RCONConnectionError:
|
||||
raise HTTPException(status_code=503, detail="Не удалось подключиться к серверу")
|
||||
except IncorrectPasswordError:
|
||||
raise HTTPException(status_code=403, detail="Неверный пароль RCON")
|
||||
|
||||
@app.get("/rcon/")
|
||||
async def execute_rcon(server_type: str, command: str):
|
||||
"""Выполняет RCON-команду на указанном сервере."""
|
||||
result = await send_rcon_command(server_type, command)
|
||||
return {"server": server_type, "command": command, "response": result}
|
||||
|
||||
@app.get("/players/online/")
|
||||
async def get_online_players(server_type: str):
|
||||
"""Возвращает список игроков онлайн на сервере."""
|
||||
players = await send_rcon_command(server_type, "list")
|
||||
return {"server": server_type, "online_players": players}
|
||||
# CORS, middleware и т.д.
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
@ -5,3 +5,10 @@ python-jose>=3.3.0
|
||||
passlib>=1.7.4
|
||||
bcrypt>=4.0.1
|
||||
python-multipart>=0.0.9
|
||||
mongoengine>=0.24.2
|
||||
python-dotenv>=1.0.0
|
||||
pydantic>=2.0.0
|
||||
cryptography>=43.0.0
|
||||
pytelegrambotapi>=2.0.0
|
||||
httpx>=0.27.2
|
||||
|
54
scripts/add_test_bonuses.py
Normal file
54
scripts/add_test_bonuses.py
Normal file
@ -0,0 +1,54 @@
|
||||
# scripts/add_test_bonuses.py
|
||||
from app.db.database import db
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
# Коллекция для бонусов
|
||||
bonus_types_collection = db.bonus_types
|
||||
|
||||
# Очищаем существующие записи
|
||||
bonus_types_collection.delete_many({})
|
||||
|
||||
# Добавляем типы бонусов
|
||||
bonus_types = [
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": "Бонус опыта",
|
||||
"description": "Увеличивает получаемый опыт на 100% (+10% за уровень)",
|
||||
"effect_type": "experience",
|
||||
"base_effect_value": 1.0, # +100%
|
||||
"effect_increment": 0.1, # +10% за уровень
|
||||
"price": 100,
|
||||
"upgrade_price": 50,
|
||||
"duration": 0, # Бесконечный
|
||||
"max_level": 0 # Без ограничения уровня
|
||||
},
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": "Бонус силы",
|
||||
"description": "Увеличивает силу атаки на 10% (+5% за уровень)",
|
||||
"effect_type": "strength",
|
||||
"base_effect_value": 0.1,
|
||||
"effect_increment": 0.05,
|
||||
"price": 75,
|
||||
"upgrade_price": 40,
|
||||
"duration": 0, # Бесконечный
|
||||
"max_level": 10 # Максимум 10 уровней
|
||||
},
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": "Бонус скорости",
|
||||
"description": "Временно увеличивает скорость передвижения на 20%",
|
||||
"effect_type": "speed",
|
||||
"base_effect_value": 0.2,
|
||||
"effect_increment": 0.05,
|
||||
"price": 40,
|
||||
"upgrade_price": 30,
|
||||
"duration": 1800, # 30 минут
|
||||
"max_level": 5
|
||||
}
|
||||
]
|
||||
|
||||
# Вставляем бонусы в БД
|
||||
bonus_types_collection.insert_many(bonus_types)
|
||||
print(f"Добавлено {len(bonus_types)} типов бонусов")
|
53
telegram_bot.py
Normal file
53
telegram_bot.py
Normal file
@ -0,0 +1,53 @@
|
||||
from telebot import TeleBot
|
||||
import httpx
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
bot = TeleBot(os.getenv("TELEGRAM_BOT_TOKEN"))
|
||||
API_URL = os.getenv("API_URL")
|
||||
|
||||
user_states = {} # {"chat_id": {"username": "DIKER0K"}}
|
||||
|
||||
@bot.message_handler(commands=['start'])
|
||||
def start(message):
|
||||
# Обработка deep link: /start{username}
|
||||
if len(message.text.split()) > 1:
|
||||
username = message.text.split()[1] # Получаем username из ссылки
|
||||
user_states[message.chat.id] = {"username": username}
|
||||
bot.reply_to(message, f"📋 Введите код из лаунчера:")
|
||||
else:
|
||||
bot.reply_to(message, "🔑 Введите ваш игровой никнейм:")
|
||||
bot.register_next_step_handler(message, process_username)
|
||||
|
||||
def process_username(message):
|
||||
user_states[message.chat.id] = {"username": message.text.strip()}
|
||||
bot.reply_to(message, "📋 Теперь введите код из лаунчера:")
|
||||
|
||||
@bot.message_handler(func=lambda m: m.chat.id in user_states)
|
||||
def verify_code(message):
|
||||
username = user_states[message.chat.id]["username"]
|
||||
code = message.text.strip()
|
||||
print(username, code, message.chat.id)
|
||||
|
||||
try:
|
||||
response = httpx.post(
|
||||
f"{API_URL}/auth/verify_code",
|
||||
json={"username": username, "code": code, "telegram_chat_id": message.chat.id}, # JSON-сериализация автоматически
|
||||
headers={"Content-Type": "application/json"} # Необязательно, httpx добавляет сам
|
||||
)
|
||||
print(response.json())
|
||||
if response.status_code == 200:
|
||||
bot.reply_to(message, "✅ Аккаунт подтвержден!")
|
||||
else:
|
||||
bot.reply_to(message, f"❌ Ошибка: {response.json().get('detail')}")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(API_URL)
|
||||
bot.reply_to(message, "⚠️ Сервер недоступен. Детальная информация: " + str(e))
|
||||
|
||||
del user_states[message.chat.id]
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot.polling(none_stop=True)
|
Reference in New Issue
Block a user