Compare commits

16 Commits

Author SHA1 Message Date
2a8f9000c9 add endpoint get room/{room_id} 2026-01-02 18:27:57 +05:00
2f32c09b6b rework voice_rooms api 2026-01-02 18:17:36 +05:00
7f660ba7a2 add Voice tag 2026-01-02 17:16:10 +05:00
f189b5b71c add voice room router 2026-01-02 17:14:27 +05:00
ed54c3a741 add voice_rooms 2026-01-02 17:12:27 +05:00
540977dc62 fix voice rooms 2026-01-02 16:34:43 +05:00
baa4341129 test voice rooms 2026-01-02 15:32:59 +05:00
16b477045c fix sell item #2 2025-12-29 21:25:17 +05:00
19d77328d9 loss desctiption 2025-12-29 21:22:20 +05:00
2288e0b239 add description in sell 2025-12-29 21:12:31 +05:00
0507624394 add description and image to marketplace items 2025-12-29 17:25:57 +05:00
daa2561b89 delete websocket in coins(не работает нихуя) 2025-12-29 13:54:51 +05:00
9d007a45c8 fix material 2025-12-29 13:34:37 +05:00
3b4e3d85ed material in prank 2025-12-29 13:28:36 +05:00
99d9741e97 add websocket for coins 2025-12-29 13:10:39 +05:00
bddd68bc25 add material in prank 2025-12-29 12:11:14 +05:00
11 changed files with 289 additions and 7 deletions

View File

@ -29,11 +29,12 @@ async def sell_item(
slot_index: int = Body(...), slot_index: int = Body(...),
amount: int = Body(...), amount: int = Body(...),
price: int = Body(...), price: int = Body(...),
server_ip: str = Body(...) server_ip: str = Body(...),
description: Optional[str] = Body(None),
): ):
"""Выставить предмет на продажу""" """Выставить предмет на продажу"""
from app.services.marketplace import MarketplaceService 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}") @router.post("/items/buy/{item_id}")
async def buy_item( async def buy_item(

87
app/api/voice_rooms.py Normal file
View 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
View 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}
)

View File

@ -5,6 +5,8 @@ class MarketplaceItemBase(BaseModel):
material: str material: str
amount: int amount: int
price: int price: int
description: Optional[str] = None
image_url: Optional[str] = None
seller_name: str seller_name: str
server_ip: str server_ip: str
display_name: Optional[str] = None display_name: Optional[str] = None

View File

@ -5,6 +5,7 @@ class PrankCommandCreate(BaseModel):
name: str name: str
description: str description: str
price: int price: int
material: str
command_template: str command_template: str
server_ids: List[str] = Field( server_ids: List[str] = Field(
default=[], default=[],
@ -17,6 +18,7 @@ class PrankCommandUpdate(BaseModel):
name: Optional[str] = None name: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
price: Optional[int] = None price: Optional[int] = None
material: Optional[str] = None
command_template: Optional[str] = None command_template: Optional[str] = None
server_ids: Optional[List[str]] = None server_ids: Optional[List[str]] = None
targetDescription: Optional[str] = None targetDescription: Optional[str] = None
@ -28,6 +30,7 @@ class PrankCommand(BaseModel):
description: str description: str
price: int price: int
command_template: str command_template: str
material: str
server_ids: List[str] = [] server_ids: List[str] = []
class PrankExecute(BaseModel): class PrankExecute(BaseModel):

View File

@ -0,0 +1,5 @@
from pydantic import BaseModel
class JoinRoomRequest(BaseModel):
code: str

41
app/realtime/voice_hub.py Normal file
View 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()

View File

@ -80,7 +80,7 @@ class MarketplaceService:
raise HTTPException(status_code=404, detail="Предмет не найден") raise HTTPException(status_code=404, detail="Предмет не найден")
return _serialize_mongodb_doc(item) 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()) operation_id = str(uuid.uuid4())
@ -93,6 +93,7 @@ class MarketplaceService:
"amount": amount, "amount": amount,
"price": price, "price": price,
"server_ip": server_ip, "server_ip": server_ip,
"description": description,
"status": "pending", "status": "pending",
"created_at": datetime.utcnow() "created_at": datetime.utcnow()
} }
@ -151,6 +152,8 @@ class MarketplaceService:
"material": item_data.get("material"), "material": item_data.get("material"),
"amount": item_data.get("amount"), "amount": item_data.get("amount"),
"price": operation.get("price"), "price": operation.get("price"),
"description": operation.get("description"),
"image_url": item_data.get("image_url"),
"seller_name": operation.get("player_name"), "seller_name": operation.get("player_name"),
"server_ip": operation.get("server_ip"), "server_ip": operation.get("server_ip"),
"display_name": item_data.get("meta", {}).get("display_name"), "display_name": item_data.get("meta", {}).get("display_name"),

View File

@ -27,6 +27,7 @@ class PrankService:
"name": command_data.name, "name": command_data.name,
"description": command_data.description, "description": command_data.description,
"price": command_data.price, "price": command_data.price,
"material": command_data.material,
"command_template": command_data.command_template, "command_template": command_data.command_template,
"server_ids": command_data.server_ids, "server_ids": command_data.server_ids,
"targetDescription": command_data.targetDescription, "targetDescription": command_data.targetDescription,
@ -49,6 +50,7 @@ class PrankService:
"name": cmd["name"], "name": cmd["name"],
"description": cmd["description"], "description": cmd["description"],
"price": cmd["price"], "price": cmd["price"],
"material": cmd.get("material"),
"command_template": cmd["command_template"], "command_template": cmd["command_template"],
"server_ids": cmd.get("server_ids", []), "server_ids": cmd.get("server_ids", []),
"targetDescription": cmd.get("targetDescription"), "targetDescription": cmd.get("targetDescription"),
@ -68,6 +70,7 @@ class PrankService:
"name": command["name"], "name": command["name"],
"description": command["description"], "description": command["description"],
"price": command["price"], "price": command["price"],
"material": command.get("material"),
"command_template": command["command_template"], "command_template": command["command_template"],
"server_ids": command.get("server_ids", []), "server_ids": command.get("server_ids", []),
"targetDescription": command.get("targetDescription"), "targetDescription": command.get("targetDescription"),
@ -88,6 +91,8 @@ class PrankService:
update["description"] = update_data.description update["description"] = update_data.description
if update_data.price is not None: if update_data.price is not None:
update["price"] = update_data.price 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 update_data.command_template is not None:
if "{targetPlayer}" not in update_data.command_template: if "{targetPlayer}" not in update_data.command_template:
raise HTTPException(status_code=400, raise HTTPException(status_code=400,

View 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
View File

@ -3,14 +3,12 @@ import os
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
import httpx 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 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.core.config import CAPES_DIR, CAPES_STORE_DIR, SKINS_DIR
from app.services.promo import PromoService from app.services.promo import PromoService
from app.webhooks import telegram 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 from app.db.database import users_collection, sessions_collection
@ -30,6 +28,11 @@ async def lifespan(app: FastAPI):
expireAfterSeconds=0 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("expires_at", expireAfterSeconds=0)
await users_collection.create_index("telegram_user_id", unique=True, sparse=True) 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(telegram.router)
app.include_router(admin_daily_quests.router) app.include_router(admin_daily_quests.router)
app.include_router(promo.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") app.mount("/skins", StaticFiles(directory=str(SKINS_DIR)), name="skins")