404 lines
15 KiB
Python
404 lines
15 KiB
Python
from flask_socketio import SocketIO, emit, join_room, leave_room
|
||
from flask import request
|
||
from app.services import SessionService
|
||
import threading
|
||
import time
|
||
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()
|
||
|
||
# ---------------------------------------------------------------------
|
||
# Helper utilities
|
||
# ---------------------------------------------------------------------
|
||
@staticmethod
|
||
def _is_correct(user_answer, question: dict) -> bool:
|
||
"""Bandingkan jawaban user dengan kunci jawaban."""
|
||
|
||
print("user answer", user_answer, "question_target", question["target_answer"])
|
||
if question["type"] == "fill_the_blank":
|
||
return (
|
||
str(user_answer).strip().lower()
|
||
== str(question["target_answer"]).strip().lower()
|
||
)
|
||
elif question["type"] == "true_false":
|
||
return bool(user_answer) == question["target_answer"]
|
||
elif question["type"] == "option":
|
||
# user_answer bisa dikirim sebagai index atau value teks option.
|
||
# Pastikan di‑cast ke int terlebih dahulu jika memungkinkan.
|
||
try:
|
||
return int(user_answer) == question["target_answer"]
|
||
except ValueError:
|
||
return (
|
||
str(user_answer).strip().lower()
|
||
== str(question["options"][question["target_answer"]])
|
||
.strip()
|
||
.lower()
|
||
)
|
||
return False
|
||
|
||
def _questions_key(self, session_code: str) -> str:
|
||
return f"session:{session_code}:questions"
|
||
|
||
def _answers_key(self, session_code: str) -> str:
|
||
return f"session:{session_code}:answers"
|
||
|
||
def _scores_key(self, session_code: str) -> str:
|
||
return f"session:{session_code}:scores"
|
||
|
||
# ---------------------------------------------------------------------
|
||
# Socket.IO event bindings
|
||
# ---------------------------------------------------------------------
|
||
def _register_events(self):
|
||
@self.socketio.on("connect")
|
||
def on_connect():
|
||
print(f"Client connected: {request.sid}")
|
||
emit("connection_response", {"status": "connected", "sid": request.sid})
|
||
|
||
@self.socketio.on("disconnect")
|
||
def on_disconnect():
|
||
print(f"Client disconnected: {request.sid}")
|
||
|
||
@self.socketio.on("join_room")
|
||
def handle_join_room(data):
|
||
session_code = data.get("session_code")
|
||
user_id = data.get("user_id")
|
||
|
||
if not session_code or not user_id:
|
||
emit("error", {"message": "session_code and user_id are required"})
|
||
return
|
||
|
||
session = self.session_service.join_session(
|
||
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",
|
||
{
|
||
"type": "join",
|
||
"message": message,
|
||
"room": session_code,
|
||
"argument": "adm_update",
|
||
"data": session if not session["is_admin"] else None,
|
||
},
|
||
room=session_code,
|
||
)
|
||
|
||
@self.socketio.on("submit_answer")
|
||
def handle_submit_answer(data):
|
||
session_code = data.get("session_id")
|
||
user_id = data.get("user_id")
|
||
question_index = data.get("question_index")
|
||
user_answer = data.get("answer")
|
||
|
||
if not all(
|
||
[
|
||
session_code,
|
||
user_id,
|
||
question_index is not None,
|
||
user_answer is not None,
|
||
]
|
||
):
|
||
emit(
|
||
"error",
|
||
{
|
||
"message": "session_id, user_id, question_index, and answer are required"
|
||
},
|
||
)
|
||
return
|
||
|
||
# ----- 1. Ambil list pertanyaan untuk mendapatkan kunci jawaban ----------
|
||
questions = (
|
||
self.redis_repo.get_data(self._questions_key(session_code)) or []
|
||
)
|
||
question = next(
|
||
(q for q in questions if q["index"] == question_index), None
|
||
)
|
||
if question is None:
|
||
emit("error", {"message": "Question not found"})
|
||
return
|
||
|
||
is_correct = self._is_correct(user_answer, question)
|
||
|
||
print(
|
||
f"User {user_id} answered Q{question_index} with '{user_answer}' -> {'✔' if is_correct else '✖'}"
|
||
)
|
||
|
||
# ----- 2. Simpan jawaban ke Redis --------------------------------------
|
||
answers = self.redis_repo.get_data(self._answers_key(session_code)) or []
|
||
answers.append(
|
||
{
|
||
"user_id": user_id,
|
||
"question_index": question_index,
|
||
"answer": user_answer,
|
||
"correct": is_correct,
|
||
}
|
||
)
|
||
self.redis_repo.set_data(self._answers_key(session_code), answers)
|
||
|
||
# ----- 3. Update skor per‑user -----------------------------------------
|
||
scores = self.redis_repo.get_data(self._scores_key(session_code)) or {}
|
||
user_score = scores.get(str(user_id), {"correct": 0, "incorrect": 0})
|
||
if is_correct:
|
||
user_score["correct"] += 1
|
||
else:
|
||
user_score["incorrect"] += 1
|
||
scores[str(user_id)] = user_score
|
||
self.redis_repo.set_data(self._scores_key(session_code), scores)
|
||
|
||
# ----- 4. Beri tahu user (ack) -----------------------------------------
|
||
emit(
|
||
"answer_submitted",
|
||
{
|
||
"user_id": user_id,
|
||
"question_index": question_index,
|
||
"answer": user_answer,
|
||
"correct": is_correct,
|
||
},
|
||
room=request.sid,
|
||
)
|
||
|
||
# ----- 5. Kirim update skor hanya ke admin -----------------------------
|
||
admin_sid = self.admin_sids.get(session_code)
|
||
if admin_sid:
|
||
emit("score_update", scores, room=admin_sid)
|
||
|
||
@self.socketio.on("leave_room")
|
||
def handle_leave_room(data):
|
||
session_code = data.get("session_id")
|
||
user_id = data.get("user_id")
|
||
username = data.get("username", "anonymous")
|
||
|
||
leave_room(session_code)
|
||
emit(
|
||
"room_message",
|
||
{
|
||
"type": "leave",
|
||
"message": f"{username} has left the room.",
|
||
"room": session_code,
|
||
"data": user_id,
|
||
},
|
||
room=session_code,
|
||
)
|
||
|
||
@self.socketio.on("send_message")
|
||
def on_send_message(data):
|
||
session_code = data.get("session_id")
|
||
message = data.get("message")
|
||
username = data.get("username", "anonymous")
|
||
emit(
|
||
"receive_message",
|
||
{"message": message, "from": username},
|
||
room=session_code,
|
||
)
|
||
|
||
@self.socketio.on("end_session")
|
||
def handle_end_session(data):
|
||
session_code = data.get("session_id")
|
||
user_id = data.get("user_id")
|
||
|
||
if not session_code or not user_id:
|
||
emit("error", {"message": "session_id and user_id required"})
|
||
return
|
||
|
||
# Validasi user berhak mengakhiri session
|
||
self.session_service.end_session(session_id=session_code, user_id=user_id)
|
||
|
||
answers = self.redis_repo.get_data(self._answers_key(session_code)) or []
|
||
scores = self.redis_repo.get_data(self._scores_key(session_code)) or {}
|
||
|
||
print("\n📦 Final Quiz Data for Session", session_code)
|
||
print("------------------------------------------------------------")
|
||
for entry in answers:
|
||
status = "✔" if entry["correct"] else "✖"
|
||
print(
|
||
f"User {entry['user_id']} - Q{entry['question_index']}: {entry['answer']} {status}"
|
||
)
|
||
|
||
print("\n🏁 Rekap Skor:")
|
||
for uid, sc in scores.items():
|
||
print(f"User {uid}: Benar {sc['correct']} | Salah {sc['incorrect']}")
|
||
|
||
# Bersihkan semua data session di Redis
|
||
for key in [
|
||
self._answers_key(session_code),
|
||
self._scores_key(session_code),
|
||
self._questions_key(session_code),
|
||
]:
|
||
self.redis_repo.delete_key(key)
|
||
|
||
emit(
|
||
"room_closed",
|
||
{"message": "Session has ended.", "room": session_code},
|
||
room=session_code,
|
||
)
|
||
|
||
@self.socketio.on("start_quiz")
|
||
def handle_start_quiz(data):
|
||
session_code = data.get("session_code")
|
||
if not session_code:
|
||
emit("error", {"message": "session_code is required"})
|
||
return
|
||
|
||
emit("quiz_started", {"message": "Quiz has started!"}, room=session_code)
|
||
threading.Thread(
|
||
target=self._simulate_quiz_flow, args=(session_code,), daemon=True
|
||
).start()
|
||
|
||
# ---------------------------------------------------------------------
|
||
# Quiz flow simulation -------------------------------------------------
|
||
# ---------------------------------------------------------------------
|
||
def _simulate_quiz_flow(self, session_code: str):
|
||
"""Mengirim list pertanyaan satu per satu secara otomatis (demo)."""
|
||
questions = [
|
||
{
|
||
"index": 1,
|
||
"question": "Kerajaan Hindu tertua di Indonesia adalah?",
|
||
"target_answer": "Kutai",
|
||
"duration": 30,
|
||
"type": "fill_the_blank",
|
||
"options": None,
|
||
},
|
||
{
|
||
"index": 2,
|
||
"question": "Apakah benar Majapahit mencapai puncak kejayaan pada masa Hayam Wuruk?",
|
||
"target_answer": True,
|
||
"duration": 30,
|
||
"type": "true_false",
|
||
"options": None,
|
||
},
|
||
{
|
||
"index": 3,
|
||
"question": "Kerajaan maritim terbesar di Asia Tenggara pada abad ke‑7 adalah?",
|
||
"target_answer": 2,
|
||
"duration": 30,
|
||
"type": "option",
|
||
"options": ["Majapahit", "Tarumanegara", "Sriwijaya", "Mataram Kuno"],
|
||
},
|
||
{
|
||
"index": 4,
|
||
"question": "Prasasti Yupa merupakan peninggalan dari kerajaan?",
|
||
"target_answer": "Kutai",
|
||
"duration": 30,
|
||
"type": "fill_the_blank",
|
||
"options": None,
|
||
},
|
||
{
|
||
"index": 5,
|
||
"question": "Apakah Tarumanegara terletak di wilayah Kalimantan Timur?",
|
||
"target_answer": False,
|
||
"duration": 30,
|
||
"type": "true_false",
|
||
"options": None,
|
||
},
|
||
{
|
||
"index": 6,
|
||
"question": "Kitab Negarakertagama ditulis oleh?",
|
||
"target_answer": 0,
|
||
"duration": 30,
|
||
"type": "option",
|
||
"options": [
|
||
"Empu Tantular",
|
||
"Empu Prapanca",
|
||
"Hayam Wuruk",
|
||
"Gajah Mada",
|
||
],
|
||
},
|
||
{
|
||
"index": 7,
|
||
"question": "Tokoh yang terkenal dengan Sumpah Palapa adalah?",
|
||
"target_answer": "Gajah Mada",
|
||
"duration": 30,
|
||
"type": "fill_the_blank",
|
||
"options": None,
|
||
},
|
||
{
|
||
"index": 8,
|
||
"question": "Candi Borobudur dibangun oleh kerajaan Hindu?",
|
||
"target_answer": False,
|
||
"duration": 30,
|
||
"type": "true_false",
|
||
"options": None,
|
||
},
|
||
{
|
||
"index": 9,
|
||
"question": "Raja terkenal dari Kerajaan Sriwijaya adalah?",
|
||
"target_answer": 1,
|
||
"duration": 30,
|
||
"type": "option",
|
||
"options": [
|
||
"Dapunta Hyang",
|
||
"Balaputradewa",
|
||
"Airlangga",
|
||
"Hayam Wuruk",
|
||
],
|
||
},
|
||
{
|
||
"index": 10,
|
||
"question": "Candi Prambanan merupakan peninggalan agama?",
|
||
"target_answer": "Hindu",
|
||
"duration": 30,
|
||
"type": "fill_the_blank",
|
||
"options": None,
|
||
},
|
||
]
|
||
|
||
# Simpan ke Redis agar bisa dipakai saat evaluasi jawaban
|
||
self.redis_repo.set_data(self._questions_key(session_code), questions)
|
||
|
||
# Beri sedikit jeda sebelum pertanyaan pertama
|
||
time.sleep(2)
|
||
|
||
for q in questions:
|
||
print(f"\n📢 Mengirim pertanyaan {q['index']} ke room {session_code}")
|
||
q.pop("target_answer")
|
||
self.socketio.emit("quiz_question", q, room=session_code)
|
||
time.sleep(q["duration"])
|
||
|
||
self.socketio.emit(
|
||
"quiz_done", {"message": "Quiz has ended!"}, room=session_code
|
||
)
|