fix: session sistem
This commit is contained in:
parent
aae53cccce
commit
5ac945eb18
|
@ -7,35 +7,14 @@ import json
|
||||||
from redis import Redis
|
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:
|
class SocketController:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
socketio: SocketIO,
|
socketio: SocketIO,
|
||||||
redis: Redis,
|
|
||||||
session_service: SessionService,
|
session_service: SessionService,
|
||||||
):
|
):
|
||||||
self.socketio = socketio
|
self.socketio = socketio
|
||||||
self.session_service = session_service
|
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()
|
self._register_events()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -101,22 +80,21 @@ class SocketController:
|
||||||
return
|
return
|
||||||
|
|
||||||
session = self.session_service.join_session(
|
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:
|
if session is None:
|
||||||
emit("error", {"message": "Failed to join session or session inactive"})
|
emit("error", {"message": "Failed to join session or session inactive"})
|
||||||
return
|
return
|
||||||
|
|
||||||
join_room(session_code)
|
join_room(session_code)
|
||||||
|
|
||||||
# Kalau user ini admin, simpan SID‑nya.
|
|
||||||
if session["is_admin"]:
|
if session["is_admin"]:
|
||||||
self.admin_sids[session_code] = request.sid
|
|
||||||
message = "Admin has joined the room."
|
message = "Admin has joined the room."
|
||||||
else:
|
else:
|
||||||
message = f"User {session['username']} has joined the room."
|
message = f"User {session['username']} has joined the room."
|
||||||
|
|
||||||
print(message)
|
|
||||||
emit(
|
emit(
|
||||||
"room_message",
|
"room_message",
|
||||||
{
|
{
|
||||||
|
@ -124,9 +102,22 @@ class SocketController:
|
||||||
"message": message,
|
"message": message,
|
||||||
"room": session_code,
|
"room": session_code,
|
||||||
"argument": "adm_update",
|
"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,
|
room=session_code,
|
||||||
|
skip_sid=request.sid, # Skip user yang baru join
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.socketio.on("submit_answer")
|
@self.socketio.on("submit_answer")
|
||||||
|
|
|
@ -6,6 +6,7 @@ from app.repositories import (
|
||||||
SubjectRepository,
|
SubjectRepository,
|
||||||
SessionRepository,
|
SessionRepository,
|
||||||
NERSRLRepository,
|
NERSRLRepository,
|
||||||
|
SessionMemoryRepository,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.services import (
|
from app.services import (
|
||||||
|
@ -44,6 +45,7 @@ class Container(containers.DeclarativeContainer):
|
||||||
subject_repository = providers.Factory(SubjectRepository, mongo.provided.db)
|
subject_repository = providers.Factory(SubjectRepository, mongo.provided.db)
|
||||||
session_repository = providers.Factory(SessionRepository, mongo.provided.db)
|
session_repository = providers.Factory(SessionRepository, mongo.provided.db)
|
||||||
ner_srl_repository = providers.Factory(NERSRLRepository)
|
ner_srl_repository = providers.Factory(NERSRLRepository)
|
||||||
|
session_memory_repository = providers.Factory(SessionMemoryRepository, redis)
|
||||||
|
|
||||||
# services
|
# services
|
||||||
auth_service = providers.Factory(
|
auth_service = providers.Factory(
|
||||||
|
@ -83,6 +85,7 @@ class Container(containers.DeclarativeContainer):
|
||||||
session_service = providers.Factory(
|
session_service = providers.Factory(
|
||||||
SessionService,
|
SessionService,
|
||||||
session_repository,
|
session_repository,
|
||||||
|
session_memory_repository,
|
||||||
user_repository,
|
user_repository,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -104,7 +107,6 @@ class Container(containers.DeclarativeContainer):
|
||||||
socket_controller = providers.Factory(
|
socket_controller = providers.Factory(
|
||||||
SocketController,
|
SocketController,
|
||||||
socketio,
|
socketio,
|
||||||
redis,
|
|
||||||
session_service,
|
session_service,
|
||||||
)
|
)
|
||||||
session_controller = providers.Factory(SessionController, session_service)
|
session_controller = providers.Factory(SessionController, session_service)
|
||||||
|
|
|
@ -14,5 +14,5 @@ class SessionEntity(BaseModel):
|
||||||
ended_at: datetime | None = None
|
ended_at: datetime | None = None
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
participan_limit: int = 10
|
participan_limit: int = 10
|
||||||
participants: List[str] = []
|
participants: List[dict] = []
|
||||||
current_question_index: int = 0
|
current_question_index: int = 0
|
||||||
|
|
|
@ -4,6 +4,7 @@ from .answer_repository import UserAnswerRepository
|
||||||
from .subject_repository import SubjectRepository
|
from .subject_repository import SubjectRepository
|
||||||
from .session_repostory import SessionRepository
|
from .session_repostory import SessionRepository
|
||||||
from .ner_srl_repository import NERSRLRepository
|
from .ner_srl_repository import NERSRLRepository
|
||||||
|
from .session_memory_repository import SessionMemoryRepository
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"UserRepository",
|
"UserRepository",
|
||||||
|
@ -12,4 +13,5 @@ __all__ = [
|
||||||
"SubjectRepository",
|
"SubjectRepository",
|
||||||
"SessionRepository",
|
"SessionRepository",
|
||||||
"NERSRLRepository",
|
"NERSRLRepository",
|
||||||
|
"SessionMemoryRepository",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
|
@ -12,7 +12,6 @@ class SessionRepository:
|
||||||
# self.collection.create_index("id", unique=True)
|
# self.collection.create_index("id", unique=True)
|
||||||
|
|
||||||
def insert(self, session_data: SessionEntity) -> str:
|
def insert(self, session_data: SessionEntity) -> str:
|
||||||
print(session_data.model_dump(by_alias=True, exclude_none=True))
|
|
||||||
result = self.collection.insert_one(
|
result = self.collection.insert_one(
|
||||||
session_data.model_dump(by_alias=True, exclude_none=True)
|
session_data.model_dump(by_alias=True, exclude_none=True)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
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.models.entities import SessionEntity
|
||||||
from app.helpers import DatetimeUtil
|
from app.helpers import DatetimeUtil
|
||||||
|
|
||||||
|
|
||||||
class SessionService:
|
class SessionService:
|
||||||
def __init__(self, repository: SessionRepository, user_repository: UserRepository):
|
def __init__(
|
||||||
self.repository = repository
|
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
|
self.user_repository = user_repository
|
||||||
|
|
||||||
def create_session(self, quiz_id: str, host_id: str, limit_participan: int) -> str:
|
def create_session(self, quiz_id: str, host_id: str, limit_participan: int) -> str:
|
||||||
|
@ -22,36 +28,59 @@ class SessionService:
|
||||||
current_question_index=0,
|
current_question_index=0,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
session_id = self.repository_mongo.insert(session)
|
||||||
|
session.id = session_id
|
||||||
|
self.repository_redis.create_session(session_id, session)
|
||||||
return {
|
return {
|
||||||
"session_id": self.repository.insert(session),
|
"session_id": session_id,
|
||||||
"session_code": generateed_code,
|
"session_code": generateed_code,
|
||||||
}
|
}
|
||||||
|
|
||||||
def join_session(self, session_code: str, user_id: str) -> dict:
|
def join_session(self, session_code: str, user_id: str) -> dict:
|
||||||
user = self.user_repository.get_user_by_id(user_id)
|
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
|
return None
|
||||||
|
|
||||||
if session.host_id == user_id:
|
is_existing_user = any(
|
||||||
return {"is_admin": True, "message": "admin joined"}
|
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 = {
|
response = {
|
||||||
"is_admin": False,
|
"is_admin": False,
|
||||||
"user_id": str(user.id),
|
"user_id": str(user.id),
|
||||||
"username": user.name,
|
"username": user.name,
|
||||||
"user_pic": user.pic_url,
|
"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
|
return response
|
||||||
|
|
||||||
def leave_session(self, session_id: str, user_id: str) -> dict:
|
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:
|
if session is None:
|
||||||
return {"error": "Session not found"}
|
return {"error": "Session not found"}
|
||||||
|
|
Loading…
Reference in New Issue