From 5ac945eb1848f3fb1e98588526441455b340def3 Mon Sep 17 00:00:00 2001 From: akhdanre Date: Fri, 16 May 2025 02:42:10 +0700 Subject: [PATCH] fix: session sistem --- app/controllers/socket_conroller.py | 43 ++--- app/di_container.py | 4 +- app/models/entities/session_entity.py | 2 +- app/repositories/__init__.py | 2 + app/repositories/session_memory_repository.py | 149 ++++++++++++++++++ app/repositories/session_repostory.py | 1 - app/services/session_service.py | 57 +++++-- 7 files changed, 215 insertions(+), 43 deletions(-) create mode 100644 app/repositories/session_memory_repository.py diff --git a/app/controllers/socket_conroller.py b/app/controllers/socket_conroller.py index 4de99d0..23ebe43 100644 --- a/app/controllers/socket_conroller.py +++ b/app/controllers/socket_conroller.py @@ -7,35 +7,14 @@ import json from redis import Redis -class RedisRepository: - """Small helper wrapper to (de)serialize python objects to Redis.""" - - def __init__(self, redis: Redis): - self.redis = redis - - def set_data(self, key: str, value): - self.redis.set(key, json.dumps(value)) - - def get_data(self, key: str): - data = self.redis.get(key) - return json.loads(data) if data else None - - def delete_key(self, key: str): - self.redis.delete(key) - - class SocketController: def __init__( self, socketio: SocketIO, - redis: Redis, session_service: SessionService, ): self.socketio = socketio self.session_service = session_service - self.redis_repo = RedisRepository(redis) - # Menyimpan SID admin untuk setiap session \u2192 {session_code: sid} - self.admin_sids: dict[str, str] = {} self._register_events() @staticmethod @@ -101,22 +80,21 @@ class SocketController: return session = self.session_service.join_session( - session_code=session_code, user_id=user_id + session_code=session_code, + user_id=user_id, ) + if session is None: emit("error", {"message": "Failed to join session or session inactive"}) return join_room(session_code) - # Kalau user ini admin, simpan SID‑nya. if session["is_admin"]: - self.admin_sids[session_code] = request.sid message = "Admin has joined the room." else: message = f"User {session['username']} has joined the room." - print(message) emit( "room_message", { @@ -124,9 +102,22 @@ class SocketController: "message": message, "room": session_code, "argument": "adm_update", - "data": session if not session["is_admin"] else None, + "data": session["quiz_info"], + }, + to=request.sid, + ) + + emit( + "room_message", + { + "type": "participan_join", + "message": message, + "room": session_code, + "argument": "adm_update", + "data": session["quiz_info"]["participants"], }, room=session_code, + skip_sid=request.sid, # Skip user yang baru join ) @self.socketio.on("submit_answer") diff --git a/app/di_container.py b/app/di_container.py index fcb1482..60a2e3e 100644 --- a/app/di_container.py +++ b/app/di_container.py @@ -6,6 +6,7 @@ from app.repositories import ( SubjectRepository, SessionRepository, NERSRLRepository, + SessionMemoryRepository, ) from app.services import ( @@ -44,6 +45,7 @@ class Container(containers.DeclarativeContainer): subject_repository = providers.Factory(SubjectRepository, mongo.provided.db) session_repository = providers.Factory(SessionRepository, mongo.provided.db) ner_srl_repository = providers.Factory(NERSRLRepository) + session_memory_repository = providers.Factory(SessionMemoryRepository, redis) # services auth_service = providers.Factory( @@ -83,6 +85,7 @@ class Container(containers.DeclarativeContainer): session_service = providers.Factory( SessionService, session_repository, + session_memory_repository, user_repository, ) @@ -104,7 +107,6 @@ class Container(containers.DeclarativeContainer): socket_controller = providers.Factory( SocketController, socketio, - redis, session_service, ) session_controller = providers.Factory(SessionController, session_service) diff --git a/app/models/entities/session_entity.py b/app/models/entities/session_entity.py index faf8a04..1974541 100644 --- a/app/models/entities/session_entity.py +++ b/app/models/entities/session_entity.py @@ -14,5 +14,5 @@ class SessionEntity(BaseModel): ended_at: datetime | None = None is_active: bool = True participan_limit: int = 10 - participants: List[str] = [] + participants: List[dict] = [] current_question_index: int = 0 diff --git a/app/repositories/__init__.py b/app/repositories/__init__.py index 15b312c..e02ad6d 100644 --- a/app/repositories/__init__.py +++ b/app/repositories/__init__.py @@ -4,6 +4,7 @@ from .answer_repository import UserAnswerRepository from .subject_repository import SubjectRepository from .session_repostory import SessionRepository from .ner_srl_repository import NERSRLRepository +from .session_memory_repository import SessionMemoryRepository __all__ = [ "UserRepository", @@ -12,4 +13,5 @@ __all__ = [ "SubjectRepository", "SessionRepository", "NERSRLRepository", + "SessionMemoryRepository", ] diff --git a/app/repositories/session_memory_repository.py b/app/repositories/session_memory_repository.py new file mode 100644 index 0000000..71852ea --- /dev/null +++ b/app/repositories/session_memory_repository.py @@ -0,0 +1,149 @@ +import json +from typing import Dict, Any, List, Optional +from redis import Redis +from app.helpers import DatetimeUtil +from app.models.entities import SessionEntity + + +class SessionMemoryRepository: + def __init__(self, redis: Redis): + self.redis = redis + + def set_data(self, key: str, value: Any): + """ + Set data in Redis + :param key: Redis key + :param value: Value to store + """ + self.redis.set(key, json.dumps(value)) + + def get_data(self, key: str) -> Optional[Any]: + """ + Get data from Redis + :param key: Redis key + :return: Decoded JSON data or None + """ + data = self.redis.get(key) + return json.loads(data) if data else None + + def delete_key(self, key: str): + """ + Delete a key from Redis + :param key: Redis key to delete + """ + self.redis.delete(key) + + def create_session(self, session_id: str, initial_data: SessionEntity) -> str: + """ + Create a new session + :param session_id: ID for the session + :param initial_data: Initial data for the session + :return: Session ID + """ + data = initial_data.model_dump() + data["created_at"] = str(data["created_at"]) + + self.set_data(f"session:{session_id}", data) + return session_id + + def get_session(self, session_id: str) -> Optional[Dict[str, Any]]: + """ + Retrieve a session by its ID + :param session_id: ID of the session + :return: Session data or None if not found + """ + return self.get_data(f"session:{session_id}") + + def close_session( + self, session_id: str, additional_data: Optional[Dict[str, Any]] = None + ) -> Optional[Dict[str, Any]]: + """ + Close/end a session and return its data before deleting + :param session_id: ID of the session to close + :param additional_data: Optional extra data to add when closing + :return: Session data if found and closed, None otherwise + """ + # Ambil data session + session = self.get_data(f"session:{session_id}") + if not session: + return None + + session["status"] = "closed" + session["closed_at"] = DatetimeUtil.now_iso + + if additional_data: + session.update(additional_data) + + try: + self.delete_key(f"session:{session_id}") + + return session + except Exception as e: + print(f"Error closing session {session_id}: {e}") + return session + + def find_session_by_code(self, session_code: str) -> Optional[Dict[str, Any]]: + """ + Find a session by its session code. + :param session_code: Session code to search for. + :return: Session data if found, None otherwise. + """ + # Dapatkan semua session keys + session_keys = self.redis.keys("session:*") + + for key in session_keys: + session_data = self.get_data(key) + if session_data and session_data.get("session_code") == session_code: + return session_data + + return None + + def add_user_to_session( + self, session_id: str, user_data: Dict[str, Any] = None + ) -> bool: + """ + Add a user to an existing session. + :param session_id: ID of the session. + :param user_id: ID of the user to add. + :param user_data: Optional additional data about the user in the session. + :return: True if user was added successfully, False if session doesn't exist. + """ + session = self.get_session(session_id) + if not session: + return False + + user_entry = { + **(user_data or {}), + "joined_at": DatetimeUtil.now_iso(), + } + + print("Final user_entry:", user_entry) + + existing_users = session.get("participants", []) + existing_users.append(user_entry) + session["participants"] = existing_users + + self.set_data(f"session:{session_id}", session) + return True + + def remove_user_from_session(self, session_id: str, user_id: str) -> bool: + """ + Remove a user from a session + :param session_id: ID of the session + :param user_id: ID of the user to remove + :return: True if user was removed, False if session doesn't exist or user not in session + """ + session = self.get_data(f"session:{session_id}") + if not session: + return False + + original_users_count = len(session.get("participans", [])) + session["participans"] = [ + user for user in session.get("participans", []) if user["id"] != user_id + ] + + if len(session["participans"]) < original_users_count: + self.set_data(f"session:{session_id}", session) + return True + + return False diff --git a/app/repositories/session_repostory.py b/app/repositories/session_repostory.py index e8f2683..998b85b 100644 --- a/app/repositories/session_repostory.py +++ b/app/repositories/session_repostory.py @@ -12,7 +12,6 @@ class SessionRepository: # self.collection.create_index("id", unique=True) def insert(self, session_data: SessionEntity) -> str: - print(session_data.model_dump(by_alias=True, exclude_none=True)) result = self.collection.insert_one( session_data.model_dump(by_alias=True, exclude_none=True) ) diff --git a/app/services/session_service.py b/app/services/session_service.py index 34a104f..d6600b3 100644 --- a/app/services/session_service.py +++ b/app/services/session_service.py @@ -1,13 +1,19 @@ from typing import Optional from uuid import uuid4 -from app.repositories import SessionRepository, UserRepository +from app.repositories import SessionRepository, UserRepository, SessionMemoryRepository from app.models.entities import SessionEntity from app.helpers import DatetimeUtil class SessionService: - def __init__(self, repository: SessionRepository, user_repository: UserRepository): - self.repository = repository + def __init__( + self, + repository_mongo: SessionRepository, + repository_redis: SessionMemoryRepository, + user_repository: UserRepository, + ): + self.repository_mongo = repository_mongo + self.repository_redis = repository_redis self.user_repository = user_repository def create_session(self, quiz_id: str, host_id: str, limit_participan: int) -> str: @@ -22,36 +28,59 @@ class SessionService: current_question_index=0, is_active=True, ) - + session_id = self.repository_mongo.insert(session) + session.id = session_id + self.repository_redis.create_session(session_id, session) return { - "session_id": self.repository.insert(session), + "session_id": session_id, "session_code": generateed_code, } def join_session(self, session_code: str, user_id: str) -> dict: user = self.user_repository.get_user_by_id(user_id) - session = self.repository.find_by_session_code(session_code=session_code) + session = self.repository_redis.find_session_by_code(session_code) - if session is None or session.is_active == False: + if session is None: return None - if session.host_id == user_id: - return {"is_admin": True, "message": "admin joined"} + is_existing_user = any( + u["id"] == user_id for u in session.get("participants", []) + ) + + if session["host_id"] == user_id: + return {"is_admin": True, "message": "admin joined", "quiz_info": session} + + if is_existing_user: + return { + "is_admin": False, + "user_id": str(user.id), + "username": user.name, + "user_pic": user.pic_url, + "quiz_info": session, + "new_user": not is_existing_user, + } + self.repository_redis.add_user_to_session( + session_id=session["id"], + user_data={ + "id": str(user.id), + "username": user.name, + "user_pic": user.pic_url, + }, + ) + session = self.repository_redis.get_session(session["id"]) - if user_id not in session.participants: - session.participants.append(user_id) - self.repository.update(session.id, {"participants": session.participants}) response = { "is_admin": False, "user_id": str(user.id), "username": user.name, "user_pic": user.pic_url, - "session_id": str(session.id), + "quiz_info": session if not is_existing_user else None, + "new_user": not is_existing_user, } return response def leave_session(self, session_id: str, user_id: str) -> dict: - session = self.repository.get_by_id(session_id) + session = self.repository_mongo.get_by_id(session_id) if session is None: return {"error": "Session not found"}