feat: working skins and capes(animated capes not :( )

This commit is contained in:
2025-07-17 02:07:32 +05:00
parent 3d310760ba
commit 9786d6d9b1
14 changed files with 307 additions and 37 deletions

View File

@ -1,7 +1,8 @@
import base64
import json
from fastapi import HTTPException
from .models import UserLogin, UserInDB, Session, UserCreate
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,
@ -11,6 +12,14 @@ from .utils import (
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
load_dotenv()
FILES_URL = os.getenv("FILES_URL")
class AuthService:
async def register(self, user: UserCreate):
@ -86,41 +95,61 @@ class AuthService:
return {"accessToken": new_access_token, "clientToken": client_token}
async def get_minecraft_profile(self, uuid: str):
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})
# Преобразуем 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=f"User not found (searched for UUID: {formatted_uuid})"
)
raise HTTPException(status_code=404, detail="User not found")
textures = {
"timestamp": int(datetime.now().timestamp()),
"profileId": formatted_uuid,
"timestamp": int(datetime.now().timestamp() * 1000),
"profileId": user["uuid"], # UUID с дефисами
"profileName": user["username"],
"textures": {
"SKIN": {"url": user.get("skin_url", "")},
"CAPE": {"url": user.get("cloak_url", "")}
} if user.get("skin_url") or user.get("cloak_url") else {}
"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()
return {
"id": formatted_uuid,
"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")
selected_profile = request_data.get("selectedProfile") # UUID без дефисов
server_id = request_data.get("serverId")
if not all([access_token, selected_profile, server_id]):
@ -134,33 +163,165 @@ class AuthService:
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")
response_uuid = user["uuid"].replace("-", "")
# Ищем сессию с этим 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": response_uuid,
"profileId": user["uuid"].replace("-", ""), # UUID без дефисов
"profileName": username,
"textures": textures
}).encode()).decode()
return {
"id": response_uuid,
"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)")
# Создаем папку для скинов, если ее нет
import os
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")
# Проверяем размер файла (максимум 2MB)
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)")
# Создаем папку для плащей, если ее нет
import os
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"}