From f712bdf3eff9705e88d5f8964ced035a74186f7f Mon Sep 17 00:00:00 2001 From: Mawoka Date: Tue, 12 Sep 2023 16:31:23 +0200 Subject: [PATCH] :sparkles: Implemented basic connection recovery --- classquiz/socket_server/__init__.py | 140 ++++++++++++++++++-------- frontend/src/routes/play/+page.svelte | 28 +++++- 2 files changed, 123 insertions(+), 45 deletions(-) diff --git a/classquiz/socket_server/__init__.py b/classquiz/socket_server/__init__.py index 1afc8990..9abdd9fd 100644 --- a/classquiz/socket_server/__init__.py +++ b/classquiz/socket_server/__init__.py @@ -80,6 +80,54 @@ class _JoinGameData(BaseModel): custom_field: str | None +class _RejoinGameData(BaseModel): + old_sid: str + game_pin: str + username: str + + +@sio.event +async def rejoin_game(sid: str, data: dict): + redis_res = await redis.get(f"game:{data['game_pin']}") + if redis_res is None: + await sio.emit("game_not_found", room=sid) + return + try: + data = _RejoinGameData(**data) + except ValidationError as e: + await sio.emit("error", room=sid) + print(e) + redis_sid_key = f"game_session:{data.game_pin}:players:{data.username}" + old_sid = await redis.get(redis_sid_key) + if old_sid != data.old_sid: + return + encrypted_datetime = fernet.encrypt(datetime.now().isoformat().encode("utf-8")).decode("utf-8") + await sio.emit("time_sync", encrypted_datetime, room=sid) + await redis.set(redis_sid_key, sid) + deleted_num = await redis.srem( + f"game_session:{data.game_pin}:players", GamePlayer(username=data.username, sid=data.old_sid).json() + ) + print(deleted_num) + 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, + "username": data.username, + "sid_custom": sid, + "admin": False, + } + await sio.save_session(sid, session) + sio.enter_room(sid, data.game_pin) + await sio.emit( + "rejoined_game", + { + **json.loads(game_data.json(exclude={"quiz_id", "questions", "user_id"})), + "question_count": len(game_data.questions), + }, + room=sid, + ) + + @sio.event async def join_game(sid: str, data: dict): redis_res = await redis.get(f"game:{data['game_pin']}") @@ -177,12 +225,13 @@ async def join_game(sid: str, data: dict): @sio.event async def start_game(sid: str, _data: dict): session = await sio.get_session(sid) - if session["admin"]: - game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) - game_data.started = True - await redis.set(f"game:{session['game_pin']}", game_data.json(), ex=7200) - await redis.delete(f"game_in_lobby:{game_data.user_id.hex}") - await sio.emit("start_game", room=session["game_pin"]) + if not session["admin"]: + return + game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) + game_data.started = True + await redis.set(f"game:{session['game_pin']}", game_data.json(), ex=7200) + await redis.delete(f"game_in_lobby:{game_data.user_id.hex}") + await sio.emit("start_game", room=session["game_pin"]) class _RegisterAsAdminData(BaseModel): @@ -225,15 +274,19 @@ 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) - if session["admin"]: - redis_res = AnswerDataList.parse_raw( - await redis.get(f"game_session:{session['game_pin']}:{data['question_number']}") - ) - game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) - game_data.question_show = False - await redis.set(f"game:{session['game_pin']}", game_data.json()) - game_pin = session["game_pin"] - await sio.emit("question_results", redis_res.dict()["__root__"], room=game_pin) + if not session["admin"]: + return + + redis_res = await redis.get(f"game_session:{session['game_pin']}:{data['question_number']}") + if redis_res is None: + redis_res = [] + else: + redis_res = AnswerDataList.parse_raw(redis_res) + game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) + game_data.question_show = False + await redis.set(f"game:{session['game_pin']}", game_data.json()) + game_pin = session["game_pin"] + await sio.emit("question_results", redis_res.dict()["__root__"], room=game_pin) class ABCDQuizAnswerWithoutSolution(BaseModel): @@ -252,12 +305,12 @@ class ReturnQuestion(QuizQuestion): @validator("answers") def answers_not_none_if_abcd_type(cls, v, values): - if values["type"] == QuizQuestionType.ABCD and type(v[0]) != ABCDQuizAnswerWithoutSolution: + if values["type"] == QuizQuestionType.ABCD and type(v[0]) is not ABCDQuizAnswerWithoutSolution: raise ValueError("Answers can't be none if type is ABCD") - if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswerWithoutSolution: + if values["type"] == QuizQuestionType.RANGE and type(v) is not RangeQuizAnswerWithoutSolution: raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE") # skipcq: PTC-W0047 - if values["type"] == QuizQuestionType.VOTING and type(v[0]) != VotingQuizAnswer: + if values["type"] == QuizQuestionType.VOTING and type(v[0]) is not VotingQuizAnswer: pass return v @@ -266,37 +319,38 @@ def answers_not_none_if_abcd_type(cls, v, values): 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) - if session["admin"]: - game_pin = session["game_pin"] - game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) - game_data.current_question = int(float(data)) - game_data.question_show = True - await redis.set(f"game:{session['game_pin']}", game_data.json(), ex=7200) - await redis.set(f"game:{session['game_pin']}:current_time", datetime.now().isoformat(), ex=7200) - temp_return = game_data.dict(include={"questions"})["questions"][int(float(data))] - if game_data.questions[int(float(data))].type == QuizQuestionType.SLIDE: - await sio.emit( - "set_question_number", - { - "question_index": int(float(data)), - }, - room=sid, - ) - return - if game_data.questions[int(float(data))].type == QuizQuestionType.VOTING: - for i in range(len(temp_return["answers"])): - temp_return["answers"][i] = VotingQuizAnswer(**temp_return["answers"][i]) - temp_return["type"] = game_data.questions[int(float(data))].type - if temp_return["type"] == QuizQuestionType.ORDER: - random.shuffle(temp_return["answers"]) + if not session["admin"]: + return + game_pin = session["game_pin"] + game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) + game_data.current_question = int(float(data)) + game_data.question_show = True + await redis.set(f"game:{session['game_pin']}", game_data.json(), ex=7200) + await redis.set(f"game:{session['game_pin']}:current_time", datetime.now().isoformat(), ex=7200) + temp_return = game_data.dict(include={"questions"})["questions"][int(float(data))] + if game_data.questions[int(float(data))].type == QuizQuestionType.SLIDE: await sio.emit( "set_question_number", { "question_index": int(float(data)), - "question": ReturnQuestion(**temp_return).dict(), }, - room=game_pin, + room=sid, ) + return + if game_data.questions[int(float(data))].type == QuizQuestionType.VOTING: + for i in range(len(temp_return["answers"])): + temp_return["answers"][i] = VotingQuizAnswer(**temp_return["answers"][i]) + temp_return["type"] = game_data.questions[int(float(data))].type + if temp_return["type"] == QuizQuestionType.ORDER: + random.shuffle(temp_return["answers"]) + await sio.emit( + "set_question_number", + { + "question_index": int(float(data)), + "question": ReturnQuestion(**temp_return).dict(), + }, + room=game_pin, + ) class _SubmitAnswerDataOrderType(BaseModel): diff --git a/frontend/src/routes/play/+page.svelte b/frontend/src/routes/play/+page.svelte index 54ade341..aaa2e902 100644 --- a/frontend/src/routes/play/+page.svelte +++ b/frontend/src/routes/play/+page.svelte @@ -17,7 +17,6 @@ SPDX-License-Identifier: MPL-2.0 import KahootResults from '$lib/play/results_kahoot.svelte'; import { getLocalization } from '$lib/i18n'; import Cookies from 'js-cookie'; - const { t } = getLocalization(); // Exports @@ -74,14 +73,38 @@ SPDX-License-Identifier: MPL-2.0 socket.emit('echo_time_sync', data); }); + socket.on("connect", () => { + console.log("Connected!") + const cookie_data = Cookies.get("joined_game") + if (!cookie_data) { + return + } + const data = JSON.parse(cookie_data) + socket.emit("rejoin_game", {old_sid: data.sid, username: data.username, game_pin: data.game_pin}) + }) + // Socket-events socket.on('joined_game', (data) => { gameData = data; // eslint-disable-next-line no-undef plausible('Joined Game', { props: { game_id: gameData.game_id } }); + Cookies.set("joined_game", JSON.stringify({sid: socket.id, username, game_pin}), {expires: 3600}) + }); + socket.on('rejoined_game', (data) => { + gameData = data; + if (data.started) { + gameMeta.started = true + } + }); socket.on('game_not_found', () => { + const cookie_data = Cookies.get("joined_game") + if (cookie_data) { + Cookies.remove("joined_game") + window.location.reload() + return + } game_pin_valid = false; }); @@ -120,6 +143,7 @@ SPDX-License-Identifier: MPL-2.0 }); socket.on('final_results', (data) => { final_results = data; + Cookies.remove("joined_game") }); socket.on('solutions', (data) => { @@ -149,7 +173,7 @@ SPDX-License-Identifier: MPL-2.0 >
{#if !gameMeta.started && gameData === undefined} - + {:else if JSON.stringify(final_results) !== JSON.stringify([null])} {:else if gameData !== undefined && question_index === ''}