From 28c20166644cffa6b178f75a487ebd5c9ef0e2d8 Mon Sep 17 00:00:00 2001 From: Richard Fox Date: Mon, 15 Oct 2018 18:41:46 -0700 Subject: [PATCH] wip intercepting --- src/ControlState.ts | 89 +++++++++++++++++++++++ src/intercept.ts | 26 +++++++ src/interfaces.ts | 24 +++++++ src/participant.ts | 170 ++++++++++++-------------------------------- 4 files changed, 184 insertions(+), 125 deletions(-) create mode 100644 src/ControlState.ts create mode 100644 src/intercept.ts create mode 100644 src/interfaces.ts diff --git a/src/ControlState.ts b/src/ControlState.ts new file mode 100644 index 0000000..187dacb --- /dev/null +++ b/src/ControlState.ts @@ -0,0 +1,89 @@ +import { EventEmitter } from 'eventemitter3'; +import { IGroup, IIncomingPacket, IScene } from './interfaces'; + +/** + * Internally retains data required to acquire a control's kind. + * @private + */ +export class ControlsState extends EventEmitter { + private scenes: { + [sceneID: string]: { [controlID: string]: { kind: string; cost: number } }; + } = {}; + private groups: { [groupID: string]: string } = {}; + private currentGroup = 'default'; + + /** + * Handles a packet sent from the client. + */ + public handleIncomingPacket({ type, method, params }: IIncomingPacket) { + if (type !== 'method') { + return; + } + + if (method === 'onControlCreate' || method === 'onControlUpdate') { + this.cacheScene(params, true); + } + + if (method === 'onSceneCreate') { + params.scenes.forEach((scene: IScene) => { + this.cacheScene(scene); + }); + } + + if (method === 'onSceneDelete') { + delete this.scenes[params.sceneID]; + } + + if (method === 'onGroupCreate' || method === 'onGroupUpdate') { + params.groups.forEach((group: IGroup) => { + this.cacheGroup(group); + }); + } + + if (method === 'onGroupDelete') { + delete this.groups[params.groupID]; + } + + if (method === 'onParticipantJoin' || method === 'onParticipantUpdate') { + this.currentGroup = params.participants[0].groupID; + } + } + + /** + * Gets a control's kind by its control ID. + */ + public getControlKind(controlID: string) { + return this.scenes[this.groups[this.currentGroup]][controlID].kind; + } + + public getControlCost(controlID: string) { + return this.scenes[this.groups[this.currentGroup]][controlID].cost; + } + + /** + * Caches the control kind for a scene. + */ + private cacheScene(scene: IScene, isPartial = false) { + if (!this.scenes[scene.sceneID] || !isPartial) { + this.scenes[scene.sceneID] = {}; + } + + if (!scene.controls) { + return; + } + + scene.controls.forEach(control => { + this.scenes[scene.sceneID][control.controlID] = { + kind: control.kind, + cost: control.cost, + }; + }); + } + + /** + * Caches a group. + */ + private cacheGroup(group: IGroup) { + this.groups[group.groupID] = group.sceneID; + } +} diff --git a/src/intercept.ts b/src/intercept.ts new file mode 100644 index 0000000..5726a3a --- /dev/null +++ b/src/intercept.ts @@ -0,0 +1,26 @@ +export type InterceptorFn = (params: any) => Promise; + +/** + * Interceptor Manager allows for the storage of method names to interceptor functions. + * + * It is used to gate a method from being transmitted to the Interactive server. + * + * @example interceptor.intercept(params => if(params.potato) { return Promise.resolve(false); }) + */ +export class InterceptorManager { + public methods: Map = new Map(); + + public add(method: string, interceptor: InterceptorFn) { + this.methods.set(method, interceptor); + } + public has(method: string) { + return this.methods.has(method); + } + + public run(method: string, params: any): Promise { + if (this.has(method)) { + return this.methods.get(method)!(params); + } + return Promise.resolve(true); + } +} diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..7be4a47 --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,24 @@ +export interface IScene { + sceneID: string; + controls: IControl[] | null; +} + +export interface IControl { + controlID: string; + kind: string; + cost: number; +} + +export interface IGroup { + groupID: string; + sceneID: string; +} + +/** + * Represents raw data received from the interactive server. + */ +export interface IIncomingPacket { + type: string; + method: string; + params: any; +} diff --git a/src/participant.ts b/src/participant.ts index ddd487f..299725d 100644 --- a/src/participant.ts +++ b/src/participant.ts @@ -1,6 +1,9 @@ import { EventEmitter } from 'eventemitter3'; import { stringify } from 'querystring'; +import { ControlsState } from './ControlState'; +import { InterceptorManager } from './intercept'; +import { IIncomingPacket } from './interfaces'; import { IPostable, RPC, RPCError } from './rpc'; import { ErrorCode, ILogEntry, ISettings, IStateDump, IVideoPositionOptions } from './typings'; @@ -77,108 +80,6 @@ function appendQueryString(url: string, qs: object) { return `${url}${delimiter}${stringify(qs)}`; } -/** - * Represents raw data received from the interactive server. - */ -interface IIncomingPacket { - type: string; - method: string; - params: any; -} - -interface IScene { - sceneID: string; - controls: IControl[] | null; -} - -interface IControl { - controlID: string; - kind: string; -} - -interface IGroup { - groupID: string; - sceneID: string; -} - -/** - * Internally retains data required to acquire a control's kind. - * @private - */ -class ControlsState extends EventEmitter { - private scenes: { [sceneID: string]: { [controlID: string]: string } } = {}; - private groups: { [groupID: string]: string } = {}; - private currentGroup = 'default'; - - /** - * Handles a packet sent from the client. - */ - public handleIncomingPacket({ type, method, params }: IIncomingPacket) { - if (type !== 'method') { - return; - } - - if (method === 'onControlCreate' || method === 'onControlUpdate') { - this.cacheScene(params, true); - } - - if (method === 'onSceneCreate') { - params.scenes.forEach((scene: IScene) => { - this.cacheScene(scene); - }); - } - - if (method === 'onSceneDelete') { - delete this.scenes[params.sceneID]; - } - - if (method === 'onGroupCreate' || method === 'onGroupUpdate') { - params.groups.forEach((group: IGroup) => { - this.cacheGroup(group); - }); - } - - if (method === 'onGroupDelete') { - delete this.groups[params.groupID]; - } - - if (method === 'onParticipantJoin' || method === 'onParticipantUpdate') { - this.currentGroup = params.participants[0].groupID; - } - } - - /** - * Gets a control's kind by its control ID. - */ - public getControlKind(controlID: string) { - return this.scenes[this.groups[this.currentGroup]][controlID]; - } - - /** - * Caches the control kind for a scene. - */ - private cacheScene(scene: IScene, isPartial = false) { - if (!this.scenes[scene.sceneID] || !isPartial) { - this.scenes[scene.sceneID] = {}; - } - - if (!scene.controls) { - return; - } - - scene.controls.forEach(control => { - this.scenes[scene.sceneID][control.controlID] = control.kind; - }); - } - - /** - * Caches a group. - */ - private cacheGroup(group: IGroup) { - this.groups[group.groupID] = group.sceneID; - } -} - /** * Participant is a bridge between the Interactive service and an iframe that * shows custom controls. It proxies calls between them and emits events @@ -191,8 +92,10 @@ export class Participant extends EventEmitter { */ public static readonly protocolVersion = '2.0'; + public interceptor = new InterceptorManager(); + /** - * Websocket connecte + * Websocket connected */ private websocket?: WebSocket; @@ -420,8 +323,13 @@ export class Participant extends EventEmitter { public on(event: string, handler: (...args: any[]) => void): this; public on(event: string, handler: (...args: any[]) => void): this { this.exposeRPC(event, (...params: any[]) => { - params.splice(0, 0, event); - this.emit.apply(this, params); + this.interceptor.run(event, params).then(res => { + if (!res) { + return; + } + params.splice(0, 0, event); + this.emit.apply(this, params); + }); }); super.on(event, handler); return this; @@ -438,6 +346,13 @@ export class Participant extends EventEmitter { } } + public getControlKind(controlId: string) { + return this.controls.getControlKind(controlId); + } + public getControlCost(controlId: string) { + return this.controls.getControlCost(controlId); + } + /** * sendInteractive broadcasts the interactive payload down to the controls, * and emits a `transmit` event. @@ -457,6 +372,17 @@ export class Participant extends EventEmitter { }); } + private giveInputHandler(data: { method: string; params: any }) { + const kind = this.controls.getControlKind(data.params.controlID); + if (!kind) { + return; + } + + this.emit('input', { + ...data.params, + kind, + }); + } /** * attachListeners is called once the frame contents load to boot up * the RPC system. @@ -472,26 +398,20 @@ export class Participant extends EventEmitter { ); this.exposeRPC<{ method: string; params: any }>('sendInteractivePacket', data => { - this.websocket!.send( - JSON.stringify({ - ...data, - type: 'method', - discard: true, - }), - ); - - if (data.method !== 'giveInput') { - return; - } - - const kind = this.controls.getControlKind(data.params.controlID); - if (!kind) { - return; - } - - this.emit('input', { - ...data.params, - kind, + this.interceptor.run(data.method, data.params).then(res => { + if (!res) { + return; + } + this.websocket!.send( + JSON.stringify({ + ...data, + type: 'method', + discard: true, + }), + ); + if (data.method === 'giveInput') { + this.giveInputHandler(data); + } }); });