fix: fix warps
Build and Push Docker Image / build-and-push (push) Has been skipped Details

This commit is contained in:
itqop 2025-12-03 00:08:55 +03:00
parent c2510fa9dc
commit a890cc67fc
10 changed files with 254 additions and 25 deletions

View File

@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Bash(poetry run pytest:*)",
"Bash(poetry add:*)",
"Bash(poetry run python:*)",
"Bash(poetry run ruff:*)",
"Bash(python -m ruff check:*)",
"Bash(.venvScriptspython.exe -m ruff check src/hubgw/core/errors.py src/hubgw/schemas/homes.py src/hubgw/services/homes_service.py)",
"Bash(python -m py_compile:*)"
],
"deny": [],
"ask": []
}
}

View File

@ -31,7 +31,7 @@
### PUT `/homes/`
Создает или обновляет дом игрока.
Создает или обновляет дом игрока с проверкой лимита.
**Тело запроса:**
```json
@ -44,10 +44,14 @@
"z": 0.0,
"yaw": 0.0,
"pitch": 0.0,
"is_public": false
"is_public": false,
"max_homes": 5
}
```
**Параметры:**
- `max_homes` (optional) - Максимальное количество домов для игрока. Если указан, проверяется лимит при создании нового дома.
**Ответ:**
```json
{
@ -66,6 +70,20 @@
}
```
**Ошибки:**
- `409 Conflict` - Лимит домов превышен (если указан `max_homes`)
```json
{
"message": "Home limit exceeded. Maximum 5 homes allowed.",
"code": "limit_exceeded",
"details": {
"player_uuid": "string",
"current_count": 5,
"max_homes": 5
}
}
```
### POST `/homes/get`
Получает конкретный дом игрока по имени.
@ -210,13 +228,16 @@
**Префикс:** `/warps`
Варпы имеют глобально уникальные имена и владельцев. Только владелец может редактировать или удалять свой варп.
### POST `/warps/`
Создает новую точку варпа.
Создает новую точку варпа с проверкой лимита per-player.
**Тело запроса:**
```json
{
"player_uuid": "string",
"name": "string",
"world": "string",
"x": 0.0,
@ -225,16 +246,23 @@
"yaw": 0.0,
"pitch": 0.0,
"is_public": true,
"description": "string"
"description": "string",
"max_warps": 10
}
```
**Параметры:**
- `player_uuid` (required) - UUID владельца варпа
- `name` (required) - Глобально уникальное имя варпа
- `max_warps` (optional) - Максимальное количество варпов для игрока
**Статус:** `201 Created`
**Ответ:**
```json
{
"id": "uuid",
"player_uuid": "string",
"name": "string",
"world": "string",
"x": 0.0,
@ -249,13 +277,38 @@
}
```
**Ошибки:**
- `409 Conflict` - Варп с таким именем уже существует
```json
{
"message": "Warp 'spawn' already exists",
"code": "already_exists",
"details": {
"owner_uuid": "другой-uuid"
}
}
```
- `409 Conflict` - Лимит варпов превышен (если указан `max_warps`)
```json
{
"message": "Warp limit exceeded. Maximum 10 warps allowed.",
"code": "limit_exceeded",
"details": {
"player_uuid": "string",
"current_count": 10,
"max_warps": 10
}
}
```
### PATCH `/warps/`
Обновляет существующую точку варпа.
Обновляет существующую точку варпа. Только владелец может редактировать варп.
**Тело запроса:**
```json
{
"player_uuid": "string",
"name": "string",
"world": "string",
"x": 0.0,
@ -263,15 +316,21 @@
"z": 0.0,
"yaw": 0.0,
"pitch": 0.0,
"is_public": true,
"is_public": false,
"description": "string"
}
```
**Параметры:**
- `player_uuid` (required) - UUID игрока (для проверки ownership)
- `name` (required) - Имя варпа для обновления
- Все остальные поля опциональны
**Ответ:**
```json
{
"id": "uuid",
"player_uuid": "string",
"name": "string",
"world": "string",
"x": 0.0,
@ -279,29 +338,52 @@
"z": 0.0,
"yaw": 0.0,
"pitch": 0.0,
"is_public": true,
"is_public": false,
"description": "string",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
```
**Ошибки:**
- `403 Forbidden` - Нет прав для редактирования варпа
```json
{
"message": "You don't have permission to update warp 'spawn'",
"code": "permission_denied",
"details": {
"warp_owner": "uuid-владельца",
"requested_by": "ваш-uuid"
}
}
```
- `404 Not Found` - Варп не найден
### DELETE `/warps/`
Удаляет точку варпа.
Удаляет точку варпа. Только владелец может удалить варп.
**Тело запроса:**
```json
{
"player_uuid": "string",
"name": "string"
}
```
**Параметры:**
- `player_uuid` (required) - UUID игрока (для проверки ownership)
- `name` (required) - Имя варпа для удаления
**Статус:** `204 No Content`
**Ошибки:**
- `403 Forbidden` - Нет прав для удаления варпа
- `404 Not Found` - Варп не найден
### POST `/warps/get`
Получает конкретную точку варпа по имени.
Получает конкретную точку варпа по имени. Доступно всем игрокам.
**Тело запроса:**
```json
@ -314,6 +396,7 @@
```json
{
"id": "uuid",
"player_uuid": "string",
"name": "string",
"world": "string",
"x": 0.0,
@ -349,6 +432,7 @@
"warps": [
{
"id": "uuid",
"player_uuid": "string",
"name": "string",
"world": "string",
"x": 0.0,
@ -954,7 +1038,26 @@ API ключ настраивается через переменную окру
Все endpoints возвращают стандартные HTTP коды ошибок:
- `400 Bad Request` - некорректные данные в запросе
- `401 Unauthorized` - отсутствует или неверный API ключ
- `403 Forbidden` - недостаточно прав для выполнения операции (например, попытка удалить чужой варп)
- `404 Not Found` - ресурс не найден
- `409 Conflict` - конфликт ресурсов (ресурс уже существует, лимит превышен)
- `500 Internal Server Error` - внутренняя ошибка сервера
Тело ответа при ошибке содержит детальное описание проблемы.
Тело ответа при ошибке имеет следующий формат:
```json
{
"message": "Human-readable error message",
"code": "error_code",
"details": {
"additional": "context"
}
}
```
**Коды ошибок:**
- `not_found` - ресурс не найден (404)
- `already_exists` - ресурс уже существует (409)
- `limit_exceeded` - превышен лимит ресурсов (409)
- `permission_denied` - нет прав на операцию (403)
- `invalid_state` - некорректное состояние данных (400)
- `cooldown_active` - кулдаун активен (400)

View File

@ -0,0 +1,15 @@
-- Migration: Add player_uuid to warps for ownership tracking
-- Date: 2024-12-02
-- Description: Add owner field to warps, enabling per-player limits and ownership control
-- Add player_uuid column (owner of the warp)
ALTER TABLE hubmc.hub_warps
ADD COLUMN player_uuid VARCHAR(36) NOT NULL
REFERENCES hubmc.luckperms_players(uuid) ON DELETE CASCADE;
-- Add index for querying warps by player
CREATE INDEX idx_hub_warps_player_uuid
ON hubmc.hub_warps(player_uuid);
-- Add comment
COMMENT ON COLUMN hubmc.hub_warps.player_uuid IS 'Owner of the warp (who created it)';

View File

@ -48,13 +48,29 @@ class CooldownActiveError(AppError):
super().__init__(message, "cooldown_active", details)
class LimitExceededError(AppError):
"""Limit exceeded error."""
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
super().__init__(message, "limit_exceeded", details)
class PermissionError(AppError):
"""Permission denied error."""
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
super().__init__(message, "permission_denied", details)
def create_http_exception(error: AppError) -> HTTPException:
"""Convert AppError to HTTPException."""
status_code = 400
if isinstance(error, NotFoundError):
status_code = 404
elif isinstance(error, AlreadyExistsError):
elif isinstance(error, (AlreadyExistsError, LimitExceededError)):
status_code = 409
elif isinstance(error, PermissionError):
status_code = 403
return HTTPException(
status_code=status_code,

View File

@ -1,13 +1,14 @@
"""Warp model."""
from sqlalchemy import REAL, Boolean, Column, Float, Index, String, Text
from sqlalchemy import REAL, Boolean, Column, Float, ForeignKey, Index, String, Text
from sqlalchemy.dialects.postgresql import UUID
from hubgw.core.config import APP_CONFIG
from hubgw.models.base import Base
# Get schema at module load time
# Get schemas at module load time
_HUB_SCHEMA = APP_CONFIG.database.hub_schema
_LUCKPERMS_SCHEMA = APP_CONFIG.database.luckperms_schema
class Warp(Base):
@ -25,6 +26,12 @@ class Warp(Base):
id = Column(
UUID(as_uuid=True), primary_key=True, server_default="gen_random_uuid()"
)
player_uuid = Column(
String(36),
ForeignKey(f"{_LUCKPERMS_SCHEMA}.luckperms_players.uuid", ondelete="CASCADE"),
nullable=False,
index=True,
)
name = Column(String(255), nullable=False, unique=True, index=True)
world = Column(Text, nullable=False)
x = Column(Float, nullable=False)

View File

@ -20,6 +20,7 @@ class WarpsRepository:
async def create(self, request: WarpCreateRequest) -> Warp:
"""Create warp."""
warp = Warp(
player_uuid=request.player_uuid,
name=request.name,
world=request.world,
x=request.x,
@ -118,3 +119,9 @@ class WarpsRepository:
async def list(self, query: WarpQuery) -> tuple[List[Warp], int]:
"""List warps (alias for query)."""
return await self.query(query)
async def count_by_player(self, player_uuid: str) -> int:
"""Count warps created by a specific player."""
stmt = select(func.count(Warp.id)).where(Warp.player_uuid == player_uuid)
result = await self.session.execute(stmt)
return result.scalar()

View File

@ -44,6 +44,7 @@ class HomeUpsertRequest(HomeBase):
"""Home upsert request schema."""
player_uuid: str
max_homes: Optional[int] = None
class Home(HomeBase, BaseSchema):

View File

@ -25,12 +25,14 @@ class WarpBase(BaseModel):
class WarpCreateRequest(WarpBase):
"""Warp create request schema."""
pass
player_uuid: str
max_warps: Optional[int] = None
class WarpUpdateRequest(BaseModel):
"""Warp update request schema."""
player_uuid: str
name: str
world: Optional[str] = None
x: Optional[float] = None
@ -45,6 +47,7 @@ class WarpUpdateRequest(BaseModel):
class WarpDeleteRequest(BaseModel):
"""Warp delete request schema."""
player_uuid: str
name: str
@ -58,6 +61,7 @@ class Warp(WarpBase, BaseSchema):
"""Warp response schema."""
id: UUID
player_uuid: str
class WarpGetResponse(Warp):

View File

@ -3,7 +3,7 @@
from sqlalchemy.ext.asyncio import AsyncSession
from hubgw.core.errors import NotFoundError
from hubgw.core.errors import LimitExceededError, NotFoundError
from hubgw.repositories.homes_repo import HomesRepository
from hubgw.schemas.homes import (Home, HomeCountResponse, HomeGetRequest,
HomeGetResponse, HomeListResponse,
@ -17,7 +17,24 @@ class HomesService:
self.repo = HomesRepository(session)
async def upsert_home(self, request: HomeUpsertRequest) -> Home:
"""Upsert home with business logic."""
"""Upsert home with business logic and limit check."""
if request.max_homes is not None:
existing_home = await self.repo.get_by_player_and_name(
request.player_uuid, request.name
)
if not existing_home:
current_count = await self.repo.count_by_player(request.player_uuid)
if current_count >= request.max_homes:
raise LimitExceededError(
f"Home limit exceeded. Maximum {request.max_homes} homes allowed.",
details={
"player_uuid": request.player_uuid,
"current_count": current_count,
"max_homes": request.max_homes,
},
)
return await self.repo.upsert(request)
async def get_home(self, request: HomeGetRequest) -> HomeGetResponse:

View File

@ -3,7 +3,12 @@
from sqlalchemy.ext.asyncio import AsyncSession
from hubgw.core.errors import AlreadyExistsError, NotFoundError
from hubgw.core.errors import (
AlreadyExistsError,
LimitExceededError,
NotFoundError,
PermissionError,
)
from hubgw.repositories.warps_repo import WarpsRepository
from hubgw.schemas.warps import (Warp, WarpCreateRequest, WarpDeleteRequest,
WarpGetRequest, WarpGetResponse,
@ -18,24 +23,61 @@ class WarpsService:
self.repo = WarpsRepository(session)
async def create_warp(self, request: WarpCreateRequest) -> Warp:
"""Create warp with business logic."""
# Check if warp with same name already exists
existing = await self.repo.get_by_request(WarpGetRequest(name=request.name))
"""Create warp with business logic and limit check."""
existing = await self.repo.get_by_name(request.name)
if existing:
raise AlreadyExistsError(f"Warp '{request.name}' already exists")
raise AlreadyExistsError(
f"Warp '{request.name}' already exists",
details={"owner_uuid": existing.player_uuid},
)
if request.max_warps is not None:
current_count = await self.repo.count_by_player(request.player_uuid)
if current_count >= request.max_warps:
raise LimitExceededError(
f"Warp limit exceeded. Maximum {request.max_warps} warps allowed.",
details={
"player_uuid": request.player_uuid,
"current_count": current_count,
"max_warps": request.max_warps,
},
)
return await self.repo.create(request)
async def update_warp(self, request: WarpUpdateRequest) -> Warp:
"""Update warp with business logic."""
warp = await self.repo.update(request)
if not warp:
"""Update warp with business logic and ownership check."""
existing = await self.repo.get_by_name(request.name)
if not existing:
raise NotFoundError(f"Warp '{request.name}' not found")
if existing.player_uuid != request.player_uuid:
raise PermissionError(
f"You don't have permission to update warp '{request.name}'",
details={
"warp_owner": existing.player_uuid,
"requested_by": request.player_uuid,
},
)
warp = await self.repo.update(request)
return warp
async def delete_warp(self, request: WarpDeleteRequest) -> None:
"""Delete warp with business logic."""
"""Delete warp with business logic and ownership check."""
existing = await self.repo.get_by_name(request.name)
if not existing:
raise NotFoundError(f"Warp '{request.name}' not found")
if existing.player_uuid != request.player_uuid:
raise PermissionError(
f"You don't have permission to delete warp '{request.name}'",
details={
"warp_owner": existing.player_uuid,
"requested_by": request.player_uuid,
},
)
success = await self.repo.delete(request)
if not success:
raise NotFoundError(f"Warp '{request.name}' not found")
@ -48,6 +90,7 @@ class WarpsService:
return WarpGetResponse(
id=warp.id,
player_uuid=warp.player_uuid,
name=warp.name,
world=warp.world,
x=warp.x,
@ -68,6 +111,7 @@ class WarpsService:
warp_list = [
Warp(
id=warp.id,
player_uuid=warp.player_uuid,
name=warp.name,
world=warp.world,
x=warp.x,