import redis.asyncio as redis from typing import Optional, Any, Awaitable, cast from core.config import settings redis_client: redis.Redis = redis.from_url( settings.REDIS_URL, decode_responses=True, ) # ========================= # Redis Keys # ========================= def speaker_key(group_id: str) -> str: return f"speaker:{group_id}" def presence_key(group_id: str) -> str: return f"presence:{group_id}" # ========================= # Speaker Lock # ========================= async def get_active_speaker(group_id: str) -> Optional[str]: return await redis_client.get(speaker_key(group_id)) async def acquire_speaker( group_id: str, user_id: str, ttl: int = 30, ) -> bool: """ Try to become the active speaker. Uses Redis SET NX. """ result = await redis_client.set( speaker_key(group_id), user_id, nx=True, ex=ttl, ) return result is True async def release_speaker(group_id: str, user_id: str) -> bool: """ Release speaker only if owned by the user. Prevents race conditions. """ lua_script = """ if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end """ result = await cast( Awaitable[Any], redis_client.eval( lua_script, 1, speaker_key(group_id), user_id, ), ) return result == 1 async def extend_speaker_lock(group_id: str, user_id: str, ttl: int = 30) -> bool: lua_script = """ if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("EXPIRE", KEYS[1], ARGV[2]) else return 0 end """ result = await redis_client.eval(lua_script, 1, speaker_key(group_id), user_id, ttl) # type: ignore return result == 1 # ========================= # Presence # ========================= async def add_presence(group_id: str, user_id: str) -> None: await redis_client.sadd(presence_key(group_id), user_id) # type:ignore async def remove_presence(group_id: str, user_id: str) -> None: await redis_client.srem(presence_key(group_id), user_id) # type:ignore async def get_presence(group_id: str) -> list[str]: members = await redis_client.smembers( presence_key(group_id) ) # type:ignore return list(members) # ========================= # Cleanup # ========================= async def clear_group_state(group_id: str) -> None: await redis_client.delete( speaker_key(group_id), presence_key(group_id), )