diff --git a/app/api/coins_ws.py b/app/api/coins_ws.py new file mode 100644 index 0000000..8debd68 --- /dev/null +++ b/app/api/coins_ws.py @@ -0,0 +1,25 @@ +# app/api/coins_ws.py +from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query +from app.realtime.coins_hub import coins_hub + +router = APIRouter(tags=["Coins WS"]) + + +@router.websocket("/ws/coins") +async def coins_ws( + websocket: WebSocket, + username: str = Query(...), +): + await coins_hub.connect(username, websocket) + try: + while True: + await websocket.receive_text() # ping / keep-alive + except WebSocketDisconnect: + pass + finally: + await coins_hub.disconnect(username, websocket) + + +@router.get("/ws/coins/ping") +async def ping(): + return {"ok": True} diff --git a/app/realtime/coins_hub.py b/app/realtime/coins_hub.py new file mode 100644 index 0000000..5a627fe --- /dev/null +++ b/app/realtime/coins_hub.py @@ -0,0 +1,53 @@ +# app/realtime/coins_hub.py +from typing import Dict, Set +from fastapi import WebSocket +import asyncio + + +class CoinsHub: + def __init__(self): + # username -> set of websockets + self._connections: Dict[str, Set[WebSocket]] = {} + self._lock = asyncio.Lock() + + async def connect(self, username: str, ws: WebSocket): + await ws.accept() + async with self._lock: + self._connections.setdefault(username, set()).add(ws) + + async def disconnect(self, username: str, ws: WebSocket): + async with self._lock: + conns = self._connections.get(username) + if not conns: + return + conns.discard(ws) + if not conns: + self._connections.pop(username, None) + + async def send_update(self, username: str, coins: int): + async with self._lock: + conns = list(self._connections.get(username, [])) + + if not conns: + return + + payload = { + "event": "coins:update", + "coins": coins, + } + + dead: list[WebSocket] = [] + + for ws in conns: + try: + await ws.send_json(payload) + except Exception: + dead.append(ws) + + if dead: + async with self._lock: + for ws in dead: + self._connections.get(username, set()).discard(ws) + + +coins_hub = CoinsHub() diff --git a/app/services/coins.py b/app/services/coins.py index 1e941b8..c3ace43 100644 --- a/app/services/coins.py +++ b/app/services/coins.py @@ -3,6 +3,7 @@ import re from app.db.database import users_collection from fastapi import HTTPException from app.db.database import db +from app.realtime.coins_hub import coins_hub MAX_MINUTES_PER_UPDATE = 120 @@ -70,6 +71,8 @@ class CoinsService: {"$set": {"coins": new_coins, "total_time_played": new_total_time}} ) + await coins_hub.send_update(user["username"], new_coins) + await coins_sessions_collection.insert_one({ "player_id": player_id, "player_name": player_name, @@ -138,7 +141,9 @@ class CoinsService: raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден") user = await users_collection.find_one({"username": username}) - return user.get("coins", 0) + new_balance = user.get("coins", 0) + await coins_hub.send_update(username, new_balance) + return new_balance async def decrease_balance(self, username: str, amount: int) -> int: """Уменьшить баланс пользователя""" @@ -154,4 +159,6 @@ class CoinsService: raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден") user = await users_collection.find_one({"username": username}) - return user.get("coins", 0) + new_balance = user.get("coins", 0) + await coins_hub.send_update(username, new_balance) + return new_balance diff --git a/app/services/dailyquests.py b/app/services/dailyquests.py index 8275507..fe75a33 100644 --- a/app/services/dailyquests.py +++ b/app/services/dailyquests.py @@ -8,6 +8,8 @@ import random from fastapi import HTTPException from app.db.database import db, users_collection +from app.services.coins import CoinsService + TZ = ZoneInfo("Asia/Yekaterinburg") # как в dailyreward :contentReference[oaicite:1]{index=1} coins_sessions_collection = db.coins_sessions @@ -168,7 +170,8 @@ class DailyQuestsService: return {"claimed": False, "reason": "not_completed"} # 2) начисляем coins - await users_collection.update_one({"username": username}, {"$inc": {"coins": reward}}) + coins_service = CoinsService() + await coins_service.increase_balance(username, reward) # 3) лог в coins_sessions (как daily_login) :contentReference[oaicite:4]{index=4} await coins_sessions_collection.insert_one({ @@ -219,7 +222,8 @@ class DailyQuestsService: continue # уже claimed/не completed # начисляем coins - await users_collection.update_one({"username": username}, {"$inc": {"coins": reward}}) + coins_service = CoinsService() + await coins_service.increase_balance(username, reward) total_added += reward # лог (как в claim) :contentReference[oaicite:2]{index=2} diff --git a/app/services/dailyreward.py b/app/services/dailyreward.py index bf9cf99..4e74322 100644 --- a/app/services/dailyreward.py +++ b/app/services/dailyreward.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta, timezone from zoneinfo import ZoneInfo from app.db.database import users_collection, db +from app.realtime.coins_hub import coins_hub coins_sessions_collection = db.coins_sessions @@ -70,6 +71,9 @@ class DailyRewardService: if result.modified_count == 0: user2 = await users_collection.find_one({"username": username}) return {"claimed": False, "reason": "already_claimed_today", "streak": user2.get("daily_streak", 0)} + + new_balance = (await users_collection.find_one({"username": username})).get("coins", 0) + await coins_hub.send_update(username, new_balance) await coins_sessions_collection.insert_one({ "player_name": username, diff --git a/main.py b/main.py index ac9904c..e607164 100644 --- a/main.py +++ b/main.py @@ -10,7 +10,7 @@ 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.api import marketplace_ws, coins_ws from app.db.database import users_collection, sessions_collection @@ -68,6 +68,7 @@ app.include_router(store.router) app.include_router(pranks.router) app.include_router(marketplace.router) app.include_router(marketplace_ws.router) +app.include_router(coins_ws.router) app.include_router(case.router) app.include_router(inventory.router) app.include_router(bonuses.router)