├── LICENSE
├── README.md
├── fileTriggers.txt
├── index.html
├── js
├── Documentation.md
├── action
│ └── actionHandler.js
├── api
│ └── apiHandler.js
├── chat
│ └── chatHandler.js
├── controller.js
├── cooldown
│ └── cooldownHandler.js
├── debug
│ └── debugHandler.js
├── discord
│ └── discordHandler.js
├── handler.js
├── index.js
├── list
│ └── listHandler.js
├── message
│ └── messageHandler.js
├── mqtt
│ ├── mqtt-websocket.js
│ └── mqttHandler.js
├── obs
│ ├── obs-websocket.js
│ ├── obs-websocket.js.map
│ ├── obs-wrapper.js
│ └── obsHandler.js
├── param
│ └── paramHandler.js
├── parser.js
├── random
│ └── randomHandler.js
├── slobs
│ ├── slobs-websocket.js
│ └── slobsHandler.js
├── streamElementsAlert
│ ├── streamElements-socket.js
│ └── streamElementsAlertHandler.js
├── streamlabs
│ ├── streamlabs-socket.js
│ └── streamlabsHandler.js
├── timer
│ └── timerHandler.js
├── tts
│ └── ttsHandler.js
├── twitch
│ ├── twitch-api.js
│ ├── twitch-eventsub-handler.js
│ ├── twitch-pub-socket.js
│ └── twitchHandler.js
├── utils
│ ├── async.js
│ ├── idb-service.js
│ ├── shlex.js
│ ├── storage-emitter.js
│ └── utils.js
├── variable
│ └── variableHandler.js
└── voicemod
│ ├── voicemod-socket.js
│ └── voicemodHandler.js
├── logo.png
├── settings
├── Settings.md
├── chat
│ ├── channel.txt
│ └── code.txt
├── mqtt
│ ├── password.txt
│ ├── username.txt
│ └── websocket.txt
├── obs
│ ├── address.txt
│ └── password.txt
├── slobs
│ └── token.txt
├── streamelements
│ └── jwtToken.txt
├── streamlabs
│ └── socketAPIToken.txt
├── twitch
│ ├── clientId.txt
│ ├── clientSecret.txt
│ ├── code.txt
│ └── user.txt
├── variable
│ └── autoload.txt
└── voicemod
│ ├── address.txt
│ └── apiKey.txt
├── sounds
└── MashiahMusic__Kygo-Style-Melody.wav
├── triggers.txt
├── triggers
└── sample.txt
└── version.txt
/LICENSE:
--------------------------------------------------------------------------------
1 | This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 4.0 Generic License. To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-nd/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kruiz Control
2 |
3 |
4 |
5 |
6 |
7 |
8 | Kruiz Control enables a pseudo code approach to manage and automatically handle Twitch chat and events, OBS or SLOBS, and StreamElements or Streamlabs alerts.
9 |
10 |
11 | Tutorial |
12 | Download |
13 | Documentation |
14 | Settings
15 |
16 |
17 |
18 | @Kruiser8 |
19 | Trello (Roadmap) |
20 | Patreon |
21 | Support Discord
22 |
23 |
24 | ## Table of Contents
25 |
26 | - [Setup Guide](#setup-guide)
27 | - [Compatibility](#compatibility)
28 | - [Installation](#installation)
29 | + [Settings](#settings)
30 | + [Add as Browser Source](#add-as-browser-source)
31 | + [OBS Websocket](#obs-websocket)
32 | - [Usage](#usage)
33 | + [Pseudo Code Format](#pseudo-code-format)
34 | + [triggers.txt](#triggerstxt)
35 | + [fileTriggers.txt and the triggers folder](#filetriggerstxt-and-the-triggers-folder)
36 | + [sounds folder](#sounds-folder)
37 | - [FAQ](#faq)
38 | + [XSplit Support](#xsplit-support)
39 | + [Support for Youtube Alerts](#support-for-youtube-alerts)
40 | + [Support for Youtube Chat](#support-for-youtube-chat)
41 | + [Will you support X](#will-you-support-x)
42 | - [Support the Project](#support-the-project)
43 | - [Associated Projects](#associated-projects)
44 | - [Credits](#credits)
45 |
46 | ***
47 |
48 | ## Setup Guide
49 |
50 | - Add the **index.html** to OBS or SLOBS as a browser source.
51 | - Fill out [the settings files](settings/Settings.md) in the settings folder.
52 | - Type `!example` in your twitch chat. If your user responds with `Success! It worked!`, you're good to go!
53 | - Customize the _triggers.txt_ with your own triggers from the [the documentation](js/Documentation.md).
54 |
55 | _Note: If you're on OBS v27 or lower, you'll also have to install the [OBS Websocket Plugin](https://github.com/Palakis/obs-websocket/releases/latest). Reopen OBS after installing._
56 |
57 | ***
58 |
59 | ## Compatibility
60 |
61 | Kruiz Control supports
62 | - Twitch Channel Management
63 | - Twitch Chat
64 | - Twitch Events (alerts, channel points, hype trains)
65 | - Streamlabs Alerts
66 | - StreamElements Alerts
67 | - OBS scene, source, and filter changes
68 | - SLOBS scene and source changes
69 | - Voicemod Voice Changer control
70 | - Playing music (mp3, wav, ogg)
71 | - Timers (triggering on an interval)
72 | - Sending API calls
73 |
74 | and more in [the documentation](js/Documentation.md)!
75 |
76 | The script should run on any broadcast software that supports browser sources, however only OBS and SLOBS support changing scenes and sources. OBS.Live is supported.
77 |
78 | ***
79 |
80 | ## Installation
81 |
82 | ### Add as Browser Source
83 | Add the **index.html** file as a browser source within your broadcast software. It is *recommended* to add this source to one scene that is included in all other scenes (like your alert scene) rather than recreate this source in every scene.
84 |
85 | ### Settings
86 | Before the script will work, you'll need to fill out all of the settings files. Please see the [settings description](settings/Settings.md) for more information.
87 |
88 | #### Steps for adding to OBS/SLOBS
89 | - In OBS, under **Sources** click the + icon to add a new **Browser** source.
90 | - Name it and select OK.
91 | - Check the `Local file` checkbox.
92 | - Click **Browse** and open the **index.html** file within the Kruiz Control script directory.
93 | - Recommended to set the width/height to 100 or less to reduce the size of the source.
94 |
95 | ### OBS Websocket
96 | In OBS, click **Tools** > **WebSockets Server Settings** and enable the websocket server.
97 |
98 | It is **highly recommended** to use a password!
99 |
100 | ***
101 |
102 | ## Usage
103 |
104 | ### Pseudo Code Format
105 | For information on the pseudo code format, please see [the documentation](js/Documentation.md).
106 |
107 | ### triggers.txt
108 | Setup your triggers inside of this file if you do not need actions to be run one after another.
109 |
110 | As an example, if the below is in the _triggers.txt_ file, then both sounds can be played at the same time.
111 |
112 | #### triggers.txt
113 | ```
114 | OnTWChannelPoint SHIKAKA
115 | Play 30 wait Shikaka.mp3
116 |
117 | OnCommand sbvm 0 !intervention
118 | Play 45 nowait MashiahMusic__Kygo-Style-Melody.wav
119 | ```
120 |
121 | ### fileTriggers.txt and the triggers folder
122 | When you need actions to be run one-after-another, create a file in the _triggers_ folder and add the name of the folder to _fileTriggers.txt_.
123 |
124 | As an example, here's a setup to make sure multiple scene changes don't happen simultaneously.
125 |
126 | #### _fileTriggers.txt_
127 | ```
128 | obs.txt
129 | ```
130 | #### _triggers/obs.txt_
131 | ```m
132 | OnSLDonation
133 | OBS Scene DonationCelebration
134 | Delay 4
135 |
136 | OnCommand mb 0 !brb
137 | OBS Scene BRB
138 | Delay 5
139 | ```
140 |
141 | ### sounds folder
142 | In order to use a sound with [`Play`](js/Documentation.md#play), add the sound file to the *sounds* folder. The supported audio formats are mp3, wav, and ogg.
143 |
144 | ***
145 |
146 | ## FAQ
147 |
148 | ### XSplit Support
149 | The script should work with XSplit _BUT_ the OBS-like functionality will not work. XSplit does not provide a direct websocket interface to do such actions. It may be possible to implement a plugin that provides a websocket interface to connect with xsplit. If you know a way to achieve this, [please reach out](mailto:kruiser.twitch@gmail.com).
150 |
151 | ### Support for Youtube Alerts
152 | I can definitely add support for Youtube alerts from Streamlabs. I just haven't had the time to implement it yet.
153 |
154 | More investigation time is needed to implement Youtube with StreamElements.
155 |
156 | ### Support for Youtube Chat
157 | Potentially if I can find the libraries to implement it. Again, if you know a way to achieve this, please reach out via [twitter](https://twitter.com/kruiser8) or [discord](https://discord.gg/wU3ZK3Q).
158 |
159 | ### Will you support X
160 | Please reach out if you have any ideas or other questions that were not covered in the documentation.
161 |
162 | Discord: [Kruiz Control Support Discord](https://discord.gg/wU3ZK3Q)
163 |
164 | Twitter: [@Kruiser8](https://twitter.com/kruiser8)
165 |
166 | ***
167 |
168 | ## Support the Project
169 | There are a number of ways to support this project.
170 |
171 | - Support Kruiser through Patreon.
172 | - Translate the documentation.
173 | - Help others in the Support Discord.
174 | - Contribute ideas for the roadmap.
175 | - Spread the word!
176 |
177 | I do take commissions to implement custom functionality when necessary. Please reach out if you have a specific request.
178 |
179 | ***
180 |
181 | ## Associated Projects
182 |
183 | - Kruiz Control Documentation.
184 | - Kruiz Control Widget Template.
185 | - Kruiz Control Configurator by CrashKoeck.
186 |
187 | ***
188 |
189 | ## Credits
190 | - [async](https://github.com/caolan/async) by Caolan McMahon (caolan)
191 | - [comfyjs](https://github.com/instafluff/ComfyJS) by Instafluff (instafluff)
192 | - [node-shlex](https://github.com/rgov/node-shlex) by Ryan Govostes (rgov)
193 | - [obs-websocket-js](https://github.com/haganbmj/obs-websocket-js) by Brendan Hagan (haganbmj)
194 | - [tesjs](https://github.com/mitchwadair/tesjs) by Mitchell Adair @mitchwadair
195 |
196 | ## License
197 | 
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 4.0 Generic License.
198 |
--------------------------------------------------------------------------------
/fileTriggers.txt:
--------------------------------------------------------------------------------
1 | sample.txt
2 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/js/action/actionHandler.js:
--------------------------------------------------------------------------------
1 | class ActionHandler extends Handler {
2 | /**
3 | * Create a new Actions handler.
4 | */
5 | constructor() {
6 | super('Action', ['OnAction']);
7 | this.actions = [];
8 | this.actionsTriggers = {};
9 | this.success();
10 | }
11 |
12 | /**
13 | * Register trigger from user input.
14 | * @param {string} trigger name to use for the handler
15 | * @param {array} triggerLine contents of trigger line
16 | * @param {number} id of the new trigger
17 | */
18 | addTriggerData = (trigger, triggerLine, triggerId) => {
19 | var { actions } = Parser.getInputs(triggerLine, ['actions'], true);
20 | // Allow action aliases
21 | actions.forEach(action => {
22 | action = action.toLowerCase();
23 | if (this.actions.indexOf(action) === -1) {
24 | this.actions.push(action);
25 | this.actionsTriggers[action] = [];
26 | }
27 | this.actionsTriggers[action].push(triggerId);
28 | });
29 | }
30 |
31 | /**
32 | * Handle the input data (take an action).
33 | * @param {array} triggerData contents of trigger line
34 | */
35 | handleData = async (triggerData) => {
36 | var action = Parser.getAction(triggerData, 'Action', -1);
37 |
38 | if (action === "action") {
39 | if (triggerData.length > 2) {
40 | triggerData.shift();
41 | return { actions: [triggerData] };
42 | } else if (triggerData.length === 2) {
43 | return { actions: [triggerData[1]] };
44 | } else {
45 | console.error('No action provided to the Action handler: ' + JSON.stringify(triggerData));
46 | }
47 | } else if (this.actions.indexOf(action) != -1) {
48 | var inputs = {};
49 | for (var i = 1; i < triggerData.length; i++) {
50 | inputs[`in${i}`] = triggerData[i];
51 | }
52 | return { "_trigId": this.actionsTriggers[action], action: action, in_count: Object.keys(inputs).length, ...inputs };
53 | } else {
54 | console.error('Unable to find parser for input: ' + action);
55 | }
56 | }
57 | }
58 |
59 | /**
60 | * Create a handler
61 | */
62 | function actionHandlerExport() {
63 | var actionHandler = new ActionHandler();
64 | }
65 | actionHandlerExport();
66 |
--------------------------------------------------------------------------------
/js/api/apiHandler.js:
--------------------------------------------------------------------------------
1 | class ApiHandler extends Handler {
2 | /**
3 | * Create a new API handler.
4 | */
5 | constructor() {
6 | super('API', []);
7 | this.apiCall = {};
8 | this.success();
9 | }
10 |
11 | /**
12 | * Handle the input data (take an action).
13 | * @param {array} triggerData contents of trigger line
14 | */
15 | handleData = async (triggerData) => {
16 | var action = Parser.getAction(triggerData, 'API');
17 | if (action === 'get') {
18 | var { url } = Parser.getInputs(triggerData, ['action', 'url']);
19 | var response = await this.callAPI('GET', url);
20 | return { api_data: response };
21 | } else {
22 | switch (action) {
23 | case 'method':
24 | var { name, method } = Parser.getInputs(triggerData, ['action', 'name', 'method']);
25 | this.initialize(name);
26 | this.apiCall[name].method = method.toUpperCase();
27 | break;
28 | case 'header':
29 | var { name, headerKey, headerValue } = Parser.getInputs(triggerData, ['action', 'name', 'headerKey', 'headerValue']);
30 | this.initialize(name);
31 | this.apiCall[name].headers[headerKey] = headerValue;
32 | break;
33 | case 'data':
34 | var { name, dataKey, dataValue } = Parser.getInputs(triggerData, ['action', 'name', 'dataKey', 'dataValue']);
35 | this.initialize(name);
36 | this.apiCall[name].data[dataKey] = dataValue;
37 | break;
38 | case 'rawdata':
39 | var { name, rawData } = Parser.getInputs(triggerData, ['action', 'name', 'rawData']);
40 | this.initialize(name);
41 | this.apiCall[name].data = rawData;
42 | break;
43 | case 'url':
44 | var { name, apiURL } = Parser.getInputs(triggerData, ['action', 'name', 'apiURL']);
45 | this.initialize(name);
46 | this.apiCall[name].url = apiURL;
47 | break;
48 | case 'send':
49 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
50 | this.initialize(name);
51 | var response = await this.callAPI(this.apiCall[name].method, this.apiCall[name].url, this.apiCall[name].data, this.apiCall[name].headers);
52 | return { api_data: response };
53 | break;
54 | case 'clear':
55 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
56 | delete this.apiCall[name];
57 | break;
58 | default:
59 | console.error(`Unexpected API (${action}). Check your event code.`);
60 | break;
61 | }
62 | }
63 | }
64 |
65 | /**
66 | * Initialize the api data if it does not exist.
67 | * @param {string} name webhook name
68 | */
69 | initialize = (name) => {
70 | if (this.apiCall[name] === undefined) {
71 | this.apiCall[name] = {
72 | method: "GET",
73 | headers: {},
74 | data: {},
75 | url: ""
76 | };
77 | }
78 | }
79 |
80 | /**
81 | * Call the input url and return the response.
82 | * @param {string} method type of API call
83 | * @param {string} url url to call
84 | * @param {object} data parameters to send with the call
85 | * @param {object} headers headers to send with the call
86 | */
87 | callAPI = async (method, url, data, headers) => {
88 | data = data || {};
89 | headers = headers || {};
90 | var response = null;
91 | try {
92 | await $.ajax({
93 | url: url,
94 | type: method,
95 | data: data,
96 | headers: headers,
97 | success: function(data) {
98 | response = data;
99 | if (data === undefined) {
100 | response = 'success';
101 | }
102 | },
103 | error: function(err) {
104 | console.error(`Error calling the ${url} API: ${JSON.stringify(err)}`);
105 | response = 'error';
106 | }
107 | });
108 | } catch (err) {
109 | console.error(`Unhandled error calling the ${url} API: ${JSON.stringify(err)}`);
110 | response = 'error';
111 | }
112 |
113 | return response;
114 | }
115 | }
116 |
117 | /**
118 | * Create a handler
119 | */
120 | function apiHandlerExport() {
121 | var api = new ApiHandler();
122 | }
123 | apiHandlerExport();
124 |
--------------------------------------------------------------------------------
/js/chat/chatHandler.js:
--------------------------------------------------------------------------------
1 | class ChatHandler extends Handler {
2 | /**
3 | * Create a new Chat handler.
4 | */
5 | constructor() {
6 | super('Chat', ['OnCommand','OnKeyword','OnEveryChatMessage','OnHypeChat', 'OnSpeak']);
7 |
8 | /* OnCommand */
9 | this.commands = [];
10 | this.commandsOther = [];
11 | this.commandsInfo = {};
12 |
13 | /* OnKeyword */
14 | this.keywords = [];
15 | this.keywordsRegex = null;
16 | this.keywordsInfo = {};
17 |
18 | /* OnSpeak */
19 | this.speaks = [];
20 | this.speaksInfo = {};
21 | this.speakers = {};
22 |
23 | /* OnEveryChatMessage */
24 | this.chatTriggers = [];
25 |
26 | /* OnHypeChat */
27 | this.onHypeChats = [];
28 | this.onHypeChatsInfo = {};
29 | }
30 |
31 | /**
32 | * Initialize the chat connection with the input user.
33 | * @param {string} channel twitch channel to connect
34 | */
35 | init = (channel) => {
36 | this.channel = channel.toLowerCase();
37 | ComfyJS.onConnected = ( address, port, isFirstConnect ) => {
38 | console.error("Chat connected successfully");
39 | if (isFirstConnect) {
40 | this.success();
41 | }
42 | }
43 |
44 | Storage.onChange("ChatOAuth", (_, value) => {
45 | if (Debug.All || Debug.Chat) {
46 | console.error("New chat oauth value received")
47 | }
48 | if (ComfyJS.GetClient() !== null) {
49 | if (Debug.All || Debug.Chat) {
50 | console.error("Disconnecting ComfyJS")
51 | }
52 | ComfyJS.Disconnect();
53 | }
54 | if (Debug.All || Debug.Chat) {
55 | console.error("Initializing ComfyJS")
56 | }
57 | ComfyJS.Init(channel, `oauth:${value}`);
58 | }, true);
59 | }
60 |
61 | /**
62 | * Register trigger from user input.
63 | * @param {string} trigger name to use for the handler
64 | * @param {array} triggerLine contents of trigger line
65 | * @param {number} id of the new trigger
66 | */
67 | addTriggerData = (trigger, triggerLine, triggerId) => {
68 | trigger = trigger.toLowerCase();
69 | switch (trigger) {
70 | case 'oncommand':
71 | var permission = '';
72 | var inputs = ['permission', 'cooldown', 'commands'];
73 | if (triggerLine.length > 1) {
74 | permission = triggerLine[1].toLowerCase();
75 | if (permission.includes('u')) {
76 | inputs.splice(1, 0, 'info');
77 | }
78 | }
79 | var { permission, info, cooldown, commands } = Parser.getInputs(triggerLine, inputs, true);
80 | permission = permission.toLowerCase();
81 | info = info ? info.toLowerCase() : "";
82 | info = info.split(',')
83 |
84 | cooldown = parseInt(cooldown);
85 | if (isNaN(cooldown)) {
86 | cooldown = 0;
87 | }
88 |
89 | commands.forEach(command => {
90 | command = command.toLowerCase();
91 | if (command.charAt(0) === "!") {
92 | command = command.substring(1);
93 | if (this.commands.indexOf(command) === -1) {
94 | this.commands.push(command);
95 | this.commandsInfo[command] = [];
96 | }
97 | } else {
98 | if (this.commandsOther.indexOf(command) === -1) {
99 | this.commandsOther.push(command);
100 | this.commandsInfo[command] = [];
101 | }
102 | }
103 | this.commandsInfo[command].push({
104 | permission: permission,
105 | info: info,
106 | trigger: triggerId,
107 | cooldown: cooldown * 1000,
108 | lastUse: 0 - (cooldown * 1000)
109 | });
110 | });
111 | break;
112 | case 'onkeyword':
113 | var permission = '';
114 | var inputs = ['permission', 'cooldown', 'keywords'];
115 | if (triggerLine.length > 1) {
116 | permission = triggerLine[1].toLowerCase();
117 | if (permission.includes('u')) {
118 | inputs.splice(1, 0, 'info');
119 | }
120 | }
121 | var { permission, info, cooldown, keywords } = Parser.getInputs(triggerLine, inputs, true);
122 | permission = permission.toLowerCase();
123 | info = info ? info.toLowerCase() : "";
124 | info = info.split(',')
125 |
126 | cooldown = parseInt(cooldown);
127 | if (isNaN(cooldown)) {
128 | cooldown = 0;
129 | }
130 |
131 | keywords.forEach(keyword => {
132 | keyword = keyword.toLowerCase();
133 | if (this.keywords.indexOf(keyword) === -1) {
134 | this.keywords.push(keyword);
135 | this.keywordsInfo[keyword] = [];
136 | }
137 | this.keywordsInfo[keyword].push({
138 | permission: permission,
139 | info: info,
140 | trigger: triggerId,
141 | cooldown: cooldown * 1000,
142 | lastUse: 0 - (cooldown * 1000)
143 | });
144 | });
145 | break;
146 | case 'onspeak':
147 | var { users } = Parser.getInputs(triggerLine, ['users'], true);
148 | users.forEach(user => {
149 | user = user.toLowerCase();
150 | if (this.speaks.indexOf(user) === -1) {
151 | this.speaks.push(user);
152 | this.speaksInfo[user] = [];
153 | }
154 | this.speaksInfo[user].push(triggerId);
155 | });
156 | break;
157 | case 'oneverychatmessage':
158 | this.chatTriggers.push(triggerId);
159 | break;
160 | case 'onhypechat':
161 | var { users } = Parser.getInputs(triggerLine, ['users'], true);
162 | users.forEach(user => {
163 | user = user.toLowerCase();
164 | if (this.onHypeChats.indexOf(user) === -1) {
165 | this.onHypeChats.push(user);
166 | this.onHypeChatsInfo[user] = [];
167 | }
168 | this.onHypeChatsInfo[user].push(triggerId);
169 | });
170 | break;
171 | default:
172 | // do nothing
173 | }
174 | return;
175 | }
176 |
177 | /**
178 | * Handle the input data (take an action).
179 | * @param {array} triggerData contents of trigger line
180 | */
181 | handleData = async (triggerData) => {
182 | var action = Parser.getAction(triggerData, 'Chat');
183 | if (action === 'send') {
184 | var { message } = Parser.getInputs(triggerData, ['action', 'message']);
185 | ComfyJS.Say(message);
186 | } else if (action === 'whisper') {
187 | var { user, message } = Parser.getInputs(triggerData, ['action', 'user', 'message']);
188 | ComfyJS.Whisper(message, user);
189 | }
190 | return;
191 | }
192 |
193 | /**
194 | * Register trigger from user input.
195 | * @param {string} user twitch display name that sent the message
196 | * @param {object} flags permission flags for the user
197 | * @param {string} permissions usability of the command or keyword
198 | * @param {string} username twitch username that sent the message
199 | * @param {string} info extra information for the usability
200 | */
201 | checkPermissions = (user, flags, permissions, username, info) => {
202 | user = user.toLowerCase();
203 | if (permissions.includes('e')) {
204 | return [true, 'e'];
205 | } else if (permissions.includes('s') && flags.subscriber) {
206 | return [true, 's'];
207 | } else if (permissions.includes('v') && flags.vip) {
208 | return [true, 'v'];
209 | } else if (permissions.includes('o') && flags.founder) {
210 | return [true, 'o'];
211 | } else if (permissions.includes('m') && flags.mod) {
212 | return [true, 'm'];
213 | } else if (permissions.includes('b') && this.channel === username) {
214 | return [true, 'b'];
215 | } else if (permissions.includes('u') && info && info.indexOf(user) !== -1) {
216 | return [true, 'u'];
217 | } else if (permissions.includes('n') && !flags.founder && !flags.subscriber && !flags.vip && !flags.mod && this.channel !== user) {
218 | return [true, 'n']
219 | }
220 |
221 | return [false, ''];
222 | }
223 |
224 | /**
225 | * Check if a trigger is on cooldown.
226 | * @param {object} info command info object
227 | */
228 | updateCooldown = (info) => {
229 | if (info.cooldown === 0) {
230 | return true;
231 | }
232 |
233 | var curTime = new Date().getTime();
234 | if (curTime >= info.lastUse + info.cooldown) {
235 | info.lastUse = curTime;
236 | return true;
237 | } else {
238 | return false;
239 | }
240 | }
241 |
242 | /**
243 | * Called after parsing all user input.
244 | */
245 | postParse = () => {
246 | // Create Keyword Regex
247 | if (this.keywords.length > 0) {
248 | this.keywordsRegex = new RegExp('(?:\\b|^|\\s)' + this.keywords.map(x => escapeRegExp(x)).join('(?:\\b|$|\\s)|(?:\\b|^|\\s)') + '(?:\\b|$|\\s)', 'gi');
249 | }
250 |
251 | ComfyJS.onCommand = ( user, command, message, flags, extra ) => {
252 | if (Debug.All || Debug.Chat) {
253 | console.error(`Command Received: ${JSON.stringify({user, command, message, flags, extra})}`);
254 | }
255 | var combined = '!' + command + ' ' + message;
256 |
257 | this.onAllChat(user, {
258 | user: user,
259 | message: combined,
260 | message_id: extra.id || '',
261 | data: {
262 | user: user,
263 | command: command,
264 | message: combined,
265 | after: message,
266 | flags: flags,
267 | extra: extra
268 | }
269 | });
270 |
271 | // Check for matching command and user permission
272 | if (this.commands.indexOf(command) !== -1) {
273 | var args = Parser.splitLine(message);
274 |
275 | var chatArgs = {};
276 | for (var i = 0; i < args.length; i++) {
277 | chatArgs[`arg${i+1}`] = args[i];
278 | }
279 | this.commandsInfo[command].forEach(info => {
280 | var [canUseCommand, grantingPermission] = this.checkPermissions(user, flags, info.permission, extra.username, info.info);
281 | if ((canUseCommand || info.permission.includes('f')) && this.updateCooldown(info)) {
282 | controller.handleData(
283 | info.trigger,
284 | {
285 | command: command,
286 | user: user,
287 | message: combined,
288 | message_id: extra.id || '',
289 | after: message,
290 | data: {
291 | user: user,
292 | command: command,
293 | message: combined,
294 | after: message,
295 | flags: flags,
296 | extra: extra
297 | },
298 | arg_count: args.length,
299 | ...chatArgs
300 | },
301 | canUseCommand && grantingPermission !== 'n' ? [] : this.addFollowerActions(info.permission, user)
302 | );
303 | }
304 | });
305 | }
306 | // Otherwise, check for keyword match
307 | else {
308 | var result = message.match(this.keywordsRegex);
309 | if (result) {
310 | var match = result[0].trim().toLowerCase();
311 | var args = Parser.splitLine(message);
312 | var chatArgs = {};
313 | for (var i = 0; i < args.length; i++) {
314 | chatArgs[`arg${i+1}`] = args[i];
315 | }
316 | this.keywordsInfo[match].forEach(info => {
317 | // Check if user has permission to trigger keyword
318 | var [canUseCommand, grantingPermission] = this.checkPermissions(user, flags, info.permission, extra.username, info.info);
319 | if ((canUseCommand || info.permission.includes('f')) && this.updateCooldown(info)) {
320 | controller.handleData(info.trigger,
321 | {
322 | user: user,
323 | keyword: match,
324 | message: message,
325 | message_id: extra.id || '',
326 | data: {
327 | user: user,
328 | keyword: match,
329 | message: message,
330 | flags: flags,
331 | extra: extra
332 | },
333 | arg_count: args.length,
334 | ...chatArgs
335 | },
336 | canUseCommand && grantingPermission !== 'n' ? [] : this.addFollowerActions(info.permission, user)
337 | );
338 | }
339 | });
340 | }
341 | }
342 | }
343 |
344 | ComfyJS.onChat = ( user, message, flags, self, extra ) => {
345 | if (Debug.All || Debug.Chat) {
346 | console.error(`Chat Received: ${JSON.stringify({user, command, message, flags, extra})}`);
347 | }
348 | this.onAllChat(user, {
349 | user: user,
350 | message: message,
351 | message_id: extra.id || '',
352 | data: {
353 | user: user,
354 | message: message,
355 | flags: flags,
356 | extra: extra
357 | }
358 | });
359 |
360 | // Check for matching command and user permission
361 | var command = message.split(' ')[0].toLowerCase();
362 | if(this.commandsOther.indexOf(command) != -1) {
363 | var args = Parser.splitLine(message).slice(1);
364 | var chatArgs = {};
365 | for (var i = 0; i < args.length; i++) {
366 | chatArgs[`arg${i+1}`] = args[i];
367 | }
368 | this.commandsInfo[command].forEach(info => {
369 | var [canUseCommand, grantingPermission] = this.checkPermissions(user, flags, info.permission, extra.username, info.info);
370 | if ((canUseCommand || info.permission.includes('f')) && this.updateCooldown(info)) {
371 | var after = args.join(' ');
372 | controller.handleData(info.trigger,
373 | {
374 | command: command,
375 | user: user,
376 | message: message,
377 | message_id: extra.id || '',
378 | after: after,
379 | data: {
380 | user: user,
381 | command: command,
382 | message: message,
383 | after: after,
384 | flags: flags,
385 | extra: extra
386 | },
387 | arg_count: args.length,
388 | ...chatArgs
389 | },
390 | canUseCommand && grantingPermission !== 'n' ? [] : this.addFollowerActions(info.permission, user)
391 | );
392 | }
393 | });
394 | }
395 | // Otherwise, check for keyword match
396 | else {
397 | var result = message.match(this.keywordsRegex);
398 | if (result) {
399 | var args = Parser.splitLine(message);
400 | var chatArgs = {};
401 | for (var i = 0; i < args.length; i++) {
402 | chatArgs[`arg${i+1}`] = args[i];
403 | }
404 | var match = result[0].trim().toLowerCase();
405 | this.keywordsInfo[match].forEach(info => {
406 | // Check if user has permission to trigger keyword
407 | var [canUseCommand, grantingPermission] = this.checkPermissions(user, flags, info.permission, extra.username, info.info);
408 | if ((canUseCommand || info.permission.includes('f')) && this.updateCooldown(info)) {
409 | controller.handleData(info.trigger,
410 | {
411 | user: user,
412 | keyword: match,
413 | message: message,
414 | message_id: extra.id || '',
415 | data: {
416 | user: user,
417 | keyword: match,
418 | message: message,
419 | flags: flags,
420 | extra: extra
421 | },
422 | arg_count: args.length,
423 | ...chatArgs
424 | },
425 | canUseCommand && grantingPermission !== 'n' ? [] : this.addFollowerActions(info.permission, user)
426 | );
427 | }
428 | });
429 | }
430 | }
431 | }
432 | return;
433 | }
434 |
435 | /**
436 | * Return actions to check for twitch follower status if requested by the event.
437 | * @param {string} permissions usability of the command or keyword
438 | * @param {string} user twitch display name that sent the message
439 | */
440 | addFollowerActions = (permissions, user) => {
441 | if (permissions.includes('n')) {
442 | return [
443 | ['Ignore', 'Twitch', 'IsFollower', user],
444 | ['Ignore', 'If', '{is_follower}', '=', 'false']
445 | ];
446 | } else if (permissions.includes('f')) {
447 | return [
448 | ['Ignore', 'Twitch', 'IsFollower', user],
449 | ['Ignore', 'If', '{is_follower}', '=', 'true']
450 | ];
451 | } else {
452 | return [];
453 | }
454 | }
455 |
456 | /**
457 | * Check if a trigger is on cooldown.
458 | * @param {string} user that sent the message
459 | * @param {data} data to send with the OnEveryChatMessage
460 | */
461 | onAllChat = (user, data) => {
462 | // OnEveryChatMessage
463 | this.chatTriggers.forEach(triggerId => {
464 | controller.handleData(triggerId, data);
465 | });
466 |
467 | // Check for OnHypeChat
468 | if ("pinned-chat-paid-amount" in data.data.extra.userState) {
469 | var userLower = user.toLowerCase();
470 | var onHypeChatTriggers = [];
471 | if (this.onHypeChats.indexOf(userLower) !== -1) {
472 | onHypeChatTriggers.push(...this.onHypeChatsInfo[userLower]);
473 | }
474 | if (this.onHypeChats.indexOf('*') !== -1) {
475 | onHypeChatTriggers.push(...this.onHypeChatsInfo['*']);
476 | }
477 | if (onHypeChatTriggers.length > 0) {
478 | var userState = data.data.extra.userState;
479 | var hypeChatData = {
480 | amount: userState["pinned-chat-paid-amount"],
481 | formatted_amount: (userState["pinned-chat-paid-amount"] / Math.pow(10, userState["pinned-chat-paid-exponent"])).toFixed(userState["pinned-chat-paid-exponent"]),
482 | currency: userState["pinned-chat-paid-currency"],
483 | exponent: userState["pinned-chat-paid-exponent"],
484 | level: userState["pinned-chat-paid-level"],
485 | is_system_message: userState["pinned-chat-paid-is-system-message"],
486 | ...data
487 | };
488 |
489 | onHypeChatTriggers.sort((a, b) => a-b);
490 | onHypeChatTriggers.forEach(triggerId => {
491 | controller.handleData(triggerId, hypeChatData);
492 | });
493 | }
494 | }
495 |
496 | // Check for OnSpeak Event
497 | var userLower = user.toLowerCase();
498 | var onSpeakTriggers = [];
499 | if (this.speakers[userLower] === undefined && this.speaks.indexOf(userLower) !== -1) {
500 | onSpeakTriggers.push(...this.speaksInfo[userLower]);
501 | }
502 | if (this.speakers[userLower] === undefined && this.speaks.indexOf('*') !== -1) {
503 | onSpeakTriggers.push(...this.speaksInfo['*']);
504 | }
505 | if (onSpeakTriggers.length > 0) {
506 | this.speakers[userLower] = true;
507 | onSpeakTriggers.sort((a, b) => a-b);
508 | onSpeakTriggers.forEach(triggerId => {
509 | controller.handleData(triggerId, data);
510 | });
511 | } else if (this.speakers[userLower] === undefined) {
512 | this.speakers[userLower] = true;
513 | }
514 | }
515 | }
516 |
517 | /**
518 | * Create a handler and read user settings
519 | */
520 | async function chatHandlerExport() {
521 | var chat = new ChatHandler();
522 | var channel = await readFile('settings/chat/channel.txt');
523 | chat.init(channel.trim());
524 | }
525 | chatHandlerExport();
526 |
--------------------------------------------------------------------------------
/js/cooldown/cooldownHandler.js:
--------------------------------------------------------------------------------
1 | class CooldownHandler extends Handler {
2 | GLOBAL_COOLDOWN_KEY = '__kc_cooldown_global_cooldowns';
3 | GLOBAL_COOLDOWN_KEY_PREFIX = '__kc_cooldown_global_cooldown_';
4 |
5 | /**
6 | * Create a new API handler.
7 | */
8 | constructor() {
9 | super('Cooldown', []);
10 | this.success();
11 |
12 | this.cooldowns = {};
13 | }
14 |
15 | /**
16 | * Called after parsing all user input.
17 | */
18 | postParse = async () => {
19 | this.global_cooldowns = await IDBService.get(this.GLOBAL_COOLDOWN_KEY) || [];
20 | for (const cooldown_id of this.global_cooldowns) {
21 | this.cooldowns[cooldown_id] = await IDBService.get(`${this.GLOBAL_COOLDOWN_KEY_PREFIX}${cooldown_id}`) || [];
22 | };
23 | }
24 |
25 | /**
26 | * Handle the input data (take an action).
27 | * @param {array} triggerData contents of trigger line
28 | */
29 | handleData = async (triggerData) => {
30 | var action = Parser.getAction(triggerData, 'Cooldown');
31 | if (action === 'global') {
32 | action = Parser.getAction(triggerData, 'Cooldown', 1);
33 | var { name, duration } = Parser.getInputs(triggerData, ['global', 'action', 'name', 'duration'], false, 1);
34 |
35 | switch (action) {
36 | case 'apply':
37 | var response = this.handleCooldown(name, parseFloat(duration));
38 | if (this.global_cooldowns.indexOf(name) === -1) {
39 | this.global_cooldowns.push(name);
40 | IDBService.set(this.GLOBAL_COOLDOWN_KEY, this.global_cooldowns);
41 | }
42 | IDBService.set(`${this.GLOBAL_COOLDOWN_KEY_PREFIX}${name}`, this.cooldowns[name]);
43 | return response;
44 | break;
45 | case 'check':
46 | return this.checkCooldown(name);
47 | break;
48 | case 'clear':
49 | delete this.cooldowns[name];
50 | var nameIndex = this.global_cooldowns.indexOf(name);
51 | if (nameIndex !== -1) {
52 | this.global_cooldowns.splice(nameIndex, 1);
53 | IDBService.set(this.GLOBAL_COOLDOWN_KEY, this.global_cooldowns);
54 | IDBService.delete(`${this.GLOBAL_COOLDOWN_KEY_PREFIX}${name}`);
55 | }
56 | break;
57 | default:
58 | console.error(`Unexpected Cooldown (${action}). Check your event code.`);
59 | break;
60 | }
61 | } else {
62 | var { name, duration } = Parser.getInputs(triggerData, ['action', 'name', 'duration'], false, 1);
63 | switch (action) {
64 | case 'apply':
65 | return this.handleCooldown(name, parseFloat(duration));
66 | break;
67 | case 'check':
68 | return this.checkCooldown(name);
69 | break;
70 | case 'clear':
71 | delete this.cooldowns[name];
72 | break;
73 | default:
74 | console.error(`Unexpected Cooldown (${action}). Check your event code.`);
75 | break;
76 | }
77 | }
78 | }
79 |
80 | /**
81 | * Check the named cooldown.
82 | *
83 | * @param {string} name name of the cooldown
84 | * @return {Object} whether or not to continue the trigger.
85 | */
86 | checkCooldown = (name) => {
87 | var response = {};
88 | response[name] = false;
89 | var curTime = new Date().getTime();
90 | if ( typeof(this.cooldowns[name]) !== 'undefined' && curTime < this.cooldowns[name] ) {
91 | response[name] = true;
92 | response['cooldown_real'] = (this.cooldowns[name] - curTime) / 1000;
93 | response['cooldown'] = Math.ceil(response['cooldown_real']);
94 | }
95 | return response;
96 | }
97 |
98 | /**
99 | * Handle the named cooldown.
100 | *
101 | * @param {string} name name of the cooldown
102 | * @param {numeric} duration duration of the cooldown
103 | * @return {Object} whether or not to continue the trigger.
104 | */
105 | handleCooldown = (name, duration) => {
106 | var response = {"continue": false};
107 | duration = duration * 1000; // convert to milliseconds
108 | var curTime = new Date().getTime();
109 | if ( typeof(this.cooldowns[name]) === 'undefined' || curTime >= this.cooldowns[name] ) {
110 | this.cooldowns[name] = curTime + duration;
111 | response["continue"] = true;
112 | }
113 | return response;
114 | }
115 | }
116 |
117 | /**
118 | * Create a handler
119 | */
120 | function cooldownHandlerExport() {
121 | var cooldown = new CooldownHandler();
122 | }
123 | cooldownHandlerExport();
124 |
--------------------------------------------------------------------------------
/js/debug/debugHandler.js:
--------------------------------------------------------------------------------
1 | class DebugHandler extends Handler {
2 | /**
3 | * Create a new Debug handler.
4 | */
5 | constructor() {
6 | super('Debug', []);
7 | this.success();
8 | this.All = false;
9 | this.Chat = false;
10 | this.MQTT = false;
11 | this.OBS = false;
12 | this.Parser = false;
13 | this.SLOBS = false;
14 | this.Storage = false;
15 | this.StreamElements = false;
16 | this.Streamlabs = false;
17 | this.Twitch = false;
18 | this.Voicemod = false;
19 | }
20 |
21 | /**
22 | * Handle the input data (take an action).
23 | * @param {array} triggerData contents of trigger line
24 | */
25 | handleData = async (triggerData) => {
26 | var { handler } = Parser.getInputs(triggerData, ['handler'], false, 1);
27 | if (handler) {
28 | handler = handler.toLowerCase();
29 | switch (handler) {
30 | case 'chat':
31 | this.Chat = true;
32 | break;
33 | case 'mqtt':
34 | this.MQTT = true;
35 | break;
36 | case 'obs':
37 | this.OBS = true;
38 | break;
39 | case 'parser':
40 | this.Parser = true;
41 | break;
42 | case 'slobs':
43 | this.SLOBS = true;
44 | break;
45 | case 'storage':
46 | this.Storage = true;
47 | break;
48 | case 'sl':
49 | case 'streamlabs':
50 | this.Streamlabs = true;
51 | break;
52 | case 'se':
53 | case 'streamelements':
54 | this.StreamElements = true;
55 | break;
56 | case 'twitch':
57 | this.Twitch = true;
58 | break;
59 | case 'voicemod':
60 | this.Voicemod = true;
61 | break;
62 | default:
63 | break;
64 | }
65 | } else {
66 | this.All = true;
67 | }
68 | }
69 | }
70 |
71 | /**
72 | * Create a handler
73 | */
74 | let Debug;
75 | function debugHandlerExport() {
76 | Debug = new DebugHandler();
77 | }
78 | debugHandlerExport();
79 |
--------------------------------------------------------------------------------
/js/discord/discordHandler.js:
--------------------------------------------------------------------------------
1 | class DiscordHandler extends Handler {
2 | /**
3 | * Create a new Discord handler.
4 | */
5 | constructor() {
6 | super('Discord', []);
7 | this.webhookUrls = {};
8 | this.webhookMessageIds = {};
9 | this.webhooks = {};
10 | this.success();
11 | }
12 |
13 | /**
14 | * Handle the input data (take an action).
15 | * @param {array} triggerData contents of trigger line
16 | */
17 | handleData = async (triggerData) => {
18 | var action = Parser.getAction(triggerData, 'Discord');
19 |
20 | switch (action) {
21 | case 'clear':
22 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
23 | delete this.webhooks[name];
24 | break;
25 | case 'color':
26 | var { name, color } = Parser.getInputs(triggerData, ['action', 'name', 'color']);
27 | this.initialize(name);
28 | var color = triggerData.slice(3).join(' ');
29 | if (color.charAt(0) === '#') {
30 | color = color.substring(1);
31 | }
32 | var colorNum = parseInt(color, 16);
33 | if (!isNaN(colorNum)) {
34 | this.webhooks[name].embed.color = colorNum;
35 | }
36 | break;
37 | case 'create':
38 | var { name, url } = Parser.getInputs(triggerData, ['action', 'name', 'url']);
39 | this.webhookUrls[name] = url;
40 | break;
41 | case 'delete':
42 | var { name, messageId } = Parser.getInputs(triggerData, ['action', 'name', 'messageId'], false, 1);
43 | messageId = messageId || this.webhookMessageIds[name];
44 | var data = await callAPI('DELETE', `${this.webhooks[name].url}/messages/${messageId}`);
45 | break;
46 | case 'description':
47 | var { name, description } = Parser.getInputs(triggerData, ['action', 'name', 'description']);
48 | this.initialize(name);
49 | this.webhooks[name].embed.description = description;
50 | break;
51 | case 'field':
52 | var { name, header, value, inline } = Parser.getInputs(triggerData, ['action', 'name', 'header', 'value', 'inline'], false, 1);
53 | this.initialize(name);
54 | var field = {
55 | name: header,
56 | value: value
57 | };
58 | if (inline && inline.toLowerCase() === 'true') {
59 | field['inline'] = true;
60 | }
61 | if (!this.webhooks[name].embed.fields) {
62 | this.webhooks[name].embed.fields = [];
63 | }
64 | this.webhooks[name].embed.fields.push(field);
65 | break;
66 | case 'file':
67 | var { name, file } = Parser.getInputs(triggerData, ['action', 'name', 'file']);
68 | this.initialize(name);
69 | this.webhooks[name].file = file;
70 | break;
71 | case 'footericon':
72 | var { name, icon } = Parser.getInputs(triggerData, ['action', 'name', 'icon']);
73 | this.initialize(name);
74 | if (!this.webhooks[name].embed.footer) {
75 | this.webhooks[name].embed.footer = {};
76 | }
77 | this.webhooks[name].embed.footer.icon_url = icon;
78 | break;
79 | case 'footertext':
80 | var { name, text } = Parser.getInputs(triggerData, ['action', 'name', 'text']);
81 | this.initialize(name);
82 | if (!this.webhooks[name].embed.footer) {
83 | this.webhooks[name].embed.footer = {};
84 | }
85 | this.webhooks[name].embed.footer.text = text;
86 | break;
87 | case 'image':
88 | var { name, image } = Parser.getInputs(triggerData, ['action', 'name', 'image']);
89 | this.initialize(name);
90 | this.webhooks[name].embed.image = { url: image };
91 | break;
92 | case 'message':
93 | var { name, message } = Parser.getInputs(triggerData, ['action', 'name', 'message']);
94 | this.initialize(name);
95 | this.webhooks[name].content = message;
96 | break;
97 | case 'send':
98 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
99 | this.initialize(name);
100 | var json = {};
101 | if (this.webhooks[name].content) {
102 | json.content = this.webhooks[name].content;
103 | }
104 |
105 | if (this.webhooks[name].embed && Object.keys(this.webhooks[name].embed).length > 0) {
106 | json.embeds = [this.webhooks[name].embed];
107 | }
108 |
109 | var data;
110 |
111 | // Try to get file information and send as form data
112 | if (this.webhooks[name].file) {
113 | var fd = new FormData();
114 | var fileObj = await convertUrlToFileObj(this.webhooks[name].file);
115 |
116 | if (fileObj !== null) {
117 | fd.append( 'file', fileObj );
118 | fd.append( 'payload_json', JSON.stringify(json) );
119 | data = await callAPI('POST', `${this.webhooks[name].url}?wait=true`, fd, {}, { contentType: false, processData: false });
120 | }
121 | }
122 |
123 | // Otherwise, send message data as JSON
124 | if (data == undefined) {
125 | data = await callAPI('POST', `${this.webhooks[name].url}?wait=true`, JSON.stringify(json), { "Content-Type": "application/json" });
126 | }
127 |
128 | this.webhookMessageIds[name] = data.id;
129 | return { "discord_msg_id": data.id };
130 | break;
131 | case 'thumbnail':
132 | var { name, thumbnail } = Parser.getInputs(triggerData, ['action', 'name', 'thumbnail']);
133 | this.initialize(name);
134 | this.webhooks[name].embed.thumbnail = { url: thumbnail };
135 | break;
136 | case 'title':
137 | var { name, title } = Parser.getInputs(triggerData, ['action', 'name', 'title']);
138 | this.initialize(name);
139 | this.webhooks[name].embed.title = title;
140 | break;
141 | case 'update':
142 | var { name, messageId } = Parser.getInputs(triggerData, ['action', 'name', 'messageId'], false, 1);
143 | this.initialize(name);
144 | messageId = messageId || this.webhookMessageIds[name];
145 |
146 | var json = {};
147 | if (this.webhooks[name].content) {
148 | json.content = this.webhooks[name].content;
149 | }
150 |
151 | if (this.webhooks[name].embed) {
152 | json.embeds = [this.webhooks[name].embed];
153 | }
154 | var data = await callAPI('PATCH', `${this.webhooks[name].url}/messages/${messageId}`, JSON.stringify(json), { "Content-Type": "application/json" });
155 | break;
156 | case 'url':
157 | var { name, url } = Parser.getInputs(triggerData, ['action', 'name', 'url']);
158 | this.initialize(name);
159 | this.webhooks[name].embed.url = url;
160 | break;
161 | }
162 | }
163 |
164 | /**
165 | * Initialize the webhook data if it does not exist.
166 | * @param {string} name webhook name
167 | */
168 | initialize = (name) => {
169 | if (!this.webhooks[name]) {
170 | this.webhooks[name] = { content: "", url: this.webhookUrls[name], embed: {}, file: "" };
171 | }
172 | }
173 | }
174 |
175 | /**
176 | * Create a handler
177 | */
178 | function discordHandlerExport() {
179 | var discord = new DiscordHandler();
180 | }
181 | discordHandlerExport();
182 |
--------------------------------------------------------------------------------
/js/handler.js:
--------------------------------------------------------------------------------
1 | class Handler {
2 | /**
3 | * Create a new handler.
4 | * @param {string} parserName name to use for the handler
5 | * @param {array} triggers list of triggers this handler is responsible for
6 | */
7 | constructor(parserName, triggers) {
8 | this.parserName = parserName;
9 | triggers = triggers || [];
10 |
11 | // Configure handler with controller
12 | controller.addParser(parserName, this);
13 | triggers.forEach(trigger => {
14 | controller.addTrigger(trigger, parserName);
15 | });
16 | }
17 |
18 | /**
19 | * Register trigger as successfully initialized.
20 | */
21 | success = () => {
22 | controller.addSuccess(this.parserName);
23 | }
24 |
25 | /**
26 | * Register trigger from user input.
27 | * @param {string} trigger name to use for the handler
28 | * @param {array} triggerLine contents of trigger line
29 | * @param {number} id of the new trigger
30 | */
31 | addTriggerData = (trigger, triggerLine, triggerId) => {
32 | return;
33 | }
34 |
35 | /**
36 | * Handle the input data (take an action).
37 | * @param {array} triggerData contents of trigger line
38 | * @param {array} triggerParams current trigger parameters
39 | */
40 | handleData = async (triggerData, triggerParams) => {
41 | return;
42 | }
43 |
44 | /**
45 | * Called before parsing user input.
46 | */
47 | preParse = () => {
48 | return;
49 | }
50 |
51 | /**
52 | * Called after parsing all user input.
53 | */
54 | postParse = () => {
55 | return;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/js/index.js:
--------------------------------------------------------------------------------
1 | // Do stuff if the document is fully loaded
2 | $(document).ready(async function() {
3 | var version = await readFile("version.txt");
4 | console.error(`Kruiz Control ${version.trim()} Initialized`);
5 | var data = await readFile("triggers.txt");
6 | await readTriggerFile(data);
7 | });
8 |
9 | /**
10 | * Read all the file triggers
11 | * @param {string} data list of files to parse
12 | */
13 | async function readTriggerFile(data) {
14 | controller.parseInput(data, false);
15 | var files = await readFile("fileTriggers.txt");
16 | await readFileTriggers(files);
17 | }
18 |
19 | /**
20 | * Read all the file triggers
21 | * @param {string} data list of files to parse
22 | */
23 | async function readFileTriggers(data) {
24 | data = data.trim();
25 | var lines = data.split(/\r\n|\n/);
26 |
27 | for (var i = 0; i < lines.length; i++) {
28 | var line = lines[i];
29 | if (!line.startsWith('#') && line.trim().length > 0) {
30 | try {
31 | var input = await readFile('triggers/' + line);
32 | controller.parseInput(input, true);
33 | } catch (error) {
34 | console.error(`Check that the ${line} file exists in the triggers folder`);
35 | }
36 | }
37 | }
38 |
39 | controller.doneParsing();
40 | setTimeout(function() {
41 | controller.runInit();
42 | }, 2000);
43 | }
44 |
--------------------------------------------------------------------------------
/js/list/listHandler.js:
--------------------------------------------------------------------------------
1 | class ListHandler extends Handler {
2 | GLOBAL_LIST_KEY = '__kc_list_global_lists';
3 | GLOBAL_LIST_KEY_PREFIX = '__kc_list_global_list_';
4 |
5 | /**
6 | * Create a new List handler.
7 | */
8 | constructor() {
9 | super('List', []);
10 | this.success();
11 | this.lists = {};
12 | }
13 |
14 | /**
15 | * Called after parsing all user input.
16 | */
17 | postParse = async () => {
18 | this.global_lists = await IDBService.get(this.GLOBAL_LIST_KEY) || [];
19 | for (const list_id of this.global_lists) {
20 | this.lists[list_id] = await IDBService.get(`${this.GLOBAL_LIST_KEY_PREFIX}${list_id}`) || [];
21 | };
22 | }
23 |
24 | /**
25 | * Handle the input data (take an action).
26 | * @param {array} triggerData contents of trigger line
27 | */
28 | handleData = async (triggerData) => {
29 | var action = Parser.getAction(triggerData, 'List');
30 |
31 | switch (action) {
32 | case 'add':
33 | var { name, value, index } = Parser.getInputs(triggerData, ['action', 'name', 'value', 'index'], false, 1);
34 | this.initialize(name);
35 | return this.add(name, value, parseInt(index));
36 | break;
37 | case 'contains':
38 | var { name, value } = Parser.getInputs(triggerData, ['action', 'name', 'value']);
39 | this.initialize(name);
40 | return { contains: this.lists[name].indexOf(value) !== -1 };
41 | break;
42 | case 'count':
43 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
44 | this.initialize(name);
45 | return { count: this.lists[name].length };
46 | break;
47 | case 'empty':
48 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
49 | this.initialize(name);
50 | this.lists[name] = [];
51 | if (this.global_lists.indexOf(name) !== -1) {
52 | IDBService.set(`${this.GLOBAL_LIST_KEY_PREFIX}${name}`, this.lists[name])
53 | }
54 | break;
55 | case 'export':
56 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
57 | this.initialize(name);
58 | return { [name]: JSON.stringify(this.lists[name]) };
59 | break;
60 | case 'get':
61 | var { name, index } = Parser.getInputs(triggerData, ['action', 'name', 'index'], false, 1);
62 | this.initialize(name);
63 | return this.get(name, index);
64 | break;
65 | case 'global':
66 | var { name, status } = Parser.getInputs(triggerData, ['action', 'name', 'status']);
67 | status = status.toLowerCase();
68 | this.initialize(name);
69 |
70 | var nameIndex = this.global_lists.indexOf(name);
71 | if (status === 'on' && nameIndex === -1) {
72 | this.global_lists.push(name);
73 | IDBService.set(this.GLOBAL_LIST_KEY, this.global_lists);
74 | IDBService.set(`${this.GLOBAL_LIST_KEY_PREFIX}${name}`, this.lists[name]);
75 | } else if (status === 'off' && nameIndex !== -1) {
76 | this.global_lists.splice(nameIndex, 1);
77 | IDBService.set(this.GLOBAL_LIST_KEY, this.global_lists);
78 | IDBService.delete(`${this.GLOBAL_LIST_KEY_PREFIX}${name}`);
79 | }
80 | break;
81 | case 'import':
82 | var { name, values } = Parser.getInputs(triggerData, ['action', 'name', 'values']);
83 | this.initialize(name);
84 | this.lists[name] = JSON.parse(values);
85 | if (this.global_lists.indexOf(name) !== -1) {
86 | IDBService.set(`${this.GLOBAL_LIST_KEY_PREFIX}${name}`, this.lists[name])
87 | }
88 | break;
89 | case 'index':
90 | var { name, value } = Parser.getInputs(triggerData, ['action', 'name', 'value']);
91 | this.initialize(name);
92 | var index = this.lists[name].indexOf(value);
93 | var position = index + 1;
94 | if (position === 0) {
95 | position = -1;
96 | }
97 | return { index: index, position: position };
98 | break;
99 | case 'join':
100 | var { name, delimiter } = Parser.getInputs(triggerData, ['action', 'name', 'delimiter']);
101 | this.initialize(name);
102 | return { joined: this.lists[name].join(delimiter) };
103 | break;
104 | case 'remove':
105 | var { name, index } = Parser.getInputs(triggerData, ['action', 'name', 'index'], false, 1);
106 | this.initialize(name);
107 | return this.remove(name, index);
108 | break;
109 | case 'set':
110 | var { name, index, value } = Parser.getInputs(triggerData, ['action', 'name', 'index', 'value']);
111 | this.initialize(name);
112 | return this.set(name, parseInt(index), value);
113 | break;
114 | case 'unique':
115 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
116 | this.initialize(name);
117 | this.lists[name] = [...new Set(this.lists[name])];
118 | break;
119 | default:
120 | // do nothing
121 | break;
122 | }
123 | }
124 |
125 | /**
126 | * Set up the list for the given name (if one doesn't exist).
127 | * @param {string} name of the list
128 | */
129 | initialize = (name) => {
130 | if (this.lists[name] === undefined) {
131 | this.lists[name] = [];
132 | }
133 | }
134 |
135 | /**
136 | * Add the value to the named list at the given index.
137 | * @param {string} name of the list
138 | * @param {string} value to add to the list
139 | * @param {string} index to add the value in the list
140 | */
141 | add = (name, value, index) => {
142 | if (!isNaN(index) && index < this.lists[name].length && index >= 0) {
143 | this.lists[name].splice(index, 0, value);
144 | if (this.global_lists.indexOf(name) !== -1) {
145 | IDBService.set(`${this.GLOBAL_LIST_KEY_PREFIX}${name}`, this.lists[name])
146 | }
147 | return { index: index, position: index + 1 }
148 | } else {
149 | this.lists[name].push(value);
150 | if (this.global_lists.indexOf(name) !== -1) {
151 | IDBService.set(`${this.GLOBAL_LIST_KEY_PREFIX}${name}`, this.lists[name])
152 | }
153 | return { index: this.lists[name].length - 1, position: this.lists[name].length }
154 | }
155 | }
156 |
157 | /**
158 | * Set the value at the given index in the named list.
159 | * @param {string} name of the list
160 | * @param {string} index to add the value in the list
161 | * @param {string} value to add to the list
162 | */
163 | set = (name, index, value) => {
164 | if (!isNaN(index) && index < this.lists[name].length && index >= 0) {
165 | this.lists[name][index] = value;
166 | if (this.global_lists.indexOf(name) !== -1) {
167 | IDBService.set(`${this.GLOBAL_LIST_KEY_PREFIX}${name}`, this.lists[name])
168 | }
169 | return { index: index, position: index + 1, value: value }
170 | }
171 | }
172 |
173 | /**
174 | * Get the integer index from the input index value.
175 | * @param {string} name of the list
176 | * @param {string} index to retrieve a numeric value
177 | */
178 | getIndexValue = (name, index) => {
179 | if (this.lists[name].length == 0) {
180 | return -1;
181 | }
182 | var intIndex = parseInt(index);
183 | if (isNaN(intIndex) && index !== undefined) {
184 | if (index.toLowerCase() === 'first') {
185 | return 0;
186 | } else if (index.toLowerCase() === 'last') {
187 | return this.lists[name].length - 1;
188 | }
189 | }
190 | if (isNaN(intIndex)) {
191 | return Math.floor(Math.random() * this.lists[name].length);
192 | }
193 | return intIndex;
194 | }
195 |
196 | /**
197 | * Get the value at the given index in the named list.
198 | * @param {string} name of the list
199 | * @param {string} index to add the value in the list
200 | */
201 | get = (name, index) => {
202 | var intIndex = this.getIndexValue(name, index);
203 | return this.getIndex(name, intIndex);
204 | }
205 |
206 | /**
207 | * Get the value at the given index in the named list.
208 | * @param {string} name of the list
209 | * @param {string} index to add the value in the list
210 | */
211 | getIndex = (name, index) => {
212 | if (index >= 0 && index < this.lists[name].length) {
213 | return { value: this.lists[name][index], index: index, position: index + 1 };
214 | } else {
215 | return { value: 'None found', index: -1, position: -1 };
216 | }
217 | }
218 |
219 | /**
220 | * Remove the value at the given index in the named list.
221 | * @param {string} name of the list
222 | * @param {string} index to add the value in the list
223 | */
224 | remove = (name, index) => {
225 | var intIndex = this.getIndexValue(name, index);
226 | var response = this.getIndex(name, intIndex);
227 | if (intIndex != -1) {
228 | this.lists[name].splice(intIndex, 1);
229 | if (this.global_lists.indexOf(name) !== -1) {
230 | IDBService.set(`${this.GLOBAL_LIST_KEY_PREFIX}${name}`, this.lists[name])
231 | }
232 | }
233 | return response;
234 | }
235 |
236 | /**
237 | * Create a named list from the provided items.
238 | * @param {string} name of the list
239 | * @param {array} items to add to the named list
240 | */
241 | createList = (name, items) => {
242 | this.initialize(name);
243 | items.forEach(item => {
244 | this.add(name, item);
245 | });
246 | }
247 | }
248 |
249 | /**
250 | * Create a handler
251 | */
252 | function listHandlerExport() {
253 | var list = new ListHandler();
254 | }
255 | listHandlerExport();
256 |
--------------------------------------------------------------------------------
/js/message/messageHandler.js:
--------------------------------------------------------------------------------
1 | class MessageHandler extends Handler {
2 | /**
3 | * Create a new Message handler.
4 | */
5 | constructor() {
6 | super('Message', ['OnMessage']);
7 | this.success();
8 | this.messages = [];
9 | this.messagesTriggers = {};
10 | }
11 |
12 | /**
13 | * Register trigger from user input.
14 | * @param {string} trigger name to use for the handler
15 | * @param {array} triggerLine contents of trigger line
16 | * @param {number} id of the new trigger
17 | */
18 | addTriggerData = (trigger, triggerLine, triggerId) => {
19 | var { messages } = Parser.getInputs(triggerLine, ['messages'], true);
20 | // Handles aliases for OnMessage
21 | messages.forEach(message => {
22 | if (this.messages.indexOf(message) === -1) {
23 | this.messages.push(message);
24 | this.messagesTriggers[message] = [];
25 | }
26 | this.messagesTriggers[message].push(triggerId);
27 | });
28 | }
29 |
30 | /**
31 | * Handle the input data (take an action).
32 | * @param {array} triggerData contents of trigger line
33 | */
34 | handleData = async (triggerData) => {
35 | var action = Parser.getAction(triggerData, 'Message');
36 | if (action === 'send') {
37 | var { message, data } = Parser.getInputs(triggerData, ['action', 'message', 'data'], false, 1);
38 | data = data || '';
39 | if (this.messages.indexOf(message) !== -1) {
40 | this.messagesTriggers[message].forEach((triggerId) => {
41 | controller.handleData(triggerId, {
42 | message: message,
43 | data: data
44 | });
45 | })
46 | }
47 | if (this.messages.indexOf('*') !== -1) {
48 | this.messagesTriggers['*'].forEach((triggerId) => {
49 | controller.handleData(triggerId, {
50 | message: message,
51 | data: data
52 | });
53 | })
54 | }
55 | }
56 | }
57 | }
58 |
59 | /**
60 | * Create a handler
61 | */
62 | function messageHandlerExport() {
63 | var message = new MessageHandler();
64 | }
65 | messageHandlerExport();
66 |
--------------------------------------------------------------------------------
/js/mqtt/mqtt-websocket.js:
--------------------------------------------------------------------------------
1 | class MQTTWebSocket {
2 |
3 | constructor(address, username, password, onConnect) {
4 | this.address = address;
5 | this.topics = [];
6 |
7 | const options = {
8 | // Clean session
9 | clean: true,
10 | connectTimeout: 4000,
11 | };
12 |
13 | if (username !== undefined && username !== '' && password !== undefined && password !== '') {
14 | options.username = username;
15 | options.password = password;
16 | }
17 |
18 | this.client = mqtt.connect(this.address, options);
19 | this.client.on('connect', onConnect);
20 | this.client.on('message', (topic, message) => {
21 | if (Debug.All || Debug.MQTT) {
22 | console.error(`MQTT Client ${topic} message received: ${message.toString()}`);
23 | }
24 | // message is Buffer
25 | var s_message = message.toString();
26 | this._handleCallbacks(topic, s_message);
27 | });
28 | }
29 |
30 | publish = async (topic, message) => {
31 | if (!this.client.connected) {
32 | return;
33 | }
34 | this.client.publish(topic, message);
35 | }
36 |
37 | subscribe = async (topic, callback) => {
38 | if (this.topics[topic] === undefined) {
39 | this.topics[topic] = [];
40 | this.client.subscribe(topic);
41 | }
42 | this.topics[topic].push(callback);
43 | }
44 |
45 | _handleCallbacks = (topic, message) => {
46 | if (this.topics[topic] !== undefined) {
47 | this.topics[topic].forEach(callback => {
48 | callback(message);
49 | });
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/js/mqtt/mqttHandler.js:
--------------------------------------------------------------------------------
1 | class MQTTHandler extends Handler {
2 | /**
3 | * Create a new MQTT handler.
4 | */
5 | constructor() {
6 | super('MQTT', ['OnMQTT']);
7 | }
8 |
9 | /**
10 | * Initialize the connection to MQTT broker with the input settings.
11 | * @param {string} address broker websocket address
12 | * @param {string} address username to use for the connection, if any
13 | * @param {string} address password to use for the connection, if any
14 | */
15 | init = (address, username, password) => {
16 | this.mqtt = new MQTTWebSocket(address, username, password, this.success);
17 | }
18 |
19 | /**
20 | * Register trigger from user input.
21 | * @param {string} trigger name to use for the handler
22 | * @param {array} triggerLine contents of trigger line
23 | * @param {number} id of the new trigger
24 | */
25 | addTriggerData = (trigger, triggerLine, triggerId) => {
26 | trigger = trigger.toLowerCase();
27 | switch (trigger) {
28 | case 'onmqtt':
29 | var { topic } = Parser.getInputs(triggerLine, ['topic'], true);
30 | this.mqtt.subscribe(topic, function (message) {
31 | if (Debug.All || Debug.MQTT) {
32 | console.error(`MQTT ${topic} message received: ${message}`);
33 | }
34 | controller.handleData(triggerId, {
35 | topic: topic,
36 | message: message
37 | });
38 | });
39 | break;
40 | default:
41 | // Do nothing
42 | }
43 | }
44 |
45 | /**
46 | * Handle the input data (take an action).
47 | * @param {array} triggerData contents of trigger line
48 | */
49 | handleData = async (triggerData) => {
50 | var action = Parser.getAction(triggerData, 'MQTT');
51 | switch (action) {
52 | case 'publish':
53 | var { topic, message } = Parser.getInputs(triggerData, ['action', 'topic', 'message']);
54 | await this.mqtt.publish(topic, message);
55 | break;
56 | default:
57 | console.error(`Unable to determine the MQTT to be taken. Found: "${action}" within ${JSON.stringify(triggerData)}.`);
58 | break;
59 | }
60 | }
61 | }
62 |
63 | /**
64 | * Create a handler and read user settings
65 | */
66 | async function mqttHandlerExport() {
67 | var mqttHandler = new MQTTHandler();
68 | var address = await readFile('settings/mqtt/websocket.txt');
69 | var username = await readFile('settings/mqtt/username.txt');
70 | var password = await readFile('settings/mqtt/password.txt');
71 | mqttHandler.init(address.trim(), username.trim(), password.trim());
72 | }
73 | mqttHandlerExport();
74 |
--------------------------------------------------------------------------------
/js/obs/obs-wrapper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Connect to the OBS websocket and setup the event handlers
3 | * @param {string} address obs websocket address
4 | * @param {string} password obs websocket password
5 | * @param {Handler} obsHandler handler to mark successful initialization
6 | * @param {function} onSwitchScenes handle switch scene messages
7 | * @param {function} onTransitionBegin handle transition starts
8 | * @param {function} onStreamStateChange handle stream state change messages
9 | * @param {function} onRecordingStateChange handle recording state change messages
10 | * @param {function} onCustomMessage handle custom messages
11 | * @param {function} onOBSSourceVisibility handle scene item visibility changes
12 | * @param {function} onOBSSourceFilterVisibility handle source filter visibility changes
13 | */
14 | function connectOBSWebsocket(address, password, obsHandler, onSwitchScenes, onTransitionBegin, onStreamStateChange, onRecordingStateChange, onCustomMessage, onOBSSourceVisibility, onOBSSourceFilterVisibility) {
15 | var obs = new OBSWebSocket();
16 | obs.connect(address, password).then(async () => {
17 | const initialize = () => {
18 | obs.getVersion().then(async data => {
19 | if (data === undefined) {
20 | console.error("Initial OBS request failed. Retrying...")
21 | setTimeout(initialize, 500);
22 | } else {
23 | console.error(`Kruiz Control connected to the OBS Websocket v${data.obsWebSocketVersion}`);
24 | obsHandler.setCurrentScene(await obs.getCurrentScene());
25 | obsHandler.success();
26 | }
27 | });
28 | }
29 | initialize();
30 | }).catch(err => { // Promise convention dictates you have a catch on every chain.
31 | console.error(JSON.stringify(err));
32 | });
33 |
34 |
35 | // You must add this handler to avoid uncaught exceptions.
36 | obs.on('error', err => {
37 | console.error('OBS Websocket Error:', err);
38 | });
39 |
40 | // Ws OnClose : try reconnect
41 | obs.on('onclose', function() {
42 | console.error('OBS Websocket Closed');
43 | });
44 |
45 | obs.on('Exiting', function() {
46 | console.error('OBS Websocket Exiting');
47 | obs.disconnect();
48 | });
49 |
50 | obs.on('CurrentProgramSceneChanged', onSwitchScenes);
51 | obs.on('SceneTransitionStarted', onTransitionBegin);
52 | obs.on('StreamStateChanged', onStreamStateChange);
53 | obs.on('RecordStateChanged', onRecordingStateChange);
54 | obs.on('CustomEvent', onCustomMessage);
55 | obs.on('SceneItemEnableStateChanged', onOBSSourceVisibility);
56 | obs.on('SourceFilterEnableStateChanged', onOBSSourceFilterVisibility);
57 |
58 | obs.getInputSettings = async function(source) {
59 | return await this.call('GetInputSettings', {
60 | 'inputName': source
61 | }).then(data => {
62 | return data;
63 | })
64 | }
65 |
66 | obs.getSceneItemId = async function(scene, source) {
67 | return await obs.call('GetSceneItemId', {
68 | sceneName: scene,
69 | sourceName: source
70 | }).then(data => {
71 | return data.sceneItemId;
72 | }).catch(async err => {
73 | if (err.code === 600) {
74 | var group = await this.getSourceGroupName(scene, source);
75 | if (group) {
76 | return await this.getSceneItemId(group, source);
77 | }
78 | }
79 | console.error(JSON.stringify(err));
80 | });
81 | }
82 |
83 | obs.getSceneItemName = async function(scene, sceneItemId) {
84 | return await this.call('GetSceneItemList', {
85 | sceneName: scene
86 | }).then(data => {
87 | for (var item of data.sceneItems) {
88 | if (item.sceneItemId === sceneItemId) {
89 | return item.sourceName;
90 | }
91 | };
92 | }).catch(async err => {
93 | if (err.code === 602) {
94 | return await this.call('GetGroupSceneItemList', {
95 | sceneName: scene
96 | }).then(data => {
97 | for (var item of data.sceneItems) {
98 | if (item.sceneItemId === sceneItemId) {
99 | return item.sourceName;
100 | }
101 | };
102 | }).catch(groupError => {
103 | console.error(JSON.stringify(groupError));
104 | });
105 | } else {
106 | console.error(JSON.stringify(err));
107 | }
108 | });
109 | }
110 |
111 | obs.getScenesForGroup = async function(group) {
112 | return await this.call('GetSceneList').then(async data => {
113 | var scenes = data.scenes.map(item => item.sceneName)
114 | if (scenes.indexOf(group) !== -1) {
115 | return [group];
116 | }
117 | var groupScenes = [];
118 | for (var item of scenes) {
119 | await this.call('GetSceneItemList', {
120 | sceneName: item
121 | }).then(sceneItems => {
122 | for (var subitem of sceneItems.sceneItems) {
123 | if (subitem.sourceName === group) {
124 | groupScenes.push(item);
125 | }
126 | };
127 | }).catch(sceneItemsError => {
128 | console.error(JSON.stringify(sceneItemsError));
129 | });
130 | };
131 | return groupScenes;
132 | }).catch(groupError => {
133 | console.error(JSON.stringify(groupError));
134 | });
135 | }
136 |
137 | // Get the group the contains the provided source within the given scene
138 | obs.getSourceGroupName = async function(scene, source) {
139 | return await this.call('GetSceneItemList', {
140 | sceneName: scene
141 | }).then(async data => {
142 | // Identify groups
143 | for (var item of data.sceneItems) {
144 | if (item.isGroup) {
145 | // Get sources within group until match is found
146 | var result = await this.call('GetGroupSceneItemList', {
147 | sceneName: item.sourceName
148 | }).then(data => {
149 | for (var subitem of data.sceneItems) {
150 | if (subitem.sourceName === source) {
151 | return item.sourceName;
152 | }
153 | };
154 | }).catch(groupError => {
155 | console.error(JSON.stringify(groupError));
156 | });
157 | if (result) {
158 | return result;
159 | }
160 | }
161 | }
162 | }).catch(async err => {
163 | console.error(JSON.stringify(err));
164 | });
165 | }
166 |
167 | obs.setInputSettings = async function(source, inputSettings) {
168 | await this.call('SetInputSettings', {
169 | 'inputName': source,
170 | 'inputSettings': inputSettings
171 | }).catch(err => {
172 | console.error(JSON.stringify(err));
173 | });
174 | }
175 |
176 | obs.getMediaInputStatus = async function(source) {
177 | return await this.call('GetMediaInputStatus', {
178 | 'inputName': source
179 | }).then(data => {
180 | return data;
181 | }).catch(err => {
182 | console.error(JSON.stringify(err));
183 | });
184 | }
185 |
186 | obs.getMediaDuration = async function(source) {
187 | return await this.getMediaInputStatus(source)
188 | .then(data => {
189 | return data.mediaDuration;
190 | }).catch(err => {
191 | console.error(JSON.stringify(err));
192 | });
193 | }
194 |
195 | obs.getCurrentScene = async function() {
196 | return await this.call('GetCurrentProgramScene')
197 | .then(data => {
198 | return data.currentProgramSceneName;
199 | }).catch(err => {
200 | console.error(JSON.stringify(err));
201 | });
202 | };
203 |
204 | obs.getSourceVisibility = async function(scene, source) {
205 | return await this.call('GetSceneItemEnabled', {
206 | sceneName: scene,
207 | sceneItemId: await this.getSceneItemId(scene, source)
208 | }).then(data => {
209 | return data.sceneItemEnabled;
210 | }).catch(async err => {
211 | if (err.code === 600) {
212 | var group = await this.getSourceGroupName(scene, source);
213 | if (group) {
214 | return await this.getSourceVisibility(group, source);
215 | }
216 | }
217 | console.error(JSON.stringify(err));
218 | });
219 | };
220 |
221 | obs.getSourceActiveStatus = async function(source) {
222 | return await this.call('GetSourceActive', {
223 | 'sourceName': source
224 | }).then(data => {
225 | return data.videoActive;
226 | }).catch(err => {
227 | console.error(JSON.stringify(err));
228 | });
229 | };
230 |
231 | obs.setSourceVisibility = async function(source, enabled, scene) {
232 | if (!scene) {
233 | scene = await this.getCurrentScene();
234 | }
235 | await this.call('SetSceneItemEnabled', {
236 | 'sceneItemId': await this.getSceneItemId(scene, source),
237 | 'sceneName': scene,
238 | 'sceneItemEnabled': enabled
239 | }).catch(async err => {
240 | if (err.code === 600) {
241 | var group = await this.getSourceGroupName(scene, source);
242 | if (group) {
243 | await this.setSourceVisibility(source, enabled, group);
244 | return;
245 | }
246 | }
247 | console.error(JSON.stringify(err));
248 | });
249 | };
250 |
251 | obs.setFilterVisibility = async function(source, filter, enabled) {
252 | await this.call('SetSourceFilterEnabled', {
253 | 'sourceName': source,
254 | 'filterName': filter,
255 | 'filterEnabled': enabled
256 | }).catch(err => {
257 | console.error(JSON.stringify(err));
258 | });
259 | };
260 |
261 | obs.setCurrentScene = async function(scene) {
262 | await this.call('SetCurrentProgramScene', {
263 | 'sceneName': scene
264 | }).catch(err => {
265 | console.error(JSON.stringify(err));
266 | });
267 | };
268 |
269 | obs.setMute = async function(source, enabled) {
270 | await this.call('SetInputMute', {
271 | 'inputName': source,
272 | 'inputMuted': enabled
273 | }).catch(err => {
274 | console.error(JSON.stringify(err));
275 | });
276 | };
277 |
278 | obs.toggleMute = async function(source) {
279 | return await this.call('ToggleInputMute', {
280 | 'inputName': source,
281 | }).then(data => {
282 | return data.inputMuted;
283 | }).catch(err => {
284 | console.error(JSON.stringify(err));
285 | });
286 | };
287 |
288 | obs.getVersion = async function() {
289 | return await this.call('GetVersion')
290 | .then(data => {
291 | return data;
292 | }).catch(err => {
293 | console.error(JSON.stringify(err));
294 | });
295 | };
296 |
297 | obs.getVolume = async function(source) {
298 | return await this.call('GetInputVolume', {
299 | 'inputName': source
300 | }).then(data => {
301 | return data;
302 | }).catch(err => {
303 | console.error(JSON.stringify(err));
304 | });
305 | };
306 |
307 | obs.setVolume = async function(source, volume, useDecibel) {
308 | const inputVolumeType = (useDecibel === true) ? 'inputVolumeDb' : 'inputVolumeMul';
309 | await this.call('SetInputVolume', {
310 | 'inputName': source,
311 | [inputVolumeType]: volume,
312 | }).catch(err => {
313 | console.error(JSON.stringify(err));
314 | });
315 | };
316 |
317 | obs.takeSourceScreenshot = async function(source, filePath) {
318 | let imageFormat = filePath.split('.').pop();
319 | await this.call('SaveSourceScreenshot', {
320 | 'sourceName': source,
321 | 'imageFilePath': filePath,
322 | 'imageFormat': imageFormat
323 | }).catch(err => {
324 | console.error(JSON.stringify(err));
325 | });
326 | };
327 |
328 | obs.triggerMediaInputAction = async function(sourceName, mediaAction) {
329 | await this.call('TriggerMediaInputAction', {
330 | 'inputName': sourceName,
331 | 'mediaAction': mediaAction
332 | }).catch(err => {
333 | console.error(JSON.stringify(err));
334 | });
335 | };
336 |
337 | obs.startStream = async function() {
338 | await this.call('StartStream').catch(err => {
339 | console.error(JSON.stringify(err));
340 | });
341 | };
342 |
343 | obs.stopStream = async function() {
344 | await this.call('StopStream').catch(err => {
345 | console.error(JSON.stringify(err));
346 | });
347 | };
348 |
349 | obs.getRecordingStatus = async function() {
350 | return await this.call('GetRecordStatus').then(data => {
351 | return data;
352 | }).catch(err => {
353 | console.error(JSON.stringify(err));
354 | });
355 | };
356 |
357 | obs.pauseRecording = async function() {
358 | await this.call('PauseRecord').catch(err => {
359 | console.error(JSON.stringify(err));
360 | });
361 | };
362 |
363 | obs.resumeRecording = async function() {
364 | await this.call('ResumeRecord').catch(err => {
365 | console.error(JSON.stringify(err));
366 | });
367 | };
368 |
369 | obs.startRecording = async function() {
370 | await this.call('StartRecord').catch(err => {
371 | console.error(JSON.stringify(err));
372 | });
373 | };
374 |
375 | obs.stopRecording = async function() {
376 | await this.call('StopRecord').catch(err => {
377 | console.error(JSON.stringify(err));
378 | });
379 | };
380 |
381 | obs.startReplayBuffer = async function() {
382 | await this.call('StartReplayBuffer').catch(err => {
383 | console.error(JSON.stringify(err));
384 | });
385 | };
386 |
387 | obs.stopReplayBuffer = async function() {
388 | await this.call('StopReplayBuffer').catch(err => {
389 | console.error(JSON.stringify(err));
390 | });
391 | };
392 |
393 | obs.saveReplayBuffer = async function() {
394 | await this.call('SaveReplayBuffer').catch(err => {
395 | console.error(JSON.stringify(err));
396 | });
397 | };
398 |
399 | obs.broadcastCustomMessage = async function(message, data) {
400 | await this.call('BroadcastCustomEvent', {
401 | 'eventData': {
402 | 'realm': 'kruiz-control',
403 | 'data': {
404 | 'message': message,
405 | 'data': data
406 | }
407 | }
408 | }).catch(err => {
409 | console.error(JSON.stringify(err));
410 | });
411 | };
412 |
413 | obs.refreshBrowser = async function(sourceName) {
414 | await this.call('PressInputPropertiesButton', {
415 | 'inputName': sourceName,
416 | 'propertyName': 'refreshnocache'
417 | }).catch(err => {
418 | console.error(JSON.stringify(err));
419 | });
420 | };
421 |
422 | obs.addSceneItem = async function(scene, source, status) {
423 | await this.call('CreateSceneItem', {
424 | 'sceneName': scene,
425 | 'sourceName': source,
426 | 'sceneItemEnabled': status
427 | }).catch(err => {
428 | console.error(JSON.stringify(err));
429 | });
430 | };
431 |
432 | obs.getSceneItemTransform = async function(scene, source) {
433 | var data = await this.call('GetSceneItemTransform', {
434 | 'sceneName': scene,
435 | 'sceneItemId': await this.getSceneItemId(scene, source)
436 | }).catch(async err => {
437 | if (err.code === 600) {
438 | var group = await this.getSourceGroupName(scene, source);
439 | if (group) {
440 | return await this.getSceneItemTransform(group, source);
441 | }
442 | }
443 | console.error(JSON.stringify(err));
444 | });
445 | return data;
446 | }
447 |
448 | obs.getSceneItemPosition = async function(scene, source) {
449 | return await this.getSceneItemTransform(scene, source)
450 | .then(data => {
451 | return {
452 | x: data.sceneItemTransform.positionX,
453 | y: data.sceneItemTransform.positionY
454 | };
455 | });
456 | }
457 |
458 | obs.setSceneItemPosition = async function(scene, source, x, y) {
459 | await this.call('SetSceneItemTransform', {
460 | 'sceneName': scene,
461 | 'sceneItemId': await this.getSceneItemId(scene, source),
462 | 'sceneItemTransform': {
463 | 'positionX': x,
464 | 'positionY': y
465 | }
466 | }).catch(async err => {
467 | if (err.code === 600) {
468 | var group = await this.getSourceGroupName(scene, source);
469 | if (group) {
470 | await this.setSceneItemPosition(group, source, x, y);
471 | return;
472 | }
473 | }
474 | console.error(JSON.stringify(err));
475 | });
476 | };
477 |
478 | obs.getSourceFilter = async function(source, filter) {
479 | return await this.call('GetSourceFilter', {
480 | 'sourceName': source,
481 | 'filterName': filter
482 | }).catch(err => {
483 | console.error(JSON.stringify(err));
484 | });
485 | }
486 |
487 | obs.getSceneItemSize = async function(scene, source) {
488 | return await this.getSceneItemTransform(scene, source)
489 | .then(data => {
490 | return {
491 | height: data.sceneItemTransform.height,
492 | sourceHeight: data.sceneItemTransform.sourceHeight,
493 | sourceWidth: data.sceneItemTransform.sourceWidth,
494 | width: data.sceneItemTransform.width
495 | };
496 | }).catch(err => {
497 | console.error(JSON.stringify(err));
498 | });
499 | }
500 |
501 | obs.setSceneItemSize = async function(scene, source, scaleX, scaleY) {
502 | await this.call('SetSceneItemTransform', {
503 | 'sceneName': scene,
504 | 'sceneItemId': await this.getSceneItemId(scene, source),
505 | 'sceneItemTransform': {
506 | 'scaleX': scaleX,
507 | 'scaleY': scaleY
508 | }
509 | }).catch(async err => {
510 | if (err.code === 600) {
511 | var group = await this.getSourceGroupName(scene, source);
512 | if (group) {
513 | await this.setSceneItemSize(group, source, scaleX, scaleY);
514 | return;
515 | }
516 | }
517 | console.error(JSON.stringify(err));
518 | });
519 | };
520 |
521 | obs.setSceneItemRotation = async function(scene, source, rotation) {
522 | await this.call('SetSceneItemTransform', {
523 | 'sceneName': scene,
524 | 'sceneItemId': await this.getSceneItemId(scene, source),
525 | 'sceneItemTransform': {
526 | 'rotation': rotation
527 | }
528 | }).catch(async err => {
529 | if (err.code === 600) {
530 | var group = await this.getSourceGroupName(scene, source);
531 | if (group) {
532 | await this.setSceneItemRotation(group, source, rotation);
533 | return;
534 | }
535 | }
536 | console.error(JSON.stringify(err));
537 | });
538 | };
539 |
540 | obs.getCurrentTransition = async function() {
541 | return await this.call('GetCurrentSceneTransition')
542 | .then(data => {
543 | return data.transitionName;
544 | }).catch(err => {
545 | console.error(JSON.stringify(err));
546 | });
547 | };
548 |
549 | obs.setCurrentTransition = async function(transition) {
550 | await this.call('SetCurrentSceneTransition', {
551 | 'transitionName': transition
552 | }).catch(err => {
553 | console.error(JSON.stringify(err));
554 | });
555 | };
556 |
557 | obs.getStats = async function() {
558 | return await this.call('GetStats').then(data => {
559 | return data;
560 | }).catch(err => {
561 | console.error(JSON.stringify(err));
562 | });
563 | }
564 |
565 | obs.getStreamStatus = async function() {
566 | return await this.call('GetStreamStatus').then(data => {
567 | return data;
568 | }).catch(err => {
569 | console.error(JSON.stringify(err));
570 | });
571 | }
572 |
573 | obs.getSceneItemIndex = async function(scene, source) {
574 | var data = await this.call('GetSceneItemIndex', {
575 | 'sceneName': scene,
576 | 'sceneItemId': await this.getSceneItemId(scene, source)
577 | }).catch(async err => {
578 | if (err.code === 600) {
579 | var group = await this.getSourceGroupName(scene, source);
580 | if (group) {
581 | return await this.getSceneItemIndex(group, source);
582 | }
583 | }
584 | console.error(JSON.stringify(err));
585 | });
586 | return data;
587 | }
588 |
589 | obs.setSceneItemIndex = async function(scene, source, index) {
590 | await this.call('SetSceneItemIndex', {
591 | 'sceneName': scene,
592 | 'sceneItemId': await this.getSceneItemId(scene, source),
593 | 'sceneItemIndex': index
594 | }).catch(async err => {
595 | if (err.code === 600) {
596 | var group = await this.getSourceGroupName(scene, source);
597 | if (group) {
598 | await this.setSceneItemIndex(group, source, index);
599 | return;
600 | }
601 | }
602 | console.error(JSON.stringify(err));
603 | });
604 | };
605 |
606 | return obs;
607 | }
608 |
--------------------------------------------------------------------------------
/js/param/paramHandler.js:
--------------------------------------------------------------------------------
1 | class ParamHandler extends Handler {
2 | /**
3 | * Create a new Param handler.
4 | */
5 | constructor() {
6 | super('Param', []);
7 | this.success();
8 | }
9 |
10 | /**
11 | * Handle the input data (take an action).
12 | * @param {array} triggerData contents of trigger line
13 | * @param {array} parameters current trigger parameters
14 | */
15 | handleData = async (triggerData, parameters) => {
16 | var action = Parser.getAction(triggerData);
17 |
18 | switch (action) {
19 | case 'create':
20 | var { name, value } = Parser.getInputs(triggerData, ['action', 'name', 'value']);
21 | return { [name]: value };
22 | break;
23 | case 'lower':
24 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
25 | if (parameters.hasOwnProperty(name)) {
26 | return { [name]: parameters[name].toLowerCase() };
27 | }
28 | break;
29 | case 'upper':
30 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
31 | if (parameters.hasOwnProperty(name)) {
32 | return { [name]: parameters[name].toUpperCase() };
33 | }
34 | break;
35 | case 'proper':
36 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
37 | if (parameters.hasOwnProperty(name)) {
38 | return { [name]: parameters[name].toProperCase() };
39 | }
40 | break;
41 | case 'add':
42 | var { name, value } = Parser.getInputs(triggerData, ['action', 'name', 'value']);
43 | if (parameters.hasOwnProperty(name)) {
44 | value = parseFloat(value);
45 | var paramValue = parseFloat(parameters[name]);
46 | if (!isNaN(value) && !isNaN(paramValue)) {
47 | return { [name]: paramValue + value };
48 | }
49 | }
50 | break;
51 | case 'subtract':
52 | var { name, value } = Parser.getInputs(triggerData, ['action', 'name', 'value']);
53 | if (parameters.hasOwnProperty(name)) {
54 | value = parseFloat(value);
55 | var paramValue = parseFloat(parameters[name]);
56 | if (!isNaN(value) && !isNaN(paramValue)) {
57 | return { [name]: paramValue - value };
58 | }
59 | }
60 | break;
61 | case 'negate':
62 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
63 | if (parameters.hasOwnProperty(name)) {
64 | switch(String(parameters[name]).toLowerCase()) {
65 | case "false":
66 | case "no":
67 | case "0":
68 | case "":
69 | return { [name]: true };
70 | default:
71 | return { [name]: false };
72 | }
73 | }
74 | break;
75 | case 'exists':
76 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
77 | return { exists: parameters.hasOwnProperty(name) };
78 | break;
79 | case 'copy':
80 | var { name, toName } = Parser.getInputs(triggerData, ['action', 'name', 'toName']);
81 | if (parameters.hasOwnProperty(name)) {
82 | return { [toName]: parameters[name] };
83 | }
84 | break;
85 | case 'replace':
86 | var { name, toReplace, replacement } = Parser.getInputs(triggerData, ['action', 'name', 'toReplace', 'replacement']);
87 | if (parameters.hasOwnProperty(name)) {
88 | return { [name]: parameters[name].replace(new RegExp(escapeRegExp(toReplace), 'g'), () => replacement) };
89 | }
90 | break;
91 | case 'keyword':
92 | var { name, keywords } = Parser.getInputs(triggerData, ['action', 'name', 'keywords'], true);
93 | if (parameters.hasOwnProperty(name)) {
94 | keywords.forEach((keyword, index) => {
95 | keywords[index] = escapeRegExp(keyword.trim());
96 | });
97 | var regex = new RegExp('(?:^|\\s)' + keywords.join('(?:$|\\s)|(?:^|\\s)') + '(?:$|\\s)', 'gi');
98 | var result = parameters[name].match(regex);
99 | var matched = false;
100 | if (result) {
101 | matched = true;
102 | var match = result[0].trim();
103 | result.forEach((res, index) => {
104 | result[index] = res.trim();
105 | });
106 | }
107 | return { matched, match, keywords: result };
108 | }
109 | break;
110 | default:
111 | console.error(`Unexpected Param action (${action}). Check your event code.`);
112 | break;
113 | }
114 | }
115 | }
116 |
117 | /**
118 | * Create a handler
119 | */
120 | function paramHandlerExport() {
121 | var param = new ParamHandler();
122 | }
123 | paramHandlerExport();
124 |
--------------------------------------------------------------------------------
/js/parser.js:
--------------------------------------------------------------------------------
1 | class Parser {
2 |
3 | /**
4 | * Splits the input string into arguments.
5 | * @param {string} line string to split
6 | */
7 | static splitLine(line) {
8 | var items = [];
9 | try {
10 | items = shlexSplit(line);
11 | } catch (err) {
12 | items = line.split(' ');
13 | }
14 | return items;
15 | }
16 |
17 | /**
18 | * Parse the file contents into a list of commands.
19 | * Primarily to account for multi-line strings in trigger files.
20 | * @param {array} lines file content containing commands
21 | */
22 | static parseCommands(lines) {
23 | var compiledLineData = [];
24 | for (var i = 0; i < lines.length; i++) {
25 | var line = lines[i].trim();
26 | if (!line.startsWith('#')) {
27 | var [isParsed, lineData] = Parser.tryParseCommand(line);
28 |
29 | // If failed to parse, look for a multi-line input
30 | if (!isParsed) {
31 | var j = i;
32 | var compiledLine = lines[i];
33 | while(!isParsed && j < lines.length) {
34 | j++;
35 | var compiledLine = `${compiledLine}\n${lines[j]}`;
36 | [isParsed, lineData] = Parser.tryParseCommand(compiledLine);
37 | }
38 |
39 | // If no multi-line string found, simply split the line
40 | if (!isParsed) {
41 | lineData = line.split(' ');
42 | } else {
43 | // Skip all lines in the multi-line string
44 | i = j;
45 | }
46 | }
47 |
48 | compiledLineData.push(lineData);
49 | }
50 | }
51 |
52 | return compiledLineData;
53 | }
54 |
55 | /**
56 | * Try to parse the input string using the shlex parsing library.
57 | * @param {string} data input string to parse for commands
58 | */
59 | static tryParseCommand(data) {
60 | try {
61 | var commandData = shlexSplit(data);
62 | return [true, commandData];
63 | } catch (err) {
64 | return [false, null];
65 | }
66 | }
67 |
68 | /**
69 | * Get the action from the input line information.
70 | * @param {array} lineData input to grab the action
71 | * @param {string} handler the name of the handler retrieving the action
72 | * @param {numeric} offset offset to apply when retrieving the action
73 | */
74 | static getAction(lineData, handler, offset) {
75 | offset = offset || 0;
76 | if (lineData.length > offset + 1) {
77 | return lineData[offset + 1].toLowerCase();
78 | }
79 | console.error(`Unable to get a ${handler} action from: ${JSON.stringify(lineData)}`);
80 | return "";
81 | }
82 |
83 | /**
84 | * Get the input values for the given line.
85 | * @param {array} lineData input to grab the action
86 | * @param {array} inputNames the list of input names to grab
87 | * @param {false} supportsAliases true if the input allows aliases
88 | * @param {numeric} optionalInputs number of optional inputs to check
89 | */
90 | static getInputs(lineData, inputNames, supportsAliases, optionalInputs) {
91 | supportsAliases = supportsAliases || false;
92 | optionalInputs = optionalInputs || 0;
93 | var res = {};
94 |
95 | var totalPossibleInputs = inputNames.length;
96 | if (supportsAliases) {
97 | res[inputNames[totalPossibleInputs-1]] = [];
98 | }
99 |
100 | if (lineData.length > totalPossibleInputs - optionalInputs) {
101 | if (!supportsAliases && lineData.length > totalPossibleInputs + 1) {
102 | console.error(`Kruiz Control: Too many inputs provided for ${lineData[0]}.\r\nExpected (${totalPossibleInputs} inputs): ${JSON.stringify(inputNames)}\r\nFound (${lineData.length - 1} inputs): ${JSON.stringify(lineData.slice(1))}`);
103 | }
104 |
105 | lineData.forEach((item, i) => {
106 | if (i != 0) {
107 | if (i < totalPossibleInputs) {
108 | res[inputNames[i-1]] = item;
109 | } else if (i == totalPossibleInputs && supportsAliases) {
110 | res[inputNames[i-1]] = lineData.slice(i);
111 | } else if (i == totalPossibleInputs) {
112 | res[inputNames[i-1]] = lineData.slice(i).join(' ');
113 | }
114 | }
115 | });
116 |
117 | if (Debug.All || Debug.Parser) {
118 | console.error(`Values parsed from ${JSON.stringify(lineData)}: ${JSON.stringify(res)}`);
119 | }
120 | return res;
121 | }
122 |
123 | console.error(`Kruiz Control: Unable to retrieve enough inputs for ${lineData[0]}.\r\nExpected (${totalPossibleInputs} inputs): ${JSON.stringify(inputNames)}\r\nFound (${lineData.length - 1} inputs): ${JSON.stringify(lineData.slice(1))}`);
124 | return {};
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/js/random/randomHandler.js:
--------------------------------------------------------------------------------
1 | class RandomHandler extends Handler {
2 | /**
3 | * Create a new Chat handler.
4 | */
5 | constructor() {
6 | super('Random', []);
7 | this.success();
8 | }
9 |
10 | /**
11 | * Handle the input data (take an action).
12 | * @param {array} triggerData contents of trigger line
13 | */
14 | handleData = async (triggerData) => {
15 | var action = Parser.getAction(triggerData, 'Random');
16 | if (action == 'number') {
17 | var { tmpmin, tmpmax } = Parser.getInputs(triggerData, ['action', 'tmpmin', 'tmpmax'], false, 2);
18 | var min = 0;
19 | var max = 100;
20 | if (tmpmin) {
21 | tmpmin = parseFloat(tmpmin);
22 | if (!isNaN(tmpmin)) {
23 | min = tmpmin;
24 | }
25 | if (tmpmax) {
26 | tmpmax = parseFloat(tmpmax);
27 | if (!isNaN(tmpmax)) {
28 | max = tmpmax;
29 | }
30 | }
31 | }
32 | var value = Math.floor((Math.random() * (max + 1 - min)) + min);
33 | return { number: value };
34 | } else if (action == 'probability') {
35 | var actions = {};
36 | var total = 0;
37 | for (var i = 2; i + 1 < triggerData.length; i = i + 2) {
38 | var action = triggerData[i];
39 | var prob = parseFloat(triggerData[i+1]);
40 | if (isNaN(prob)) {
41 | return;
42 | }
43 | actions[action] = prob;
44 | total += prob;
45 | }
46 | if (total > 0) {
47 | var multiplier = 100 / total;
48 | var index = 0;
49 | var prev = 0;
50 | var actionIndexes = [];
51 | for (var action in actions) {
52 | var value = (actions[action] * multiplier) + prev;
53 | prev = value;
54 | actionIndexes.push([value, action]);
55 | }
56 | var probability = Math.random() * 100;
57 | for (var i = 0; i < actionIndexes.length; i++) {
58 | if (probability < actionIndexes[i][0]) {
59 | return { actions: [actionIndexes[i][1]] };
60 | }
61 | }
62 | }
63 | } else {
64 | var exclude = 2;
65 | if (triggerData[1].toLowerCase() !== 'equal') {
66 | exclude = 1;
67 | }
68 | var choice = Math.floor(Math.random() * (triggerData.length - exclude)) + exclude;
69 | return { actions: [triggerData[choice]] };
70 | }
71 | }
72 | }
73 |
74 | /**
75 | * Create a handler
76 | */
77 | function randomHandlerExport() {
78 | var random = new RandomHandler();
79 | }
80 | randomHandlerExport();
81 |
--------------------------------------------------------------------------------
/js/slobs/slobs-websocket.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Connect to Streamlabs OBS JSON RPC API and setup the event handlers
3 | * @param {Handler} slobsHandler handler to mark successful initialization
4 | * @param {string} token SLOBS API Token
5 | * @param {function} onSwitchScenes handle switch scene messages
6 | * @param {function} onStreamStart handle stream start messages
7 | * @param {function} onStreamStop handle stream stop messages
8 | */
9 | function connectSLOBSWebsocket(slobsHandler, token, onSwitchScenes, onStreamStarted, onStreamStopped) {
10 | var socket = new SockJS('http://127.0.0.1:59650/api');
11 | var slobsSocket = {
12 | requestId: 1,
13 | socket: socket,
14 | scenes: {},
15 | activeScene: '',
16 | responses: {}
17 | };
18 |
19 | socket.onopen = () => {
20 | slobsHandler.success();
21 | slobsSocket.sendSLOBS("auth", "TcpServerService", [token])
22 | slobsSocket.sendSLOBS("getScenes", "ScenesService");
23 | slobsSocket.sendSLOBS("activeScene", "ScenesService");
24 | slobsSocket.sendSLOBS("sceneSwitched", "ScenesService");
25 | slobsSocket.sendSLOBS("sceneAdded", "ScenesService");
26 | slobsSocket.sendSLOBS("sceneRemoved", "ScenesService");
27 | slobsSocket.sendSLOBS("streamingStatusChange", "StreamingService");
28 | };
29 |
30 | socket.onmessage = (e) => {
31 | if (e.type === 'message') {
32 | if (Debug.All || Debug.SLOBS) {
33 | console.error('SLOBS Message: ' + e.data);
34 | }
35 | var data = JSON.parse(e.data);
36 |
37 | if (data.error) {
38 | console.error(`Received SLOBS error: ${JSON.stringify(data)}`);
39 | } else {
40 | if (data.id === 2) {
41 | data.result.forEach(scene => {
42 | slobsSocket.scenes[scene.name] = scene;
43 | });
44 | } else if (data.id === 3) {
45 | slobsSocket.activeScene = data.result.name;
46 | } else if (data.result && data.result.resourceId === 'ScenesService.sceneAdded' && data.result.data) {
47 | slobsSocket.scenes[data.result.data.name] = data.result.data;
48 | } else if (data.result && data.result.resourceId === 'ScenesService.sceneRemoved' && data.result.data) {
49 | delete slobsSocket.scenes[data.result.data.name];
50 | } else if (data.result && data.result.resourceId === 'ScenesService.sceneSwitched' && data.result.data) {
51 | slobsSocket.activeScene = data.result.data.name;
52 | onSwitchScenes(data.result.data);
53 | } else if (data.result && data.result.resourceId === 'StreamingService.streamingStatusChange' && data.result.data) {
54 | if (data.result.data === 'starting') {
55 | onStreamStarted();
56 | } else if (data.result.data === 'ending') {
57 | onStreamStopped();
58 | }
59 | } else if (slobsSocket.responses.hasOwnProperty(data.id.toString())) {
60 | slobsSocket.responses[data.id][0](data, ...slobsSocket.responses[data.id][1]);
61 | delete slobsSocket.responses[data.id];
62 | }
63 | }
64 | }
65 | };
66 |
67 | socket.onclose = (e) => {
68 | console.log('Closed SLOBS connection', e);
69 | };
70 |
71 | slobsSocket.getCurrentScene = function() {
72 | return slobsSocket.activeScene;
73 | }
74 |
75 | slobsSocket.setCurrentScene = function(scene) {
76 | if (slobsSocket.scenes[scene]) {
77 | var current = slobsSocket.activeScene;
78 | slobsSocket.sendSLOBS('makeSceneActive', 'ScenesService', [slobsSocket.scenes[scene].id])
79 | return { previous_scene: current };
80 | } else {
81 | console.error('No scene found with name', scene);
82 | }
83 | }
84 |
85 | slobsSocket.setSourceVisibility = function(scene, source, enabled) {
86 | scene = scene || slobsSocket.activeScene;
87 | var sceneInfo = slobsSocket.scenes[scene];
88 | if (sceneInfo) {
89 | sceneInfo.nodes.forEach(sceneItem => {
90 | if (sceneItem.name === source) {
91 | var sceneItemId = `SceneItem["${sceneInfo.id}","${sceneItem.id}","${sceneItem.sourceId}"]`;
92 | slobsSocket.sendSLOBS("setVisibility", sceneItemId, [enabled]);
93 | }
94 | });
95 | }
96 | }
97 |
98 | slobsSocket.getSourceVisibility = async function(scene, source) {
99 | scene = scene || slobsSocket.activeScene;
100 | var sceneInfo = slobsSocket.scenes[scene];
101 | return await new Promise((resolve) => {
102 | var id = slobsSocket.sendSLOBS("getScene", "ScenesService", [sceneInfo.id]);
103 | slobsSocket.responses[id] = [this._getSourceVisibility, [resolve, source]];
104 | });
105 | }
106 |
107 | slobsSocket._getSourceVisibility = async function(sceneInfo, resolve, source) {
108 | sceneInfo.result.nodes.forEach(async sceneItem => {
109 | if (sceneItem.name === source) {
110 | resolve(sceneItem.visible);
111 | }
112 | });
113 | }
114 |
115 | slobsSocket.setFolderVisibility = function(scene, folder, enabled) {
116 | scene = scene || slobsSocket.activeScene;
117 | var sceneInfo = slobsSocket.scenes[scene];
118 | if (sceneInfo) {
119 | sceneInfo.nodes.forEach(sceneItem => {
120 | if (sceneItem.name === folder && sceneItem.sceneNodeType === 'folder') {
121 | sceneInfo.nodes.forEach(source => {
122 | if (sceneItem.childrenIds.indexOf(source.id) !== -1) {
123 | var sceneItemId = `SceneItem["${sceneInfo.id}","${source.id}","${source.sourceId}"]`;
124 | slobsSocket.sendSLOBS("setVisibility", sceneItemId, [enabled]);
125 | }
126 | });
127 | }
128 | });
129 | }
130 | }
131 |
132 | slobsSocket.flipSourceX = function(scene, source) {
133 | scene = scene || slobsSocket.activeScene;
134 | var sceneInfo = slobsSocket.scenes[scene];
135 | if (sceneInfo) {
136 | sceneInfo.nodes.forEach(sceneItem => {
137 | if (sceneItem.name === source) {
138 | var sceneItemId = `SceneItem["${sceneInfo.id}","${sceneItem.id}","${sceneItem.sourceId}"]`;
139 | slobsSocket.sendSLOBS("flipX", sceneItemId);
140 | }
141 | });
142 | }
143 | }
144 |
145 | slobsSocket.flipSourceY = function(scene, source) {
146 | scene = scene || slobsSocket.activeScene;
147 | var sceneInfo = slobsSocket.scenes[scene];
148 | if (sceneInfo) {
149 | sceneInfo.nodes.forEach(sceneItem => {
150 | if (sceneItem.name === source) {
151 | var sceneItemId = `SceneItem["${sceneInfo.id}","${sceneItem.id}","${sceneItem.sourceId}"]`;
152 | slobsSocket.sendSLOBS("flipY", sceneItemId);
153 | }
154 | });
155 | }
156 | }
157 |
158 | slobsSocket.setVolume = async function(source, volume) {
159 | return await new Promise((resolve) => {
160 | var id = slobsSocket.sendSLOBS("getSources", "AudioService");
161 | slobsSocket.responses[id] = [slobsSocket._setVolume, [resolve, source, volume]];
162 | });
163 | }
164 |
165 | slobsSocket._setVolume = async function(data, resolve, source, volume) {
166 | data.result.forEach((sourceByName, i) => {
167 | if (sourceByName.name === source) {
168 | resolve(sourceByName.fader.deflection);
169 | slobsSocket.sendSLOBS("setDeflection", sourceByName.resourceId, [volume]);
170 | }
171 | });
172 | }
173 |
174 | slobsSocket.rotateSource = function(scene, source, degree) {
175 | scene = scene || slobsSocket.activeScene;
176 | var sceneInfo = slobsSocket.scenes[scene];
177 | if (sceneInfo) {
178 | sceneInfo.nodes.forEach(sceneItem => {
179 | if (sceneItem.name === source) {
180 | var sceneItemId = `SceneItem["${sceneInfo.id}","${sceneItem.id}","${sceneItem.sourceId}"]`;
181 | slobsSocket.sendSLOBS("setTransform", sceneItemId, [{rotation: degree}]);
182 | }
183 | });
184 | }
185 | }
186 |
187 | slobsSocket.setAudioMute = function(source, isMuted) {
188 | var id = slobsSocket.sendSLOBS("getSourcesForCurrentScene", "AudioService");
189 | slobsSocket.responses[id] = [slobsSocket._setAudioMute, [source, isMuted]];
190 | }
191 |
192 | slobsSocket._setAudioMute = function(data, source, isMuted) {
193 | data.result.forEach((audioSource, i) => {
194 | if (audioSource.name === source) {
195 | if (isMuted == 'toggle') {
196 | isMuted = !audioSource.muted
197 | }
198 | slobsSocket.sendSLOBS("setMuted", audioSource.resourceId, [isMuted]);
199 | }
200 | });
201 | }
202 |
203 | slobsSocket.setPosition = function(scene, source, x, y) {
204 | scene = scene || slobsSocket.activeScene;
205 | var sceneInfo = slobsSocket.scenes[scene];
206 | if (sceneInfo) {
207 | sceneInfo.nodes.forEach(sceneItem => {
208 | if (sceneItem.name === source) {
209 | var sceneItemId = `SceneItem["${sceneInfo.id}","${sceneItem.id}","${sceneItem.sourceId}"]`;
210 | slobsSocket.sendSLOBS("setTransform", sceneItemId, [{position: { x: x, y: y} }]);
211 | }
212 | });
213 | }
214 | }
215 |
216 | slobsSocket.toggleStream = function() {
217 | slobsSocket.sendSLOBS("toggleStreaming", 'StreamingService');
218 | }
219 |
220 | slobsSocket.startReplayBuffer = function() {
221 | slobsSocket.sendSLOBS("startReplayBuffer", 'StreamingService');
222 | }
223 |
224 | slobsSocket.stopReplayBuffer = function() {
225 | slobsSocket.sendSLOBS("stopReplayBuffer", 'StreamingService');
226 | }
227 |
228 | slobsSocket.saveReplayBuffer = function() {
229 | slobsSocket.sendSLOBS("saveReplay", 'StreamingService');
230 | }
231 |
232 | slobsSocket.pushNotification = function(message) {
233 | slobsSocket.sendSLOBS("push", 'NotificationsService', [{message: message}]);
234 | }
235 |
236 | slobsSocket.sendSLOBS = function(method, resource, args) {
237 | args = args || [];
238 | var request = {
239 | jsonrpc: "2.0",
240 | method: method,
241 | params: {
242 | resource: resource,
243 | args: args
244 | },
245 | id: slobsSocket.requestId++
246 | }
247 | slobsSocket.socket.send(JSON.stringify(request));
248 | return request.id;
249 | }
250 |
251 | return slobsSocket;
252 | }
253 |
--------------------------------------------------------------------------------
/js/slobs/slobsHandler.js:
--------------------------------------------------------------------------------
1 | class SLOBSHandler extends Handler {
2 | /**
3 | * Create a new SLOBS handler.
4 | */
5 | constructor() {
6 | super('SLOBS', ['OnSLOBSSwitchScenes', 'OnSLOBSStreamStarted', 'OnSLOBSStreamStopped']);
7 | this.onSwitch = [];
8 | this.onSwitchTrigger = {};
9 | this.onStartTrigger = [];
10 | this.onStopTrigger = [];
11 | }
12 |
13 | /**
14 | * Initialize the connection to SLOBS with the input settings.
15 | * @param {string} token slobs api token
16 | */
17 | init(token) {
18 | this.slobs = connectSLOBSWebsocket(
19 | this,
20 | token,
21 | this.onSwitchScenes,
22 | this.onStreamStart,
23 | this.onStreamStop
24 | );
25 | }
26 |
27 | /**
28 | * Register trigger from user input.
29 | * @param {string} trigger name to use for the handler
30 | * @param {array} triggerLine contents of trigger line
31 | * @param {number} id of the new trigger
32 | */
33 | addTriggerData = (trigger, triggerLine, triggerId) => {
34 | trigger = trigger.toLowerCase();
35 | switch (trigger) {
36 | case 'onslobsswitchscenes':
37 | var { scenes } = Parser.getInputs(triggerLine, ['scenes'], true);
38 | scenes.forEach(scene => {
39 | if (this.onSwitch.indexOf(scene) === -1) {
40 | this.onSwitch.push(scene);
41 | this.onSwitchTrigger[scene] = [];
42 | }
43 | this.onSwitchTrigger[scene].push(triggerId);
44 | });
45 | break;
46 | case 'onslobsstreamstarted':
47 | this.onStartTrigger.push(triggerId);
48 | break;
49 | case 'onslobsstreamstopped':
50 | this.onStopTrigger.push(triggerId);
51 | break;
52 | default:
53 | // do nothing
54 | }
55 | return;
56 | }
57 |
58 | /**
59 | * Handle switch scene messages from slobs subscription.
60 | * @param {Object} data scene information
61 | */
62 | onSwitchScenes = async (data) => {
63 | var sceneTriggers = [];
64 | if (this.onSwitch.indexOf(data.name) !== -1) {
65 | sceneTriggers.push(...this.onSwitchTrigger[data.name]);
66 | }
67 | if (this.onSwitch.indexOf('*') !== -1) {
68 | sceneTriggers.push(...this.onSwitchTrigger['*']);
69 | }
70 | if (sceneTriggers.length > 0) {
71 | sceneTriggers.sort((a,b) => a-b);
72 | sceneTriggers.forEach(triggerId => {
73 | controller.handleData(triggerId, {
74 | scene: data.name
75 | });
76 | });
77 | }
78 | }
79 |
80 | /**
81 | * Handle stream start messages from slobs subscription.
82 | */
83 | onStreamStart = () => {
84 | if (this.onStartTrigger.length > 0) {
85 | this.onStartTrigger.forEach(trigger => {
86 | controller.handleData(trigger);
87 | })
88 | }
89 | }
90 |
91 | /**
92 | * Handle stream stop messages from slobs subscription.
93 | */
94 | onStreamStop = () => {
95 | if (this.onStopTrigger.length > 0) {
96 | this.onStopTrigger.forEach(trigger => {
97 | controller.handleData(trigger);
98 | })
99 | }
100 | }
101 |
102 | /**
103 | * Handle the input data (take an action).
104 | * @param {array} triggerData contents of trigger line
105 | */
106 | handleData = async (triggerData) => {
107 | var action = Parser.getAction(triggerData, 'SLOBS');
108 | switch (action) {
109 | case 'currentscene':
110 | var scene = this.slobs.getCurrentScene();
111 | return { current_scene: scene };
112 | break;
113 | case 'isscenesourcevisible':
114 | var { scene, source } = Parser.getInputs(triggerData, ['action', 'scene', 'source']);
115 | var data = await this.slobs.getSourceVisibility(scene, source);
116 | return { is_visible: data };
117 | break;
118 | case 'scene':
119 | var { scene } = Parser.getInputs(triggerData, ['action', 'scene']);
120 | return await this.slobs.setCurrentScene(scene);
121 | break;
122 | case 'source':
123 | var { source, status } = Parser.getInputs(triggerData, ['action', 'source', 'status']);
124 | status = status.toLowerCase() === 'on' ? true : false;
125 | await this.slobs.setSourceVisibility('', source, status);
126 | break;
127 | case 'scenesource':
128 | var { scene, source, status } = Parser.getInputs(triggerData, ['action', 'scene', 'source', 'status']);
129 | status = status.toLowerCase() === 'on' ? true : false;
130 | await this.slobs.setSourceVisibility(scene, source, status);
131 | break;
132 | case 'scenefolder':
133 | var { scene, folder, status } = Parser.getInputs(triggerData, ['action', 'scene', 'folder', 'status']);
134 | status = status.toLowerCase() === 'on' ? true : false;
135 | await this.slobs.setFolderVisibility(scene, folder, status);
136 | break;
137 | case 'flip':
138 | var { scene, source, direction } = Parser.getInputs(triggerData, ['action', 'scene', 'source', 'direction']);
139 | if (direction.toLowerCase() === 'y') {
140 | await this.slobs.flipSourceY(scene, source);
141 | } else {
142 | await this.slobs.flipSourceX(scene, source);
143 | }
144 | break;
145 | case 'mute':
146 | var { source, status } = Parser.getInputs(triggerData, ['action', 'source', 'status']);
147 | status = status.toLowerCase();
148 | if (status != 'toggle') {
149 | status = status === 'on' ? true : false;
150 | }
151 | await this.slobs.setAudioMute(source, status);
152 | break;
153 | case 'notification':
154 | var { message } = Parser.getInputs(triggerData, ['action', 'message']);
155 | await this.slobs.pushNotification(message);
156 | break;
157 | case 'position':
158 | var { scene, source, x, y } = Parser.getInputs(triggerData, ['action', 'scene', 'source', 'x', 'y']);
159 | var x = parseFloat(x);
160 | var y = parseFloat(y);
161 | if (!isNaN(x) && !isNaN(y)) {
162 | await this.slobs.setPosition(scene, source, x, y);
163 | }
164 | break;
165 | case 'rotate':
166 | var { scene, source, degree } = Parser.getInputs(triggerData, ['action', 'scene', 'source', 'degree']);
167 | degree = parseFloat(degree);
168 | if (!isNaN(degree)) {
169 | await this.slobs.rotateSource(scene, source, degree);
170 | }
171 | break;
172 | case 'savereplaybuffer':
173 | await this.slobs.saveReplayBuffer();
174 | break;
175 | case 'startreplaybuffer':
176 | await this.slobs.startReplayBuffer();
177 | break;
178 | case 'stopreplaybuffer':
179 | await this.slobs.stopReplayBuffer();
180 | break;
181 | case 'togglestream':
182 | await this.slobs.toggleStream();
183 | break;
184 | case 'volume':
185 | var { source, volume } = Parser.getInputs(triggerData, ['action', 'source', 'volume']);
186 | volume = parseFloat(volume);
187 | if (!isNaN(volume)) {
188 | var currentVolume = await this.slobs.setVolume(source, volume);
189 | return { previous_volume: currentVolume };
190 | } else {
191 | console.error('Unable to parse volume value: ' + triggerData[triggerData.length - 1]);
192 | }
193 | break;
194 | }
195 | return;
196 | }
197 | }
198 |
199 | /**
200 | * Create a handler.
201 | */
202 | async function slobsHandlerExport() {
203 | var slobsHandler = new SLOBSHandler();
204 | var token = await readFile('settings/slobs/token.txt');
205 | slobsHandler.init(token.trim());
206 | }
207 | slobsHandlerExport();
208 |
--------------------------------------------------------------------------------
/js/streamElementsAlert/streamElements-socket.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Connect to the StreamElements websocket and setup the event handlers
3 | * @param {Handler} streamElementHandler StreamElements Handler
4 | * @param {string} token StreamElements JWT token
5 | * @param {method} onTestEvent method to call when test events are received
6 | * @param {method} onEvent method to call when events are received
7 | */
8 | function connectStreamElementsWebsocket(streamElementHandler, token, onTestEvent, onEvent) {
9 | const socket = io('https://realtime.streamelements.com', {
10 | transports: ['websocket']
11 | });
12 | // Socket connected
13 | socket.on('connect', function() {
14 | console.log('Successfully connected to the streamelements websocket');
15 | streamElementHandler.success();
16 | socket.emit('authenticate', {
17 | method: 'jwt',
18 | token: token
19 | });
20 | });
21 |
22 | // Socket got disconnected
23 | socket.on('disconnect', function() {
24 | console.log('Disconnected from websocket');
25 | });
26 |
27 | // Socket is authenticated
28 | socket.on('authenticated', function(data) {
29 | const channelId = data.channelId;
30 | console.log(`Successfully connected to channel ${channelId}`);
31 | });
32 |
33 | socket.on('event:test', onTestEvent);
34 | socket.on('event', onEvent);
35 | }
36 |
--------------------------------------------------------------------------------
/js/streamElementsAlert/streamElementsAlertHandler.js:
--------------------------------------------------------------------------------
1 | class StreamElementsAlertHandler extends Handler {
2 | /**
3 | * Create a new StreamElements Alert handler.
4 | */
5 | constructor() {
6 | super('StreamElementsAlert',['OnSETwitchBits','OnSETwitchBulkGiftSub','OnSEDonation','OnSETwitchFollow','OnSETwitchGiftSub','OnSETwitchHost','OnSETwitchRaid','OnSETwitchSub']);
7 | this.alerts = [];
8 | this.alertsTrigger = {
9 | 'cheer': [],
10 | 'bulk_sub': [],
11 | 'tip': [],
12 | 'follow': [],
13 | 'gift_sub': [],
14 | 'host': [],
15 | 'raid': [],
16 | 'subscriber': []
17 | };
18 | this.alertMapping = {
19 | 'onsetwitchbits': 'cheer',
20 | 'onsetwitchbulkgiftsub': 'bulk_sub',
21 | 'onsedonation': 'tip',
22 | 'onsetwitchfollow': 'follow',
23 | 'onsetwitchgiftsub': 'gift_sub',
24 | 'onsetwitchhost': 'host',
25 | 'onsetwitchraid': 'raid',
26 | 'onsetwitchsub': 'subscriber'
27 | };
28 | this.eventHandlers = {
29 | 'cheer': this.getBitParameters,
30 | 'bulk_sub': this.getBulkGiftParameters,
31 | 'tip': this.getDonationParameters,
32 | 'follow': this.getFollowParameters,
33 | 'gift_sub': this.getGiftSubParameters,
34 | 'host': this.getHostParameters,
35 | 'raid': this.getRaidParameters,
36 | 'subscriber': this.getSubParameters
37 | };
38 | this.testEventMapper = {
39 | 'cheer-latest':'cheer',
40 | 'bulk_sub': 'bulk_sub',
41 | 'tip-latest': 'tip',
42 | 'follower-latest': 'follow',
43 | 'gift_sub': 'gift_sub',
44 | 'host-latest': 'host',
45 | 'raid-latest': 'raid',
46 | 'subscriber-latest': 'subscriber'
47 | };
48 | }
49 |
50 | /**
51 | * Initialize the connection to streamelements with the input token.
52 | * @param {string} jwtToken streamelements jwt token
53 | */
54 | init = (jwtToken) => {
55 | connectStreamElementsWebsocket(this, jwtToken, this.onStreamElementsTestMessage, this.onStreamElementsMessage);
56 | }
57 |
58 | /**
59 | * Register trigger from user input.
60 | * @param {string} trigger name to use for the handler
61 | * @param {array} triggerLine contents of trigger line
62 | * @param {number} id of the new trigger
63 | */
64 | addTriggerData = (trigger, triggerLine, triggerId) => {
65 | trigger = trigger.toLowerCase()
66 | this.alerts.push(this.alertMapping[trigger]);
67 | this.alertsTrigger[this.alertMapping[trigger]].push(triggerId);
68 | return;
69 | }
70 |
71 | /**
72 | * Handle event messages from streamelements websocket.
73 | * @param {Object} message streamelements test event message
74 | */
75 | onStreamElementsTestMessage = (message) => {
76 | if (Debug.All || Debug.StreamElements) {
77 | console.error('StreamElements Message: ' + JSON.stringify(message));
78 | }
79 | var type = message.listener;
80 | if (type === 'subscriber-latest') {
81 | if (message.event.gifted) {
82 | type = 'gift_sub';
83 | } else if (message.event.bulkGifted) {
84 | type = 'bulk_sub';
85 | }
86 | }
87 | type = this.testEventMapper[type];
88 | if (this.alerts.indexOf(type) != -1) {
89 | var params = this.eventHandlers[type](message.event);
90 | this.alertsTrigger[type].forEach(triggerId => {
91 | controller.handleData(triggerId, params);
92 | });
93 | }
94 | }
95 |
96 | /**
97 | * Handle event messages from streamelements websocket.
98 | * @param {Object} message streamelements event message
99 | */
100 | onStreamElementsMessage = (message) => {
101 | if (Debug.All || Debug.StreamElements) {
102 | console.error('StreamElements Message: ' + JSON.stringify(message));
103 | }
104 | var type = message.type;
105 | if (type === 'subscriber') {
106 | if (message.data.gifted) {
107 | type = 'gift_sub';
108 | } else if (message.data.bulkGifted) {
109 | type = 'bulk_sub';
110 | }
111 | }
112 | if (this.alerts.indexOf(type) != -1) {
113 | var params = this.eventHandlers[type](message.data);
114 | this.alertsTrigger[type].forEach(triggerId => {
115 | controller.handleData(triggerId, params);
116 | });
117 | }
118 | }
119 |
120 | /**
121 | * Retrieve the parameters for the bit event.
122 | * @param {Object} event streamelements event
123 | */
124 | getBitParameters = (event) => {
125 | return {
126 | 'data': event,
127 | 'amount': event.amount,
128 | 'message': htmlDecode(event.message),
129 | 'user': (event.displayName) ? event.displayName : event.name
130 | }
131 | }
132 |
133 | /**
134 | * Retrieve the parameters for the bulk gift sub event.
135 | * @param {Object} event streamelements event
136 | */
137 | getBulkGiftParameters = (event) => {
138 | return {
139 | 'data': event,
140 | 'amount': event.amount,
141 | 'user': event.sender
142 | }
143 | }
144 |
145 | /**
146 | * Retrieve the parameters for the donation event.
147 | * @param {Object} event streamelements event
148 | */
149 | getDonationParameters = (event) => {
150 | return {
151 | 'data': event,
152 | 'amount': event.amount,
153 | 'message': htmlDecode(event.message),
154 | 'user': (event.username) ? event.username : event.name
155 | }
156 | }
157 |
158 | /**
159 | * Retrieve the parameters for the follow event.
160 | * @param {Object} event streamelements event
161 | */
162 | getFollowParameters = (event) => {
163 | return {
164 | 'data': event,
165 | 'user': (event.displayName) ? event.displayName : event.name
166 | }
167 | }
168 |
169 | /**
170 | * Retrieve the parameters for the gift sub event.
171 | * @param {Object} event streamelements event
172 | */
173 | getGiftSubParameters = (event) => {
174 | return {
175 | 'data': event,
176 | 'user': (event.displayName) ? event.displayName : event.name,
177 | 'gifter': event.sender,
178 | 'tier': event.tier === 'prime' ? 'Prime' : 'Tier ' + (parseInt(event.tier) / 1000)
179 | }
180 | }
181 |
182 | /**
183 | * Retrieve the parameters for the host event.
184 | * @param {Object} event streamelements event
185 | */
186 | getHostParameters = (event) => {
187 | return {
188 | 'data': event,
189 | 'user': (event.displayName) ? event.displayName : event.name,
190 | 'viewers': event.amount
191 | }
192 | }
193 |
194 | /**
195 | * Retrieve the parameters for the raid event.
196 | * @param {Object} event streamelements event
197 | */
198 | getRaidParameters = (event) => {
199 | return {
200 | 'data': event,
201 | 'user': (event.displayName) ? event.displayName : event.name,
202 | 'raiders': event.amount
203 | }
204 | }
205 |
206 | /**
207 | * Retrieve the parameters for the sub event.
208 | * @param {Object} event streamelements event
209 | */
210 | getSubParameters = (event) => {
211 | return {
212 | 'data': event,
213 | 'user': (event.displayName) ? event.displayName : event.name,
214 | 'months': event.amount,
215 | 'message': htmlDecode(event.message),
216 | 'tier': event.tier === 'prime' ? 'Prime' : 'Tier ' + (parseInt(event.tier) / 1000)
217 | }
218 | }
219 | }
220 |
221 | /**
222 | * Create a handler and read user settings
223 | */
224 | async function streamElementsAlertHandlerExport() {
225 | var streamElementsAlert = new StreamElementsAlertHandler();
226 | var token = await readFile('settings/streamelements/jwtToken.txt');
227 | streamElementsAlert.init(token.trim());
228 | }
229 | streamElementsAlertHandlerExport();
230 |
--------------------------------------------------------------------------------
/js/streamlabs/streamlabs-socket.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Connect to the Streamlabs websocket and setup the event handlers
3 | * @param {Handler} streamlabsAlertHandler Streamlabs Alert Handler
4 | * @param {string} token Streamlabs Socket API token
5 | * @param {method} onEvent method to call when events are received
6 | */
7 | function connectStreamlabsWebsocket(streamlabsAlertHandler, token, onEvent) {
8 | //Connect to socket
9 | var streamlabs = io(`https://sockets.streamlabs.com?token=${token}`, {transports: ['websocket']});
10 |
11 | streamlabs.on('connect', function() {
12 | console.log('Successfully connected to the streamlabs websocket');
13 | streamlabsAlertHandler.success();
14 | });
15 |
16 | streamlabs.onclose = function () {
17 | console.error('Error connecting to streamlabs socket: Incorrect token or connection error');
18 | }
19 |
20 | //Perform Action on event
21 | streamlabs.on('event', onEvent);
22 | }
23 |
--------------------------------------------------------------------------------
/js/streamlabs/streamlabsHandler.js:
--------------------------------------------------------------------------------
1 | class StreamlabsHandler extends Handler {
2 | /**
3 | * Create a new Streamlabs handler.
4 | */
5 | constructor() {
6 | super('Streamlabs', ['OnSLTwitchBits','OnSLDonation','OnSLTiltifyDonation','OnSLPatreonPledge','OnSLTwitchFollow','OnSLTwitchGiftSub','OnSLTwitchCommunityGiftSub','OnSLTwitchHost','OnSLTwitchRaid','OnSLTwitchSub','OnSLTwitchBitsNoSync','OnSLDonationNoSync','OnSLTiltifyDonationNoSync','OnSLPatreonPledgeNoSync','OnSLTwitchFollowNoSync','OnSLTwitchGiftSubNoSync','OnSLTwitchCommunityGiftSubNoSync','OnSLTwitchHostNoSync','OnSLTwitchRaidNoSync','OnSLTwitchSubNoSync']);
7 | this.alerts = [];
8 | this.alertsTrigger = {
9 | 'bits': [],
10 | 'donation': [],
11 | 'follow': [],
12 | 'gift_sub': [],
13 | 'cgift_sub': [],
14 | 'host': [],
15 | 'pledge': [],
16 | 'raid': [],
17 | 'subscription': [],
18 | 'tiltifydonation': []
19 | };
20 | this.alertsNoSync = [];
21 | this.alertsNoSyncTrigger = {
22 | 'bits': [],
23 | 'donation': [],
24 | 'follow': [],
25 | 'gift_sub': [],
26 | 'cgift_sub': [],
27 | 'host': [],
28 | 'pledge': [],
29 | 'raid': [],
30 | 'subscription': [],
31 | 'tiltifydonation': []
32 | };
33 | this.alertMapping = {
34 | 'onsltwitchbits': 'bits',
35 | 'onsldonation': 'donation',
36 | 'onsltiltifydonation': 'tiltifydonation',
37 | 'onslpatreonpledge': 'pledge',
38 | 'onsltwitchfollow': 'follow',
39 | 'onsltwitchgiftsub': 'gift_sub',
40 | 'onsltwitchcommunitygiftsub': 'cgift_sub',
41 | 'onsltwitchhost': 'host',
42 | 'onsltwitchraid': 'raid',
43 | 'onsltwitchsub': 'subscription',
44 | 'onsltwitchbitsnosync': 'bits',
45 | 'onsldonationnosync': 'donation',
46 | 'onsltiltifydonationnosync': 'tiltifydonation',
47 | 'onslpatreonpledgenosync': 'pledge',
48 | 'onsltwitchfollownosync': 'follow',
49 | 'onsltwitchgiftsubnosync': 'gift_sub',
50 | 'onsltwitchcommunitygiftsubnosync': 'cgift_sub',
51 | 'onsltwitchhostnosync': 'host',
52 | 'onsltwitchraidnosync': 'raid',
53 | 'onsltwitchsubnosync': 'subscription'
54 | };
55 | this.eventHandlers = {
56 | 'bits': this.getBitParameters,
57 | 'donation': this.getDonationParameters,
58 | 'follow': this.getFollowParameters,
59 | 'gift_sub': this.getGiftSubParameters,
60 | 'cgift_sub': this.getCommunityGiftSubParameters,
61 | 'host': this.getHostParameters,
62 | 'pledge': this.getPatreonPledgeParameters,
63 | 'raid': this.getRaidParameters,
64 | 'subscription': this.getSubParameters,
65 | 'tiltifydonation': this.getTiltifyDonationParameters
66 | };
67 | this.alertIds = [];
68 | this.alertIdsNoSync = [];
69 | this.onSLMessageQueue = async.queue(this.parseStreamlabsMessage, 1);
70 | }
71 |
72 | /**
73 | * Initialize the connection to streamlabs with the input token.
74 | * @param {string} socketToken streamlabs socket api token
75 | */
76 | init = (socketToken) => {
77 | connectStreamlabsWebsocket(this, socketToken, this.onStreamlabsMessage);
78 | }
79 |
80 | /**
81 | * Register trigger from user input.
82 | * @param {string} trigger name to use for the handler
83 | * @param {array} triggerLine contents of trigger line
84 | * @param {number} id of the new trigger
85 | */
86 | addTriggerData = (trigger, triggerLine, triggerId) => {
87 | trigger = trigger.toLowerCase();
88 | if (trigger.endsWith('nosync')) {
89 | this.alertsNoSync.push(this.alertMapping[trigger]);
90 | this.alertsNoSyncTrigger[this.alertMapping[trigger]].push(triggerId);
91 | } else {
92 | this.alerts.push(this.alertMapping[trigger]);
93 | this.alertsTrigger[this.alertMapping[trigger]].push(triggerId);
94 | }
95 | return;
96 | }
97 |
98 | /**
99 | * Handle event messages from streamlabs websocket.
100 | * @param {Object} message streamlabs event message
101 | */
102 | onStreamlabsMessage = (message) => {
103 | if (Debug.All || Debug.Streamlabs) {
104 | console.error('Streamlabs Message: ' + JSON.stringify(message));
105 | }
106 | this.onSLMessageQueue.push(message);
107 | }
108 |
109 | /**
110 | * Handle event messages from streamlabs websocket.
111 | * @param {Object} message streamlabs event message
112 | */
113 | parseStreamlabsMessage = async (message, callback) => {
114 | if (message.type === 'alertPlaying') {
115 | if (this.alertIds.indexOf(message.message['_id']) === -1) {
116 | this.alertIds.push(message.message['_id']);
117 | var type = message.message.type;
118 | if (type === 'subscription' && message.message.gifter_display_name) {
119 | type = 'gift_sub';
120 | } else if (type === 'subMysteryGift') {
121 | type = 'cgift_sub';
122 | }
123 | if (this.alerts.indexOf(type) != -1) {
124 | var params = this.eventHandlers[type](message.message);
125 | this.alertsTrigger[type].forEach(triggerId => {
126 | controller.handleData(triggerId, params);
127 | });
128 | }
129 | }
130 | } else if (this.alertsNoSync.indexOf(message.type) !== -1) {
131 | message.message.forEach(alertMessage => {
132 | if (this.alertIdsNoSync.indexOf(alertMessage['_id']) === -1) {
133 | this.alertIdsNoSync.push(alertMessage['_id']);
134 | var type = message.type;
135 | if (type === 'subscription' && alertMessage.gifter_display_name) {
136 | type = 'gift_sub';
137 | } else if (type === 'subMysteryGift') {
138 | type = 'cgift_sub';
139 | }
140 | var params = this.eventHandlers[type](alertMessage);
141 | this.alertsNoSyncTrigger[type].forEach(triggerId => {
142 | controller.handleData(triggerId, params);
143 | });
144 | }
145 | });
146 | }
147 | }
148 |
149 | /**
150 | * Retrieve the parameters for the bit event.
151 | * @param {Object} message streamlabs event message
152 | */
153 | getBitParameters = (message) => {
154 | return {
155 | 'data': message,
156 | 'amount': message.amount,
157 | 'message': message.message || "",
158 | 'user': message.name
159 | }
160 | }
161 |
162 | /**
163 | * Retrieve the parameters for the donation event.
164 | * @param {Object} message streamlabs event message
165 | */
166 | getDonationParameters = (message) => {
167 | return {
168 | 'data': message,
169 | 'amount': (message.payload && message.payload.amount) ? message.payload.amount : message.amount,
170 | 'formatted': (message.payload && message.payload.formatted_amount) ? message.payload.formatted_amount : message.formatted_amount,
171 | 'message': message.message || "",
172 | 'user': message.from
173 | }
174 | }
175 |
176 | /**
177 | * Retrieve the parameters for the tiltify donation event.
178 | * @param {Object} message streamlabs event message
179 | */
180 | getTiltifyDonationParameters = (message) => {
181 | return {
182 | 'data': message,
183 | 'amount': (message.payload && message.payload.amount) ? message.payload.amount : message.amount,
184 | 'formatted': (message.payload && message.payload.formatted_amount) ? message.payload.formattedAmount : message.formattedAmount,
185 | 'message': message.message || "",
186 | 'user': message.from
187 | }
188 | }
189 |
190 | /**
191 | * Retrieve the parameters for the patreon pledge event.
192 | * @param {Object} message streamlabs event message
193 | */
194 | getPatreonPledgeParameters = (message) => {
195 | return {
196 | 'data': message,
197 | 'amount': (message.payload && message.payload.amount) ? message.payload.amount : message.amount,
198 | 'formatted': (message.payload && message.payload.formatted_amount) ? message.payload.formatted_amount : message.formatted_amount,
199 | 'user': message.from
200 | }
201 | }
202 |
203 | /**
204 | * Retrieve the parameters for the follow event.
205 | * @param {Object} message streamlabs event message
206 | */
207 | getFollowParameters = (message) => {
208 | return {
209 | 'data': message,
210 | 'user': message.name
211 | }
212 | }
213 |
214 | /**
215 | * Retrieve the parameters for the gift sub event.
216 | * @param {Object} message streamlabs event message
217 | */
218 | getGiftSubParameters = (message) => {
219 | var gifter = message.gifter_display_name;
220 | if (!gifter) {
221 | gifter = message.gifter;
222 | }
223 |
224 | var name = message.display_name;
225 | if (!name) {
226 | name = message.name;
227 | }
228 |
229 | var subPlan = message.sub_plan;
230 | if (!subPlan) {
231 | subPlan = message.subPlan;
232 | }
233 |
234 | return {
235 | 'data': message,
236 | 'user': name,
237 | 'gifter': gifter,
238 | 'months': message.months,
239 | 'tier': subPlan === 'Prime' ? 'Prime' : 'Tier ' + (parseInt(subPlan) / 1000)
240 | }
241 | }
242 |
243 | /**
244 | * Retrieve the parameters for the gift sub event.
245 | * @param {Object} message streamlabs event message
246 | */
247 | getCommunityGiftSubParameters = (message) => {
248 | var gifter = message.gifter_display_name;
249 | if (!gifter) {
250 | gifter = message.gifter;
251 | }
252 |
253 | var subPlan = message.sub_plan;
254 | if (!subPlan) {
255 | subPlan = message.subPlan;
256 | }
257 |
258 | return {
259 | 'data': message,
260 | 'amount': message.amount,
261 | 'gifter': gifter,
262 | 'tier': subPlan === 'Prime' ? 'Prime' : 'Tier ' + (parseInt(subPlan) / 1000)
263 | }
264 | }
265 |
266 | /**
267 | * Retrieve the parameters for the host event.
268 | * @param {Object} message streamlabs event message
269 | */
270 | getHostParameters = (message) => {
271 | return {
272 | 'data': message,
273 | 'user': message.name,
274 | 'viewers': message.viewers
275 | }
276 | }
277 |
278 | /**
279 | * Retrieve the parameters for the raid event.
280 | * @param {Object} message streamlabs event message
281 | */
282 | getRaidParameters = (message) => {
283 | return {
284 | 'data': message,
285 | 'user': message.name,
286 | 'raiders': message.raiders
287 | }
288 | }
289 |
290 | /**
291 | * Retrieve the parameters for the sub event.
292 | * @param {Object} message streamlabs event message
293 | */
294 | getSubParameters = (message) => {
295 | var name = message.display_name;
296 | if (!name) {
297 | name = message.name;
298 | }
299 |
300 | var subPlan = message.sub_plan;
301 | if (!subPlan) {
302 | subPlan = message.subPlan;
303 | }
304 |
305 | return {
306 | 'data': message,
307 | 'user': name,
308 | 'months': message.months,
309 | 'message': message.message || "",
310 | 'tier': subPlan === 'Prime' ? 'Prime' : 'Tier ' + (parseInt(subPlan) / 1000)
311 | }
312 | }
313 | }
314 |
315 | /**
316 | * Create a handler and read user settings
317 | */
318 | async function streamlabsHandlerExport() {
319 | var streamlabs = new StreamlabsHandler();
320 | var socket = await readFile('settings/streamlabs/socketAPIToken.txt');
321 | streamlabs.init(socket.trim());
322 | }
323 | streamlabsHandlerExport();
324 |
--------------------------------------------------------------------------------
/js/timer/timerHandler.js:
--------------------------------------------------------------------------------
1 | class TimerHandler extends Handler {
2 | /**
3 | * Create a new Timer handler.
4 | */
5 | constructor() {
6 | super('Timer', ['OnTimer']);
7 | this.timerNames = [];
8 | this.timers = {};
9 | this.intervals = {};
10 | this.success();
11 | }
12 |
13 | /**
14 | * Register trigger from user input.
15 | * @param {string} trigger name to use for the handler
16 | * @param {array} triggerLine contents of trigger line
17 | * @param {number} id of the new trigger
18 | */
19 | addTriggerData = (trigger, triggerLine, triggerId) => {
20 | var { name, interval, temp } = Parser.getInputs(triggerLine, ['name', 'interval', 'temp'], false, 1);
21 |
22 | interval = parseFloat(interval);
23 |
24 | var offset = 0;
25 | if (temp !== undefined) {
26 | temp = parseFloat(temp);
27 | if (!isNaN(temp)) {
28 | offset = temp;
29 | }
30 | }
31 | if (this.timerNames.indexOf(name) === -1) {
32 | this.timerNames.push(name);
33 | this.timers[name] = [];
34 | this.intervals[name] = [];
35 | }
36 | this.timers[name].push([triggerId, name, interval, offset]);
37 | }
38 |
39 | /**
40 | * Handle the input data (take an action).
41 | * @param {array} triggerData contents of trigger line
42 | */
43 | handleData = async (triggerData) => {
44 | var action = Parser.getAction(triggerData, 'Timer');
45 | // Timer Reset Name
46 | if (action === 'reset' || action === 'start') {
47 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
48 | this.intervals[name].forEach((item, i) => {
49 | clearInterval(item);
50 | var info = this.timers[name][i];
51 | var triggerId = info[0];
52 | var interval = info[2];
53 | this.intervals[name][i] = setInterval(function() {
54 | controller.handleData(triggerId);
55 | }, interval * 1000);
56 | }, this);
57 | } else if (action === 'stop') {
58 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
59 | this.intervals[name].forEach((item, i) => {
60 | clearInterval(item);
61 | });
62 | }
63 | }
64 |
65 | /**
66 | * Called after parsing all user input.
67 | */
68 | postParse = () => {
69 | this.timerNames.forEach((name) => {
70 | this.timers[name].forEach((timer) => {
71 | var triggerId = timer[0];
72 | var name = timer[1];
73 | var interval = timer[2];
74 | var offset = timer[3];
75 | setTimeout(function() {
76 | this.intervals[name].push(setInterval(function() {
77 | controller.handleData(triggerId);
78 | }, interval * 1000));
79 | }.bind(this), (offset + 1) * 1000);
80 | });
81 | });
82 | }
83 | }
84 |
85 | /**
86 | * Create a handler
87 | */
88 | function timerHandlerExport() {
89 | var timer = new TimerHandler();
90 | }
91 | timerHandlerExport();
92 |
--------------------------------------------------------------------------------
/js/tts/ttsHandler.js:
--------------------------------------------------------------------------------
1 | class TTSHandler extends Handler {
2 | /**
3 | * Create a new TTS handler.
4 | */
5 | constructor() {
6 | super('TTS', []);
7 | this.voices = {};
8 | this.success();
9 | }
10 |
11 | /**
12 | * Handle the input data (take an action).
13 | * @param {array} triggerData contents of trigger line
14 | * @param {array} parameters current trigger parameters
15 | */
16 | handleData = async (triggerData, parameters) => {
17 | var action = Parser.getAction(triggerData, 'TTS');
18 | if (action === 'stop') {
19 | window.speechSynthesis.cancel();
20 | } else if (action === 'voices') {
21 | var { name } = Parser.getInputs(triggerData, ['action', 'name']);
22 | var listParser = controller.getParser("list");
23 | var voices = window.speechSynthesis.getVoices();
24 | if (voices.length === 0) {
25 | await timeout(1000);
26 | voices = window.speechSynthesis.getVoices();
27 | }
28 | this.updateVoices(voices);
29 | listParser.createList(name, voices.map(voice => voice.name));
30 | } else {
31 | var inputs = Parser.getInputs(triggerData, ['voice', 'volume', 'pitch', 'rate', 'wait', 'message']);
32 | if (Object.keys(inputs).length === 0) {
33 | inputs = Parser.getInputs(triggerData, ['voice', 'volume', 'wait', 'message']);
34 | }
35 | var { voice, volume, pitch, rate, wait, message } = inputs;
36 |
37 | volume = parseInt(volume);
38 | if (isNaN(volume)) {
39 | volume = 0.8;
40 | } else {
41 | volume = volume / 100;
42 | }
43 |
44 | pitch = parseInt(pitch);
45 | if (isNaN(pitch)) {
46 | pitch = 1;
47 | } else {
48 | pitch = pitch / 100 * 2;
49 | }
50 |
51 | rate = parseInt(rate);
52 | if (isNaN(rate)) {
53 | rate = 1;
54 | } else {
55 | rate = (rate / 100 * 9.9) + 0.1;
56 | }
57 |
58 | var msg = new SpeechSynthesisUtterance();
59 | msg.text = message;
60 | msg.volume = volume;
61 | msg.pitch = pitch;
62 | msg.rate = rate;
63 |
64 | // Load voices if not loaded yet
65 | if (Object.keys(this.voices).length === 0) {
66 | await this.initializeVoices();
67 | }
68 |
69 | if (this.voices[voice]) {
70 | msg.voice = this.voices[voice];
71 | }
72 |
73 | if (wait === 'wait') {
74 | await new Promise((resolve) => {
75 | msg.addEventListener('end', value => {
76 | resolve(value);
77 | });
78 | window.speechSynthesis.speak(msg);
79 | });
80 | } else {
81 | window.speechSynthesis.speak(msg);
82 | }
83 | }
84 |
85 | return;
86 | }
87 |
88 | /**
89 | * Initialize the internal list of voices
90 | */
91 | initializeVoices = async () => {
92 | var voices = window.speechSynthesis.getVoices();
93 | if (voices.length === 0) {
94 | await timeout(1000);
95 | voices = window.speechSynthesis.getVoices();
96 | }
97 | this.updateVoices(voices);
98 | }
99 |
100 | /**
101 | * Update the internal voice dictionary.
102 | * @param {array} voices list of SpeechSynthesisVoice objects
103 | */
104 | updateVoices = (voices) => {
105 | this.voices = {};
106 | voices.forEach(voice => {
107 | this.voices[voice.name] = voice;
108 | });
109 |
110 | }
111 | }
112 |
113 | /**
114 | * Create a handler and read user settings
115 | */
116 | async function ttsHandlerExport() {
117 | var ttsHandler = new TTSHandler();
118 | }
119 | ttsHandlerExport();
120 |
--------------------------------------------------------------------------------
/js/twitch/twitch-eventsub-handler.js:
--------------------------------------------------------------------------------
1 | class EventSubHandler {
2 | wsUrl = "wss://eventsub.wss.twitch.tv/ws";
3 | /**
4 | * Create an EventSub handler to control the websocket connection
5 | *
6 | * @param {TwitchAPI} api twitch api interface
7 | * @param {string} channelId twitch channel id
8 | * @param {function} onMessage method to call when events are received
9 | */
10 | constructor(api, channelId, onMessage) {
11 | this.api = api;
12 | this.channelId = channelId;
13 | this.onMessage = onMessage;
14 | this.wsId = "";
15 |
16 | this.addConnection(async (id) => {
17 | this.wsId = id;
18 | await this.addSubscriptions();
19 | await this.deleteOldSubscriptions();
20 | }, this.wsUrl);
21 | }
22 |
23 | addConnection = (onWelcome, url = this.wsUrl) => {
24 | const ws = new WebSocket(url);
25 | ws.onmessage = (event) => {
26 | if (Debug.Twitch || Debug.All) {
27 | console.error(`Received EventSub Message: ${JSON.stringify(event)}`);
28 | }
29 |
30 | const {
31 | metadata: { message_type },
32 | payload,
33 | } = JSON.parse(event.data);
34 | if (message_type === "session_welcome") {
35 | const {
36 | session: { id, keepalive_timeout_seconds },
37 | } = payload;
38 | if (Debug.Twitch || Debug.All) {
39 | console.error(`Received welcome message for session "${id}"`);
40 | }
41 | ws.resetTimeout = () => {
42 | if (ws.keepaliveTimeout) {
43 | clearTimeout(ws.keepaliveTimeout);
44 | }
45 | ws.keepaliveTimeout = setTimeout(() => {
46 | console.error("Connection to Twitch EventSub lost");
47 | ws.close();
48 | }, keepalive_timeout_seconds * 1000 + 100);
49 | };
50 | ws.resetTimeout();
51 | onWelcome(id);
52 | } else if (message_type === "session_keepalive") {
53 | ws.resetTimeout();
54 | } else if (message_type === "session_reconnect") {
55 | const {
56 | session: { id, reconnect_url },
57 | } = payload;
58 | if (Debug.Twitch || Debug.All) {
59 | console.error(`Received reconnect message for session "${id}"`);
60 | }
61 | this.addConnection((newId) => {
62 | clearTimeout(ws.keepaliveTimeout);
63 | ws.close();
64 | this.wsId = newId;
65 | }, reconnect_url);
66 | } else if (message_type === "notification") {
67 | ws.resetTimeout();
68 | const { subscription, event } = payload;
69 | if (Debug.Twitch || Debug.All) {
70 | console.error(`Received notification for type ${subscription.type}`);
71 | }
72 | this.onMessage(event, subscription);
73 | } else if (message_type === "revocation") {
74 | ws.resetTimeout();
75 | const { subscription } = payload;
76 | if (Debug.Twitch || Debug.All) {
77 | console.error(`Received revocation notification for subscription id ${subscription.id}`);
78 | }
79 | this.resubscribe(subscription.type, subscription.version, subscription.condition);
80 | } else {
81 | console.error(`Unhandled WebSocket message type "${message_type}"`);
82 | }
83 | };
84 | ws.onclose = (event) => {
85 | const { code, reason } = event;
86 | console.error(`Twitch EventSub WebSocket connection closed. ${code}:${reason}`);
87 | this.addConnection(async (newId) => {
88 | console.error("Twitch EventSub reconnected after connection closed.")
89 | clearTimeout(ws.keepaliveTimeout);
90 | if (this.wsId !== newId) {
91 | this.wsId = newId;
92 | await this.addSubscriptions();
93 | await this.deleteOldSubscriptions();
94 | }
95 | });
96 | };
97 | }
98 |
99 | resubscribe = async (type, version, condition) => {
100 | if (Debug.Twitch || Debug.All) {
101 | console.error(`Resubscribing to ${type}`);
102 | }
103 | await this.api.createEventSubSubscription(type, version, condition, this.wsId);
104 | if (Debug.Twitch || Debug.All) {
105 | console.error(`Resubscribing to ${type}`);
106 | }
107 | }
108 |
109 | addSubscriptions = async () => {
110 | const subscriptions = [
111 | {
112 | type: 'channel.update',
113 | data: { broadcaster_user_id: this.channelId },
114 | version: 1
115 | },
116 | {
117 | type: 'channel.follow',
118 | data: { broadcaster_user_id: this.channelId, moderator_user_id: this.channelId },
119 | version: "2"
120 | },
121 | {
122 | type: 'channel.ad_break.begin',
123 | data: { broadcaster_user_id: this.channelId },
124 | version: 1
125 | },
126 | {
127 | type: 'channel.chat.clear',
128 | data: { broadcaster_user_id: this.channelId, user_id: this.channelId },
129 | version: 1
130 | },
131 | {
132 | type: 'channel.chat.clear_user_messages',
133 | data: { broadcaster_user_id: this.channelId, user_id: this.channelId },
134 | version: 1
135 | },
136 | {
137 | type: 'channel.subscribe',
138 | data: { broadcaster_user_id: this.channelId },
139 | version: 1
140 | },
141 | {
142 | type: 'channel.subscription.gift',
143 | data: { broadcaster_user_id: this.channelId },
144 | version: 1
145 | },
146 | {
147 | type: 'channel.subscription.message',
148 | data: { broadcaster_user_id: this.channelId },
149 | version: 1
150 | },
151 | {
152 | type: 'channel.cheer',
153 | data: { broadcaster_user_id: this.channelId },
154 | version: 1
155 | },
156 | {
157 | type: 'channel.raid',
158 | data: { to_broadcaster_user_id: this.channelId },
159 | version: 1
160 | },
161 | {
162 | type: 'channel.ban',
163 | data: { broadcaster_user_id: this.channelId },
164 | version: 1
165 | },
166 | {
167 | type: 'channel.unban',
168 | data: { broadcaster_user_id: this.channelId },
169 | version: 1
170 | },
171 | {
172 | type: 'channel.moderator.add',
173 | data: { broadcaster_user_id: this.channelId },
174 | version: 1
175 | },
176 | {
177 | type: 'channel.moderator.remove',
178 | data: { broadcaster_user_id: this.channelId },
179 | version: 1
180 | },
181 | {
182 | type: 'channel.channel_points_custom_reward_redemption.add',
183 | data: { broadcaster_user_id: this.channelId },
184 | version: 1
185 | },
186 | {
187 | type: 'channel.channel_points_custom_reward_redemption.update',
188 | data: { broadcaster_user_id: this.channelId },
189 | version: 1
190 | },
191 | {
192 | type: 'channel.poll.begin',
193 | data: { broadcaster_user_id: this.channelId },
194 | version: 1
195 | },
196 | {
197 | type: 'channel.poll.progress',
198 | data: { broadcaster_user_id: this.channelId },
199 | version: 1
200 | },
201 | {
202 | type: 'channel.poll.end',
203 | data: { broadcaster_user_id: this.channelId },
204 | version: 1
205 | },
206 | {
207 | type: 'channel.prediction.begin',
208 | data: { broadcaster_user_id: this.channelId },
209 | version: 1
210 | },
211 | {
212 | type: 'channel.prediction.progress',
213 | data: { broadcaster_user_id: this.channelId },
214 | version: 1
215 | },
216 | {
217 | type: 'channel.prediction.lock',
218 | data: { broadcaster_user_id: this.channelId },
219 | version: 1
220 | },
221 | {
222 | type: 'channel.prediction.end',
223 | data: { broadcaster_user_id: this.channelId },
224 | version: 1
225 | },
226 | {
227 | type: 'channel.suspicious_user.message',
228 | data: { broadcaster_user_id: this.channelId, moderator_user_id: this.channelId },
229 | version: 1
230 | },
231 | {
232 | type: 'channel.vip.add',
233 | data: { broadcaster_user_id: this.channelId },
234 | version: 1
235 | },
236 | {
237 | type: 'channel.vip.remove',
238 | data: { broadcaster_user_id: this.channelId },
239 | version: 1
240 | },
241 | {
242 | type: 'channel.charity_campaign.donate',
243 | data: { broadcaster_user_id: this.channelId },
244 | version: 1
245 | },
246 | {
247 | type: 'channel.charity_campaign.start',
248 | data: { broadcaster_user_id: this.channelId },
249 | version: 1
250 | },
251 | {
252 | type: 'channel.charity_campaign.progress',
253 | data: { broadcaster_user_id: this.channelId },
254 | version: 1
255 | },
256 | {
257 | type: 'channel.charity_campaign.stop',
258 | data: { broadcaster_user_id: this.channelId },
259 | version: 1
260 | },
261 | {
262 | type: 'channel.goal.begin',
263 | data: { broadcaster_user_id: this.channelId },
264 | version: 1
265 | },
266 | {
267 | type: 'channel.goal.progress',
268 | data: { broadcaster_user_id: this.channelId },
269 | version: 1
270 | },
271 | {
272 | type: 'channel.goal.end',
273 | data: { broadcaster_user_id: this.channelId },
274 | version: 1
275 | },
276 | {
277 | type: 'channel.hype_train.begin',
278 | data: { broadcaster_user_id: this.channelId },
279 | version: 1
280 | },
281 | {
282 | type: 'channel.hype_train.progress',
283 | data: { broadcaster_user_id: this.channelId },
284 | version: 1
285 | },
286 | {
287 | type: 'channel.hype_train.end',
288 | data: { broadcaster_user_id: this.channelId },
289 | version: 1
290 | },
291 | {
292 | type: 'channel.shield_mode.begin',
293 | data: { broadcaster_user_id: this.channelId, moderator_user_id: this.channelId },
294 | version: 1
295 | },
296 | {
297 | type: 'channel.shield_mode.end',
298 | data: { broadcaster_user_id: this.channelId, moderator_user_id: this.channelId },
299 | version: 1
300 | },
301 | {
302 | type: 'channel.shoutout.create',
303 | data: { broadcaster_user_id: this.channelId, moderator_user_id: this.channelId },
304 | version: 1
305 | },
306 | {
307 | type: 'channel.shoutout.receive',
308 | data: { broadcaster_user_id: this.channelId, moderator_user_id: this.channelId },
309 | version: 1
310 | },
311 | {
312 | type: 'stream.online',
313 | data: { broadcaster_user_id: this.channelId },
314 | version: 1
315 | },
316 | {
317 | type: 'stream.offline',
318 | data: { broadcaster_user_id: this.channelId },
319 | version: 1
320 | }
321 | ];
322 |
323 | for (var i = 0; i < subscriptions.length; i++) {
324 | var {type, data, version} = subscriptions[i];
325 | await new Promise(async (resolve) => {
326 | await this.api.createEventSubSubscription(type, version, data, this.wsId);
327 | resolve();
328 | });
329 | }
330 | }
331 |
332 | deleteOldSubscriptions = async () => {
333 | var { total, data } = await this.api.getEventSubSubscriptions();
334 | if (total > 0) {
335 | for (var i = 0; i < data.length; i++) {
336 | if (data[i]["transport"]["session_id"] !== this.wsId) {
337 | await this.api.deleteEventSubSubscription(data[i]["id"]);
338 | }
339 | }
340 | }
341 | }
342 | }
343 |
--------------------------------------------------------------------------------
/js/twitch/twitch-pub-socket.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Connect to the Twitch PubSub websocket and setup the event handlers
3 | * @param {string} channelId twitch channel id
4 | * @param {function} onMessage method to call when events are received
5 | */
6 | function connectPubSubWebsocket(channelId, onMessage) {
7 | // Create the websocket connection
8 | var socket = new WebSocket('wss://pubsub-edge.twitch.tv');
9 |
10 | var nonce = randomString(18);
11 | var wasOpened = false;
12 |
13 | // WS OnOpen event : authenticate
14 | socket.onopen = function() {
15 | console.error('Twitch Pubsub Websocket Opened');
16 | wasOpened = true;
17 |
18 | // Create authentication payload and request required events
19 | var auth = {
20 | "type": "LISTEN",
21 | "nonce": nonce,
22 | "data": {
23 | "topics": [
24 | "community-points-channel-v1." + channelId
25 | ],
26 | }
27 | };
28 |
29 | // Send authentication payload to Twitch PubSub
30 | socket.send(JSON.stringify(auth));
31 | };
32 |
33 | setInterval(function() {
34 | setTimeout(function() {
35 | socket.send(JSON.stringify({
36 | "type": "PING"
37 | }));
38 | }, Math.random() * 10000 + 1000);
39 | }, 240000);
40 |
41 | // Ws OnClose : try reconnect
42 | socket.onclose = function() {
43 | console.error('Twitch Pubsub Websocket Closed');
44 | socket = null;
45 | if (wasOpened) {
46 | connectPubSubWebsocket(channelId, onMessage);
47 | }
48 | };
49 |
50 | // WS OnMessage event : handle events
51 | socket.onmessage = function(message) {
52 | if (message.type === "RECONNECT" || message.type === "AUTH_REVOKED") {
53 | console.error(`Twitch Pubsub Received ${message.type} Message`);
54 | socket.close();
55 | } else if (message.type === "PONG") {
56 | console.error('Twitch Pubsub Received Pong Message');
57 | } else {
58 | onMessage(message);
59 | }
60 | };
61 | };
62 |
--------------------------------------------------------------------------------
/js/utils/idb-service.js:
--------------------------------------------------------------------------------
1 | /**
2 | * IDBService is a proxy for interacting with IndexedDB through idbKeyval.
3 | */
4 | class IDBService {
5 |
6 | /**
7 | * Waits for the idbKeyval global module to be loaded.
8 | */
9 | static async awaitModuleLoad() {
10 | if (typeof idbKeyval !== 'undefined') {
11 | return;
12 | }
13 |
14 | return await new Promise((resolve) => {
15 | setTimeout(async () => {
16 | await IDBService.awaitModuleLoad();
17 | resolve();
18 | }, 100);
19 | });
20 | }
21 |
22 | /**
23 | * Retrieve a value from IndexedDB by the key.
24 | * @param {string} key id of the value to retrieve
25 | */
26 | static async get(key) {
27 | await IDBService.awaitModuleLoad();
28 | return await idbKeyval.get(key);
29 | }
30 |
31 | /**
32 | * Retrieve all keys from IndexedDB.
33 | */
34 | static async keys() {
35 | await IDBService.awaitModuleLoad();
36 | return await idbKeyval.keys();
37 | }
38 |
39 | /**
40 | * Retrieve all entries from IndexedDB.
41 | */
42 | static async entries() {
43 | await IDBService.awaitModuleLoad();
44 | var entries = await idbKeyval.entries()
45 | return entries.reduce(
46 | (prev, cur) => ({ ...prev, [cur[0]]: cur[1] }),
47 | {}
48 | );
49 | }
50 |
51 | /**
52 | * Set a key-value pair in IndexedDB.
53 | * @param {string} key id of the entry to store
54 | * @param {object} value object to store in the entry
55 | */
56 | static async set(key, value) {
57 | await IDBService.awaitModuleLoad();
58 | await idbKeyval.set(key, value);
59 | }
60 |
61 | /**
62 | * Delete a key-value entry from IndexedDB by the key.
63 | * @param {string} key id of the value to delete
64 | */
65 | static async delete(key) {
66 | await IDBService.awaitModuleLoad();
67 | idbKeyval.del(key);
68 | }
69 |
70 | /**
71 | * Clear all entries from IndexedDB.
72 | */
73 | static async clear() {
74 | await IDBService.awaitModuleLoad();
75 | idbKeyval.clear();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/js/utils/shlex.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Port of a subset of the features of CPython's shlex module, which provides a
3 | * shell-like lexer. Original code by Eric S. Raymond and other contributors.
4 | *
5 | * Ported to single js file by Kruiser8
6 | *
7 | * Author: Ryan Govostes (rgov)
8 | * License: MIT
9 | * Repository: https://github.com/rgov/node-shlex
10 | */
11 | class Shlexer {
12 | constructor (string) {
13 | this.i = 0
14 | this.string = string
15 |
16 | /**
17 | * Characters that will be considered whitespace and skipped. Whitespace
18 | * bounds tokens. By default, includes space, tab, linefeed and carriage
19 | * return.
20 | */
21 | this.whitespace = ' \t\r\n'
22 |
23 | /**
24 | * Characters that will be considered string quotes. The token accumulates
25 | * until the same quote is encountered again (thus, different quote types
26 | * protect each other as in the shell.) By default, includes ASCII single
27 | * and double quotes.
28 | */
29 | this.quotes = `'"`
30 |
31 | /**
32 | * Characters that will be considered as escape. Just `\` by default.
33 | */
34 | this.escapes = '\\'
35 |
36 | /**
37 | * The subset of quote types that allow escaped characters. Just `"` by default.
38 | */
39 | this.escapedQuotes = '"'
40 |
41 | /**
42 | * Whether to support ANSI C-style $'' quotes
43 | * https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
44 | */
45 | this.ansiCQuotes = true
46 |
47 | /**
48 | * Whether to support localized $"" quotes
49 | * https://www.gnu.org/software/bash/manual/html_node/Locale-Translation.html
50 | *
51 | * The behavior is as if the current locale is set to C or POSIX, i.e., the
52 | * contents are not translated.
53 | */
54 | this.localeQuotes = true
55 |
56 | this.debug = false
57 | }
58 |
59 | readChar () {
60 | return this.string.charAt(this.i++)
61 | }
62 |
63 | processEscapes (string, quote, isAnsiCQuote) {
64 | if (!isAnsiCQuote && !this.escapedQuotes.includes(quote)) {
65 | // This quote type doesn't support escape sequences
66 | return string
67 | }
68 |
69 | // We need to form a regex that matches any of the escape characters,
70 | // without interpreting any of the characters as a regex special character.
71 | let anyEscape = '[' + this.escapes.replace(/(.)/g, '\\$1') + ']'
72 |
73 | // In regular quoted strings, we can only escape an escape character, and
74 | // the quote character itself.
75 | if (!isAnsiCQuote && this.escapedQuotes.includes(quote)) {
76 | let re = new RegExp(
77 | anyEscape + '(' + anyEscape + '|\\' + quote + ')', 'g')
78 | return string.replace(re, '$1')
79 | }
80 |
81 | // ANSI C quoted strings support a wide variety of escape sequences
82 | if (isAnsiCQuote) {
83 | let patterns = {
84 | // Literal characters
85 | '([\\\\\'"?])': (x) => x,
86 |
87 | // Non-printable ASCII characters
88 | 'a': () => '\x07',
89 | 'b': () => '\x08',
90 | 'e|E': () => '\x1b',
91 | 'f': () => '\x0c',
92 | 'n': () => '\x0a',
93 | 'r': () => '\x0d',
94 | 't': () => '\x09',
95 | 'v': () => '\x0b',
96 |
97 | // Octal bytes
98 | '([0-7]{1,3})': (x) => String.fromCharCode(parseInt(x, 8)),
99 |
100 | // Hexadecimal bytes
101 | 'x([0-9a-fA-F]{1,2})': (x) => String.fromCharCode(parseInt(x, 16)),
102 |
103 | // Unicode code units
104 | 'u([0-9a-fA-F]{1,4})': (x) => String.fromCharCode(parseInt(x, 16)),
105 | 'U([0-9a-fA-F]{1,8})': (x) => String.fromCharCode(parseInt(x, 16)),
106 |
107 | // Control characters
108 | // https://en.wikipedia.org/wiki/Control_character#How_control_characters_map_to_keyboards
109 | 'c(.)': (x) => {
110 | if (x === '?') {
111 | return '\x7f'
112 | } else if (x === '@') {
113 | return '\x00'
114 | } else {
115 | return String.fromCharCode(x.charCodeAt(0) & 31)
116 | }
117 | }
118 | }
119 |
120 | // Construct an uber-RegEx that catches all of the above pattern
121 | let re = new RegExp(
122 | anyEscape + '(' + Object.keys(patterns).join('|') + ')', 'g')
123 |
124 | // For each match, figure out which subpattern matched, and apply the
125 | // corresponding function
126 | return string.replace(re, function (m, p1) {
127 | for (let matched in patterns) {
128 | let mm = new RegExp('^' + matched + '$').exec(p1)
129 | if (mm === null) {
130 | continue
131 | }
132 |
133 | return patterns[matched].apply(null, mm.slice(1))
134 | }
135 | })
136 | }
137 |
138 | // Should not get here
139 | return undefined
140 | }
141 |
142 | * [Symbol.iterator] () {
143 | let inQuote = false
144 | let inDollarQuote = false
145 | let escaped = false
146 | let lastDollar = -2 // position of last dollar sign we saw
147 | let token
148 |
149 | if (this.debug) {
150 | console.log('full input:', '>' + this.string + '<')
151 | }
152 |
153 | while (true) {
154 | const pos = this.i
155 | const char = this.readChar()
156 |
157 | if (this.debug) {
158 | console.log(
159 | 'position:', pos,
160 | 'input:', '>' + char + '<',
161 | 'accumulated:', token,
162 | 'inQuote:', inQuote,
163 | 'inDollarQuote:', inDollarQuote,
164 | 'lastDollar:', lastDollar,
165 | 'escaped:', escaped
166 | )
167 | }
168 |
169 | // Ran out of characters, we're done
170 | if (char === '') {
171 | if (inQuote) { throw new Error('Got EOF while in a quoted string') }
172 | if (escaped) { throw new Error('Got EOF while in an escape sequence') }
173 | if (token !== undefined) { yield token }
174 | return
175 | }
176 |
177 | // We were in an escape sequence, complete it
178 | if (escaped) {
179 | if (char === '\n') {
180 | // An escaped newline just means to continue the command on the next
181 | // line. We just need to ignore it.
182 | } else if (inQuote) {
183 | // If we are in a quote, just accumulate the whole escape sequence,
184 | // as we will interpret escape sequences later.
185 | token = (token || '') + escaped + char
186 | } else {
187 | // Just use the literal character
188 | token = (token || '') + char
189 | }
190 |
191 | escaped = false
192 | continue
193 | }
194 |
195 | if (this.escapes.includes(char)) {
196 | if (!inQuote || inDollarQuote !== false || this.escapedQuotes.includes(inQuote)) {
197 | // We encountered an escape character, which is going to affect how
198 | // we treat the next character.
199 | escaped = char
200 | continue
201 | } else {
202 | // This string type doesn't use escape characters. Ignore for now.
203 | }
204 | }
205 |
206 | // We were in a string
207 | if (inQuote !== false) {
208 | // String is finished. Don't grab the quote character.
209 | if (char === inQuote) {
210 | token = this.processEscapes(token, inQuote, inDollarQuote === '\'')
211 | inQuote = false
212 | inDollarQuote = false
213 | continue
214 | }
215 |
216 | // String isn't finished yet, accumulate the character
217 | token = (token || '') + char
218 | continue
219 | }
220 |
221 | // This is the start of a new string, don't accumulate the quotation mark
222 | if (this.quotes.includes(char)) {
223 | inQuote = char
224 | if (lastDollar === pos - 1) {
225 | if (char === '\'' && !this.ansiCQuotes) {
226 | // Feature not enabled
227 | } else if (char === '"' && !this.localeQuotes) {
228 | // Feature not enabled
229 | } else {
230 | inDollarQuote = char
231 | }
232 | }
233 |
234 | token = (token || '') // fixes blank string
235 |
236 | if (inDollarQuote !== false) {
237 | // Drop the opening $ we captured before
238 | token = token.slice(0, -1)
239 | }
240 |
241 | continue
242 | }
243 |
244 | // This is a dollar sign, record that we saw it in case it's the start of
245 | // an ANSI C or localized string
246 | if (inQuote === false && char === '$') {
247 | lastDollar = pos
248 | }
249 |
250 | // This is whitespace, so yield the token if we have one
251 | if (this.whitespace.includes(char)) {
252 | if (token !== undefined) { yield token }
253 | token = undefined
254 | continue
255 | }
256 |
257 | // Otherwise, accumulate the character
258 | token = (token || '') + char
259 | }
260 | }
261 | }
262 |
263 |
264 | /**
265 | * Splits a given string using shell-like syntax.
266 | *
267 | * @param {String} s String to split.
268 | * @returns {String[]}
269 | */
270 | shlexSplit = function (s) {
271 | return Array.from(new Shlexer(s))
272 | }
273 |
274 | /**
275 | * Escapes a potentially shell-unsafe string using quotes.
276 | *
277 | * @param {String} s String to quote
278 | * @returns {String}
279 | */
280 | shlexQuote = function (s) {
281 | if (s === '') { return '\'\'' }
282 |
283 | var unsafeRe = /[^\w@%\-+=:,./]/
284 | if (!unsafeRe.test(s)) { return s }
285 |
286 | return '\'' + s.replace(/'/g, '\'"\'"\'') + '\''
287 | }
288 |
--------------------------------------------------------------------------------
/js/utils/storage-emitter.js:
--------------------------------------------------------------------------------
1 | class StorageEmitter {
2 | constructor() {
3 | this.data = new Map();
4 | this.events = new Map();
5 | }
6 |
7 | onChange = (key, callback, fireOnInit) => {
8 | if (this.events.has(key)) {
9 | this.events.get(key).push(callback);
10 | } else {
11 | this.events.set(key, [callback]);
12 | }
13 |
14 | if (fireOnInit && this.data.has(key)) {
15 | if (Debug.All || Debug.Storage) {
16 | console.error(`Storage onChange fireOnInit for ${key} with value: ${this.data.get(key)}`);
17 | }
18 | callback(key, this.data.get(key));
19 | }
20 | }
21 |
22 | set = (key, value) => {
23 | if (Debug.All || Debug.Storage) {
24 | console.error(`Storage set ${key} to: ${value}`);
25 | }
26 | if (
27 | !this.data.has(key) || this.data.get(key) !== value
28 | ) {
29 | if (this.events.has(key)) {
30 | if (Debug.All || Debug.Storage) {
31 | console.error(`Storage callback requests for ${key} initiated`);
32 | }
33 | this.events.get(key).forEach(callback =>
34 | callback(key, value, this.data.has(key) ? this.data.get(key) : null));
35 | }
36 | this.data.set(key, value);
37 | }
38 | }
39 | }
40 |
41 | var Storage = new StorageEmitter();
42 |
--------------------------------------------------------------------------------
/js/utils/utils.js:
--------------------------------------------------------------------------------
1 | let AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
2 |
3 | /**
4 | * Clamp a number between two values.
5 | * @param {number} num number to clamp
6 | * @param {number} min minimum number allowed
7 | * @param {number} max maximum number allowed
8 | */
9 | const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
10 |
11 | /**
12 | * Create a random string of the provided length.
13 | * @param {number} length string length to generate
14 | */
15 | function randomString(length) {
16 | var text = "";
17 | var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
18 | for(var i = 0; i < length; i++) {
19 | text += possible.charAt(Math.floor(Math.random() * possible.length));
20 | }
21 | return text;
22 | }
23 |
24 | /**
25 | * Retrieve the data from a url and convert to a blob.
26 | * @param {string} url URL to download data
27 | */
28 | function getBlobFromUrl(url) {
29 | return new Promise((resolve, reject) => {
30 | let request = new XMLHttpRequest();
31 | request.open('GET', url, true);
32 | request.responseType = 'blob';
33 | request.onload = () => {
34 | resolve(request.response);
35 | };
36 | request.onerror = reject;
37 | request.send();
38 | })
39 | }
40 |
41 | /**
42 | * Convert the URL to a file into a File object.
43 | * @param {string} fileUrl contents of trigger line
44 | */
45 | async function convertUrlToFileObj(fileUrl) {
46 | try {
47 | var fileName = fileUrl.substring(fileUrl.lastIndexOf('/'));
48 | var fileBlob = await getBlobFromUrl(fileUrl);
49 | var file = new File([fileBlob], fileName);
50 | return file;
51 | } catch (err) {
52 | console.log(err);
53 | return null;
54 | }
55 | }
56 |
57 | /**
58 | * Read the file and send the data to the callback.
59 | * @param {string} method type of API call
60 | * @param {string} url url to call
61 | * @param {object} data parameters to send with the call
62 | * @param {object} headers headers to send with the call
63 | * @param {object} ajaxArgs options added to the $.ajax call
64 | */
65 | async function callAPI(method, url, data, headers, ajaxArgs) {
66 | data = data || {};
67 | headers = headers || {};
68 | ajaxArgs = ajaxArgs || {};
69 | var response = null;
70 | try {
71 | await $.ajax({
72 | url: url,
73 | type: method,
74 | data: data,
75 | headers: headers,
76 | success: function(data) {
77 | response = data;
78 | },
79 | error: function(data) {
80 | console.error(`Error calling the ${url} API: ${JSON.stringify(data)}`);
81 | },
82 | ...ajaxArgs
83 | });
84 | } catch (err) {
85 | response = 'Error';
86 | }
87 |
88 | return response;
89 | }
90 |
91 | /**
92 | * Read the file and send the data to the callback.
93 | * @param {string} file name of the file
94 | */
95 | async function readFile(file) {
96 | var response = "";
97 | await $.ajax({
98 | url: file,
99 | type: 'GET',
100 | dataType: 'text',
101 | success: function(data) {
102 | response = data;
103 | },
104 | error: function(data) {
105 | console.error(`Error reading the ${file} file! Please open the html in Microsoft Edge or your broadcasting software.`);
106 | }
107 | });
108 | return response;
109 | }
110 |
111 | /**
112 | * Get the user's id and send to the callback.
113 | * @param {string} user name user
114 | * @param {function} callback function to call with file data
115 | */
116 | async function getIdFromUser(user) {
117 | var response = "";
118 | await $.ajax({
119 | url: 'https://decapi.me/twitch/id/' + user,
120 | type: 'GET',
121 | dataType: 'text',
122 | success: function(data) {
123 | response = data;
124 | },
125 | error: function(data) {
126 | console.error(`Error getting the user id for ${user}! Please double-check that your user is spelled correctly.`);
127 | }
128 | });
129 |
130 | if (response === `User not found: ${user}`) {
131 | console.error(`Unable to get the ID for the input user: ${user}`);
132 | return '';
133 | }
134 |
135 | return response;
136 | }
137 |
138 | /**
139 | * Escape a string for use in a RegExp
140 | * @param {string} value input string to escape
141 | */
142 | function escapeRegExp(value) {
143 | return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
144 | }
145 |
146 | /**
147 | * Check if the input is numeric
148 | * @param {mixed} n input
149 | */
150 | function isNumeric(n) {
151 | return !isNaN(parseFloat(n)) && isFinite(n);
152 | }
153 |
154 | /**
155 | * Decode any html encoded values in the input
156 | * @param {string} value input string to decode
157 | */
158 | function htmlDecode(value) {
159 | return new DOMParser().parseFromString(value,'text/html').querySelector('html').textContent;
160 | }
161 |
162 | /**
163 | * Increment the value by the input increment.
164 | * If the value is not numeric, sets to 0.
165 | *
166 | * @param {mixed} value starting value
167 | * @param {numeric} increment incremental value
168 | */
169 | function incrementVar(value, increment) {
170 | if (!isNumeric(value)) {
171 | value = 0;
172 | } else {
173 | value = parseFloat(value);
174 | }
175 | return value + increment;
176 | }
177 |
178 | /**
179 | * Proper cases the input string.
180 | * - capitalizes the first letter of every word.
181 | */
182 | Object.defineProperty(String.prototype, "toProperCase", {
183 | value: function toProperCase() {
184 | var sentence = this.toLowerCase().split(" ");
185 | for(var i = 0; i < sentence.length; i++){
186 | sentence[i] = sentence[i][0].toUpperCase() + sentence[i].slice(1);
187 | }
188 | return sentence.join(' ');
189 | },
190 | writable: true,
191 | configurable: true
192 | });
193 |
194 | /**
195 | * Return a promise for the specified amount of milliseconds
196 | * @param {number} ms Milliseconds to wait in the promise
197 | */
198 | function timeout(ms) {
199 | return new Promise(resolve => setTimeout(resolve, ms));
200 | }
201 |
--------------------------------------------------------------------------------
/js/variable/variableHandler.js:
--------------------------------------------------------------------------------
1 | class VariableHandler extends Handler {
2 | /**
3 | * Create a new Variable handler.
4 | */
5 | constructor() {
6 | super('Variable', []);
7 | this.autoload = false;
8 | this.globals = {};
9 | this.variables = {};
10 | this.success();
11 | }
12 |
13 | /**
14 | * Initialize the variable handler with the input settings.
15 | * @param {string} autoload on/off to toggle variables autoloading
16 | */
17 | init = (autoload) => {
18 | this.autoload = autoload.toLowerCase() === "on" ? true : false;
19 | if (this.autoload) {
20 | this.loadGlobalVariables();
21 | }
22 | }
23 |
24 | /**
25 | * Handle the input data (take an action).
26 | * @param {array} triggerData contents of trigger line
27 | */
28 | handleData = async (triggerData) => {
29 | var action = Parser.getAction(triggerData, 'Variable');
30 | if (action === 'global') {
31 | action = Parser.getAction(triggerData, 'Variable', 1)
32 | // Loads a global variable
33 | if (action === 'load') {
34 | var { varName } = Parser.getInputs(triggerData, ['global', 'action', 'varName']);
35 | var variable = await IDBService.get(varName) || 'No variable found';
36 | this.globals[varName] = variable;
37 | return {[varName]: variable};
38 | }
39 | // Clears all global variables
40 | else if (action === 'clear') {
41 | this.globals = {};
42 | IDBService.clear();
43 | }
44 | // Remove a global variable
45 | else if (action === 'remove') {
46 | var { varName } = Parser.getInputs(triggerData, ['global', 'action', 'varName']);
47 | delete this.globals[varName];
48 | IDBService.delete(varName);
49 | }
50 | // Set a global variable
51 | else if (action === 'set') {
52 | var { varName, variable } = Parser.getInputs(triggerData, ['global', 'action', 'varName', 'variable']);
53 | this.globals[varName] = variable;
54 | IDBService.set(varName, variable);
55 | return {[varName]: variable};
56 | }
57 | } else {
58 | // Load a variable
59 | if (action === 'load') {
60 | var { varName } = Parser.getInputs(triggerData, ['action', 'varName']);
61 | var variable = this.variables[varName] || 'No variable found';
62 | return {[varName]: variable};
63 | }
64 | // Sets a variable
65 | else if (action === 'set') {
66 | var { varName, variable } = Parser.getInputs(triggerData, ['action', 'varName', 'variable']);
67 | this.variables[varName] = variable;
68 | return {[varName]: variable};
69 | }
70 | // Removes a variable
71 | else if (action === 'remove') {
72 | var { varName } = Parser.getInputs(triggerData, ['action', 'varName']);
73 | if (this.variables.hasOwnProperty(varName)) {
74 | delete this.variables[varName];
75 | }
76 | }
77 | }
78 | }
79 |
80 | /**
81 | * Retrieve all variables (session and global).
82 | */
83 | getVariables = async () => {
84 | if (this.autoload) {
85 | return {
86 | ...this.globals,
87 | ...this.variables
88 | };
89 | }
90 |
91 | return {};
92 | }
93 |
94 | /**
95 | * Load all global variables from storage.
96 | */
97 | loadGlobalVariables = async () => {
98 | this.globals = await IDBService.entries();
99 | }
100 | }
101 |
102 | /**
103 | * Create a handler
104 | */
105 | async function variableHandlerExport() {
106 | var variable = new VariableHandler();
107 | var autoload = await readFile('settings/variable/autoload.txt');
108 | variable.init(autoload.trim());
109 | }
110 | variableHandlerExport();
111 |
--------------------------------------------------------------------------------
/js/voicemod/voicemod-socket.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Connect to the Voicemod websocket.
3 | * @param {string} address host for the Voicemod API
4 | * @param {string} apiKey Voicemod API key
5 | * @param {Function} onConnect Function to call when Voicemod is connected
6 | */
7 | function connectVoicemodWebsocket(address, apiKey, onConnect) {
8 | var promises = {};
9 | var apiWrapper = {};
10 |
11 | //Connect to socket
12 | websocket = new WebSocket(address);
13 |
14 | websocket.onopen = () => {
15 | if (Debug.Voicemod || Debug.All) {
16 | console.error('Successfully opened the voicemod websocket');
17 | }
18 | onConnect();
19 | send('registerClient', {
20 | clientKey: apiKey
21 | });
22 | };
23 |
24 | websocket.onclose = (evt) => {
25 | console.error('Voicemod websocket connection closed');
26 | };
27 |
28 | websocket.onerror = (err) => {
29 | console.error('Voicemod websocket error');
30 | console.error(JSON.stringify(err));
31 | };
32 |
33 | websocket.onmessage = (e) => {
34 | if (e.data) {
35 | message = JSON.parse(e.data);
36 |
37 | if (Debug.Voicemod || Debug.All) {
38 | console.error(e.data);
39 | }
40 |
41 | var messageId = message.id || message.actionId;
42 |
43 | if (promises[messageId]) {
44 | promises[messageId](message);
45 | }
46 | }
47 | }
48 |
49 | var send = async (action, payload) => {
50 | payload = payload || {};
51 | var id = uuidv4();
52 | var request = {
53 | id,
54 | action,
55 | payload
56 | };
57 |
58 | return await new Promise((resolve) => {
59 | promises[id] = resolve;
60 | websocket.send(JSON.stringify(request));
61 | });
62 | }
63 |
64 | apiWrapper.getVoices = async () => {
65 | var data = await send('getVoices');
66 | return data.payload.voices;
67 | }
68 |
69 | apiWrapper.getCurrentVoice = async () => {
70 | var data = await send('getVoices');
71 | if (data.getCurrentVoice === 'nofx') {
72 | return 'nofx';
73 | }
74 |
75 | var voice = data.actionObject.voices.filter(option => option.id === data.currentVoice)[0];
76 | return voice.friendlyName;
77 | }
78 |
79 | apiWrapper.getAllSoundboard = async () => {
80 | var data = await send('getAllSoundboard');
81 | return data.payload.soundboards;
82 | }
83 |
84 | apiWrapper.setVoice = async (voiceID) => {
85 | await send('loadVoice', {
86 | voiceID
87 | });
88 | }
89 |
90 | apiWrapper.setRandomVoice = async (mode) => {
91 | var payload = {};
92 | var modeOptions = {
93 | custom: "CustomVoices",
94 | favorite: "FavoriteVoices"
95 | }
96 |
97 | if (mode && modeOptions[mode]) {
98 | payload.mode = modeOptions[mode];
99 | }
100 | send('selectRandomVoice', payload);
101 | }
102 |
103 | apiWrapper.getHearMyselfStatus = async () => {
104 | var data = await send('getHearMyselfStatus');
105 | return data.actionObject.value;
106 | }
107 |
108 | apiWrapper.setHearMyselfStatus = async (status) => {
109 | if (status === 'toggle') {
110 | send('toggleHearMyVoice');
111 | return;
112 | }
113 |
114 | var currentStatus = await apiWrapper.getHearMyselfStatus();
115 | if (
116 | (status === 'on' && !currentStatus) ||
117 | (status === 'off' && currentStatus)
118 | ) {
119 | send('toggleHearMyVoice');
120 | }
121 | }
122 |
123 | apiWrapper.getVoiceChangerStatus = async () => {
124 | var data = await send('getVoiceChangerStatus');
125 | return data.actionObject.value;
126 | }
127 |
128 | apiWrapper.setVoiceChangerStatus = async (status) => {
129 | if (status === 'toggle') {
130 | send('toggleVoiceChanger');
131 | return;
132 | }
133 |
134 | var currentStatus = await apiWrapper.getVoiceChangerStatus();
135 | if (
136 | (status === 'on' && !currentStatus) ||
137 | (status === 'off' && currentStatus)
138 | ) {
139 | send('toggleVoiceChanger');
140 | }
141 | }
142 |
143 | apiWrapper.getBackgroundEffectStatus = async () => {
144 | var data = await send('getBackgroundEffectStatus');
145 | return data.actionObject.value;
146 | }
147 |
148 | apiWrapper.setBackgroundEffectStatus = async (status) => {
149 | if (status === 'toggle') {
150 | send('toggleBackground');
151 | return;
152 | }
153 |
154 | var currentStatus = await apiWrapper.getBackgroundEffectStatus();
155 | if (
156 | (status === 'on' && !currentStatus) ||
157 | (status === 'off' && currentStatus)
158 | ) {
159 | send('toggleBackground');
160 | }
161 | }
162 |
163 | apiWrapper.getMuteMicStatus = async () => {
164 | var data = await send('getMuteMicStatus');
165 | return data.actionObject.value;
166 | }
167 |
168 | apiWrapper.setMuteMicStatus = async (status) => {
169 | if (status === 'toggle') {
170 | send('toggleMuteMic');
171 | return;
172 | }
173 |
174 | var currentStatus = await apiWrapper.getMuteMicStatus();
175 | if (
176 | (status === 'on' && !currentStatus) ||
177 | (status === 'off' && currentStatus)
178 | ) {
179 | send('toggleMuteMic');
180 | }
181 | }
182 |
183 | apiWrapper.beep = async (duration) => {
184 | send('setBeepSound', { badLanguage: 1 });
185 | setTimeout(() => {
186 | send('setBeepSound', { badLanguage: 0 })
187 | }, duration);
188 | }
189 |
190 | apiWrapper.playAudio = async (FileName) => {
191 | send('playMeme', { FileName, "IsKeyDown": true });
192 | }
193 |
194 | apiWrapper.stopAudio = async () => {
195 | send('stopAllMemeSounds');
196 | }
197 |
198 | return apiWrapper;
199 | }
200 |
--------------------------------------------------------------------------------
/js/voicemod/voicemodHandler.js:
--------------------------------------------------------------------------------
1 | class VoicemodHandler extends Handler {
2 | /**
3 | * Create a new Voicemod handler.
4 | */
5 | constructor() {
6 | super('Voicemod', []);
7 | }
8 |
9 | /**
10 | * Initialize the connection to voicemod with the input settings.
11 | * @param {string} address voicemod websocket address
12 | * @param {string} password voicemod websocket password
13 | */
14 | init = (address, apiKey) => {
15 | this.api = connectVoicemodWebsocket(
16 | address, apiKey, this.success
17 | );
18 | }
19 |
20 | /**
21 | * Handle the input data (take an action).
22 | * @param {array} triggerData contents of trigger line
23 | */
24 | handleData = async (triggerData) => {
25 | var action = Parser.getAction(triggerData, 'Voicemod');
26 | switch (action) {
27 | case 'background':
28 | var { status } = Parser.getInputs(triggerData, ['action', 'status']);
29 |
30 | status = status.toLowerCase();
31 | var statuses = ['off', 'on', 'toggle'];
32 | if (statuses.indexOf(status) === -1) {
33 | console.error(`Invalid status value for Voicemod Background. Found "${status}", expected one of "${statuses.join('", "')}".`)
34 | return;
35 | }
36 |
37 | await this.api.setBackgroundEffectStatus(status);
38 | break;
39 | case 'beep':
40 | var { duration = 1 } = Parser.getInputs(triggerData, ['action', 'duration'], false, 1);
41 |
42 | var beep_duration = 1000;
43 | if (isNumeric(duration)) {
44 | beep_duration = parseInt(duration) * 1000;
45 | }
46 | await this.api.beep(beep_duration);
47 | break;
48 | case 'hear':
49 | var { status } = Parser.getInputs(triggerData, ['action', 'status']);
50 |
51 | status = status.toLowerCase();
52 | var statuses = ['off', 'on', 'toggle'];
53 | if (statuses.indexOf(status) === -1) {
54 | console.error(`Invalid status value for Voicemod Hear. Found "${status}", expected one of "${statuses.join('", "')}".`)
55 | return;
56 | }
57 |
58 | await this.api.setHearMyselfStatus(status);
59 | break;
60 | case 'mute':
61 | var { status } = Parser.getInputs(triggerData, ['action', 'status']);
62 |
63 | status = status.toLowerCase();
64 | var statuses = ['off', 'on', 'toggle'];
65 | if (statuses.indexOf(status) === -1) {
66 | console.error(`Invalid status value for Voicemod Mute. Found "${status}", expected one of "${statuses.join('", "')}".`)
67 | return;
68 | }
69 |
70 | await this.api.setMuteMicStatus(status);
71 | break;
72 | case 'random':
73 | var { mode = '' } = Parser.getInputs(triggerData, ['action', 'mode'], false, 1);
74 |
75 | mode = mode.toLowerCase();
76 | var modes = ['custom', 'favorite'];
77 | if (mode && modes.indexOf(mode) === -1) {
78 | console.error(`Invalid mode value for Voicemod Random. Found "${mode}", expected one of "${modes.join('", "')}".`)
79 | return;
80 | }
81 |
82 | await this.api.setRandomVoice(mode);
83 | break;
84 | case 'voice':
85 | var { voice } = Parser.getInputs(triggerData, ['action', 'voice']);
86 | var voices = await this.api.getVoices();
87 | var match = voices.filter(option => option.friendlyName === voice)[0];
88 |
89 | if (!match) {
90 | console.error(`Unable to find voice id Voicemod Voice given: "${voice}"`)
91 | }
92 |
93 | await this.api.setVoice(match.id);
94 | break;
95 | case 'voicechanger':
96 | var { status } = Parser.getInputs(triggerData, ['action', 'status']);
97 |
98 | status = status.toLowerCase();
99 | var statuses = ['off', 'on', 'toggle'];
100 | if (statuses.indexOf(status) === -1) {
101 | console.error(`Invalid status value for Voicemod VoiceChanger. Found "${status}", expected one of "${statuses.join('", "')}".`)
102 | return;
103 | }
104 |
105 | await this.api.setVoiceChangerStatus(status);
106 | break;
107 | case 'play':
108 | var { soundboard, sound } = Parser.getInputs(triggerData, ['action', 'soundboard', 'sound']);
109 | var soundboards = await this.api.getAllSoundboard();
110 |
111 | var board = soundboards.filter(option => option.name === soundboard)[0];
112 | if (!board) {
113 | console.error(`Unable to find Voicemod Play soundboard with name: "${soundboard}"`)
114 | return;
115 | }
116 |
117 | var match = board.sounds.filter(option => option.name === sound)[0];
118 | if (!match) {
119 | console.error(`Unable to find Voicemod Play sound with name, "${sound}, in the soundboard, ${soundboard}"`)
120 | return;
121 | }
122 |
123 | await this.api.playAudio(match.id);
124 | break;
125 | case 'stop':
126 | this.api.stopAudio();
127 | break;
128 | }
129 | }
130 | }
131 |
132 | /**
133 | * Create a handler
134 | */
135 | async function voicemodHandlerExport() {
136 | var voicemod = new VoicemodHandler();
137 | var address = await readFile('settings/voicemod/address.txt');
138 | var apiKey = await readFile('settings/voicemod/apiKey.txt');
139 | voicemod.init(address.trim(), apiKey.trim());
140 | }
141 | voicemodHandlerExport();
142 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kruiser8/Kruiz-Control/20f6dc7509847055ff95cebad1904524e3cab346/logo.png
--------------------------------------------------------------------------------
/settings/Settings.md:
--------------------------------------------------------------------------------
1 | # Kruiz Control Settings
2 |
3 | The following details each of the settings files that need to be filled out for each handler. An example is provided for each file.
4 |
5 | All settings files are found in the `settings` folder.
6 |
7 | This script uses text files to improve the average user experience.
8 |
9 | ## Table of Contents
10 |
11 | - [Chat](#chat)
12 | - [MQTT](#mqtt)
13 | - [OBS](#obs)
14 | - [SLOBS](#slobs)
15 | - [StreamElements](#streamelements)
16 | - [Streamlabs](#streamlabs)
17 | - [Twitch](#twitch)
18 | - [Variable](#variable)
19 | - [Voicemod](#voicemod)
20 |
21 | ***
22 |
23 | ## Chat
24 |
25 | ### channel.txt
26 | **Location:** `settings/chat/channel.txt`
27 |
28 | Specify the twitch channel to connect for chatting.
29 | ```
30 | kruiser8
31 | ```
32 |
33 | ### code.txt
34 | **Location:** `settings/chat/code.txt`
35 |
36 | _**NOTE: Leave this file blank if you want Kruiz Control to send messages as your main Twitch account. That is, only use this if you want to send messages as a separate bot account.**_
37 |
38 | **DO NOT COPY YOUR TWITCH CODE.TXT VALUE HERE**
39 |
40 | Follow the instructions in the [Twitch Code](#code) section below. At step 5, login with the desired account. Copy the generated code into this file.
41 |
42 | ```
43 | exampleoauth4kruizcontrol12345
44 | ```
45 |
46 | *Reminder:*
47 | - _channel.txt_ specifies the channel to connect to.
48 | - _code.txt_ specifies the user to send messages as.
49 |
50 | ***
51 |
52 | ## MQTT
53 |
54 | ### websocket.txt
55 | **Location:** `settings/mqtt/websocket.txt`
56 |
57 | Specify the IP (or hostname) + port to use for the MQTT websocket.
58 | ```
59 | ws://localhost:8883
60 | ```
61 |
62 | ### username.txt
63 | **Location:** `settings/mqtt/username.txt`
64 |
65 | _**NOTE: Leave this file blank if your broker doesn't require authentication.**_
66 |
67 | Specify the username required by your broker, if any.
68 | ```
69 | kcmqttuser
70 | ```
71 |
72 | ### password.txt
73 | **Location:** `settings/mqtt/password.txt`
74 |
75 | _**NOTE: Leave this file blank if your broker doesn't require authentication.**_
76 |
77 | Specify the password required by your broker, if any
78 | ```
79 | p@55w0rd
80 | ```
81 |
82 | ***
83 |
84 | ## OBS
85 | These settings can be found through **Tools** > **WebSockets Server Settings**.
86 |
87 | ### address.txt
88 | **Location:** `settings/obs/address.txt`
89 |
90 | Specify the IP + port to use for the OBS websocket.
91 | ```
92 | ws://127.0.0.1:4455
93 | ```
94 |
95 | ***
96 |
97 | ### password.txt
98 | **Location:** `settings/obs/password.txt`
99 |
100 | Specify the password to use when connecting to the websocket.
101 | ```
102 | my0b5p455w0rd
103 | ```
104 |
105 | ***
106 |
107 | ## SLOBS
108 | Follow the below steps to enable the SLOBS API for Kruiz Control.
109 | - Open SLOBS
110 | - Click the settings gear in the bottom left
111 | - Open the `Remote Control` tab
112 | - Click the QR Code so that it shows.
113 | - Click the `Show details` text/link that appears.
114 | - Copy the **API Token** value into the **token.txt** file (referenced below).
115 | - Close SLOBS.
116 | - Run SLOBS as administrator to enable the SLOBS Remote Control API.
117 |
118 | ### token.txt
119 | **Location:** `settings/slobs/token.txt`
120 |
121 | ```
122 | eyJAFOI3qoi4ut6345ogno5iuyt890
123 | ```
124 |
125 | ***
126 |
127 | ## StreamElements
128 | To capture alerts through [StreamElements](https://streamelements.com/), you'll need a JWT token. To get your JWT token,
129 | - Go to [your account settings](https://streamelements.com/dashboard/account/channels)
130 | - Click the **Show secrets** toggle on the right
131 | - Copy the JWT Token value that appears
132 |
133 | ### jwtToken.txt
134 | **Location:** `settings/streamelements/jwtToken.txt`
135 |
136 | ```
137 | eyJAFOI3qoi4ut6345ogno5iuyt89058gn589tyjh589h98h509ASUDF98Uuf98adshf9asfha89hga9hg9H8HA98HG98DAH98ADH8HG98ha989a9H9HG98DHh9DSHG89shg98h89DH98hh8H98gsdhg9D8SHD89GH9dshg89DSHG98HFSFNLJKFH98HNSDINVC98DSHGFw08hwewf
138 | ```
139 |
140 | ***
141 |
142 | ## Streamlabs
143 | To capture alerts through [Streamlabs](https://streamlabs.com/), you'll need your **Socket API Token**. To get your token,
144 |
145 | - Go to https://streamlabs.com/dashboard#/settings/api-settings
146 | - Click the **API Tokens** tab
147 | - Copy the **Your Socket API Token** value
148 |
149 | ### socketAPIToken.txt
150 | **Location:** `settings/streamlabs/socketAPIToken.txt`
151 |
152 | ```
153 | eyJhaoiuh798a99h7HBN879DHF98A789aigfba8790gfh987Fb78987BgUYF4SD56gI9Uh98786rf7tVBg97Gf56dxCilbh8OYf6r5SDX6cuyoIB97768FD76d546SD6iGVBUIb9i980YH897676f8FiUB9OIu8g78D6d5BiIU
154 | ```
155 |
156 | ***
157 |
158 | ## Twitch
159 | To capture Twitch events, you'll need a **Client Id** and **Client Secret** and a generated auth token for your Twitch user.
160 |
161 | ### Client ID and Secret
162 | To capture Twitch events, you'll need a **Client Id** and **Client Secret**. To create your own,
163 | 1. Go to https://dev.twitch.tv/login and login with your Twitch account.
164 | 2. Navigate to https://dev.twitch.tv/console.
165 | 3. Once logged in, on the left sidebar click `Applications`.
166 | 4. Click the `+ Register Your Application` button and enter the following details:
167 | - Name: `YOUR_USERNAME Kruiz Control` (Any name works, as long as it is unique)
168 | - OAuth Redirect URLs: `http://localhost`
169 | - Category: `Chat Bot`
170 | 5. Click the `Create` button at the bottom.
171 | 6. Click the `Manage` button on the right hand side for the application you created.
172 | 7. Copy the **Client ID** value and put it in the `settings/twitch/clientId.txt` file.
173 | 8. Click the `New Secret` button, confirm the prompt, and copy and paste the value into the `settings/twitch/clientSecret.txt` file.
174 |
175 | ### Code
176 | Some APIs and events require a user authenticated auth code. To generate one of these, first follow the Client ID and Secret instructions to update the `settings/twitch/clientId.txt` and `settings/twitch/clientSecret.txt` files. Then follow the below steps:
177 | 1. Add the below to your Kruiz Control `triggers.txt` file to add an authentication link to your OBS or SLOBS log file.
178 | ```
179 | ### Twitch Authenticate ###
180 | OnInit
181 | Twitch Authenticate
182 | Error {auth_url}
183 | ```
184 | 2. Reset Kruiz Control (refresh the browser source) to generate the link .
185 | 3. Open your OBS or SLOBS log file to find the URL.
186 | - For OBS, go to `Help` > `Log Files` > `View Current Log`.
187 | - For SLOBS, open the settings cog (bottom left) and then go to `Get Support` and click the `Show Cache Directory` option under `Cache Directory`. Open the `node-obs` folder and then the `logs` folder. Open the most recently modified file.
188 | 4. Copy the link at the bottom of the log file, and open it in a browser.
189 | - The link will start with `https://id.twitch.tv/oauth2/authorize`
190 | 5. Login to your Twitch account. This provides the ability to control your stream via the created https://dev.twitch.tv application.
191 | 6. After login, you'll be redirected to a link that looks like the below.
192 | ```
193 | http://localhost/?code=YOUR_CODE_HERE&scope=bits%3Aread...
194 | ```
195 | 7. Copy the `YOUR_CODE_HERE` value and put that into your `settings/twitch/code.txt` file.
196 | 8. Once you have generated a code, you can remove the `### Twitch Authenticate ###` event from your `triggers.txt` file.
197 |
198 |
199 | ### clientId.txt
200 | **Location:** `settings/twitch/clientId.txt`
201 |
202 | Specify the Twitch Client ID to use when making API calls.
203 | ```
204 | wasdfe23r9yujasbnvo9qhfiuqa328
205 | ```
206 |
207 | ### clientSecret.txt
208 | **Location:** `settings/twitch/clientSecret.txt`
209 |
210 | Specify the Twitch Client Secret to use when making API calls.
211 | ```
212 | h30984thfa9uegh90awiejhtgfqgq3
213 | ```
214 |
215 | ### code.txt
216 | **Location:** `settings/twitch/code.txt`
217 |
218 | Specify the Twitch auth code generated after logging in to your Twitch account.
219 | ```
220 | 098wh4ijbngse7w9r87yqu3gbq398f
221 | ```
222 |
223 | ### user.txt
224 | **Location:** `settings/twitch/user.txt`
225 |
226 | Specify the Twitch channel that Kruiz Control will react to and control through triggers and actions.
227 | ```
228 | kruiser8
229 | ```
230 |
231 | ***
232 |
233 | ## Variable
234 | Use the below settings to toggle how Variables are configured in Kruiz Control.
235 |
236 | ### autoload.txt
237 | **Location:** `settings/variable/autoload.txt`
238 |
239 | **Format:** ``
240 |
241 | Specify whether or not Kruiz Control should automatically load all variables (session and global) without having to use [`Variable Load`](js/Documentation.md#variable-load) or [`Variable Global Load`](js/Documentation.md#variable-global-load).
242 |
243 | (Default) If set to `off`, variables must be loaded before they can be used.
244 |
245 | If set to `on`, all variables are usable as soon as they are set or Kruiz Control initializes.
246 | ```
247 | off
248 | ```
249 |
250 | ***
251 |
252 | ## Voicemod
253 | To connect to [Voicemod](https://www.voicemod.net/), you'll need an **API Key**. To get your API Key,
254 |
255 | - Fill out [Voicemod's form](https://voicemod.typeform.com/to/Zh5ZHRED) to request a key.
256 | - After submitting the form, check your email inbox for an email from control.api.devs@voicemod.net with your `clientKey`.
257 | - Copy the `clientKey` value. It should look something like `abc-defh12345`.
258 | - Paste the `clientKey` into the `settings/voicemod/apiKey.txt` file.
259 |
260 | ### address.txt
261 | **Location:** `settings/voicemod/address.txt`
262 |
263 | ```
264 | ws://localhost:59129/v1
265 | ```
266 |
267 | ### apiKey.txt
268 | **Location:** `settings/voicemod/apiKey.txt`
269 |
270 | ```
271 | abc-defg12345
272 | ```
273 |
--------------------------------------------------------------------------------
/settings/chat/channel.txt:
--------------------------------------------------------------------------------
1 | username
--------------------------------------------------------------------------------
/settings/chat/code.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kruiser8/Kruiz-Control/20f6dc7509847055ff95cebad1904524e3cab346/settings/chat/code.txt
--------------------------------------------------------------------------------
/settings/mqtt/password.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kruiser8/Kruiz-Control/20f6dc7509847055ff95cebad1904524e3cab346/settings/mqtt/password.txt
--------------------------------------------------------------------------------
/settings/mqtt/username.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kruiser8/Kruiz-Control/20f6dc7509847055ff95cebad1904524e3cab346/settings/mqtt/username.txt
--------------------------------------------------------------------------------
/settings/mqtt/websocket.txt:
--------------------------------------------------------------------------------
1 | ws://localhost:8883
--------------------------------------------------------------------------------
/settings/obs/address.txt:
--------------------------------------------------------------------------------
1 | ws://127.0.0.1:4455
2 |
--------------------------------------------------------------------------------
/settings/obs/password.txt:
--------------------------------------------------------------------------------
1 | password
2 |
--------------------------------------------------------------------------------
/settings/slobs/token.txt:
--------------------------------------------------------------------------------
1 | ex4mpletok3n
--------------------------------------------------------------------------------
/settings/streamelements/jwtToken.txt:
--------------------------------------------------------------------------------
1 | jwtToken
2 |
--------------------------------------------------------------------------------
/settings/streamlabs/socketAPIToken.txt:
--------------------------------------------------------------------------------
1 | socketAPIToken
2 |
--------------------------------------------------------------------------------
/settings/twitch/clientId.txt:
--------------------------------------------------------------------------------
1 | exampleclientid
2 |
--------------------------------------------------------------------------------
/settings/twitch/clientSecret.txt:
--------------------------------------------------------------------------------
1 | exampleclientsecret
2 |
--------------------------------------------------------------------------------
/settings/twitch/code.txt:
--------------------------------------------------------------------------------
1 | examplecode
2 |
--------------------------------------------------------------------------------
/settings/twitch/user.txt:
--------------------------------------------------------------------------------
1 | username
2 |
--------------------------------------------------------------------------------
/settings/variable/autoload.txt:
--------------------------------------------------------------------------------
1 | off
--------------------------------------------------------------------------------
/settings/voicemod/address.txt:
--------------------------------------------------------------------------------
1 | ws://localhost:59129/v1
2 |
--------------------------------------------------------------------------------
/settings/voicemod/apiKey.txt:
--------------------------------------------------------------------------------
1 | api-key
2 |
--------------------------------------------------------------------------------
/sounds/MashiahMusic__Kygo-Style-Melody.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kruiser8/Kruiz-Control/20f6dc7509847055ff95cebad1904524e3cab346/sounds/MashiahMusic__Kygo-Style-Melody.wav
--------------------------------------------------------------------------------
/triggers.txt:
--------------------------------------------------------------------------------
1 | OnInit
2 | Chat Send "Kruiz Control Initialized"
3 |
4 | OnCommand b 0 !example
5 | Chat Send "Success! It worked!"
6 |
7 | OnCommand b 0 !kcreset
8 | Reset
9 |
--------------------------------------------------------------------------------
/triggers/sample.txt:
--------------------------------------------------------------------------------
1 | OnCommand b 0 !sample1
2 | Delay 10
3 | Chat Send "The first sample"
4 |
5 | OnCommand b 0 !sample2
6 | Chat Send "The second sample"
--------------------------------------------------------------------------------
/version.txt:
--------------------------------------------------------------------------------
1 | v2.1.0
2 |
--------------------------------------------------------------------------------