import { AnyAction , Store } from 'redux' ; import { toast } from 'react-toastify' ; import { CommsNode } from './commsNode' ; import { ScenarioReducerActionType , setScenarioLocalAction , updateHeadActionIdsAction } from '../redux/scenarioReducer' ; import { promiseSleep } from './promiseSleep' ; import { addPendingActionAction , discardPendingActionAction , setLastCommonScenarioAction , TabletopValidationType } from '../redux/tabletopValidationReducer' ; import { addConnectedUserAction , challengeResponseAction , challengeUserAction , ConnectedUserActionTypes , ConnectedUserReducerAction , verifyConnectionAction , verifyGMAction } from '../redux/connectedUserReducer' ; import { getConnectedUsersFromStore , getDeviceLayoutFromStore , getLoggedInUserFromStore , getScenarioFromStore , getTabletopFromStore , getTabletopValidationFromStore , ReduxStoreType } from '../redux/mainReducer' ; import { isScenarioAction , NetworkedAction , NetworkedMeta } from './types' ; import { getNetworkHubId , isUserAllowedOnTabletop , scenarioToJson } from './scenarioUtils' ; import { enc , HmacSHA256 } from 'crypto-js' ; import { setTabletopIdAction } from '../redux/locationReducer' ; export enum MessageTypeEnum { CHECK_ACTIONS_MESSAGE = 'check-actions' , MISSING_ACTION_MESSAGE = 'missing-action' , RESEND_ACTIONS_MESSAGE = 'resend-actions' , CLOSE_MESSAGE = 'close' } interface CheckActionsMessageType { message : MessageTypeEnum . CHECK_ACTIONS_MESSAGE ; headActionIds : string [ ] ; } export function checkActionsMessage ( headActionIds ) { return { message : MessageTypeEnum . CHECK_ACTIONS_MESSAGE , headActionIds } ; } interface MissingActionMessageType { message : MessageTypeEnum . MISSING_ACTION_MESSAGE ; actionId ? : string ; missingActionIds : string [ ] ; knownActionIds : string [ ] ; } export function missingActionMessage ( missingActionIds , knownActionIds , actionId ? ) { return { message : MessageTypeEnum . MISSING_ACTION_MESSAGE , missingActionIds , knownActionIds , actionId } ; } type AncestorActions = { [ actionId ] : AnyAction ; } | null ; interface ResendActionsMessage { message : MessageTypeEnum . RESEND_ACTIONS_MESSAGE ; pendingActionId ? : string ; missingActionIds : string [ ] ; actions : AncestorActions ; } export function resendActionsMessage ( missingActionIds , actions , pendingActionId ? ) { return { message : MessageTypeEnum . RESEND_ACTIONS_MESSAGE , missingActionIds , actions , pendingActionId } ; } interface CloseMessage { message : MessageTypeEnum . CLOSE_MESSAGE ; reason : string ; } export function closeMessage ( reason ) { return { message : MessageTypeEnum . CLOSE_MESSAGE , reason } ; } type MessageType = CheckActionsMessageType | MissingActionMessageType | ResendActionsMessage | CloseMessage ; function findAncestorActions ( validation , knownActionIds , actionIds , result = { } ) { return actionIds . reduce ( ( all , actionId ) => { if ( all && ! knownActionIds [ actionId ] && ! all [ actionId ] ) { const action = validation . actionHistory [ actionId ] ; if ( ! action ) { return null ; } all [ actionId ] = action ; if ( isScenarioAction ( action ) ) { return findAncestorActions ( validation , knownActionIds , action . headActionIds , result ) ; } } return all ; } , result ) ; } function postOrderActions ( root , actions ) { if ( root . headActionIds ) { return [ ... root . headActionIds . reduce ( ( all , actionId ) => { const action = actions [ actionId ] ; if ( action ) { delete ( actions [ actionId ] ) ; all . push ( ... postOrderActions ( action , actions ) ) ; } return all ; } , [ ] ) , root ] ; } else { return [ root ] ; } } async function sendScenarioState ( state , commsNode , peerId ) { const [ fullScenario , playerScenario ] = scenarioToJson ( getScenarioFromStore ( state ) ) ; const peerUser = getConnectedUsersFromStore ( state ) . users [ peerId ] ; const scenario = peerUser . verifiedGM ? fullScenario : playerScenario ; await commsNode . sendTo ( { ... setScenarioLocalAction ( scenario ) , gmOnly : peerUser . verifiedGM } , { only : [ peerId ] } ) ; } async function receiveMessageFromPeer ( store , commsNode , peerId , message ) { const state = store . getState ( ) ; let validation = getTabletopValidationFromStore ( state ) ; const loggedInUser = getLoggedInUserFromStore ( state ) ! ; switch ( message . message ) { case MessageTypeEnum . CHECK_ACTIONS_MESSAGE : const missingActionIds = findMissingActionIds ( validation , message . headActionIds ) ; if ( missingActionIds . length > 0 ) { const knownActionIds = Object . keys ( validation . actionHistory ) . concat ( Object . keys ( validation . initialActionIds ) ) ; await commsNode . sendTo ( missingActionMessage ( missingActionIds , knownActionIds ) , { only : [ peerId ] } ) ; } break ; case MessageTypeEnum . MISSING_ACTION_MESSAGE : const knownActionIds = message . knownActionIds . reduce ( ( all , actionId ) => { all [ actionId ] = true ; return all ; } , { } ) ; const resendActions = findAncestorActions ( validation , knownActionIds , message . missingActionIds ) ; if ( resendActions === null && loggedInUser . emailAddress === getTabletopFromStore ( state ) . gm ) { await sendScenarioState ( state , commsNode , peerId ) ; } else { await commsNode . sendTo ( resendActionsMessage ( message . missingActionIds , resendActions , message . actionId ) , { only : [ peerId ] } ) ; } break ; case MessageTypeEnum . RESEND_ACTIONS_MESSAGE : if ( message . actions === null ) { await sendScenarioState ( state , commsNode , peerId ) ; if ( message . pendingActionId ) { store . dispatch ( discardPendingActionAction ( message . pendingActionId ) ) ; } return ; } const pendingAction = message . pendingActionId ? validation . pendingActions [ message . pendingActionId ] : undefined ; const dispatchActions = pendingAction ? { ... message . actions , [ pendingAction . actionId ] : pendingAction } : message . actions ; const actionIds = message . pendingActionId ? [ message . pendingActionId ] : message . missingActionIds ; for ( let pendingActionId of actionIds ) { if ( ! dispatchActions [ pendingActionId ] ) { continue ; } const orderedActions = postOrderActions ( dispatchActions [ pendingActionId ] , dispatchActions ) ; for ( let action of orderedActions ) { if ( isScenarioAction ( action ) ) { validation = getTabletopValidationFromStore ( store . getState ( ) ) ; const missingActionIds = findMissingActionIds ( validation , action . headActionIds ) ; if ( missingActionIds . length > 0 ) { console . error ( 'Still have unknown action IDs - this should not happen' ) ; if ( loggedInUser . emailAddress === getTabletopFromStore ( state ) . gm ) { await sendScenarioState ( store . getState ( ) , commsNode , peerId ) ; if ( message . pendingActionId ) { store . dispatch ( discardPendingActionAction ( message . pendingActionId ) ) ; } } else { await commsNode . sendTo ( resendActionsMessage ( [ ] , null ) , { only : [ peerId ] } ) ; } return ; } } await dispatchGoodAction ( store , commsNode , peerId , { ... action , peerKey : undefined , private : true } ) ; } } break ; case MessageTypeEnum . CLOSE_MESSAGE : store . dispatch ( setTabletopIdAction ( ) ) ; if ( message . reason ) { toast ( message . reason ) ; } break ; } } function findMissingActionIds ( validation , headActionIds ) { return headActionIds . filter ( ( actionId ) => ( ! validation . actionHistory [ actionId ] && ! validation . initialActionIds [ actionId ] ) ) ; } export async function handleConnectionActions ( action , fromPeerId , store , commsNode ) { const tabletop = getTabletopFromStore ( store . getState ( ) ) ; switch ( action . type ) { case ConnectedUserActionTypes . ADD_CONNECTED_USER : if ( tabletop . gmSecret && action . user . emailAddress === tabletop . gm && ! getConnectedUsersFromStore ( store . getState ( ) ) . users [ action . peerId ] . verifiedGM ) { const challengeAction = challengeUserAction ( fromPeerId ) ; store . dispatch ( challengeAction ) ; await commsNode . sendTo ( challengeAction , { only : [ fromPeerId ] } ) ; } else { const tabletop = getTabletopFromStore ( store . getState ( ) ) ; const allowed = isUserAllowedOnTabletop ( tabletop . gm , action . user . emailAddress , tabletop . tabletopUserControl ) ; if ( allowed !== null ) { store . dispatch ( verifyConnectionAction ( fromPeerId , allowed ) ) ; } const playerTabletopValidation = getTabletopValidationFromStore ( store . getState ( ) ) ; if ( playerTabletopValidation . lastCommonScenario ) { await commsNode . sendTo ( checkActionsMessage ( playerTabletopValidation . lastCommonScenario . playerHeadActionIds ) , { only : [ fromPeerId ] } ) ; } } break ; case ConnectedUserActionTypes . CHALLENGE_USER : if ( ! action . challenge ) { throw new Error ( 'no challenge in action ' + JSON . stringify ( action ) ) ; } if ( ! tabletop . gmSecret ) { throw new Error ( 'no gmSecret in tabletop ' + JSON . stringify ( tabletop ) ) ; } const challengeHash = HmacSHA256 ( action . challenge , tabletop . gmSecret ) ; const responseAction = challengeResponseAction ( fromPeerId , enc . Base64 . stringify ( challengeHash ) ) ; await commsNode . sendTo ( responseAction , { only : [ fromPeerId ] } ) ; break ; case ConnectedUserActionTypes . CHALLENGE_RESPONSE : const connectedUsers = getConnectedUsersFromStore ( store . getState ( ) ) . users ; const responseHash = HmacSHA256 ( connectedUsers [ fromPeerId ] . challenge , tabletop . gmSecret ) ; if ( action . response === enc . Base64 . stringify ( responseHash ) ) { store . dispatch ( verifyConnectionAction ( fromPeerId , true ) ) ; store . dispatch ( verifyGMAction ( fromPeerId , true ) ) ; const tabletopValidation = getTabletopValidationFromStore ( store . getState ( ) ) ; if ( tabletopValidation . lastCommonScenario ) { await commsNode . sendTo ( checkActionsMessage ( tabletopValidation . lastCommonScenario . headActionIds ) , { only : [ fromPeerId ] } ) ; } } break ; } } async function receiveActionFromPeer ( store , commsNode , peerId , action ) { if ( isScenarioAction ( action ) ) { let validation = getTabletopValidationFromStore ( store . getState ( ) ) ; let missingActionIds = findMissingActionIds ( validation , action . headActionIds ) ; if ( missingActionIds . length > 0 ) { await promiseSleep ( 500 ) ; validation = getTabletopValidationFromStore ( store . getState ( ) ) ; missingActionIds = findMissingActionIds ( validation , action . headActionIds ) ; if ( missingActionIds . length > 0 ) { store . dispatch ( addPendingActionAction ( action ) ) ; const knownActionIds = Object . keys ( validation . actionHistory ) . concat ( Object . keys ( validation . initialActionIds ) ) ; await commsNode . sendTo ( missingActionMessage ( missingActionIds , knownActionIds , action . actionId ) , { only : [ peerId ] } ) ; return ; } } } await dispatchGoodAction ( store , commsNode , peerId , action ) ; } async function dispatchGoodAction ( store , commsNode , peerId , action ) { if ( ! commsNode . options . shouldDispatchLocally || commsNode . options . shouldDispatchLocally ( action , store . getState ( ) , commsNode ) ) { store . dispatch ( action ) ; } if ( isScenarioAction ( action ) ) { store . dispatch ( updateHeadActionIdsAction ( action ) ) ; store . dispatch ( setLastCommonScenarioAction ( getScenarioFromStore ( store . getState ( ) ) , action as ScenarioReducerActionType ) ) ; } await handleConnectionActions ( action , peerId , store , commsNode ) ; const state = store . getState ( ) ; if ( ! action . private && commsNode . peerId === getNetworkHubId ( commsNode . userId , commsNode . peerId , getTabletopFromStore ( state ) . gm , getConnectedUsersFromStore ( state ) . users ) ) { const connectedUsers = getConnectedUsersFromStore ( state ) . users ; const only = Object . keys ( connectedUsers ) . filter ( ( peerId ) => ( peerId !== action . fromPeerId && ( ! action . gmOnly || connectedUsers [ peerId ] . verifiedGM ) && ( action . type !== ConnectedUserActionTypes . ADD_CONNECTED_USER || peerId !== action . peerId ) ) ) ; await commsNode . sendTo ( action , { only } ) ; if ( action . type === ConnectedUserActionTypes . ADD_CONNECTED_USER ) { const deviceLayouts = getDeviceLayoutFromStore ( state ) ; for ( let otherPeerId of Object . keys ( connectedUsers ) ) { if ( otherPeerId !== peerId ) { const user = connectedUsers [ otherPeerId ] ; await commsNode . sendTo ( addConnectedUserAction ( otherPeerId , user . user , user . version ! , user . deviceWidth , user . deviceHeight , deviceLayouts [ otherPeerId ] ) , { only : [ peerId ] } ) ; } } } } } function buildNetworkMetadata ( state , fromPeerId , originPeerId ? ) { const tabletop = getTabletopFromStore ( state ) ; const connectedUsers = getConnectedUsersFromStore ( state ) ; const fromGM = ( connectedUsers [ fromPeerId ] && connectedUsers [ fromPeerId ] . verifiedConnection && connectedUsers [ fromPeerId ] . user . emailAddress === tabletop . gm ) ; return { fromPeerId , originPeerId : originPeerId || fromPeerId , fromGM } ; } export default async function peerMessageHandler ( store , peerNode , peerId , data ) { const rawMessage = JSON . parse ( data ) ; const meta = buildNetworkMetadata ( store . getState ( ) , peerId , rawMessage . meta ?. originPeerId || rawMessage . originPeerId ) ; const message = { ... rawMessage , ... meta , meta : { ... rawMessage . meta , ... meta } } ; if ( message . type ) { await receiveActionFromPeer ( store , peerNode , peerId , message as NetworkedAction ) ; } else { await receiveMessageFromPeer ( store , peerNode , peerId , message as MessageType ) ; } }