Compare commits
16 Commits
520ad99099
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a8f9000c9 | |||
| 2f32c09b6b | |||
| 7f660ba7a2 | |||
| f189b5b71c | |||
| ed54c3a741 | |||
| 540977dc62 | |||
| baa4341129 | |||
| 16b477045c | |||
| 19d77328d9 | |||
| 2288e0b239 | |||
| 0507624394 | |||
| daa2561b89 | |||
| 9d007a45c8 | |||
| 3b4e3d85ed | |||
| 99d9741e97 | |||
| bddd68bc25 |
@ -29,11 +29,12 @@ async def sell_item(
|
||||
slot_index: int = Body(...),
|
||||
amount: int = Body(...),
|
||||
price: int = Body(...),
|
||||
server_ip: str = Body(...)
|
||||
server_ip: str = Body(...),
|
||||
description: Optional[str] = Body(None),
|
||||
):
|
||||
"""Выставить предмет на продажу"""
|
||||
from app.services.marketplace import MarketplaceService
|
||||
return await MarketplaceService().add_item(username, slot_index, amount, price, server_ip)
|
||||
return await MarketplaceService().add_item(username, slot_index, amount, price, server_ip, description)
|
||||
|
||||
@router.post("/items/buy/{item_id}")
|
||||
async def buy_item(
|
||||
|
||||
87
app/api/voice_rooms.py
Normal file
87
app/api/voice_rooms.py
Normal file
@ -0,0 +1,87 @@
|
||||
from fastapi import APIRouter, Body, HTTPException
|
||||
from app.models.voice_rooms import JoinRoomRequest
|
||||
from app.services.voice_rooms import VoiceRoomService
|
||||
from app.realtime.voice_hub import voice_hub
|
||||
|
||||
router = APIRouter(prefix="/api/voice", tags=["Voice"])
|
||||
service = VoiceRoomService()
|
||||
|
||||
|
||||
@router.get("/rooms")
|
||||
async def list_rooms():
|
||||
rooms = await service.list_rooms()
|
||||
result = []
|
||||
|
||||
for room in rooms:
|
||||
room_id = room["id"]
|
||||
users_map = voice_hub.rooms.get(room_id, {})
|
||||
usernames = list(users_map.keys())
|
||||
users_count = len(usernames)
|
||||
|
||||
if room["public"]:
|
||||
# 🟢 публичная — отдаём всё безопасное
|
||||
result.append({
|
||||
"id": room["id"],
|
||||
"name": room["name"],
|
||||
"public": True,
|
||||
"owner": room.get("owner"),
|
||||
"max_users": room.get("max_users"),
|
||||
"users": users_count,
|
||||
"usernames": usernames,
|
||||
"created_at": room["created_at"].isoformat()
|
||||
if room.get("created_at") else None,
|
||||
})
|
||||
else:
|
||||
# 🔒 приватная — ТОЛЬКО метаданные
|
||||
result.append({
|
||||
"name": room["name"],
|
||||
"public": False,
|
||||
"users": users_count,
|
||||
"usernames": usernames,
|
||||
"max_users": room.get("max_users"),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@router.get("/rooms/{room_id}")
|
||||
async def get_room(room_id: str):
|
||||
room = await service.get_room(room_id)
|
||||
|
||||
users_map = voice_hub.rooms.get(room_id, {})
|
||||
usernames = list(users_map.keys())
|
||||
users_count = len(usernames)
|
||||
|
||||
if room["public"]:
|
||||
# 🟢 публичная — полная информация
|
||||
return {
|
||||
"id": room["id"],
|
||||
"name": room["name"],
|
||||
"public": True,
|
||||
"owner": room.get("owner"),
|
||||
"max_users": room.get("max_users"),
|
||||
"users": users_count,
|
||||
"usernames": usernames,
|
||||
"created_at": room["created_at"].isoformat()
|
||||
if room.get("created_at") else None,
|
||||
}
|
||||
|
||||
# 🔒 приватная — НЕЛЬЗЯ получать по room_id напрямую
|
||||
# иначе это обход invite-кода
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Access to private room is forbidden",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/rooms")
|
||||
async def create_room(
|
||||
name: str = Body(...),
|
||||
public: bool = Body(True),
|
||||
owner: str = Body(...),
|
||||
):
|
||||
return await service.create_room(name, owner, public)
|
||||
|
||||
|
||||
@router.post("/rooms/join")
|
||||
async def join_private(payload: JoinRoomRequest):
|
||||
return await service.join_by_code(payload.code)
|
||||
57
app/api/voice_ws.py
Normal file
57
app/api/voice_ws.py
Normal file
@ -0,0 +1,57 @@
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
|
||||
from app.realtime.voice_hub import voice_hub
|
||||
from app.services.voice_rooms import voice_rooms_collection
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.websocket("/ws/voice")
|
||||
async def voice_ws(
|
||||
ws: WebSocket,
|
||||
room_id: str = Query(...),
|
||||
username: str = Query(...)
|
||||
):
|
||||
room = await voice_rooms_collection.find_one({"id": room_id})
|
||||
if not room:
|
||||
await ws.close(code=4004)
|
||||
return
|
||||
if username in voice_hub.rooms.get(room_id, {}):
|
||||
await ws.close(code=4001)
|
||||
return
|
||||
|
||||
if len(voice_hub.rooms.get(room_id, {})) >= room.get("max_users", 5):
|
||||
await ws.close(code=4003)
|
||||
return
|
||||
|
||||
await voice_hub.connect(room_id, username, ws)
|
||||
|
||||
users = list(voice_hub.rooms.get(room_id, {}).keys())
|
||||
await ws.send_json({
|
||||
"type": "users",
|
||||
"users": users
|
||||
})
|
||||
|
||||
await voice_hub.broadcast_except(
|
||||
room_id,
|
||||
username,
|
||||
{"type": "join", "user": username}
|
||||
)
|
||||
|
||||
try:
|
||||
while True:
|
||||
msg = await ws.receive_json()
|
||||
if msg["type"] == "signal":
|
||||
await voice_hub.send_to(
|
||||
room_id,
|
||||
msg["to"],
|
||||
{
|
||||
"type": "signal",
|
||||
"from": username,
|
||||
"data": msg["data"]
|
||||
}
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
voice_hub.disconnect(room_id, username)
|
||||
await voice_hub.broadcast(
|
||||
room_id,
|
||||
{"type": "leave", "user": username}
|
||||
)
|
||||
@ -5,6 +5,8 @@ class MarketplaceItemBase(BaseModel):
|
||||
material: str
|
||||
amount: int
|
||||
price: int
|
||||
description: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
seller_name: str
|
||||
server_ip: str
|
||||
display_name: Optional[str] = None
|
||||
|
||||
@ -5,6 +5,7 @@ class PrankCommandCreate(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
price: int
|
||||
material: str
|
||||
command_template: str
|
||||
server_ids: List[str] = Field(
|
||||
default=[],
|
||||
@ -17,6 +18,7 @@ class PrankCommandUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
price: Optional[int] = None
|
||||
material: Optional[str] = None
|
||||
command_template: Optional[str] = None
|
||||
server_ids: Optional[List[str]] = None
|
||||
targetDescription: Optional[str] = None
|
||||
@ -28,6 +30,7 @@ class PrankCommand(BaseModel):
|
||||
description: str
|
||||
price: int
|
||||
command_template: str
|
||||
material: str
|
||||
server_ids: List[str] = []
|
||||
|
||||
class PrankExecute(BaseModel):
|
||||
|
||||
5
app/models/voice_rooms.py
Normal file
5
app/models/voice_rooms.py
Normal file
@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class JoinRoomRequest(BaseModel):
|
||||
code: str
|
||||
41
app/realtime/voice_hub.py
Normal file
41
app/realtime/voice_hub.py
Normal file
@ -0,0 +1,41 @@
|
||||
from typing import Dict
|
||||
from fastapi import WebSocket
|
||||
|
||||
class VoiceHub:
|
||||
def __init__(self):
|
||||
# room_id -> username -> websocket
|
||||
self.rooms: Dict[str, Dict[str, WebSocket]] = {}
|
||||
|
||||
async def connect(self, room_id: str, username: str, ws: WebSocket):
|
||||
await ws.accept()
|
||||
self.rooms.setdefault(room_id, {})[username] = ws
|
||||
|
||||
def disconnect(self, room_id: str, username: str):
|
||||
room = self.rooms.get(room_id)
|
||||
if not room:
|
||||
return
|
||||
room.pop(username, None)
|
||||
if not room:
|
||||
self.rooms.pop(room_id, None)
|
||||
|
||||
async def send_to(self, room_id: str, to_user: str, payload: dict):
|
||||
ws = self.rooms.get(room_id, {}).get(to_user)
|
||||
if ws:
|
||||
await ws.send_json(payload)
|
||||
|
||||
async def broadcast(self, room_id: str, payload: dict):
|
||||
for ws in self.rooms.get(room_id, {}).values():
|
||||
try:
|
||||
await ws.send_json(payload)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def broadcast_except(self, room_id: str, except_user: str, payload: dict):
|
||||
for user, ws in self.rooms.get(room_id, {}).items():
|
||||
if user != except_user:
|
||||
try:
|
||||
await ws.send_json(payload)
|
||||
except:
|
||||
pass
|
||||
|
||||
voice_hub = VoiceHub()
|
||||
@ -80,7 +80,7 @@ class MarketplaceService:
|
||||
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):
|
||||
async def add_item(self, username: str, slot_index: int, amount: int, price: int, server_ip: str, description: str):
|
||||
"""Выставить предмет на продажу"""
|
||||
# Создаем операцию продажи
|
||||
operation_id = str(uuid.uuid4())
|
||||
@ -93,6 +93,7 @@ class MarketplaceService:
|
||||
"amount": amount,
|
||||
"price": price,
|
||||
"server_ip": server_ip,
|
||||
"description": description,
|
||||
"status": "pending",
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
@ -151,6 +152,8 @@ class MarketplaceService:
|
||||
"material": item_data.get("material"),
|
||||
"amount": item_data.get("amount"),
|
||||
"price": operation.get("price"),
|
||||
"description": operation.get("description"),
|
||||
"image_url": item_data.get("image_url"),
|
||||
"seller_name": operation.get("player_name"),
|
||||
"server_ip": operation.get("server_ip"),
|
||||
"display_name": item_data.get("meta", {}).get("display_name"),
|
||||
|
||||
@ -27,6 +27,7 @@ class PrankService:
|
||||
"name": command_data.name,
|
||||
"description": command_data.description,
|
||||
"price": command_data.price,
|
||||
"material": command_data.material,
|
||||
"command_template": command_data.command_template,
|
||||
"server_ids": command_data.server_ids,
|
||||
"targetDescription": command_data.targetDescription,
|
||||
@ -49,6 +50,7 @@ class PrankService:
|
||||
"name": cmd["name"],
|
||||
"description": cmd["description"],
|
||||
"price": cmd["price"],
|
||||
"material": cmd.get("material"),
|
||||
"command_template": cmd["command_template"],
|
||||
"server_ids": cmd.get("server_ids", []),
|
||||
"targetDescription": cmd.get("targetDescription"),
|
||||
@ -68,6 +70,7 @@ class PrankService:
|
||||
"name": command["name"],
|
||||
"description": command["description"],
|
||||
"price": command["price"],
|
||||
"material": command.get("material"),
|
||||
"command_template": command["command_template"],
|
||||
"server_ids": command.get("server_ids", []),
|
||||
"targetDescription": command.get("targetDescription"),
|
||||
@ -88,6 +91,8 @@ class PrankService:
|
||||
update["description"] = update_data.description
|
||||
if update_data.price is not None:
|
||||
update["price"] = update_data.price
|
||||
if update_data.material is not None:
|
||||
update["material"] = update_data.material
|
||||
if update_data.command_template is not None:
|
||||
if "{targetPlayer}" not in update_data.command_template:
|
||||
raise HTTPException(status_code=400,
|
||||
|
||||
73
app/services/voice_rooms.py
Normal file
73
app/services/voice_rooms.py
Normal file
@ -0,0 +1,73 @@
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import uuid4
|
||||
from fastapi import HTTPException
|
||||
from app.db.database import db
|
||||
|
||||
voice_rooms_collection = db.voice_rooms
|
||||
|
||||
|
||||
def _serialize(doc):
|
||||
if not doc:
|
||||
return None
|
||||
doc["_id"] = str(doc["_id"])
|
||||
if "created_at" in doc:
|
||||
doc["created_at"] = doc["created_at"].isoformat()
|
||||
if "expires_at" in doc and doc["expires_at"]:
|
||||
doc["expires_at"] = doc["expires_at"].isoformat()
|
||||
return doc
|
||||
|
||||
|
||||
class VoiceRoomService:
|
||||
|
||||
async def list_rooms(self):
|
||||
rooms = await voice_rooms_collection.find({}) \
|
||||
.sort("created_at", -1) \
|
||||
.to_list(100)
|
||||
|
||||
return rooms
|
||||
|
||||
async def create_room(
|
||||
self,
|
||||
name: str,
|
||||
owner: str,
|
||||
public: bool,
|
||||
max_users: int = 5,
|
||||
ttl_minutes: int | None = None,
|
||||
):
|
||||
room_id = str(uuid4())
|
||||
invite_code = None if public else uuid4().hex[:6]
|
||||
|
||||
room = {
|
||||
"id": room_id,
|
||||
"name": name,
|
||||
"public": public,
|
||||
"invite_code": invite_code,
|
||||
"owner": owner,
|
||||
"max_users": max_users,
|
||||
"created_at": datetime.utcnow(),
|
||||
"expires_at": (
|
||||
datetime.utcnow() + timedelta(minutes=ttl_minutes)
|
||||
if ttl_minutes else None
|
||||
),
|
||||
}
|
||||
|
||||
await voice_rooms_collection.insert_one(room)
|
||||
return _serialize(room)
|
||||
|
||||
async def get_room(self, room_id: str):
|
||||
room = await voice_rooms_collection.find_one({"id": room_id})
|
||||
if not room:
|
||||
raise HTTPException(404, "Room not found")
|
||||
return room
|
||||
|
||||
async def join_by_code(self, code: str):
|
||||
room = await voice_rooms_collection.find_one({"invite_code": code})
|
||||
if not room:
|
||||
raise HTTPException(404, "Invalid invite code")
|
||||
|
||||
return {
|
||||
"id": room["id"],
|
||||
"name": room["name"],
|
||||
"public": False,
|
||||
"max_users": room.get("max_users"),
|
||||
}
|
||||
13
main.py
13
main.py
@ -3,14 +3,12 @@ import os
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import httpx
|
||||
from app.api import admin_daily_quests, inventory, news, users, skins, capes, meta, server, store, pranks, marketplace, bonuses, case, promo
|
||||
from app.api import admin_daily_quests, inventory, news, users, skins, capes, meta, server, store, pranks, marketplace, marketplace_ws, bonuses, case, promo, voice_rooms, voice_ws
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.services.voice_rooms import voice_rooms_collection
|
||||
from app.core.config import CAPES_DIR, CAPES_STORE_DIR, SKINS_DIR
|
||||
from app.services.promo import PromoService
|
||||
from app.webhooks import telegram
|
||||
from app.db.database import users_collection
|
||||
from app.api import marketplace_ws
|
||||
from app.db.database import users_collection, sessions_collection
|
||||
|
||||
|
||||
@ -30,6 +28,11 @@ async def lifespan(app: FastAPI):
|
||||
expireAfterSeconds=0
|
||||
)
|
||||
|
||||
await voice_rooms_collection.create_index(
|
||||
"expires_at",
|
||||
expireAfterSeconds=0
|
||||
)
|
||||
|
||||
await users_collection.create_index("expires_at", expireAfterSeconds=0)
|
||||
await users_collection.create_index("telegram_user_id", unique=True, sparse=True)
|
||||
|
||||
@ -75,6 +78,8 @@ app.include_router(news.router)
|
||||
app.include_router(telegram.router)
|
||||
app.include_router(admin_daily_quests.router)
|
||||
app.include_router(promo.router)
|
||||
app.include_router(voice_ws.router)
|
||||
app.include_router(voice_rooms.router)
|
||||
|
||||
# Монтируем статику
|
||||
app.mount("/skins", StaticFiles(directory=str(SKINS_DIR)), name="skins")
|
||||
|
||||
Reference in New Issue
Block a user