Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: http url interpolation SOFIE-3310 #342

Merged
merged 6 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/timeline-state-resolver-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,13 @@ export enum ActionExecutionResultCode {
Error = 'ERROR',
Ok = 'OK',
}

/** This resolves to a string, where parts can be defined by the datastore */
export interface TemplateString {
/** The string template. Example: "http://google.com?q={{searchString}}" */
key: string
/** Values for the arguments in the key string. Example: { searchString: "TSR" } */
args?: {
[k: string]: any
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DeviceType } from '..'
import { DeviceType, TemplateString } from '..'

export enum TimelineContentTypeCasparCg { // CasparCG-state
MEDIA = 'media',
Expand Down Expand Up @@ -122,7 +122,7 @@ export interface TimelineContentCCGInput extends TimelineContentCasparCGBase, Ti
export interface TimelineContentCCGHTMLPage extends TimelineContentCasparCGBase, TimelineContentCCGProducerBase {
type: TimelineContentTypeCasparCg.HTMLPAGE
/** The URL to load */
url: string
url: string | TemplateString
}
export interface TimelineContentCCGTemplate extends TimelineContentCasparCGBase, TimelineContentCCGProducerBase {
type: TimelineContentTypeCasparCg.TEMPLATE
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { DeviceType, HTTPSendCommandContent } from '..'
import { DeviceType, HTTPSendCommandContent, TemplateString } from '..'

export type TimelineContentHTTPSendAny = TimelineContentHTTPRequest
export interface TimelineContentHTTPSendBase {
deviceType: DeviceType.HTTPSEND
}

export type TimelineContentHTTPRequest = TimelineContentHTTPSendBase & HTTPSendCommandContent
export interface HTTPSendCommandContentExt extends Omit<HTTPSendCommandContent, 'url'> {
url: string | TemplateString
}

export type TimelineContentHTTPRequest = TimelineContentHTTPSendBase & HTTPSendCommandContentExt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DeviceType } from '..'
import { DeviceType, TemplateString } from '..'

export enum TimelineContentTypeSofieChef {
URL = 'url',
Expand All @@ -13,5 +13,5 @@ export interface TimelineContentSofieChef {
export interface TimelineContentSofieChefScene extends TimelineContentSofieChef {
type: TimelineContentTypeSofieChef.URL

url: string
url: string | TemplateString
}
34 changes: 34 additions & 0 deletions packages/timeline-state-resolver/src/__tests__/lib.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { interpolateTemplateString, interpolateTemplateStringIfNeeded } from '../lib'

describe('interpolateTemplateString', () => {
test('basic input', () => {
expect(interpolateTemplateString('Hello there {{name}}', { name: 'Bob' })).toEqual('Hello there Bob')
})

test('missing arg', () => {
expect(interpolateTemplateString('Hello there {{name}}', {})).toEqual('Hello there name')
})

test('repeated arg', () => {
expect(interpolateTemplateString('Hello there {{name}} {{name}} {{name}}', { name: 'Bob' })).toEqual(
'Hello there Bob Bob Bob'
)
})
})

describe('interpolateTemplateStringIfNeeded', () => {
test('string input', () => {
const input = 'Hello there'

expect(interpolateTemplateStringIfNeeded(input)).toEqual(input)
})

test('object input', () => {
expect(
interpolateTemplateStringIfNeeded({
key: 'Hello there {{name}}',
args: { name: 'Bob' },
})
).toEqual('Hello there Bob')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,15 @@ import { DoOnTime, SendMode } from '../../devices/doOnTime'
import got from 'got'
import { InternalTransitionHandler } from '../../devices/transitions/transitionHandler'
import Debug from 'debug'
import { actionNotFoundMessage, deepMerge, endTrace, literal, startTrace, t } from '../../lib'
import {
actionNotFoundMessage,
deepMerge,
endTrace,
interpolateTemplateStringIfNeeded,
literal,
startTrace,
t,
} from '../../lib'
import { ClsParameters } from 'casparcg-connection/dist/parameters'
const debug = Debug('timeline-state-resolver:casparcg')

Expand Down Expand Up @@ -392,7 +400,7 @@ export class CasparCGDevice extends DeviceWithState<State, DeviceOptionsCasparCG
id: layer.id,
layerNo: mapping.layer,
content: LayerContentType.HTMLPAGE,
media: content.url,
media: interpolateTemplateStringIfNeeded(content.url),

playTime: startTime || null,
playing: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ActionExecutionResultCode,
DeviceStatus,
HTTPSendCommandContent,
HTTPSendCommandContentExt,
HTTPSendOptions,
HttpSendActions,
SendCommandResult,
Expand All @@ -15,7 +16,7 @@ import {
} from 'timeline-state-resolver-types'
import _ = require('underscore')
import got, { OptionsOfTextResponseBody, RequestError } from 'got'
import { t } from '../../lib'
import { interpolateTemplateStringIfNeeded, t } from '../../lib'
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'
import CacheableLookup from 'cacheable-lookup'

Expand All @@ -24,7 +25,7 @@ export type HttpSendDeviceState = Timeline.TimelineState<TSRTimelineContent>
export interface HttpSendDeviceCommand extends CommandWithContext {
command: {
commandName: 'added' | 'changed' | 'removed' | 'retry' | 'manual'
content: HTTPSendCommandContent
content: HTTPSendCommandContentExt
layer: string
}
}
Expand Down Expand Up @@ -69,7 +70,7 @@ export class HTTPSendDevice extends Device<HTTPSendOptions, HttpSendDeviceState,
}

private async executeSendCommandAction(
cmd?: HTTPSendCommandContent
cmd?: HTTPSendCommandContentExt
): Promise<ActionExecutionResult<SendCommandResult>> {
if (!cmd)
return {
Expand Down Expand Up @@ -133,7 +134,7 @@ export class HTTPSendDevice extends Device<HTTPSendOptions, HttpSendDeviceState,
context: `added: ${newLayer.id}`,
command: {
commandName: 'added',
content: newLayer.content as HTTPSendCommandContent,
content: newLayer.content as HTTPSendCommandContentExt,
layer: layerKey,
},
})
Expand All @@ -146,7 +147,7 @@ export class HTTPSendDevice extends Device<HTTPSendOptions, HttpSendDeviceState,
context: `changed: ${newLayer.id} (previously: ${oldLayer.id})`,
command: {
commandName: 'changed',
content: newLayer.content as HTTPSendCommandContent,
content: newLayer.content as HTTPSendCommandContentExt,
layer: layerKey,
},
})
Expand All @@ -161,7 +162,7 @@ export class HTTPSendDevice extends Device<HTTPSendOptions, HttpSendDeviceState,
commands.push({
timelineObjId: oldLayer.id,
context: `removed: ${oldLayer.id}`,
command: { commandName: 'removed', content: oldLayer.content as HTTPSendCommandContent, layer: layerKey },
command: { commandName: 'removed', content: oldLayer.content as HTTPSendCommandContentExt, layer: layerKey },
})
}
})
Expand Down Expand Up @@ -220,15 +221,17 @@ export class HTTPSendDevice extends Device<HTTPSendOptions, HttpSendDeviceState,
headers: command.content.headers,
}

const url = new URL(command.content.url)
if (!this.options.noProxy?.includes(url.host)) {
if (url.protocol === 'http:' && this.options.httpProxy) {
const commandUrl: string = interpolateTemplateStringIfNeeded(command.content.url)

const parsedUrl = new URL(commandUrl)
if (!this.options.noProxy?.includes(parsedUrl.host)) {
if (parsedUrl.protocol === 'http:' && this.options.httpProxy) {
options.agent = {
http: new HttpProxyAgent({
proxy: this.options.httpProxy,
}),
}
} else if (url.protocol === 'https:' && this.options.httpsProxy) {
} else if (parsedUrl.protocol === 'https:' && this.options.httpsProxy) {
options.agent = {
https: new HttpsProxyAgent({
proxy: this.options.httpsProxy,
Expand All @@ -252,7 +255,7 @@ export class HTTPSendDevice extends Device<HTTPSendOptions, HttpSendDeviceState,
}
}

const response = await httpReq(command.content.url, options)
const response = await httpReq(commandUrl, options)

if (response.statusCode === 200) {
this.context.logger.debug(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DeviceType,
} from 'timeline-state-resolver-types'
import type { SofieChefState } from '.'
import { interpolateTemplateStringIfNeeded } from '../../lib'

export function buildSofieChefState(
timelineState: Timeline.TimelineState<TSRTimelineContent>,
Expand All @@ -23,7 +24,7 @@ export function buildSofieChefState(

if (mapping && content.deviceType === DeviceType.SOFIE_CHEF) {
sofieChefState.windows[mapping.options.windowId] = {
url: content.url,
url: interpolateTemplateStringIfNeeded(content.url),
urlTimelineObjId: layerState.id,
}
}
Expand Down
23 changes: 23 additions & 0 deletions packages/timeline-state-resolver/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ActionExecutionResultCode,
TimelineDatastoreReferences,
ActionExecutionResult,
TemplateString,
} from 'timeline-state-resolver-types'
import * as _ from 'underscore'
import { PartialDeep } from 'type-fest'
Expand Down Expand Up @@ -307,3 +308,25 @@ export function actionNotFoundMessage(id: never): ActionExecutionResult<any> {
export function cloneDeep<T>(input: T): T {
return klona(input)
}

/**
* Interpolate a translation style string
*/
export function interpolateTemplateString(key: string, args: { [key: string]: any } | undefined): string {
if (!args || typeof key !== 'string') {
return String(key)
}

let interpolated = String(key)
for (const placeholder of key.match(/[^{}]+(?=})/g) || []) {
const value = args[placeholder] || placeholder
interpolated = interpolated.replace(`{{${placeholder}}}`, value)
}

return interpolated
}

export function interpolateTemplateStringIfNeeded(str: string | TemplateString): string {
if (typeof str === 'string') return str
return interpolateTemplateString(str.key, str.args)
}
Loading