From 7a5bc76a9e2154e0532e81482f8f8541be6246ad Mon Sep 17 00:00:00 2001 From: arc-alex Date: Fri, 3 Nov 2023 23:28:18 +0100 Subject: [PATCH] add http integration --- apps/server/src/app.ts | 28 +++-- apps/server/src/models/dataModel.ts | 11 ++ .../integration-service/HttpIntegration.ts | 106 ++++++++++++++++++ apps/server/src/utils/parser.ts | 3 +- apps/server/src/utils/parserFunctions.ts | 71 ++++++++++-- apps/server/test-db/db.json | 11 ++ apps/test-db/db.json | 11 ++ demo-db/db.json | 11 ++ .../types/src/definitions/DataModel.type.ts | 2 + .../src/definitions/core/HttpSettings.type.ts | 7 ++ packages/types/src/index.ts | 1 + 11 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 apps/server/src/services/integration-service/HttpIntegration.ts create mode 100644 packages/types/src/definitions/core/HttpSettings.type.ts diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 812ea0205..74dc8a99d 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -1,4 +1,4 @@ -import { LogOrigin, OSCSettings } from 'ontime-types'; +import { HTTPSettings, LogOrigin, OSCSettings } from 'ontime-types'; import 'dotenv/config'; import express from 'express'; @@ -29,6 +29,7 @@ import { eventLoader } from './classes/event-loader/EventLoader.js'; import { integrationService } from './services/integration-service/IntegrationService.js'; import { logger } from './classes/Logger.js'; import { oscIntegration } from './services/integration-service/OscIntegration.js'; +import { httpIntegration } from './services/integration-service/HttpIntegration.js'; import { populateStyles } from './modules/loadStyles.js'; import { eventStore, getInitialPayload } from './stores/EventStore.js'; import { PlaybackService } from './services/PlaybackService.js'; @@ -161,7 +162,6 @@ export const startServer = async () => { }; /** - * @description starts OSC server * @description starts OSC server * @param overrideConfig * @return {Promise} @@ -190,20 +190,30 @@ export const startOSCServer = async (overrideConfig = null) => { /** * starts integrations */ -export const startIntegrations = async (config?: { osc: OSCSettings }) => { +export const startIntegrations = async (config?: { osc: OSCSettings; http: HTTPSettings }) => { checkStart(OntimeStartOrder.InitIO); - const { osc } = config ?? DataProvider.getData(); + const { osc, http } = config ?? DataProvider.getData(); if (!osc) { return 'OSC Invalid configuration'; + } else { + const { success, message } = oscIntegration.init(osc); + logger.info(LogOrigin.Tx, message); + + if (success) { + integrationService.register(oscIntegration); + } } + if (!http) { + return 'HTTP Invalid configuration'; + } else { + const { success, message } = httpIntegration.init(http); + logger.info(LogOrigin.Tx, message); - const { success, message } = oscIntegration.init(osc); - logger.info(LogOrigin.Tx, message); - - if (success) { - integrationService.register(oscIntegration); + if (success) { + integrationService.register(httpIntegration); + } } }; diff --git a/apps/server/src/models/dataModel.ts b/apps/server/src/models/dataModel.ts index 68fa10c6a..79e4372b0 100644 --- a/apps/server/src/models/dataModel.ts +++ b/apps/server/src/models/dataModel.ts @@ -56,4 +56,15 @@ export const dbModel: DatabaseModel = { onFinish: [], }, }, + http: { + enabledOut: false, + subscriptions: { + onLoad: [], + onStart: [], + onPause: [], + onStop: [], + onUpdate: [], + onFinish: [], + }, + }, }; diff --git a/apps/server/src/services/integration-service/HttpIntegration.ts b/apps/server/src/services/integration-service/HttpIntegration.ts new file mode 100644 index 000000000..7f447cead --- /dev/null +++ b/apps/server/src/services/integration-service/HttpIntegration.ts @@ -0,0 +1,106 @@ +//TODO: cleanup stuff left over from OSC copy +import http from 'node:http'; +import { HTTPSettings, Subscription } from 'ontime-types'; + +import IIntegration, { TimerLifeCycleKey } from './IIntegration.js'; +import { parseTemplateNested } from './integrationUtils.js'; +import { isObject } from '../../utils/varUtils.js'; +import { dbModel } from '../../models/dataModel.js'; +import { validateHttpObject } from '../../utils/parserFunctions.js'; + +type Action = TimerLifeCycleKey | string; + +/** + * @description Class contains logic towards outgoing HTTP communications + * @class + */ +export class HttpIntegration implements IIntegration { + subscriptions: Subscription; + + constructor() { + // this.httpClient = null; + this.subscriptions = dbModel.http.subscriptions; + } + + /** + * Initializes httpClient + */ + init(config: HTTPSettings) { + const { subscriptions } = config; + + this.initSubscriptions(subscriptions); + + try { + // this allows re-calling the init function during runtime + // this.httpClient?.close(); + return { + success: true, + message: `HTTP integration client}`, + }; + } catch (error) { + return { + success: false, + message: `Failed initialising HTTP: ${error}`, + }; + } + } + + initSubscriptions(subscriptionOptions: Subscription) { + if (validateHttpObject(subscriptionOptions)) { + this.subscriptions = { ...subscriptionOptions }; + } + } + + dispatch(action: Action, state?: object) { + if (false) { + return { + success: false, + message: 'Client not initialised', + }; + } + + if (!action) { + return { + success: false, + message: 'HTTP called with no action', + }; + } + + // check subscriptions for action + const eventSubscriptions = this.subscriptions?.[action] || []; + + eventSubscriptions.forEach((sub) => { + const { enabled, message } = sub; + if (enabled && message) { + const parsedMessage = parseTemplateNested(message, state || {}); + this.emit(parsedMessage); + } + }); + } + + emit(path: string) { + http.get(path, (res) => { + if (res.statusCode < 300) { + return { + success: true, + message: 'HTTP Message sent', + }; + } else { + return { + success: false, + message: `Error sending message responds: ${res.statusCode}`, + }; + } + }); + } + + shutdown() { + console.log('Shutting down HTTP integration'); + // if (this.httpClient) { + // // this.httpClient?.close(); + // this.httpClient = null; + // } + } +} + +export const httpIntegration = new HttpIntegration(); diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts index 260bab21b..6bd679c14 100644 --- a/apps/server/src/utils/parser.ts +++ b/apps/server/src/utils/parser.ts @@ -20,6 +20,7 @@ import { parseAliases, parseProject, parseOsc, + parseHttp, parseRundown, parseSettings, parseUserFields, @@ -319,7 +320,7 @@ export const parseJson = async (jsonData, enforce = false): Promise { * Parses and validates subscription entry * @param data */ -export const validateOscSubscriptionEntry = (data: OscSubscriptionOptions): boolean => { +export const validateOscSubscriptionEntry = (data: SubscriptionOptions): boolean => { for (const subscription in data) { if (typeof data[subscription].message !== 'string' || typeof data[subscription].enabled !== 'boolean') { return false; @@ -203,7 +204,7 @@ export const validateOscSubscriptionEntry = (data: OscSubscriptionOptions): bool * Parses and validates subscription object * @param data */ -export const validateOscObject = (data: OscSubscription): boolean => { +export const validateOscObject = (data: Subscription): boolean => { if (!data) { return false; } @@ -252,22 +253,78 @@ export const parseOsc = ( } else return {}; }; +/** + * Parses and validates subscription entry + * @param data + */ +export const validateHttpSubscriptionEntry = (data: SubscriptionOptions): boolean => { + for (const subscription in data) { + if (typeof data[subscription].message !== 'string' || typeof data[subscription].enabled !== 'boolean') { + return false; + } + } + return true; +}; + +/** + * Parses and validates subscription object + * @param data + */ +export const validateHttpObject = (data: Subscription): boolean => { + if (!data) { + return false; + } + const timerKeys = Object.keys(TimerLifeCycle); + for (const key of timerKeys) { + if (!(key in data) || !Array.isArray(data[key])) { + return false; + } + for (const subscription of data[key]) { + if (typeof subscription.message !== 'string' || typeof subscription.enabled !== 'boolean') { + return false; + } + } + } + return true; +}; + /** * Parse Http portion of an entry * @param {object} data - data object * @param {boolean} enforce - whether to create a definition if one is missing * @returns {object} - event object data */ -export const parseHttp = (data, enforce) => { - const newHttp = {}; +export const parseHttp = ( + data: { + http?: Partial; + }, + enforce: boolean, +): HTTPSettings | Record => { if ('http' in data) { console.log('Found HTTP definition, importing...'); + + const loadedConfig = data?.http || {}; + const validatedSubscriptions = validateHttpObject(loadedConfig.subscriptions) + ? loadedConfig.subscriptions + : dbModel.http.subscriptions; + + return { + portIn: loadedConfig.portIn ?? dbModel.http.portIn, + portOut: loadedConfig.portOut ?? dbModel.http.portOut, + targetIP: loadedConfig.targetIP ?? dbModel.http.targetIP, + enabledIn: loadedConfig.enabledIn ?? dbModel.http.enabledIn, + enabledOut: loadedConfig.enabledOut ?? dbModel.http.enabledOut, + subscriptions: validatedSubscriptions, + }; } else if (enforce) { - /* Not yet */ - } - return newHttp; + console.log('Created HTTP object in db'); + return { ...dbModel.http }; + } else return {}; }; + + + /** * Parse aliases portion of an entry * @param {object} data - data object diff --git a/apps/server/test-db/db.json b/apps/server/test-db/db.json index 295f3f5a5..f644f4d59 100644 --- a/apps/server/test-db/db.json +++ b/apps/server/test-db/db.json @@ -259,6 +259,17 @@ "targetIP": "127.0.0.1", "enabled": true }, + "http": { + "enabledOut": false, + "subscriptions": { + "onLoad": [], + "onStart": [], + "onPause": [], + "onStop": [], + "onUpdate": [], + "onFinish": [] + } + }, "aliases": [ { "enabled": true, diff --git a/apps/test-db/db.json b/apps/test-db/db.json index 63c4dc6ea..3438ce28c 100644 --- a/apps/test-db/db.json +++ b/apps/test-db/db.json @@ -148,5 +148,16 @@ "onUpdate": [], "onFinish": [] } + }, + "http": { + "enabledOut": true, + "subscriptions": { + "onLoad": [], + "onStart": [], + "onPause": [], + "onStop": [], + "onUpdate": [], + "onFinish": [] + } } } \ No newline at end of file diff --git a/demo-db/db.json b/demo-db/db.json index 457e77606..f10babdd0 100644 --- a/demo-db/db.json +++ b/demo-db/db.json @@ -468,5 +468,16 @@ ], "onFinish": [] } + }, + "http": { + "enabledOut": true, + "subscriptions": { + "onLoad": [], + "onStart": [], + "onPause": [], + "onStop": [], + "onUpdate": [], + "onFinish": [] + } } } \ No newline at end of file diff --git a/packages/types/src/definitions/DataModel.type.ts b/packages/types/src/definitions/DataModel.type.ts index 71282e904..156f8aff1 100644 --- a/packages/types/src/definitions/DataModel.type.ts +++ b/packages/types/src/definitions/DataModel.type.ts @@ -5,6 +5,7 @@ import { OSCSettings } from './core/OscSettings.type.js'; import { Settings } from './core/Settings.type.js'; import { UserFields } from './core/UserFields.type.js'; import { ViewSettings } from './core/Views.type.js'; +import { HTTPSettings } from '../index.js'; export type DatabaseModel = { rundown: OntimeRundown; @@ -14,4 +15,5 @@ export type DatabaseModel = { aliases: Alias[]; userFields: UserFields; osc: OSCSettings; + http: HTTPSettings; }; diff --git a/packages/types/src/definitions/core/HttpSettings.type.ts b/packages/types/src/definitions/core/HttpSettings.type.ts new file mode 100644 index 000000000..08980fca9 --- /dev/null +++ b/packages/types/src/definitions/core/HttpSettings.type.ts @@ -0,0 +1,7 @@ +import { Subscription } from './Subscription.type.js'; + + +export interface HTTPSettings { + enabledOut: boolean; + subscriptions: Subscription; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index fc8e1d27f..9c410b176 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -33,6 +33,7 @@ export type { Subscription, SubscriptionOptions } from './definitions/core/Subsc // ---> OSC export type { OSCSettings } from './definitions/core/OscSettings.type.js'; // ---> HTTP +export type { HTTPSettings } from './definitions/core/HttpSettings.type.js'; // SERVER RUNTIME export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';