Skip to content

Commit 631f6b2

Browse files
committed
Release version 1.0.0
1 parent fdb1158 commit 631f6b2

18 files changed

+3384
-1
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules/
2+
.idea/
3+
dist/
4+
package-lock.json
5+
/pkg
6+
/pkg.tgz
7+
*.tgz
8+
DEBUG-INSPECT
9+
DEBUG-PACKAGED
10+
.yarn/*

.yarnrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodeLinker: node-modules

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 Bitfocus AS - Open Source
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
1+
# companion-module-bitfire-api
2+
3+
See [HELP.md](./companion/HELP.md) and [LICENSE](./LICENSE)
4+
5+
# Release Versions
6+
## V1.0.0
7+
- Initial release

companion/HELP.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## BitFire API
2+
This Companion module connects to a `MacroEngine` element present on a BitFire Spark™ Environment.
3+
There are no predefined `Actions` in this module; the `Actions` available are determined by what the `MacroEngine` is
4+
connected to. Upon successfully connecting, this module should populate `Actions`.
5+
6+
### Requirements
7+
8+
- Spark™ Environment
9+
- `MacroEngine` element on target Spark™ Environment
10+
11+
### Configuration
12+
There will be a connection string generated with your Spark™ Environment. It should take the form of a Websocket
13+
connection url. Do not remove the `wss://` prefix. This module expects the whole url to be passed.

companion/manifest.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"$schema": "../node_modules/@companion-module/base/assets/manifest.schema.json",
3+
"id": "bitfire-api",
4+
"name": "Bitfire API",
5+
"shortname": "Bitfire",
6+
"description": "A module that allows you to send commands to BitFire cloud production tools via a Macro Engine",
7+
"version": "0.0.0",
8+
"license": "MIT",
9+
"repository": "git+https://github.com/bitfocus/companion-module-bitfire-api.git",
10+
"bugs": "https://github.com/bitfocus/companion-module-bitfire-api/issues",
11+
"maintainers": [
12+
{
13+
"name": "Pat Bierach",
14+
"email": "pbierach@bitfire.tv"
15+
}
16+
],
17+
"runtime": {
18+
"type": "node22",
19+
"api": "nodejs-ipc",
20+
"apiVersion": "0.0.0",
21+
"entrypoint": "../dist/main.js"
22+
},
23+
"legacyIds": [],
24+
"manufacturer": "BitFire",
25+
"products": ["API"],
26+
"keywords": ["BF", "bf", "API", "api"]
27+
}

eslint.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { generateEslintConfig } from '@companion-module/tools/eslint/config.mjs'
2+
3+
export default generateEslintConfig({
4+
enableTypescript: true,
5+
})

package.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "bitfire-api",
3+
"version": "1.0.0",
4+
"main": "dist/main.js",
5+
"type": "module",
6+
"scripts": {
7+
"postinstall": "husky",
8+
"format": "prettier -w .",
9+
"package": "run build && companion-module-build",
10+
"build": "rimraf dist && run build:main",
11+
"build:main": "tsc -p tsconfig.build.json",
12+
"dev": "tsc -p tsconfig.build.json --watch",
13+
"lint:raw": "eslint",
14+
"lint": "run lint:raw ."
15+
},
16+
"license": "MIT",
17+
"repository": {
18+
"type": "git",
19+
"url": "git+https://github.com/bitfocus/companion-module-bitfire-api.git"
20+
},
21+
"engines": {
22+
"node": "^22.20",
23+
"yarn": "^4"
24+
},
25+
"dependencies": {
26+
"@companion-module/base": "~1.14.1",
27+
"ws": "^8.19.0"
28+
},
29+
"devDependencies": {
30+
"@companion-module/tools": "^2.6.1",
31+
"@types/node": "^22.19.3",
32+
"@types/ws": "^8.18.1",
33+
"eslint": "^9.39.2",
34+
"husky": "^9.1.7",
35+
"lint-staged": "^16.2.7",
36+
"prettier": "^3.7.4",
37+
"rimraf": "^6.1.2",
38+
"typescript": "~5.9.3",
39+
"typescript-eslint": "^8.51.0"
40+
},
41+
"prettier": "@companion-module/tools/.prettierrc.json",
42+
"lint-staged": {
43+
"*.{css,json,md,scss}": [
44+
"prettier --write"
45+
],
46+
"*.{ts,tsx,js,jsx}": [
47+
"yarn lint:raw --fix"
48+
]
49+
},
50+
"packageManager": "yarn@4.12.0"
51+
}

src/actions.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import type { BitFireInstance } from './main.js'
2+
import {
3+
CompanionActionDefinitions,
4+
CompanionInputFieldDropdown,
5+
CompanionInputFieldTextInput
6+
} from "@companion-module/base/dist/index.js";
7+
8+
export interface BitfireArgumentChoice {
9+
[key: string]: string
10+
}
11+
12+
export interface BitFireCommandArg {
13+
description: string
14+
required: boolean,
15+
default?: string,
16+
// For right now, number is same as string.
17+
type: "string" | "number" | "dropdown",
18+
// Empty object when there are no choices defined.
19+
choices: BitfireArgumentChoice[] | Record<string, never>
20+
}
21+
22+
export interface BitFireCommandArgs {
23+
[argName: string]: BitFireCommandArg
24+
}
25+
26+
export interface BitFireCommand {
27+
description: string
28+
args: BitFireCommandArgs
29+
}
30+
31+
export interface BitFireProviderMessage {
32+
/// Provider name
33+
name: string
34+
uri: string
35+
commands: { [commandName: string]: BitFireCommand}
36+
}
37+
38+
export function validateProviderMessage(msg: unknown): msg is BitFireProviderMessage {
39+
if (typeof msg !== 'object' || msg === null) return false;
40+
41+
const obj = msg as Record<string, any>;
42+
43+
if (typeof obj.name !== 'string') return false;
44+
if (typeof obj.uri !== 'string') return false;
45+
if (typeof obj.commands !== 'object' || obj.commands === null) return false;
46+
const commands = Object.values(obj.commands);
47+
return commands.every(validateBitFireCommand);
48+
}
49+
50+
function validateChoices(choices: any): boolean {
51+
if(typeof choices !== 'object' || choices === null) return false;
52+
53+
if(Array.isArray(choices)) {
54+
return choices.every(choice =>
55+
typeof choice === "object" &&
56+
choice !== null &&
57+
!Array.isArray(choice)
58+
);
59+
}
60+
61+
// empty object {}, otherwise any non-null object would pass
62+
return Object.keys(choices).length === 0;
63+
}
64+
65+
function validateBitFireCommand(cmd: any): boolean {
66+
if (typeof cmd !== 'object' || cmd === null) return false;
67+
if (typeof cmd.description !== 'string') return false;
68+
if (typeof cmd.args !== 'object' || cmd.args === null) return false;
69+
70+
const validTypes = ["string", "number", "dropdown"];
71+
72+
return Object.values(cmd.args).every((arg: any) => {
73+
return (
74+
typeof arg === 'object' && arg !== null &&
75+
typeof arg.description === 'string' &&
76+
typeof arg.required === 'boolean' &&
77+
validTypes.includes(arg.type) &&
78+
validateChoices(arg.choices)
79+
);
80+
});
81+
}
82+
83+
/**
84+
* Given a 'set_provider' message from a Macro Engine, attempt to convert it to the `BitFireProviderMessage` interface,
85+
* and then create `CompanionActionDefinition` objects to update the actions of `self`.
86+
* @param module - `BitFireInstance` module
87+
* @param msg - 'set_providers' Macro Engine API response
88+
*/
89+
export function handleSetProviderMessage(module: BitFireInstance, msg: unknown): void {
90+
if(!validateProviderMessage(msg)){
91+
module.log('error', `Received invalid provider message.`)
92+
return;
93+
}
94+
95+
let bf_provider = msg
96+
let actions = makeActionsFromProviderMessage(bf_provider, module)
97+
98+
// the module keeps track of all possible actions by provider. we generate a 'master' list to pass to
99+
// setActionDefinitions everytime a new 'set_provider' message comes in
100+
module.updateProviders(bf_provider, actions)
101+
let allProviders = module.getProviders()
102+
const masterProviderList: CompanionActionDefinitions = Object.assign({}, ...Object.values(allProviders))
103+
module.setActionDefinitions(masterProviderList)
104+
}
105+
106+
/**
107+
* Create a `CompanionInputField` from a `BitFireCommandArg`.
108+
* @param name Name of the BF Command.
109+
* @param args data in the shape of a `BitFireCommandArg`
110+
*/
111+
function makeActionInput(name: string, args: BitFireCommandArg): CompanionInputFieldTextInput | CompanionInputFieldDropdown {
112+
const base = {
113+
id: name,
114+
label: name,
115+
tooltip: args.description,
116+
}
117+
118+
if(args.type === 'dropdown' && Array.isArray(args.choices)) {
119+
return {
120+
...base,
121+
type: 'dropdown',
122+
choices: args.choices.map(choice => {
123+
const [id, label] = Object.entries(choice)[0]
124+
return {id,label}
125+
}),
126+
// Use default if it's present, otherwise first choice. an empty string as a last resort.
127+
default: args.default ?? Object.keys(args.choices[0])[0] ?? '',
128+
}
129+
}
130+
131+
return {
132+
...base,
133+
type: 'textinput' as const,
134+
// Required should always be set in BitFireCommandArg, but this is safer for potential undefined
135+
required: args.required === true,
136+
}
137+
}
138+
/**
139+
* Create `CompanionActionDefinitions` from a `BitFireProviderMessage`.
140+
* @param providerMessage - 'set_provider' message parsed into a `BitFireProviderMessage` interface
141+
* @param module - `BitFireInstance` module
142+
*/
143+
function makeActionsFromProviderMessage(providerMessage: BitFireProviderMessage, module: BitFireInstance): CompanionActionDefinitions {
144+
let actions: CompanionActionDefinitions = {}
145+
146+
// iterate over all the commands present in the provider message
147+
// and create the options portion of a CompanionActionDefinition
148+
for (const [commandName, bfCommand] of Object.entries(providerMessage.commands)) {
149+
const options = Object.entries(bfCommand.args).map(([name, args]) =>
150+
makeActionInput(name, args)
151+
)
152+
153+
// Some providers share commands (i.e. All providers have a "send" command). Include the
154+
// provider name so shared commands don't get combined when we generate `masterProviderList`.
155+
let uniqueID = `${providerMessage.name}_${commandName}`;
156+
actions[uniqueID] = {
157+
name: providerMessage.name + ": " + commandName,
158+
description: bfCommand.description,
159+
options: options,
160+
callback: async (event) => {
161+
module.sendMessage(providerMessage.name, commandName, event.options)
162+
}
163+
}
164+
}
165+
166+
return actions
167+
}

src/config.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { type SomeCompanionConfigField } from '@companion-module/base'
2+
3+
export interface ModuleConfig {
4+
connectionString: string
5+
allowUnsecureConnection: boolean
6+
}
7+
8+
export function GetConfigFields(): SomeCompanionConfigField[] {
9+
return [
10+
{
11+
type: 'textinput',
12+
id: 'connectionString',
13+
label: 'Spark™ Environment Connection String',
14+
tooltip: 'Connection string as a websocket (wss://)',
15+
width: 50
16+
},
17+
{
18+
type: "checkbox",
19+
default: false,
20+
id: 'allowUnsecureConnection',
21+
label: 'Allow unsecure connection',
22+
tooltip: "NOT RECOMMENDED: Your connection string will be unencrypted. Only enable this for local " +
23+
"testing or trusted private networks.",
24+
width: 1
25+
},
26+
]
27+
}

0 commit comments

Comments
 (0)