import json import asyncio from channels.generic.websocket import AsyncWebsocketConsumer from channels.db import database_sync_to_async from django.utils import timezone from datetime import timedelta from .models import QuizGame, QuizGameParticipant class LobbyConsumer(AsyncWebsocketConsumer): async def connect(self): self.join_code = self.scope['url_route']['kwargs']['join_code'] self.room_group_name = f'game_{self.join_code}' self.heartbeat_task = None # Join room group await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() # Start heartbeat check self.heartbeat_task = asyncio.create_task(self.check_participants_heartbeat()) # Send current participants list participants = await self.get_participants() await self.send(text_data=json.dumps({ 'type': 'participant_list', 'participants': participants })) async def disconnect(self, close_code): # Cancel heartbeat task if self.heartbeat_task: self.heartbeat_task.cancel() try: await self.heartbeat_task except asyncio.CancelledError: pass # Leave room group await self.channel_layer.group_discard( self.room_group_name, self.channel_name ) async def receive(self, text_data): text_data_json = json.loads(text_data) message_type = text_data_json['type'] if message_type == 'heartbeat': # Update participant's last activity timestamp participant_id = text_data_json.get('participant_id') if participant_id: await self._update_participant_heartbeat(participant_id) elif message_type == 'update_participants': participants = await self.get_participants() await self.channel_layer.group_send( self.room_group_name, { 'type': 'participant_list_update', 'participants': participants } ) async def participant_list_update(self, event): participants = event['participants'] await self.send(text_data=json.dumps({ 'type': 'participant_list', 'participants': participants })) @database_sync_to_async def get_participants(self): game = QuizGame.objects.get(join_code=self.join_code) participants = QuizGameParticipant.objects.filter(quiz_game=game) return [{'id': str(p.participant_id), 'name': p.display_name} for p in participants] @database_sync_to_async def _update_participant_heartbeat(self, participant_id): try: participant = QuizGameParticipant.objects.get(participant_id=participant_id) participant.save() # This will update last_heartbeat due to auto_now=True except QuizGameParticipant.DoesNotExist: pass @database_sync_to_async def _remove_inactive_participants(self): # Remove participants who haven't sent a heartbeat in the last 30 seconds timeout = timezone.now() - timedelta(seconds=30) quiz_game = QuizGame.objects.get(join_code=self.join_code) QuizGameParticipant.objects.filter( quiz_game=quiz_game, last_heartbeat__lt=timeout ).delete() async def check_participants_heartbeat(self): while True: try: await asyncio.sleep(10) # Check every 10 seconds await self._remove_inactive_participants() # Send updated participant list participants = await self.get_participants() await self.channel_layer.group_send( self.room_group_name, { 'type': 'participant_list_update', 'participants': participants } ) except asyncio.CancelledError: break except Exception as e: print(f'Error in heartbeat check: {e}') await asyncio.sleep(10) # Wait before retrying