feat(daily-challenge): complete daily-challenge

This commit is contained in:
MingxuanGame
2025-08-09 14:34:46 +00:00
parent 8531e67423
commit 5fe3f36055
9 changed files with 279 additions and 25 deletions

View File

@@ -0,0 +1,48 @@
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from typing import Any
from app.dependencies.database import get_redis_pubsub
class RedisSubscriber:
def __init__(self):
self.pubsub = get_redis_pubsub()
self.handlers: dict[str, list[Callable[[str, str], Awaitable[Any]]]] = {}
self.task: asyncio.Task | None = None
async def subscribe(self, channel: str):
await self.pubsub.subscribe(channel)
if channel not in self.handlers:
self.handlers[channel] = []
async def unsubscribe(self, channel: str):
if channel in self.handlers:
del self.handlers[channel]
await self.pubsub.unsubscribe(channel)
async def listen(self):
while True:
message = await self.pubsub.get_message(
ignore_subscribe_messages=True, timeout=None
)
if message is not None and message["type"] == "message":
method = self.handlers.get(message["channel"])
if method:
await asyncio.gather(
*[
handler(message["channel"], message["data"])
for handler in method
]
)
def start(self):
if self.task is None or self.task.done():
self.task = asyncio.create_task(self.listen())
def stop(self):
if self.task is not None and not self.task.done():
self.task.cancel()
self.task = None

View File

@@ -0,0 +1,87 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from app.database import PlaylistBestScore, Score
from app.database.playlist_best_score import get_position
from app.dependencies.database import engine
from app.models.metadata_hub import MultiplayerRoomScoreSetEvent
from .base import RedisSubscriber
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
if TYPE_CHECKING:
from app.signalr.hub import MetadataHub
CHANNEL = "score:processed"
class ScoreSubscriber(RedisSubscriber):
def __init__(self):
super().__init__()
self.room_subscriber: dict[int, list[int]] = {}
self.metadata_hub: "MetadataHub | None " = None
self.subscribed = False
self.handlers[CHANNEL] = [self._handler]
async def subscribe_room_score(self, room_id: int, user_id: int):
if room_id not in self.room_subscriber:
await self.subscribe(CHANNEL)
self.start()
self.room_subscriber.setdefault(room_id, []).append(user_id)
async def unsubscribe_room_score(self, room_id: int, user_id: int):
if room_id in self.room_subscriber:
self.room_subscriber[room_id].remove(user_id)
if not self.room_subscriber[room_id]:
del self.room_subscriber[room_id]
async def _notify_room_score_processed(self, score_id: int):
if not self.metadata_hub:
return
async with AsyncSession(engine) as session:
score = await session.get(Score, score_id)
if (
not score
or not score.passed
or score.room_id is None
or score.playlist_item_id is None
):
return
if not self.room_subscriber.get(score.room_id, []):
return
new_rank = None
user_best = (
await session.exec(
select(PlaylistBestScore).where(
PlaylistBestScore.user_id == score.user_id,
PlaylistBestScore.room_id == score.room_id,
)
)
).first()
if user_best and user_best.score_id == score_id:
new_rank = await get_position(
user_best.room_id,
user_best.playlist_id,
user_best.score_id,
session,
)
event = MultiplayerRoomScoreSetEvent(
room_id=score.room_id,
playlist_item_id=score.playlist_item_id,
score_id=score_id,
user_id=score.user_id,
total_score=score.total_score,
new_rank=new_rank,
)
await self.metadata_hub.notify_room_score_processed(event)
async def _handler(self, channel: str, data: str):
score_id = int(data)
if self.metadata_hub:
await self._notify_room_score_processed(score_id)