From 4bce3fcba0d56c82e7f8423732e5c745b4939ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 12 May 2021 16:07:24 +0200 Subject: [PATCH 01/15] Fix socket authorization --- client/src/sockets.ts | 38 +++++++++++++++++++------------------- server/app/core/sockets.py | 12 ++++++------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/client/src/sockets.ts b/client/src/sockets.ts index dabaf4b9..e2767c10 100644 --- a/client/src/sockets.ts +++ b/client/src/sockets.ts @@ -30,30 +30,30 @@ let socket: SocketIOClient.Socket * in the documentation, no more needed. */ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') => { - if (!socket) { - const token = localStorage[role] - socket = io('localhost:5000', { - transportOptions: { - polling: { - extraHeaders: { - Authorization: token, - }, + if (socket) return + + const token = localStorage[`${role}Token`] + socket = io('localhost:5000', { + transportOptions: { + polling: { + extraHeaders: { + Authorization: token, }, }, - }) + }, + }) - socket.on('set_slide', (data: SetSlideInterface) => { - setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState) - }) + socket.on('set_slide', (data: SetSlideInterface) => { + setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState) + }) - socket.on('set_timer', (data: SetTimerInterface) => { - setPresentationTimer(data.timer)(store.dispatch) - }) + socket.on('set_timer', (data: SetTimerInterface) => { + setPresentationTimer(data.timer)(store.dispatch) + }) - socket.on('end_presentation', () => { - socket.disconnect() - }) - } + socket.on('end_presentation', () => { + socket.disconnect() + }) } export const socketStartPresentation = () => { diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index 71871cda..78ca30d0 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -57,12 +57,12 @@ def protect_route(allowed_views=None): return wrapper -@sio.on("connect") +@sio.event def connect() -> None: logger.info(f"Client '{request.sid}' connected") -@sio.on("disconnect") +@sio.event def disconnect() -> None: """ Remove client from the presentation it was in. Delete presentation if no @@ -108,8 +108,8 @@ def start_presentation(data: Dict) -> None: logger.info(f"Client '{request.sid}' started competition '{competition_id}'") +@sio.event @protect_route(allowed_views=["Operator"]) -@sio.on("end_presentation") def end_presentation(data: Dict) -> None: """ End a presentation by sending end_presentation to all connected clients. @@ -146,7 +146,7 @@ def end_presentation(data: Dict) -> None: logger.info(f"Client '{request.sid}' ended presentation '{competition_id}'") -@sio.on("join_presentation") +@sio.event def join_presentation(data: Dict) -> None: """ Join a currently active presentation. @@ -186,8 +186,8 @@ def join_presentation(data: Dict) -> None: logger.info(f"Client '{request.sid}' joined competition '{competition_id}'") +@sio.event @protect_route(allowed_views=["Operator"]) -@sio.on("set_slide") def set_slide(data: Dict) -> None: """ Sync slides between all clients in the same presentation by sending @@ -234,8 +234,8 @@ def set_slide(data: Dict) -> None: logger.info(f"Client '{request.sid}' set slide '{slide_order}' in competition '{competition_id}'") +@sio.event @protect_route(allowed_views=["Operator"]) -@sio.on("set_timer") def set_timer(data: Dict) -> None: """ Sync slides between all clients in the same presentation by sending -- GitLab From db810fea4a18260471cc8922c5519595d6fbc90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 12 May 2021 16:30:33 +0200 Subject: [PATCH 02/15] Sockets now use authorization headers to get competition_id instead of payload --- client/src/sockets.ts | 12 +++++------- server/app/core/sockets.py | 20 +++++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/client/src/sockets.ts b/client/src/sockets.ts index e2767c10..1d53a5a4 100644 --- a/client/src/sockets.ts +++ b/client/src/sockets.ts @@ -57,15 +57,15 @@ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') } export const socketStartPresentation = () => { - socket.emit('start_presentation', { competition_id: store.getState().presentation.competition.id }) + socket.emit('start_presentation') } export const socketJoinPresentation = () => { - socket.emit('join_presentation', { code: store.getState().presentation.code }) // TODO: Send code gotten from auth/login/<code> api call + socket.emit('join_presentation') } export const socketEndPresentation = () => { - socket.emit('end_presentation', { competition_id: store.getState().presentation.competition.id }) + socket.emit('end_presentation') } export const socketSetSlideNext = () => { @@ -73,7 +73,7 @@ export const socketSetSlideNext = () => { .getState() .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId) if (!activeSlide) return - socketSetSlide(activeSlide.order + 1) // TODO: Check that this slide exists + socketSetSlide(activeSlide.order + 1) } export const socketSetSlidePrev = () => { @@ -81,7 +81,7 @@ export const socketSetSlidePrev = () => { .getState() .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId) if (!activeSlide) return - socketSetSlide(activeSlide.order - 1) // TODO: Check that this slide exists + socketSetSlide(activeSlide.order - 1) } /** @@ -97,14 +97,12 @@ export const socketSetSlide = (slide_order: number) => { } socket.emit('set_slide', { - competition_id: store.getState().presentation.competition.id, slide_order: slide_order, }) } export const socketSetTimer = (timer: Timer) => { socket.emit('set_timer', { - competition_id: store.getState().presentation.competition.id, timer: timer, }) } diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index 78ca30d0..c5ec33ae 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -81,14 +81,14 @@ def disconnect() -> None: logger.info(f"Client '{request.sid}' disconnected") +@sio.event @protect_route(allowed_views=["Operator"]) -@sio.on("start_presentation") -def start_presentation(data: Dict) -> None: +def start_presentation() -> None: """ Starts a presentation if that competition is currently not active. """ - competition_id = data["competition_id"] + competition_id = get_jwt_claims().get("competition_id") if competition_id in presentations: logger.error( @@ -110,7 +110,7 @@ def start_presentation(data: Dict) -> None: @sio.event @protect_route(allowed_views=["Operator"]) -def end_presentation(data: Dict) -> None: +def end_presentation() -> None: """ End a presentation by sending end_presentation to all connected clients. @@ -119,7 +119,8 @@ def end_presentation(data: Dict) -> None: Log error message if no presentation exists with the send id or if this client is not in that presentation. """ - competition_id = data["competition_id"] + + competition_id = get_jwt_claims().get("competition_id") if competition_id not in presentations: logger.error( @@ -147,14 +148,15 @@ def end_presentation(data: Dict) -> None: @sio.event -def join_presentation(data: Dict) -> None: +@protect_route(allowed_views=["*"]) +def join_presentation() -> None: """ Join a currently active presentation. Log error message if given code doesn't exist, if not presentation associated with that code exists or if client is already in the presentation. """ - code = data["code"] + code = get_jwt_claims().get("code") item_code = db.session.query(Code).filter(Code.code == code).first() if not item_code: @@ -197,7 +199,7 @@ def set_slide(data: Dict) -> None: that presentation or the client is not the one who started the presentation. """ - competition_id = data["competition_id"] + competition_id = get_jwt_claims().get("competition_id") slide_order = data["slide_order"] if competition_id not in presentations: @@ -244,7 +246,7 @@ def set_timer(data: Dict) -> None: Log error if the given competition_id is not active, if client is not in that presentation or the client is not the one who started the presentation. """ - competition_id = data["competition_id"] + competition_id = get_jwt_claims().get("competition_id") timer = data["timer"] if competition_id not in presentations: -- GitLab From 26cb256f3bf5e6c54707bbbc5704042e79bfda75 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Wed, 12 May 2021 17:55:50 +0200 Subject: [PATCH 03/15] Refactor timer --- client/src/actions/presentation.ts | 16 +------ client/src/pages/views/JudgeViewPage.tsx | 2 + client/src/pages/views/OperatorViewPage.tsx | 30 +++++++++---- client/src/pages/views/components/Timer.tsx | 47 ++++++--------------- client/src/reducers/presentationReducer.ts | 15 ++----- client/src/sockets.ts | 30 ++----------- server/app/core/sockets.py | 6 +-- 7 files changed, 49 insertions(+), 97 deletions(-) diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts index 0c3f2466..89b9e596 100644 --- a/client/src/actions/presentation.ts +++ b/client/src/actions/presentation.ts @@ -3,8 +3,7 @@ This file handles actions for the presentation redux state */ import axios from 'axios' -import { Timer } from '../interfaces/Timer' -import store, { AppDispatch, RootState } from './../store' +import { AppDispatch, RootState } from './../store' import Types from './types' /** Save competition in presentation state from input id */ @@ -36,17 +35,6 @@ export const setPresentationCode = (code: string) => (dispatch: AppDispatch) => dispatch({ type: Types.SET_PRESENTATION_CODE, payload: code }) } /** Set timer to input value */ -export const setPresentationTimer = (timer: Timer) => (dispatch: AppDispatch) => { +export const setPresentationTimer = (timer: number | null) => (dispatch: AppDispatch) => { dispatch({ type: Types.SET_PRESENTATION_TIMER, payload: timer }) } - -/** Decrement timer */ -export const setPresentationTimerDecrement = () => (dispatch: AppDispatch) => { - dispatch({ - type: Types.SET_PRESENTATION_TIMER, - payload: { - enabled: store.getState().presentation.timer.enabled, - value: store.getState().presentation.timer.value - 1, - }, - }) -} diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index a28348a7..be9e4885 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -11,6 +11,7 @@ import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { SlideListItem } from '../presentationEditor/styled' import JudgeScoreDisplay from './components/JudgeScoreDisplay' import JudgeScoringInstructions from './components/JudgeScoringInstructions' +import Timer from './components/Timer' import { Content, InnerContent, @@ -77,6 +78,7 @@ const JudgeViewPage: React.FC = () => { return ( <div style={{ height: '100%' }}> <JudgeAppBar position="fixed"> + <Timer /> <JudgeToolbar> <JudgeQuestionsLabel variant="h5">Frågor</JudgeQuestionsLabel> {operatorActiveSlideOrder !== undefined && ( diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx index 5bb96fcb..d873a731 100644 --- a/client/src/pages/views/OperatorViewPage.tsx +++ b/client/src/pages/views/OperatorViewPage.tsx @@ -34,11 +34,10 @@ import { RichTeam } from '../../interfaces/ApiRichModels' import { socketConnect, socketEndPresentation, - socketSetSlide, socketSetSlideNext, socketSetSlidePrev, + socketSetTimer, socketStartPresentation, - socketStartTimer, } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { Center } from '../presentationEditor/components/styled' @@ -111,6 +110,7 @@ const OperatorViewPage: React.FC = () => { const competitionId = useAppSelector((state) => state.competitionLogin.data?.competition_id) const presentation = useAppSelector((state) => state.presentation) const activeId = useAppSelector((state) => state.presentation.competition.id) + const timer = useAppSelector((state) => state.presentation.timer) const history = useHistory() const viewTypes = useAppSelector((state) => state.types.viewTypes) const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Audience')?.id @@ -119,9 +119,11 @@ const OperatorViewPage: React.FC = () => { (state) => state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId)?.order ) + const slideTimer = useAppSelector((state) => + activeSlideOrder !== undefined ? state.presentation.competition.slides[activeSlideOrder].timer : null + ) useEffect(() => { socketConnect('Operator') - socketSetSlide handleOpenCodes() setTimeout(startCompetition, 1000) // Wait for socket to connect }, []) @@ -222,6 +224,14 @@ const OperatorViewPage: React.FC = () => { return totalScore } + const handleStartTimer = () => { + if (!slideTimer) return + // has active timer already, so cancel it + if (timer && timer - Date.now() > 0) socketSetTimer(Date.now()) + // Either has expired timer or no timer so start one + else socketSetTimer(Date.now() + 1000 * slideTimer) + } + return ( <OperatorContainer> <Dialog @@ -325,12 +335,14 @@ const OperatorViewPage: React.FC = () => { </OperatorButton> </Tooltip> - <Tooltip title="Starta Timer" arrow> - <OperatorButton onClick={socketStartTimer} variant="contained"> - <TimerIcon fontSize="large" /> - <Timer></Timer> - </OperatorButton> - </Tooltip> + {slideTimer && ( + <Tooltip title="Starta Timer" arrow> + <OperatorButton onClick={() => handleStartTimer()} variant="contained"> + <TimerIcon fontSize="large" /> + <Timer /> + </OperatorButton> + </Tooltip> + )} <Tooltip title="Ställning" arrow> <OperatorButton onClick={handleOpenPopover} variant="contained"> diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx index 29f95349..f1612e6b 100644 --- a/client/src/pages/views/components/Timer.tsx +++ b/client/src/pages/views/components/Timer.tsx @@ -1,46 +1,27 @@ -import React, { useEffect } from 'react' -import { setPresentationTimer, setPresentationTimerDecrement } from '../../../actions/presentation' -import { useAppDispatch, useAppSelector } from '../../../hooks' -import store from '../../../store' - -/* const mapStateToProps = (state: any) => { - return { - timer: state.presentation.timer, - timer_start_value: state.presentation.slide.timer, - } -} - -const mapDispatchToProps = (dispatch: any) => { - return { - // tickTimer: () => dispatch(tickTimer(1)), - } -} */ +import React, { useEffect, useState } from 'react' +import { useAppSelector } from '../../../hooks' let timerIntervalId: NodeJS.Timeout const Timer: React.FC = () => { - const dispatch = useAppDispatch() - const slide = store - .getState() - .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId) - const timerStartValue = slide?.timer const timer = useAppSelector((state) => state.presentation.timer) + const [remainingTimer, setRemainingTimer] = useState<number>(0) + const slideTimer = useAppSelector( + (state) => + state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId)?.timer + ) useEffect(() => { - if (!slide) return - dispatch(setPresentationTimer({ enabled: false, value: slide.timer })) - }, [timerStartValue]) - - useEffect(() => { - if (timer.enabled) { + if (timer !== null && timer - Date.now() > 0) { + setRemainingTimer(timer - Date.now()) timerIntervalId = setInterval(() => { - dispatch(setPresentationTimerDecrement()) - }, 1000) + setRemainingTimer(timer - Date.now()) + }, 500) } else { clearInterval(timerIntervalId) } - }, [timer.enabled]) - - return <div>{timer.value}</div> + }, [timer]) + // :) + return <div>{timer !== null ? (timer - Date.now() > 0 ? Math.round(remainingTimer / 1000) : 0) : slideTimer}</div> } export default Timer diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts index 4df814a8..cb74c1ba 100644 --- a/client/src/reducers/presentationReducer.ts +++ b/client/src/reducers/presentationReducer.ts @@ -1,6 +1,5 @@ import { AnyAction } from 'redux' import Types from '../actions/types' -import { Timer } from '../interfaces/Timer' import { RichCompetition } from './../interfaces/ApiRichModels' /** Define a type for the presentation state */ @@ -8,7 +7,7 @@ interface PresentationState { competition: RichCompetition activeSlideId: number code: string - timer: Timer + timer: number | null } /** Define the initial values for the presentation state */ @@ -24,10 +23,7 @@ const initialState: PresentationState = { }, activeSlideId: -1, code: '', - timer: { - enabled: false, - value: 0, - }, + timer: null, } /** Intercept actions for presentation state and update the state */ @@ -41,7 +37,7 @@ export default function (state = initialState, action: AnyAction) { case Types.SET_PRESENTATION_CODE: return { ...state, - code: action.payload, + code: action.payload as string, } case Types.SET_PRESENTATION_SLIDE_ID: return { @@ -49,12 +45,9 @@ export default function (state = initialState, action: AnyAction) { activeSlideId: action.payload as number, } case Types.SET_PRESENTATION_TIMER: - if (action.payload.value == 0) { - action.payload.enabled = false - } return { ...state, - timer: action.payload, + timer: action.payload as number, } default: return state diff --git a/client/src/sockets.ts b/client/src/sockets.ts index 1d53a5a4..b5de4c49 100644 --- a/client/src/sockets.ts +++ b/client/src/sockets.ts @@ -1,34 +1,17 @@ -/** - * This is a comment on the module level, i.e. the entire file. - * - * For this to appear in the documentation this is needed at the bottom. - * @module - */ - import io from 'socket.io-client' import { setCurrentSlideByOrder, setPresentationTimer } from './actions/presentation' -import { Timer } from './interfaces/Timer' import store from './store' interface SetSlideInterface { slide_order: number } -interface TimerInterface { - value: number - enabled: boolean -} - interface SetTimerInterface { - timer: TimerInterface + timer: number | null } let socket: SocketIOClient.Socket -/** - * You can also comment functions, like usual. This will automatically appear - * in the documentation, no more needed. - */ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') => { if (socket) return @@ -84,13 +67,6 @@ export const socketSetSlidePrev = () => { socketSetSlide(activeSlide.order - 1) } -/** - * You can also comment a function like, adding more information to either - * the paramters or the return value. - * - * @param slide_order This is a parameter to the function. - * @returns This function returns nothing. - */ export const socketSetSlide = (slide_order: number) => { if (slide_order < 0 || store.getState().presentation.competition.slides.length <= slide_order) { return @@ -101,12 +77,12 @@ export const socketSetSlide = (slide_order: number) => { }) } -export const socketSetTimer = (timer: Timer) => { +export const socketSetTimer = (timer: number | null) => { socket.emit('set_timer', { timer: timer, }) } export const socketStartTimer = () => { - socketSetTimer({ enabled: true, value: store.getState().presentation.timer.value }) + socketSetTimer(store.getState().presentation.timer) } diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index c5ec33ae..9c26fa05 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -99,7 +99,7 @@ def start_presentation() -> None: presentations[competition_id] = { "clients": {request.sid: {"view_type": "Operator"}}, "slide": None, - "timer": {"enabled": False, "start_value": None, "value": None}, + "timer": None, } join_room(competition_id) @@ -184,7 +184,7 @@ def join_presentation() -> None: join_room(competition_id) logger.debug(f"Client '{request.sid}' joined room {competition_id}") - + emit("set_timer", {"timer": presentations[competition_id]["timer"]}) logger.info(f"Client '{request.sid}' joined competition '{competition_id}'") @@ -271,5 +271,5 @@ def set_timer(data: Dict) -> None: emit("set_timer", {"timer": timer}, room=competition_id, include_self=True) logger.debug(f"Emitting event 'set_timer' to room {competition_id} including self") - + presentations[competition_id]["timer"] = timer logger.info(f"Client '{request.sid}' set timer '{timer}' in presentation '{competition_id}'") -- GitLab From 31eb3c4c59a1dc2985df721fa8a58874b3d9e68e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 12 May 2021 19:21:49 +0200 Subject: [PATCH 04/15] Refactor sockets on server --- server/app/core/sockets.py | 226 +++++++++---------------------------- server/requirements.txt | Bin 3330 -> 3366 bytes 2 files changed, 52 insertions(+), 174 deletions(-) diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index 9c26fa05..ff53b284 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -8,7 +8,9 @@ from functools import wraps from typing import Dict from app.core import db -from app.database.models import Code, Slide, ViewType +from app.database.controller.add import competition +from app.database.models import Slide +from decorator import decorator from flask.globals import request from flask_jwt_extended import verify_jwt_in_request from flask_jwt_extended.utils import get_jwt_claims @@ -18,7 +20,7 @@ logger = logging.getLogger(__name__) logger.propagate = False logger.setLevel(logging.INFO) -formatter = logging.Formatter("[%(levelname)s] %(funcName)s: %(message)s") +formatter = logging.Formatter("[%(levelname)s] %(message)s") stream_handler = logging.StreamHandler() stream_handler.setFormatter(formatter) logger.addHandler(stream_handler) @@ -32,194 +34,92 @@ def _is_allowed(allowed, actual): return actual and "*" in allowed or actual in allowed -def protect_route(allowed_views=None): - def wrapper(f): - @wraps(f) - def inner(*args, **kwargs): - try: - verify_jwt_in_request() - except: - logger.warning("Missing Authorization Header") - return +@decorator +def authorize_client(f, allowed_views=None, require_active_competition=True, *args, **kwargs): + try: + verify_jwt_in_request() + except: + logger.error("Cant call function '{f.__name__}': Missing Authorization Header") + return - nonlocal allowed_views - allowed_views = allowed_views or [] - claims = get_jwt_claims() - view = claims.get("view") - if not _is_allowed(allowed_views, view): - logger.warning(f"View '{view}' is not allowed to access route only accessible by '{allowed_views}'") - return + claims = get_jwt_claims() - return f(*args, **kwargs) + competition_id = claims.get("competition_id") + if require_active_competition and competition_id not in presentations: + logger.error(f"Cant call function '{f.__name__}': Competition '{competition_id}' is not active") + return - return inner + allowed_views = allowed_views or [] + view = claims.get("view") + if not _is_allowed(allowed_views, view): + logger.error(f"Cant call function '{f.__name__}': View '{view}' is not in '{''.join(allowed_views)}'") + return - return wrapper + return f(*args, **kwargs) @sio.event +@authorize_client(require_active_competition=False, allowed_views=["*"]) def connect() -> None: - logger.info(f"Client '{request.sid}' connected") + + claims = get_jwt_claims() + view = claims.get("view") + competition_id = claims.get("competition_id") + + if competition_id in presentations: + presentations[competition_id]["client_count"] += 1 + join_room(competition_id) + emit("set_timer", {"timer": presentations[competition_id]["timer"]}) + logger.info(f"Client '{request.sid}' with view '{view}' joined competition '{competition_id}'") + elif view == "Operator": + join_room(competition_id) + presentations[competition_id] = { + "client_count": 1, + "slide": None, + "timer": None, + } + logger.info(f"Client '{request.sid}' started competition {competition_id}") @sio.event +@authorize_client(allowed_views=["*"]) def disconnect() -> None: """ Remove client from the presentation it was in. Delete presentation if no clients are connected to it. """ - for competition_id, presentation in presentations.items(): - if request.sid in presentation["clients"]: - del presentation["clients"][request.sid] - logger.debug(f"Client '{request.sid}' left presentation '{competition_id}'") - break - - if presentations and not presentations[competition_id]["clients"]: - del presentations[competition_id] - logger.info(f"No people left in presentation '{competition_id}', ended presentation") - - logger.info(f"Client '{request.sid}' disconnected") - - -@sio.event -@protect_route(allowed_views=["Operator"]) -def start_presentation() -> None: - """ - Starts a presentation if that competition is currently not active. - """ competition_id = get_jwt_claims().get("competition_id") + presentations[competition_id]["client_count"] -= 1 + logger.info(f"Client '{request.sid}' disconnected") - if competition_id in presentations: - logger.error( - f"Client '{request.sid}' failed to start competition '{competition_id}', presentation already active" - ) - return - - presentations[competition_id] = { - "clients": {request.sid: {"view_type": "Operator"}}, - "slide": None, - "timer": None, - } - - join_room(competition_id) - logger.debug(f"Client '{request.sid}' joined room {competition_id}") - - logger.info(f"Client '{request.sid}' started competition '{competition_id}'") + if presentations[competition_id]["client_count"] <= 0: + del presentations[competition_id] + logger.info(f"No people left in presentation '{competition_id}', ended presentation") @sio.event -@protect_route(allowed_views=["Operator"]) +@authorize_client(allowed_views=["Operator"]) def end_presentation() -> None: """ End a presentation by sending end_presentation to all connected clients. - - The only clients allowed to do this is the one that started the presentation. - - Log error message if no presentation exists with the send id or if this - client is not in that presentation. """ competition_id = get_jwt_claims().get("competition_id") - - if competition_id not in presentations: - logger.error( - f"Client '{request.sid}' failed to end presentation '{competition_id}', no such presentation exists" - ) - return - - if request.sid not in presentations[competition_id]["clients"]: - logger.error( - f"Client '{request.sid}' failed to end presentation '{competition_id}', client not in presentation" - ) - return - - if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator": - logger.error(f"Client '{request.sid}' failed to end presentation '{competition_id}', client is not operator") - return - - del presentations[competition_id] - logger.debug(f"Deleted presentation {competition_id}") - emit("end_presentation", room=competition_id, include_self=True) - logger.debug(f"Emitting event 'end_presentation' to room {competition_id} including self") - - logger.info(f"Client '{request.sid}' ended presentation '{competition_id}'") @sio.event -@protect_route(allowed_views=["*"]) -def join_presentation() -> None: - """ - Join a currently active presentation. - - Log error message if given code doesn't exist, if not presentation associated - with that code exists or if client is already in the presentation. - """ - code = get_jwt_claims().get("code") - item_code = db.session.query(Code).filter(Code.code == code).first() - - if not item_code: - logger.error(f"Client '{request.sid}' failed to join presentation with code '{code}', no such code exists") - return - - competition_id = item_code.competition_id - - if competition_id not in presentations: - logger.error( - f"Client '{request.sid}' failed to join presentation '{competition_id}', no such presentation exists" - ) - return - - if request.sid in presentations[competition_id]["clients"]: - logger.error( - f"Client '{request.sid}' failed to join presentation '{competition_id}', client already in presentation" - ) - return - - # TODO: Write function in database controller to do this - view_type_name = db.session.query(ViewType).filter(ViewType.id == item_code.view_type_id).one().name - - presentations[competition_id]["clients"][request.sid] = {"view_type": view_type_name} - - join_room(competition_id) - logger.debug(f"Client '{request.sid}' joined room {competition_id}") - emit("set_timer", {"timer": presentations[competition_id]["timer"]}) - logger.info(f"Client '{request.sid}' joined competition '{competition_id}'") - - -@sio.event -@protect_route(allowed_views=["Operator"]) +@authorize_client(allowed_views=["Operator"]) def set_slide(data: Dict) -> None: """ Sync slides between all clients in the same presentation by sending set_slide to them. - - Log error if the given competition_id is not active, if client is not in - that presentation or the client is not the one who started the presentation. """ competition_id = get_jwt_claims().get("competition_id") slide_order = data["slide_order"] - if competition_id not in presentations: - logger.error( - f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', no such presentation exists" - ) - return - - if request.sid not in presentations[competition_id]["clients"]: - logger.error( - f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client not in presentation" - ) - return - - if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator": - logger.error( - f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client is not operator" - ) - return - num_slides = db.session.query(Slide).filter(Slide.competition_id == competition_id).count() if not (0 <= slide_order < num_slides): @@ -231,13 +131,12 @@ def set_slide(data: Dict) -> None: presentations[competition_id]["slide"] = slide_order emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True) - logger.debug(f"Emitting event 'set_slide' to room {competition_id} including self") logger.info(f"Client '{request.sid}' set slide '{slide_order}' in competition '{competition_id}'") @sio.event -@protect_route(allowed_views=["Operator"]) +@authorize_client(allowed_views=["Operator"]) def set_timer(data: Dict) -> None: """ Sync slides between all clients in the same presentation by sending @@ -249,27 +148,6 @@ def set_timer(data: Dict) -> None: competition_id = get_jwt_claims().get("competition_id") timer = data["timer"] - if competition_id not in presentations: - logger.error( - f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', no such presentation exists" - ) - return - - if request.sid not in presentations[competition_id]["clients"]: - logger.error( - f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client not in presentation" - ) - return - - if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator": - logger.error( - f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client is not operator" - ) - return - - # TODO: Save timer in presentation, maybe? - - emit("set_timer", {"timer": timer}, room=competition_id, include_self=True) - logger.debug(f"Emitting event 'set_timer' to room {competition_id} including self") presentations[competition_id]["timer"] = timer - logger.info(f"Client '{request.sid}' set timer '{timer}' in presentation '{competition_id}'") + emit("set_timer", {"timer": timer}, room=competition_id, include_self=True) + logger.info(f"({competition_id}) Timer set to '{timer}' ") diff --git a/server/requirements.txt b/server/requirements.txt index 6a9e48fc29b527a9a3609716953d4ba5a52fe4e2..472b12cdf8229664d6d4bc679e6a85cb941341d7 100644 GIT binary patch delta 42 scmZpYS|+u@fJru$A(<hcp@<=op#)6ZGT1VhGUzcF0I~UIO{SY%0M7IX?f?J) delta 12 TcmZ1`)g-mSfN66G(<Lqd98Ux! -- GitLab From 90efe0c2624c92d02a4823678856802016dd5590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 12 May 2021 19:37:26 +0200 Subject: [PATCH 05/15] Minor refactoring in sockets on backend --- server/app/core/sockets.py | 59 +++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index ff53b284..4ee37088 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -30,8 +30,13 @@ sio = SocketIO(cors_allowed_origins="http://localhost:3000") presentations = {} -def _is_allowed(allowed, actual): - return actual and "*" in allowed or actual in allowed +def unpack_claims(): + claims = get_jwt_claims() + return (claims["competition_id"], claims["view"]) + + +def is_active_competition(competition_id): + return competition_id in presentations @decorator @@ -39,20 +44,23 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar try: verify_jwt_in_request() except: - logger.error("Cant call function '{f.__name__}': Missing Authorization Header") + logger.error(f"Won't call function '{f.__name__}': Missing Authorization Header") return + def _is_allowed(allowed, actual): + return actual and "*" in allowed or actual in allowed + claims = get_jwt_claims() competition_id = claims.get("competition_id") - if require_active_competition and competition_id not in presentations: - logger.error(f"Cant call function '{f.__name__}': Competition '{competition_id}' is not active") + if require_active_competition and not is_active_competition(competition_id): + logger.error(f"Won't call function '{f.__name__}': Competition '{competition_id}' is not active") return allowed_views = allowed_views or [] view = claims.get("view") if not _is_allowed(allowed_views, view): - logger.error(f"Cant call function '{f.__name__}': View '{view}' is not in '{''.join(allowed_views)}'") + logger.error(f"Won't call function '{f.__name__}': View '{view}' is not in '{''.join(allowed_views)}'") return return f(*args, **kwargs) @@ -62,13 +70,12 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar @authorize_client(require_active_competition=False, allowed_views=["*"]) def connect() -> None: - claims = get_jwt_claims() - view = claims.get("view") - competition_id = claims.get("competition_id") + competition_id, view = unpack_claims() - if competition_id in presentations: + if is_active_competition(competition_id): presentations[competition_id]["client_count"] += 1 join_room(competition_id) + emit("set_slide", {"slide": presentations[competition_id]["slide"]}) emit("set_timer", {"timer": presentations[competition_id]["timer"]}) logger.info(f"Client '{request.sid}' with view '{view}' joined competition '{competition_id}'") elif view == "Operator": @@ -78,7 +85,7 @@ def connect() -> None: "slide": None, "timer": None, } - logger.info(f"Client '{request.sid}' started competition {competition_id}") + logger.info(f"Client '{request.sid}' started competition '{competition_id}'") @sio.event @@ -89,9 +96,9 @@ def disconnect() -> None: clients are connected to it. """ - competition_id = get_jwt_claims().get("competition_id") + competition_id, _ = unpack_claims() presentations[competition_id]["client_count"] -= 1 - logger.info(f"Client '{request.sid}' disconnected") + logger.info(f"Client '{request.sid}' disconnected from competition '{competition_id}'") if presentations[competition_id]["client_count"] <= 0: del presentations[competition_id] @@ -105,7 +112,7 @@ def end_presentation() -> None: End a presentation by sending end_presentation to all connected clients. """ - competition_id = get_jwt_claims().get("competition_id") + competition_id, _ = unpack_claims() emit("end_presentation", room=competition_id, include_self=True) @@ -117,22 +124,11 @@ def set_slide(data: Dict) -> None: set_slide to them. """ - competition_id = get_jwt_claims().get("competition_id") + competition_id, _ = unpack_claims() slide_order = data["slide_order"] - - num_slides = db.session.query(Slide).filter(Slide.competition_id == competition_id).count() - - if not (0 <= slide_order < num_slides): - logger.error( - f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', slide number {slide_order} does not exist" - ) - return - presentations[competition_id]["slide"] = slide_order - emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True) - - logger.info(f"Client '{request.sid}' set slide '{slide_order}' in competition '{competition_id}'") + logger.info(f"Client '{request.sid}' set slide to '{slide_order}' in competition '{competition_id}'") @sio.event @@ -141,13 +137,10 @@ def set_timer(data: Dict) -> None: """ Sync slides between all clients in the same presentation by sending set_timer to them. - - Log error if the given competition_id is not active, if client is not in - that presentation or the client is not the one who started the presentation. """ - competition_id = get_jwt_claims().get("competition_id") - timer = data["timer"] + competition_id, _ = unpack_claims() + timer = data["timer"] presentations[competition_id]["timer"] = timer emit("set_timer", {"timer": timer}, room=competition_id, include_self=True) - logger.info(f"({competition_id}) Timer set to '{timer}' ") + logger.info(f"Client '{request.sid}' set timer to '{timer}' in competition '{competition_id}'") -- GitLab From b563dac4e06297ebb9899357393482c7ff922dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 12 May 2021 19:40:09 +0200 Subject: [PATCH 06/15] Add underscore to beginning of function names in sockets --- server/app/core/sockets.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index 4ee37088..ef463c07 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -30,12 +30,12 @@ sio = SocketIO(cors_allowed_origins="http://localhost:3000") presentations = {} -def unpack_claims(): +def _unpack_claims(): claims = get_jwt_claims() - return (claims["competition_id"], claims["view"]) + return claims["competition_id"], claims["view"] -def is_active_competition(competition_id): +def _is_active_competition(competition_id): return competition_id in presentations @@ -50,15 +50,13 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar def _is_allowed(allowed, actual): return actual and "*" in allowed or actual in allowed - claims = get_jwt_claims() + competition_id, view = _unpack_claims() - competition_id = claims.get("competition_id") - if require_active_competition and not is_active_competition(competition_id): + if require_active_competition and not _is_active_competition(competition_id): logger.error(f"Won't call function '{f.__name__}': Competition '{competition_id}' is not active") return allowed_views = allowed_views or [] - view = claims.get("view") if not _is_allowed(allowed_views, view): logger.error(f"Won't call function '{f.__name__}': View '{view}' is not in '{''.join(allowed_views)}'") return @@ -70,9 +68,9 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar @authorize_client(require_active_competition=False, allowed_views=["*"]) def connect() -> None: - competition_id, view = unpack_claims() + competition_id, view = _unpack_claims() - if is_active_competition(competition_id): + if _is_active_competition(competition_id): presentations[competition_id]["client_count"] += 1 join_room(competition_id) emit("set_slide", {"slide": presentations[competition_id]["slide"]}) @@ -96,7 +94,7 @@ def disconnect() -> None: clients are connected to it. """ - competition_id, _ = unpack_claims() + competition_id, _ = _unpack_claims() presentations[competition_id]["client_count"] -= 1 logger.info(f"Client '{request.sid}' disconnected from competition '{competition_id}'") @@ -112,7 +110,7 @@ def end_presentation() -> None: End a presentation by sending end_presentation to all connected clients. """ - competition_id, _ = unpack_claims() + competition_id, _ = _unpack_claims() emit("end_presentation", room=competition_id, include_self=True) @@ -124,7 +122,7 @@ def set_slide(data: Dict) -> None: set_slide to them. """ - competition_id, _ = unpack_claims() + competition_id, _ = _unpack_claims() slide_order = data["slide_order"] presentations[competition_id]["slide"] = slide_order emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True) @@ -139,7 +137,7 @@ def set_timer(data: Dict) -> None: set_timer to them. """ - competition_id, _ = unpack_claims() + competition_id, _ = _unpack_claims() timer = data["timer"] presentations[competition_id]["timer"] = timer emit("set_timer", {"timer": timer}, room=competition_id, include_self=True) -- GitLab From e7a47ce4d108c436b817bb7e9dc56c0463a1d6ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 12 May 2021 20:19:13 +0200 Subject: [PATCH 07/15] Refactor setTimer and setSlide to just sync --- client/src/pages/views/AudienceViewPage.tsx | 3 +- client/src/pages/views/JudgeViewPage.tsx | 3 +- client/src/pages/views/OperatorViewPage.tsx | 8 ++--- client/src/pages/views/TeamViewPage.tsx | 3 +- client/src/sockets.ts | 34 +++++------------- server/app/core/sockets.py | 40 +++++++++------------ 6 files changed, 31 insertions(+), 60 deletions(-) diff --git a/client/src/pages/views/AudienceViewPage.tsx b/client/src/pages/views/AudienceViewPage.tsx index af1f18b3..e78db8b0 100644 --- a/client/src/pages/views/AudienceViewPage.tsx +++ b/client/src/pages/views/AudienceViewPage.tsx @@ -2,7 +2,7 @@ import { Snackbar, Typography } from '@material-ui/core' import { Alert } from '@material-ui/lab' import React, { useEffect, useState } from 'react' import { useAppSelector } from '../../hooks' -import { socketConnect, socketJoinPresentation } from '../../sockets' +import { socketConnect } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { PresentationBackground, PresentationContainer } from './styled' @@ -15,7 +15,6 @@ const AudienceViewPage: React.FC = () => { useEffect(() => { if (code && code !== '') { socketConnect('Audience') - socketJoinPresentation() } }, []) if (activeViewTypeId) { diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index be9e4885..b77f145e 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react' import { getPresentationCompetition } from '../../actions/presentation' import { useAppDispatch, useAppSelector } from '../../hooks' import { RichSlide } from '../../interfaces/ApiRichModels' -import { socketConnect, socketJoinPresentation } from '../../sockets' +import { socketConnect } from '../../sockets' import { renderSlideIcon } from '../../utils/renderSlideIcon' import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { SlideListItem } from '../presentationEditor/styled' @@ -64,7 +64,6 @@ const JudgeViewPage: React.FC = () => { useEffect(() => { if (code && code !== '') { socketConnect('Judge') - socketJoinPresentation() } }, []) useEffect(() => { diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx index d873a731..fcede248 100644 --- a/client/src/pages/views/OperatorViewPage.tsx +++ b/client/src/pages/views/OperatorViewPage.tsx @@ -36,8 +36,7 @@ import { socketEndPresentation, socketSetSlideNext, socketSetSlidePrev, - socketSetTimer, - socketStartPresentation, + socketSyncTimer, } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { Center } from '../presentationEditor/components/styled' @@ -145,7 +144,6 @@ const OperatorViewPage: React.FC = () => { } const startCompetition = () => { - socketStartPresentation() // Calls the socket to start competition console.log('started competition for') console.log(competitionId) } @@ -227,9 +225,9 @@ const OperatorViewPage: React.FC = () => { const handleStartTimer = () => { if (!slideTimer) return // has active timer already, so cancel it - if (timer && timer - Date.now() > 0) socketSetTimer(Date.now()) + if (timer && timer - Date.now() > 0) socketSyncTimer(Date.now()) // Either has expired timer or no timer so start one - else socketSetTimer(Date.now() + 1000 * slideTimer) + else socketSyncTimer(Date.now() + 1000 * slideTimer) } return ( diff --git a/client/src/pages/views/TeamViewPage.tsx b/client/src/pages/views/TeamViewPage.tsx index fd51c884..e98bd0e5 100644 --- a/client/src/pages/views/TeamViewPage.tsx +++ b/client/src/pages/views/TeamViewPage.tsx @@ -2,7 +2,7 @@ import { Snackbar } from '@material-ui/core' import { Alert } from '@material-ui/lab' import React, { useEffect, useState } from 'react' import { useAppSelector } from '../../hooks' -import { socketConnect, socketJoinPresentation } from '../../sockets' +import { socketConnect } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { PresentationBackground, PresentationContainer } from './styled' @@ -19,7 +19,6 @@ const TeamViewPage: React.FC = () => { useEffect(() => { if (code && code !== '') { socketConnect('Team') - socketJoinPresentation() } }, []) return ( diff --git a/client/src/sockets.ts b/client/src/sockets.ts index b5de4c49..a5f3ac42 100644 --- a/client/src/sockets.ts +++ b/client/src/sockets.ts @@ -2,11 +2,8 @@ import io from 'socket.io-client' import { setCurrentSlideByOrder, setPresentationTimer } from './actions/presentation' import store from './store' -interface SetSlideInterface { +interface SyncInterface { slide_order: number -} - -interface SetTimerInterface { timer: number | null } @@ -26,11 +23,8 @@ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') }, }) - socket.on('set_slide', (data: SetSlideInterface) => { + socket.on('sync', (data: SyncInterface) => { setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState) - }) - - socket.on('set_timer', (data: SetTimerInterface) => { setPresentationTimer(data.timer)(store.dispatch) }) @@ -39,14 +33,6 @@ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') }) } -export const socketStartPresentation = () => { - socket.emit('start_presentation') -} - -export const socketJoinPresentation = () => { - socket.emit('join_presentation') -} - export const socketEndPresentation = () => { socket.emit('end_presentation') } @@ -56,7 +42,7 @@ export const socketSetSlideNext = () => { .getState() .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId) if (!activeSlide) return - socketSetSlide(activeSlide.order + 1) + socketSyncSlide(activeSlide.order + 1) } export const socketSetSlidePrev = () => { @@ -64,25 +50,21 @@ export const socketSetSlidePrev = () => { .getState() .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId) if (!activeSlide) return - socketSetSlide(activeSlide.order - 1) + socketSyncSlide(activeSlide.order - 1) } -export const socketSetSlide = (slide_order: number) => { +export const socketSyncSlide = (slide_order: number) => { if (slide_order < 0 || store.getState().presentation.competition.slides.length <= slide_order) { return } - socket.emit('set_slide', { + socket.emit('sync', { slide_order: slide_order, }) } -export const socketSetTimer = (timer: number | null) => { - socket.emit('set_timer', { +export const socketSyncTimer = (timer: number | null) => { + socket.emit('sync', { timer: timer, }) } - -export const socketStartTimer = () => { - socketSetTimer(store.getState().presentation.timer) -} diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index ef463c07..7057f8e9 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -39,6 +39,10 @@ def _is_active_competition(competition_id): return competition_id in presentations +def _get_sync_variables(presentation): + return {key: value for key, value in presentation.items() if key != "client_count"} + + @decorator def authorize_client(f, allowed_views=None, require_active_competition=True, *args, **kwargs): try: @@ -71,16 +75,16 @@ def connect() -> None: competition_id, view = _unpack_claims() if _is_active_competition(competition_id): - presentations[competition_id]["client_count"] += 1 + presentation = presentations[competition_id] + presentation["client_count"] += 1 join_room(competition_id) - emit("set_slide", {"slide": presentations[competition_id]["slide"]}) - emit("set_timer", {"timer": presentations[competition_id]["timer"]}) + emit("sync", _get_sync_variables(presentation)) logger.info(f"Client '{request.sid}' with view '{view}' joined competition '{competition_id}'") elif view == "Operator": join_room(competition_id) presentations[competition_id] = { "client_count": 1, - "slide": None, + "slide_order": None, "timer": None, } logger.info(f"Client '{request.sid}' started competition '{competition_id}'") @@ -116,29 +120,19 @@ def end_presentation() -> None: @sio.event @authorize_client(allowed_views=["Operator"]) -def set_slide(data: Dict) -> None: +def sync(data: Dict) -> None: """ - Sync slides between all clients in the same presentation by sending - set_slide to them. + Sync presentation for all clients connected to competition. """ competition_id, _ = _unpack_claims() - slide_order = data["slide_order"] - presentations[competition_id]["slide"] = slide_order - emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True) - logger.info(f"Client '{request.sid}' set slide to '{slide_order}' in competition '{competition_id}'") + presentation = presentations[competition_id] + for key, value in data.items(): + if key not in presentation: + logger.warning(f"Invalid sync data: '{key}':'{value}'") -@sio.event -@authorize_client(allowed_views=["Operator"]) -def set_timer(data: Dict) -> None: - """ - Sync slides between all clients in the same presentation by sending - set_timer to them. - """ + if value is not None: + presentation[key] = value - competition_id, _ = _unpack_claims() - timer = data["timer"] - presentations[competition_id]["timer"] = timer - emit("set_timer", {"timer": timer}, room=competition_id, include_self=True) - logger.info(f"Client '{request.sid}' set timer to '{timer}' in competition '{competition_id}'") + emit("sync", _get_sync_variables(presentation), room=competition_id, include_self=True) -- GitLab From 5624519cff39c6a266036de85d77b15f9082b771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 12 May 2021 20:39:57 +0200 Subject: [PATCH 08/15] Remove unused import, comment some functions and add is_active_competition() --- server/app/apis/auth.py | 4 +- server/app/core/sockets.py | 86 +++++++++++++++++++++++--------------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py index 26301d64..e29fd4ee 100644 --- a/server/app/apis/auth.py +++ b/server/app/apis/auth.py @@ -8,9 +8,9 @@ from datetime import datetime, timedelta import app.core.http_codes as codes import app.database.controller as dbc from app.apis import item_response, protect_route, text_response -from app.core import sockets from app.core.codes import verify_code from app.core.dto import AuthDTO +from app.core.sockets import is_active_competition from app.database.models import User, Whitelist from flask import current_app, has_app_context from flask_jwt_extended import create_access_token, get_jti, get_raw_jwt @@ -164,7 +164,7 @@ class AuthLoginCode(Resource): item_code = dbc.get.code_by_code(code) if item_code.view_type_id != 4: - if item_code.competition_id not in sockets.presentations: + if not is_active_competition(item_code.competition_id): api.abort(codes.UNAUTHORIZED, "Competition not active") # Create jwt that is only valid for 8 hours diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index 7057f8e9..341a269f 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -1,15 +1,9 @@ """ -Contains all functionality related sockets. That is starting and ending a presentation, -joining and leaving a presentation and syncing slides and timer bewteen all clients -connected to the same presentation. +Contains all functionality related sockets. That is starting, joining, ending, +disconnecting from and syncing active competitions. """ import logging -from functools import wraps -from typing import Dict -from app.core import db -from app.database.controller.add import competition -from app.database.models import Slide from decorator import decorator from flask.globals import request from flask_jwt_extended import verify_jwt_in_request @@ -27,24 +21,40 @@ logger.addHandler(stream_handler) sio = SocketIO(cors_allowed_origins="http://localhost:3000") -presentations = {} +active_competitions = {} def _unpack_claims(): + """ + :return: A tuple containing competition_id and view, gotten from claim + :rtype: tuple + """ + claims = get_jwt_claims() return claims["competition_id"], claims["view"] -def _is_active_competition(competition_id): - return competition_id in presentations +def is_active_competition(competition_id): + """ + :return: True if competition with competition_id is currently active else False + :rtype: bool + """ + return competition_id in active_competitions -def _get_sync_variables(presentation): - return {key: value for key, value in presentation.items() if key != "client_count"} +def _get_sync_variables(active_competition): + return {key: value for key, value in active_competition.items() if key != "client_count"} @decorator def authorize_client(f, allowed_views=None, require_active_competition=True, *args, **kwargs): + """ + Decorator used to authorize a client that sends socket events. Check that + the client has authorization headers, that client view gotten from claims + is in allowed_views and that the competition the clients is in is active + if require_active_competition is True. + """ + try: verify_jwt_in_request() except: @@ -56,7 +66,7 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar competition_id, view = _unpack_claims() - if require_active_competition and not _is_active_competition(competition_id): + if require_active_competition and not is_active_competition(competition_id): logger.error(f"Won't call function '{f.__name__}': Competition '{competition_id}' is not active") return @@ -71,68 +81,76 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar @sio.event @authorize_client(require_active_competition=False, allowed_views=["*"]) def connect() -> None: + """ + Connect to a active competition. If competition with competition_id is not active, + start it if client is an operator, otherwise ignore it. + """ competition_id, view = _unpack_claims() - if _is_active_competition(competition_id): - presentation = presentations[competition_id] - presentation["client_count"] += 1 + if is_active_competition(competition_id): + active_competition = active_competitions[competition_id] + active_competition["client_count"] += 1 join_room(competition_id) - emit("sync", _get_sync_variables(presentation)) + emit("sync", _get_sync_variables(active_competition)) logger.info(f"Client '{request.sid}' with view '{view}' joined competition '{competition_id}'") elif view == "Operator": join_room(competition_id) - presentations[competition_id] = { + active_competitions[competition_id] = { "client_count": 1, "slide_order": None, "timer": None, } - logger.info(f"Client '{request.sid}' started competition '{competition_id}'") + logger.info(f"Client '{request.sid}' with view '{view}' started competition '{competition_id}'") + else: + logger.error( + f"Client '{request.sid}' with view '{view}' tried to join non active competition '{competition_id}'" + ) @sio.event @authorize_client(allowed_views=["*"]) def disconnect() -> None: """ - Remove client from the presentation it was in. Delete presentation if no + Remove client from the active_competition it was in. Delete active_competition if no clients are connected to it. """ competition_id, _ = _unpack_claims() - presentations[competition_id]["client_count"] -= 1 + active_competitions[competition_id]["client_count"] -= 1 logger.info(f"Client '{request.sid}' disconnected from competition '{competition_id}'") - if presentations[competition_id]["client_count"] <= 0: - del presentations[competition_id] - logger.info(f"No people left in presentation '{competition_id}', ended presentation") + if active_competitions[competition_id]["client_count"] <= 0: + del active_competitions[competition_id] + logger.info(f"No people left in active_competition '{competition_id}', ended active_competition") @sio.event @authorize_client(allowed_views=["Operator"]) -def end_presentation() -> None: +def end_active_competition() -> None: """ - End a presentation by sending end_presentation to all connected clients. + End a active_competition by sending end_active_competition to all connected clients. """ competition_id, _ = _unpack_claims() - emit("end_presentation", room=competition_id, include_self=True) + emit("end_active_competition", room=competition_id, include_self=True) @sio.event @authorize_client(allowed_views=["Operator"]) -def sync(data: Dict) -> None: +def sync(data) -> None: """ - Sync presentation for all clients connected to competition. + Sync active_competition for all clients connected to competition. """ competition_id, _ = _unpack_claims() - presentation = presentations[competition_id] + active_competition = active_competitions[competition_id] for key, value in data.items(): - if key not in presentation: + if key not in active_competition: logger.warning(f"Invalid sync data: '{key}':'{value}'") if value is not None: - presentation[key] = value + active_competition[key] = value - emit("sync", _get_sync_variables(presentation), room=competition_id, include_self=True) + emit("sync", _get_sync_variables(active_competition), room=competition_id, include_self=True) -- GitLab From a0df047b0911edcbb90cf5e0c5e7867e8094a48d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 12 May 2021 20:45:17 +0200 Subject: [PATCH 09/15] Update error message --- server/app/core/sockets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index 341a269f..fd1b2fed 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -72,7 +72,7 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar allowed_views = allowed_views or [] if not _is_allowed(allowed_views, view): - logger.error(f"Won't call function '{f.__name__}': View '{view}' is not in '{''.join(allowed_views)}'") + logger.error(f"Won't call function '{f.__name__}': View '{view}' is not {' or '.join(allowed_views)}") return return f(*args, **kwargs) -- GitLab From bc9375235bff207766dfaa9bc384bea6c091ef8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 12 May 2021 20:48:17 +0200 Subject: [PATCH 10/15] Default to first slide on server --- server/app/core/sockets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index fd1b2fed..fdb1a7f2 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -98,7 +98,7 @@ def connect() -> None: join_room(competition_id) active_competitions[competition_id] = { "client_count": 1, - "slide_order": None, + "slide_order": 0, "timer": None, } logger.info(f"Client '{request.sid}' with view '{view}' started competition '{competition_id}'") -- GitLab From 997eb6ae636cb7509876bea5f75ecdb176e72262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 12 May 2021 21:09:44 +0200 Subject: [PATCH 11/15] Fix bug with wrong event name --- server/app/core/sockets.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index fdb1a7f2..4c3c4a4c 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -127,13 +127,13 @@ def disconnect() -> None: @sio.event @authorize_client(allowed_views=["Operator"]) -def end_active_competition() -> None: +def end_presentation() -> None: """ - End a active_competition by sending end_active_competition to all connected clients. + End a active_competition by sending end_presentation to all connected clients. """ competition_id, _ = _unpack_claims() - emit("end_active_competition", room=competition_id, include_self=True) + emit("end_presentation", room=competition_id, include_self=True) @sio.event @@ -143,7 +143,7 @@ def sync(data) -> None: Sync active_competition for all clients connected to competition. """ - competition_id, _ = _unpack_claims() + competition_id, view = _unpack_claims() active_competition = active_competitions[competition_id] for key, value in data.items(): @@ -154,3 +154,6 @@ def sync(data) -> None: active_competition[key] = value emit("sync", _get_sync_variables(active_competition), room=competition_id, include_self=True) + logger.info( + f"Client '{request.sid}' with view '{view}' synced values {_get_sync_variables(active_competition)} in competition'{competition_id}'" + ) -- GitLab From 99e44586cd9248bba5085b9d8bcba202d301c465 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Fri, 14 May 2021 08:59:14 +0200 Subject: [PATCH 12/15] pull dev --- client/src/reducers/presentationReducer.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts index e0368f7f..f62bf2e9 100644 --- a/client/src/reducers/presentationReducer.test.ts +++ b/client/src/reducers/presentationReducer.test.ts @@ -13,10 +13,7 @@ const initialState = { }, activeSlideId: -1, code: '', - timer: { - enabled: false, - value: 0, - }, + timer: null, } it('should return the initial state', () => { -- GitLab From a840b23b1d19a403580c10141da400579dcb3c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Mon, 17 May 2021 14:29:29 +0200 Subject: [PATCH 13/15] Fix timer and refactor sockets on frontend --- client/src/actions/presentation.ts | 4 +- client/src/interfaces/Timer.ts | 4 +- client/src/pages/views/JudgeViewPage.tsx | 18 +++--- client/src/pages/views/OperatorViewPage.tsx | 42 +++++++------ client/src/pages/views/components/Timer.tsx | 67 ++++++++++++++++----- client/src/reducers/presentationReducer.ts | 10 ++- client/src/sockets.ts | 41 +++---------- server/app/core/sockets.py | 18 +++--- 8 files changed, 113 insertions(+), 91 deletions(-) diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts index 89b9e596..0faa64a2 100644 --- a/client/src/actions/presentation.ts +++ b/client/src/actions/presentation.ts @@ -3,6 +3,7 @@ This file handles actions for the presentation redux state */ import axios from 'axios' +import { TimerState } from '../interfaces/Timer' import { AppDispatch, RootState } from './../store' import Types from './types' @@ -34,7 +35,8 @@ export const setCurrentSlideByOrder = (order: number) => (dispatch: AppDispatch, export const setPresentationCode = (code: string) => (dispatch: AppDispatch) => { dispatch({ type: Types.SET_PRESENTATION_CODE, payload: code }) } + /** Set timer to input value */ -export const setPresentationTimer = (timer: number | null) => (dispatch: AppDispatch) => { +export const setPresentationTimer = (timer: TimerState) => (dispatch: AppDispatch) => { dispatch({ type: Types.SET_PRESENTATION_TIMER, payload: timer }) } diff --git a/client/src/interfaces/Timer.ts b/client/src/interfaces/Timer.ts index 49d1909e..03704c83 100644 --- a/client/src/interfaces/Timer.ts +++ b/client/src/interfaces/Timer.ts @@ -1,4 +1,4 @@ -export interface Timer { +export interface TimerState { + value: number | null enabled: boolean - value: number } diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index c07562b5..675da803 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -11,7 +11,6 @@ import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { SlideListItem } from '../presentationEditor/styled' import JudgeScoreDisplay from './components/JudgeScoreDisplay' import JudgeScoringInstructions from './components/JudgeScoringInstructions' -import Timer from './components/Timer' import { Content, InnerContent, @@ -75,18 +74,17 @@ const JudgeViewPage: React.FC = () => { dispatch(getPresentationCompetition(competitionId.toString())) } }, [operatorActiveSlideId]) - useEffect(() => { - // Every second tic of the timer, load new answers - // TODO: use a set interval that updates every second ( look in Timer.tsx in clien/src/pages/views/components ) - // Then clear interval when timer - Date.now() is negative - if (timer !== null && timer - (Date.now() % 2) === 0 && competitionId) { - dispatch(getPresentationCompetition(competitionId.toString())) - } - }, [timer]) + // useEffect(() => { + // // Every second tic of the timer, load new answers + // // TODO: use a set interval that updates every second ( look in Timer.tsx in clien/src/pages/views/components ) + // // Then clear interval when timer - Date.now() is negative + // if (timer !== null && timer - (Date.now() % 2) === 0 && competitionId) { + // dispatch(getPresentationCompetition(competitionId.toString())) + // } + // }, [timer]) return ( <div style={{ height: '100%' }}> <JudgeAppBar position="fixed"> - <Timer /> <JudgeToolbar> <JudgeQuestionsLabel variant="h5">Frågor</JudgeQuestionsLabel> {operatorActiveSlideOrder !== undefined && ( diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx index 9a918cf2..6193b4ce 100644 --- a/client/src/pages/views/OperatorViewPage.tsx +++ b/client/src/pages/views/OperatorViewPage.tsx @@ -31,13 +31,7 @@ import React, { useEffect, useState } from 'react' import { useHistory } from 'react-router-dom' import { useAppSelector } from '../../hooks' import { RichTeam } from '../../interfaces/ApiRichModels' -import { - socketConnect, - socketEndPresentation, - socketSetSlideNext, - socketSetSlidePrev, - socketSyncTimer, -} from '../../sockets' +import { socketConnect, socketEndPresentation, socketSync } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { Center } from '../presentationEditor/components/styled' import Timer from './components/Timer' @@ -60,11 +54,6 @@ import { * * =========================================== * TODO: - * - Instead of copying code for others to join the competition, copy URL. - * - * - * - Fix scoreboard - * * - When two userers are connected to the same Localhost:5000 and updates/starts/end competition it * creates a bug where the competition can't be started. * =========================================== @@ -120,6 +109,8 @@ const OperatorViewPage: React.FC = () => { const slideTimer = useAppSelector((state) => activeSlideOrder !== undefined ? state.presentation.competition.slides[activeSlideOrder].timer : null ) + const isFirstSlide = activeSlideOrder === 0 + const isLastSlide = useAppSelector((state) => activeSlideOrder === state.presentation.competition.slides.length - 1) useEffect(() => { socketConnect('Operator') }, []) @@ -203,10 +194,19 @@ const OperatorViewPage: React.FC = () => { const handleStartTimer = () => { if (!slideTimer) return - // has active timer already, so cancel it - if (timer && timer - Date.now() > 0) socketSyncTimer(Date.now()) - // Either has expired timer or no timer so start one - else socketSyncTimer(Date.now() + 1000 * slideTimer) + + if (!timer.enabled) socketSync({ timer: { value: Date.now() + 1000 * slideTimer, enabled: true } }) + else socketSync({ timer: { ...timer, enabled: false } }) + } + + const handleSetNextSlide = () => { + if (activeSlideOrder !== undefined) + socketSync({ slide_order: activeSlideOrder + 1, timer: { value: null, enabled: false } }) + } + + const handleSetPrevSlide = () => { + if (activeSlideOrder !== undefined) + socketSync({ slide_order: activeSlideOrder - 1, timer: { value: null, enabled: false } }) } return ( @@ -302,14 +302,18 @@ const OperatorViewPage: React.FC = () => { <OperatorFooter> <ToolBarContainer> <Tooltip title="Föregående" arrow> - <OperatorButton onClick={socketSetSlidePrev} variant="contained"> + <OperatorButton onClick={handleSetPrevSlide} variant="contained" disabled={isFirstSlide}> <ChevronLeftIcon fontSize="large" /> </OperatorButton> </Tooltip> {slideTimer && ( <Tooltip title="Starta Timer" arrow> - <OperatorButton onClick={() => handleStartTimer()} variant="contained"> + <OperatorButton + onClick={handleStartTimer} + variant="contained" + disabled={timer.value !== null && !timer.enabled} + > <TimerIcon fontSize="large" /> <Timer disableText /> </OperatorButton> @@ -329,7 +333,7 @@ const OperatorViewPage: React.FC = () => { </Tooltip> <Tooltip title="Nästa" arrow> - <OperatorButton onClick={socketSetSlideNext} variant="contained"> + <OperatorButton onClick={handleSetNextSlide} variant="contained" disabled={isLastSlide}> <ChevronRightIcon fontSize="large" /> </OperatorButton> </Tooltip> diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx index 2b12af43..620fcd34 100644 --- a/client/src/pages/views/components/Timer.tsx +++ b/client/src/pages/views/components/Timer.tsx @@ -1,34 +1,69 @@ import React, { useEffect, useState } from 'react' -import { useAppSelector } from '../../../hooks' - -let timerIntervalId: NodeJS.Timeout +import { setPresentationTimer } from '../../../actions/presentation' +import { useAppDispatch, useAppSelector } from '../../../hooks' type TimerProps = { disableText?: boolean } const Timer = ({ disableText }: TimerProps) => { + const dispatch = useAppDispatch() const timer = useAppSelector((state) => state.presentation.timer) const [remainingTimer, setRemainingTimer] = useState<number>(0) + const [timerIntervalId, setTimerIntervalId] = useState<NodeJS.Timeout | null>(null) const slideTimer = useAppSelector( (state) => state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId)?.timer ) + useEffect(() => { - if (timer !== null && timer - Date.now() > 0) { - setRemainingTimer(timer - Date.now()) - timerIntervalId = setInterval(() => { - setRemainingTimer(timer - Date.now()) - }, 500) - } else { - clearInterval(timerIntervalId) + if (slideTimer) setRemainingTimer(slideTimer) + }, [slideTimer]) + + useEffect(() => { + console.log(timer) + if (!timer.enabled) { + console.log('interval id: ', timerIntervalId) + console.log('slide timer: ', slideTimer) + + if (timerIntervalId !== null) clearInterval(timerIntervalId) + + console.log('timer enabled false') + console.log('timer: ', timer) + + if (timer.value !== null) { + console.log('timer value not null') + + setRemainingTimer(0) + } else if (slideTimer) { + console.log('timer value null and slideTimer has value') + + setRemainingTimer(slideTimer * 1000) + } + + return } - }, [timer]) - return ( - <div>{`${!disableText ? 'Tid kvar:' : ''} ${ - timer !== null ? (timer - Date.now() > 0 ? Math.round(remainingTimer / 1000) : 0) : slideTimer - }`}</div> - ) + + setTimerIntervalId( + setInterval(() => { + console.log('interval tick') + if (timer.value === null) return + if (timer.enabled === false && timerIntervalId !== null) clearInterval(timerIntervalId) + + if (timer.value - Date.now() < 0) { + console.log('timer reached zero') + setRemainingTimer(0) + dispatch(setPresentationTimer({ ...timer, enabled: false })) + if (timerIntervalId !== null) clearInterval(timerIntervalId) + return + } + + setRemainingTimer(timer.value - Date.now()) + }, 500) + ) + }, [timer.enabled, slideTimer]) + + return <div>{`${!disableText ? 'Tid kvar:' : ''} ${Math.round(remainingTimer / 1000)}`}</div> } export default Timer diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts index cb74c1ba..69c7817f 100644 --- a/client/src/reducers/presentationReducer.ts +++ b/client/src/reducers/presentationReducer.ts @@ -1,5 +1,6 @@ import { AnyAction } from 'redux' import Types from '../actions/types' +import { TimerState } from '../interfaces/Timer' import { RichCompetition } from './../interfaces/ApiRichModels' /** Define a type for the presentation state */ @@ -7,7 +8,7 @@ interface PresentationState { competition: RichCompetition activeSlideId: number code: string - timer: number | null + timer: TimerState } /** Define the initial values for the presentation state */ @@ -23,7 +24,10 @@ const initialState: PresentationState = { }, activeSlideId: -1, code: '', - timer: null, + timer: { + value: null, + enabled: false, + }, } /** Intercept actions for presentation state and update the state */ @@ -47,7 +51,7 @@ export default function (state = initialState, action: AnyAction) { case Types.SET_PRESENTATION_TIMER: return { ...state, - timer: action.payload as number, + timer: action.payload as TimerState, } default: return state diff --git a/client/src/sockets.ts b/client/src/sockets.ts index a5f3ac42..35c9abc6 100644 --- a/client/src/sockets.ts +++ b/client/src/sockets.ts @@ -1,10 +1,11 @@ import io from 'socket.io-client' import { setCurrentSlideByOrder, setPresentationTimer } from './actions/presentation' +import { TimerState } from './interfaces/Timer' import store from './store' interface SyncInterface { - slide_order: number - timer: number | null + slide_order?: number + timer?: TimerState } let socket: SocketIOClient.Socket @@ -24,8 +25,9 @@ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') }) socket.on('sync', (data: SyncInterface) => { - setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState) - setPresentationTimer(data.timer)(store.dispatch) + // The order of these is important, for some reason + if (data.timer !== undefined) setPresentationTimer(data.timer)(store.dispatch) + if (data.slide_order !== undefined) setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState) }) socket.on('end_presentation', () => { @@ -37,34 +39,9 @@ export const socketEndPresentation = () => { socket.emit('end_presentation') } -export const socketSetSlideNext = () => { - const activeSlide = store - .getState() - .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId) - if (!activeSlide) return - socketSyncSlide(activeSlide.order + 1) -} - -export const socketSetSlidePrev = () => { - const activeSlide = store - .getState() - .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId) - if (!activeSlide) return - socketSyncSlide(activeSlide.order - 1) -} - -export const socketSyncSlide = (slide_order: number) => { - if (slide_order < 0 || store.getState().presentation.competition.slides.length <= slide_order) { - return - } - - socket.emit('sync', { - slide_order: slide_order, - }) -} - -export const socketSyncTimer = (timer: number | null) => { +export const socketSync = ({ slide_order, timer }: SyncInterface) => { socket.emit('sync', { - timer: timer, + slide_order, + timer, }) } diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index 4c3c4a4c..be9873c7 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -42,8 +42,8 @@ def is_active_competition(competition_id): return competition_id in active_competitions -def _get_sync_variables(active_competition): - return {key: value for key, value in active_competition.items() if key != "client_count"} +def _get_sync_variables(active_competition, sync_values): + return {key: value for key, value in active_competition.items() if key in sync_values} @decorator @@ -92,14 +92,17 @@ def connect() -> None: active_competition = active_competitions[competition_id] active_competition["client_count"] += 1 join_room(competition_id) - emit("sync", _get_sync_variables(active_competition)) + emit("sync", _get_sync_variables(active_competition, ["slide_order", "timer"])) logger.info(f"Client '{request.sid}' with view '{view}' joined competition '{competition_id}'") elif view == "Operator": join_room(competition_id) active_competitions[competition_id] = { "client_count": 1, "slide_order": 0, - "timer": None, + "timer": { + "value": None, + "enabled": False, + }, } logger.info(f"Client '{request.sid}' with view '{view}' started competition '{competition_id}'") else: @@ -150,10 +153,9 @@ def sync(data) -> None: if key not in active_competition: logger.warning(f"Invalid sync data: '{key}':'{value}'") - if value is not None: - active_competition[key] = value + active_competition[key] = value - emit("sync", _get_sync_variables(active_competition), room=competition_id, include_self=True) + emit("sync", _get_sync_variables(active_competition, data), room=competition_id, include_self=True) logger.info( - f"Client '{request.sid}' with view '{view}' synced values {_get_sync_variables(active_competition)} in competition'{competition_id}'" + f"Client '{request.sid}' with view '{view}' synced values {_get_sync_variables(active_competition, data)} in competition '{competition_id}'" ) -- GitLab From de60926ba424ef5664803e6957602201db05be9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Mon, 17 May 2021 14:32:05 +0200 Subject: [PATCH 14/15] Fix backend tests --- server/tests/test_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 880a41bd..4b704bd7 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -447,7 +447,7 @@ def test_authorization(client): add_default_values() # Fake that competition 1 is active - sockets.presentations[1] = {} + sockets.active_competitions[1] = {} #### TEAM #### # Login in with team code -- GitLab From b1ca087cfa465b4d7055843c2d6e2457074c8f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Mon, 17 May 2021 14:34:55 +0200 Subject: [PATCH 15/15] Fix frontend tests --- client/src/reducers/presentationReducer.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts index f62bf2e9..10362e68 100644 --- a/client/src/reducers/presentationReducer.test.ts +++ b/client/src/reducers/presentationReducer.test.ts @@ -13,7 +13,10 @@ const initialState = { }, activeSlideId: -1, code: '', - timer: null, + timer: { + value: null, + enabled: false, + }, } it('should return the initial state', () => { -- GitLab