From e6d7a8d08768992adddc210638d123ca33bdd0e9 Mon Sep 17 00:00:00 2001 From: Mawoka Date: Thu, 11 Jul 2024 19:50:22 +0200 Subject: [PATCH] :sparkles: Move basic SocketIO session handling to redis --- classquiz/socket_server/__init__.py | 115 +++++++++++++++++-------- classquiz/socket_server/session.py | 26 ++++++ frontend/src/routes/admin/+page.svelte | 3 + frontend/src/routes/play/+page.svelte | 3 + 4 files changed, 113 insertions(+), 34 deletions(-) create mode 100644 classquiz/socket_server/session.py diff --git a/classquiz/socket_server/__init__.py b/classquiz/socket_server/__init__.py index 83b6139..4ebbf6f 100644 --- a/classquiz/socket_server/__init__.py +++ b/classquiz/socket_server/__init__.py @@ -28,6 +28,7 @@ from pydantic import BaseModel, ValidationError, validator from datetime import datetime from classquiz.socket_server.export_helpers import save_quiz_to_storage +from classquiz.socket_server.session import get_session, save_session sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins=[]) settings = settings() @@ -105,9 +106,13 @@ async def rejoin_game(sid: str, data: dict): await sio.emit("time_sync", encrypted_datetime, room=sid) await redis.set(redis_sid_key, sid) await redis.srem( - f"game_session:{data.game_pin}:players", GamePlayer(username=data.username, sid=data.old_sid).json() + f"game_session:{data.game_pin}:players", + GamePlayer(username=data.username, sid=data.old_sid).json(), + ) + await redis.sadd( + f"game_session:{data.game_pin}:players", + GamePlayer(username=data.username, sid=sid).json(), ) - await redis.sadd(f"game_session:{data.game_pin}:players", GamePlayer(username=data.username, sid=sid).json()) game_data = PlayGame.parse_raw(redis_res) session = { "game_pin": data.game_pin, @@ -115,7 +120,7 @@ async def rejoin_game(sid: str, data: dict): "sid_custom": sid, "admin": False, } - await sio.save_session(sid, session) + await save_session(sid, sio, session) await sio.enter_room(sid, data.game_pin) await sio.emit( "rejoined_game", @@ -151,7 +156,10 @@ async def join_game(sid: str, data: dict): try: async with session.post( "https://hcaptcha.com/siteverify", - data={"response": data.captcha, "secret": settings.hcaptcha_key}, + data={ + "response": data.captcha, + "secret": settings.hcaptcha_key, + }, ) as resp: resp_data = await resp.json() if not resp_data["success"]: @@ -163,7 +171,10 @@ async def join_game(sid: str, data: dict): elif settings.recaptcha_key is not None: async with session.post( "https://www.google.com/recaptcha/api/siteverify", - data={"secret": settings.recaptcha_key, "response": data.captcha}, + data={ + "secret": settings.recaptcha_key, + "response": data.captcha, + }, ) as resp: try: resp_data = await resp.json() @@ -186,7 +197,7 @@ async def join_game(sid: str, data: dict): "sid_custom": sid, "admin": False, } - await sio.save_session(sid, session) + await save_session(sid, sio, session) await sio.emit( "joined_game", { @@ -198,11 +209,18 @@ async def join_game(sid: str, data: dict): redis_res = await redis.get(f"game_session:{data.game_pin}") redis_res = GameSession.parse_raw(redis_res) await redis.set(f"game_session:{data.game_pin}:players:{data.username}", sid, ex=7200) - await redis.sadd(f"game_session:{data.game_pin}:players", GamePlayer(username=data.username, sid=sid).json()) + await redis.sadd( + f"game_session:{data.game_pin}:players", + GamePlayer(username=data.username, sid=sid).json(), + ) if data.custom_field == "": data.custom_field = None if data.custom_field is not None: - await redis.hset(f"game:{data.game_pin}:players:custom_fields", data.username, data.custom_field) + await redis.hset( + f"game:{data.game_pin}:players:custom_fields", + data.username, + data.custom_field, + ) # await redis.set( # f"game_session:{data.game_pin}", @@ -223,7 +241,7 @@ async def join_game(sid: str, data: dict): @sio.event async def start_game(sid: str, _data: dict): - session = await sio.get_session(sid) + session = await get_session(sid, sio) if not session["admin"]: return game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) @@ -260,10 +278,11 @@ async def register_as_admin(sid: str, data: dict): {"game_id": game_id, "game": await redis.get(f"game:{game_pin}")}, room=sid, ) - async with sio.session(sid) as session: - session["game_pin"] = game_pin - session["admin"] = True - session["remote"] = False + session = {} + session["game_pin"] = game_pin + session["admin"] = True + session["remote"] = False + await save_session(sid, sio, session) await sio.enter_room(sid, game_pin) await sio.enter_room(sid, f"admin:{data.game_pin}") else: @@ -272,7 +291,7 @@ async def register_as_admin(sid: str, data: dict): @sio.event async def get_question_results(sid: str, data: dict): - session = await sio.get_session(sid) + session = await get_session(sid, sio) if not session["admin"]: return @@ -318,7 +337,7 @@ class ReturnQuestion(QuizQuestion): @sio.event async def set_question_number(sid, data: str): # data is just a number (as a str) of the question - session = await sio.get_session(sid) + session = await get_session(sid, sio) if not session["admin"]: return game_pin = session["game_pin"] @@ -372,7 +391,7 @@ async def submit_answer(sid: str, data: dict): await sio.emit("error", room=sid) print(e) return - session = await sio.get_session(sid) + session = await get_session(sid, sio) game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) answer_right = False if game_data.questions[int(float(data.question_index))].type == QuizQuestionType.ABCD: @@ -423,14 +442,15 @@ async def submit_answer(sid: str, data: dict): answer_right = bool(correct_string == data.answer) else: raise NotImplementedError - latency = int(float((await sio.get_session(sid))["ping"])) + latency = int(float((await get_session(sid, sio))["ping"])) time_q_started = datetime.fromisoformat(await redis.get(f"game:{session['game_pin']}:current_time")) diff = (time_q_started - now).total_seconds() * 1000 # - timedelta(milliseconds=latency) score = 0 if answer_right: score = calculate_score( - abs(diff) - latency, int(float(game_data.questions[int(float(data.question_index))].time)) + abs(diff) - latency, + int(float(game_data.questions[int(float(data.question_index))].time)), ) if score > 1000: score = 1000 @@ -444,7 +464,10 @@ async def submit_answer(sid: str, data: dict): ) answers = await redis.get(f"game_session:{session['game_pin']}:{data.question_index}") answers = await set_answer( - answers, game_pin=session["game_pin"], data=answer_data, q_index=int(float(data.question_index)) + answers, + game_pin=session["game_pin"], + data=answer_data, + q_index=int(float(data.question_index)), ) player_count = await redis.scard(f"game_session:{session['game_pin']}:players") await sio.emit("player_answer", {}) @@ -465,7 +488,7 @@ async def submit_answer(sid: str, data: dict): @sio.event async def get_final_results(sid: str, _data: dict): - session: dict = await sio.get_session(sid) + session: dict = await get_session(sid, sio) game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}"))) if not session["admin"]: return @@ -475,7 +498,7 @@ async def get_final_results(sid: str, _data: dict): @sio.event async def get_export_token(sid: str): - session = await sio.get_session(sid) + session = await get_session(sid, sio) if not session["admin"]: return game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}"))) @@ -487,11 +510,15 @@ async def get_export_token(sid: str): @sio.event async def show_solutions(sid: str, _data: dict): - session: dict = await sio.get_session(sid) + session: dict = await get_session(sid, sio) game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}"))) if not session["admin"]: return - await sio.emit("solutions", game_data.questions[game_data.current_question].dict(), room=session["game_pin"]) + await sio.emit( + "solutions", + game_data.questions[game_data.current_question].dict(), + room=session["game_pin"], + ) @sio.event @@ -500,8 +527,9 @@ async def echo_time_sync(sid: str, data: str): then = datetime.fromisoformat(then_dec) now = datetime.now() delta = now - then - async with sio.session(sid) as session: - session["ping"] = delta.microseconds / 1000 + session = await get_session(sid, sio) + session["ping"] = delta.microseconds / 1000 + await save_session(sid, sio, session) class _KickPlayerInput(BaseModel): @@ -517,13 +545,14 @@ async def kick_player(sid: str, data: dict): print(e) return - session: dict = await sio.get_session(sid) + session: dict = await get_session(sid, sio) if not session["admin"]: return player_sid = await redis.get(f"game_session:{session['game_pin']}:players:{data.username}") await redis.srem( - f"game_session:{session['game_pin']}:players", GamePlayer(username=data.username, sid=player_sid).json() + f"game_session:{session['game_pin']}:players", + GamePlayer(username=data.username, sid=player_sid).json(), ) await sio.leave_room(player_sid, session["game_pin"]) await sio.emit("kick", room=player_sid) @@ -548,10 +577,11 @@ async def register_as_remote(sid: str, data: dict): room=sid, ) await sio.emit("control_visibility", {"visible": False}, room=f"admin:{data.game_pin}") - async with sio.session(sid) as session: - session["game_pin"] = data.game_pin - session["admin"] = True - session["remote"] = True + session = await get_session(sid, sio) + session["game_pin"] = data.game_pin + session["admin"] = True + session["remote"] = True + await save_session(sid, sio, session) await sio.enter_room(sid, data.game_pin) await sio.enter_room(sid, f"admin:{data.game_pin}") @@ -568,14 +598,31 @@ async def set_control_visibility(sid: str, data: dict): await sio.emit("error", room=sid) print(e) return - session: dict = await sio.get_session(sid) - await sio.emit("control_visibility", {"visible": data.visible}, room=f"admin:{session['game_pin']}") + session: dict = await get_session(sid, sio) + await sio.emit( + "control_visibility", + {"visible": data.visible}, + room=f"admin:{session['game_pin']}", + ) @sio.event async def save_quiz(sid: str): - session: dict = await sio.get_session(sid) + session: dict = await get_session(sid, sio) if not session["admin"]: return await save_quiz_to_storage(session["game_pin"]) await sio.emit("results_saved_successfully") + + +class ConnectSessionIdEvent(BaseModel): + session_id: str + + +@sio.event +async def connect(sid: str, _environ, _auth): + session_id = os.urandom(16).hex() + print("Connection opened with handler") + sio_session = {"session_id": session_id} + await sio.save_session(sid, sio_session) + await sio.emit("session_id", ConnectSessionIdEvent(session_id=session_id).dict()) diff --git a/classquiz/socket_server/session.py b/classquiz/socket_server/session.py new file mode 100644 index 0000000..c53e100 --- /dev/null +++ b/classquiz/socket_server/session.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) +# +# SPDX-License-Identifier: MPL-2.0 + +from classquiz.config import redis +import json +from typing import Any +from socketio import AsyncServer +from socketio.exceptions import ConnectionRefusedError + + +async def get_session(sid: str, sio: AsyncServer, disconnect_on_error: bool = True) -> dict: + session_id = (await sio.get_session(sid)).get("session_id") + if session_id is None: + raise ConnectionRefusedError("Session not configured") + val = await redis.get(f"socket_io_session:{session_id}") + if disconnect_on_error and val is None: + raise ConnectionRefusedError("session not available") + return json.loads(val) + + +async def save_session(sid: str, sio: AsyncServer, data: Any, disconnect_on_error: bool = True) -> None: + session_id = (await sio.get_session(sid)).get("session_id") + if session_id is None: + raise ConnectionRefusedError("Session not configured") + await redis.set(f"socket_io_session:{session_id}", json.dumps(data), ex=3600) diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 23ea2f5..7c5f7e9 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -60,6 +60,9 @@ SPDX-License-Identifier: MPL-2.0 connect(); } }); + socket.on("session_id", (d) => { + const session_id = d.session_id + }) socket.on('registered_as_admin', (data) => { quiz_data = JSON.parse(data['game']); diff --git a/frontend/src/routes/play/+page.svelte b/frontend/src/routes/play/+page.svelte index 6ed103a..133fa9f 100644 --- a/frontend/src/routes/play/+page.svelte +++ b/frontend/src/routes/play/+page.svelte @@ -72,6 +72,9 @@ SPDX-License-Identifier: MPL-2.0 socket.on('time_sync', (data) => { socket.emit('echo_time_sync', data); }); + socket.on("session_id", (d) => { + const session_id = d.session_id + }) socket.on('connect', async () => { console.log('Connected!');