from typing import Any, Dict, Optional from uuid import uuid4 from app.repositories import ( SessionRepository, UserRepository, SessionMemoryRepository, QuizRepository, UserAnswerRepository, QuizMemoryRepository, AnswerMemoryRepository, ScoreMemoryRepository, ) from app.models.entities import SessionEntity from app.helpers import DatetimeUtil from flask_socketio import SocketIO import time class SessionService: def __init__( self, session_mongo_repository: SessionRepository, session_redis_repository: SessionMemoryRepository, quiz_redis_repository: QuizMemoryRepository, answer_redis_repository: AnswerMemoryRepository, score_redis_repostory: ScoreMemoryRepository, user_repository: UserRepository, quiz_repository: QuizRepository, answer_repository: UserAnswerRepository, ): self.session_mongo_repository = session_mongo_repository self.session_redis_repository = session_redis_repository self.quiz_redis_repository = quiz_redis_repository self.answer_redis_repository = answer_redis_repository self.score_redis_repository = score_redis_repostory self.user_repository = user_repository self.quiz_repository = quiz_repository self.answer_repository = answer_repository def create_session(self, quiz_id: str, host_id: str, limit_participan: int) -> str: generateed_code = uuid4().hex[:6].upper() session = SessionEntity( session_code=generateed_code, quiz_id=quiz_id, host_id=host_id, created_at=DatetimeUtil.now_iso(), limit_participan=limit_participan, participants=[], current_question_index=0, is_active=True, ) session_id = self.session_mongo_repository.insert(session) session.id = session_id self.session_redis_repository.create_session(session_id, session) data = self.quiz_repository.get_by_id(quiz_id=quiz_id) self.quiz_redis_repository.set_quiz_for_session(session_id, data) return { "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.session_redis_repository.find_session_by_code(session_code) if session is None: return None session_id = session["id"] is_existing_user = any( u["id"] == user_id for u in session.get("participants", []) ) session_quiz = self.quiz_redis_repository.get_quiz_for_session(session["id"]) quiz_info = { "title": session_quiz.title, "description": session_quiz.description, "total_quiz": session_quiz.total_quiz, "limit_duration": session_quiz.limit_duration, } if session["host_id"] == user_id: return { "session_id": session_id, "is_admin": True, "message": "admin joined", "session_info": session, "quiz_info": quiz_info, } if is_existing_user: return { "session_id": session_id, "is_admin": False, "user_id": str(user.id), "username": user.name, "user_pic": user.pic_url, "session_info": session, "quiz_info": quiz_info, "new_user": not is_existing_user, } self.session_redis_repository.add_user_to_session( session_id=session["id"], user_data={ "id": str(user.id), "username": user.name, "user_pic": user.pic_url, }, ) session = self.session_redis_repository.get_session(session["id"]) response = { "session_id": session_id, "is_admin": False, "user_id": str(user.id), "username": user.name, "user_pic": user.pic_url, "session_info": session if not is_existing_user else None, "quiz_info": quiz_info, "new_user": not is_existing_user, } return response def leave_session(self, session_id: str, user_id: str) -> dict: is_success = self.session_redis_repository.remove_user_from_session( session_id, user_id ) if is_success: participant_left = self.session_redis_repository.get_user_in_session( session_id ) return {"is_success": True, "participants": participant_left} return {"is_success": False} def run_quiz_flow(self, session_id: str, socketio: SocketIO): quiz = self.quiz_redis_repository.get_quiz_for_session(session_id) questions = quiz.question_listings time.sleep(2) for q in questions: print(f"\nMengirim pertanyaan {q.index} ke room {session_id}") question_to_send = q.model_dump(exclude={"target_answer"}) socketio.emit("quiz_question", question_to_send, room=session_id) time.sleep(q.duration) self.answer_redis_repository.auto_fill_incorrect_answers( session_id=session_id, question_index=q.index, default_time_spent=q.duration, ) socketio.emit("quiz_done", room=session_id) def submit_answer( self, session_id: str, user_id: str, question_index: int, answer: Any, time_spent: int, ) -> Dict[str, Any]: quiz = self.quiz_redis_repository.get_quiz_for_session(session_id) question = next( (q for q in quiz.question_listings if q.index == question_index), None, ) if question is None: raise ValueError( f"Question {question_index} not found in session {session_id}" ) is_correct = self._is_correct(question, answer) self.answer_redis_repository.save_user_answer( session_id=session_id, user_id=user_id, question_index=question_index, answer=answer, correct=is_correct, time_spent=time_spent, ) scores = self.score_redis_repository.update_user_score( session_id=session_id, user_id=user_id, correct=is_correct, ) return { "user_id": user_id, "question_index": question_index, "answer": answer, "correct": is_correct, "scores": scores, } def get_ranked_scores(self, session_id: str): raw = self.score_redis_repository.get_scores(session_id) ranked = [ { "user_id": uid, "correct": v.get("correct", 0), "incorrect": v.get("incorrect", 0), "total_score": v.get("correct", 0) * 10, } for uid, v in raw.items() ] ranked.sort(key=lambda x: x["total_score"], reverse=True) return ranked def _is_correct(self, q, ans) -> bool: if q.type in {"multiple_choice", "true_false"}: return str(ans).strip().lower() == str(q.target_answer).strip().lower() if q.type == "essay": return str(q.target_answer).lower() in str(ans).lower() # fallback return False