automation wip

This commit is contained in:
Chris Ham
2018-09-13 15:51:30 -07:00
parent bd93260e21
commit 950ad92364
6 changed files with 339 additions and 44 deletions

View File

@@ -25,7 +25,30 @@
"websocket": { "websocket": {
"address": "192.168.0.111:4444", "address": "192.168.0.111:4444",
"password": "goodnewseveryone" "password": "goodnewseveryone"
},
"availablePlaylists": [
{
"sceneItem": "ttas-segments",
"activity": "TTAS Segments !ttas",
"chatName": "TTAS Segments"
},
{
"sceneItem": "room-grind",
"activity": "TTAS Room Grind !ttas",
"chatName": "TTAS Room Grind"
},
{
"sceneItem": "gold-segments",
"activity": "Any% NMG Gold Segments",
"chatName": "NMG Golds"
},
{
"sceneItem": "personal-bests",
"activity": "Past Personal Bests",
"chatName": "Personal Bests"
} }
],
"defaultPlaylist": "room-grind"
}, },
"debug": false "debug": false
} }

View File

49
lib/util.js Executable file
View File

@@ -0,0 +1,49 @@
// Converts seconds to human-readable time
String.prototype.toHHMMSS = function () {
let sec_num = parseInt(this, 10); // don't forget the second param
let hours = Math.floor(sec_num / 3600);
let minutes = Math.floor((sec_num - (hours * 3600)) / 60);
let seconds = sec_num - (hours * 3600) - (minutes * 60);
if (hours < 10) {hours = "0"+hours;}
if (minutes < 10) {minutes = "0"+minutes;}
if (seconds < 10) {seconds = "0"+seconds;}
return hours+':'+minutes+':'+seconds;
};
var exports = module.exports = {};
exports.asyncForEach = async function(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array)
}
};
exports.range = function(start,stop) {
var result=[];
for (var idx=start.charCodeAt(0),end=stop.charCodeAt(0); idx <=end; ++idx){
result.push(String.fromCharCode(idx));
}
return result;
};
exports.randElement = function(arr) {
return arr[Math.floor(Math.random() * arr.length)];
};
exports.sum = function(e) {
let sum = 0;
for (let i = 0; i < e.length; i++) {
sum += parseInt(e[i], 10);
}
return sum;
};
exports.average = function(e) {
let sum = exports.sum(e);
let avg = sum / e.length;
return avg;
};

58
package-lock.json generated
View File

@@ -118,6 +118,15 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
}, },
"cron-parser": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-2.6.0.tgz",
"integrity": "sha512-KGfDDTjBIx85MnVYcdhLccoJH/7jcYW+5Z/t3Wsg2QlJhmmjf+97z+9sQftS71lopOYYapjEKEvmWaCsym5Z4g==",
"requires": {
"is-nan": "1.2.1",
"moment-timezone": "0.5.21"
}
},
"crypt": { "crypt": {
"version": "0.0.2", "version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
@@ -139,6 +148,14 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"requires": {
"object-keys": "1.0.12"
}
},
"delayed-stream": { "delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -264,6 +281,14 @@
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
}, },
"is-nan": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.2.1.tgz",
"integrity": "sha1-n69ltvttskt/XAYoR16nH5iEAeI=",
"requires": {
"define-properties": "1.1.3"
}
},
"is-typedarray": { "is-typedarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
@@ -311,6 +336,11 @@
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
}, },
"long-timeout": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz",
"integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ="
},
"md5": { "md5": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
@@ -331,6 +361,14 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
}, },
"moment-timezone": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz",
"integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==",
"requires": {
"moment": "2.22.2"
}
},
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -361,11 +399,26 @@
"ogg-packet": "1.0.0" "ogg-packet": "1.0.0"
} }
}, },
"node-schedule": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-1.3.0.tgz",
"integrity": "sha512-NNwO9SUPjBwFmPh3vXiPVEhJLn4uqYmZYvJV358SRGM06BR4UoIqxJpeJwDDXB6atULsgQA97MfD1zMd5xsu+A==",
"requires": {
"cron-parser": "2.6.0",
"long-timeout": "0.1.1",
"sorted-array-functions": "1.2.0"
}
},
"oauth-sign": { "oauth-sign": {
"version": "0.9.0", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
}, },
"object-keys": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz",
"integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag=="
},
"obs-websocket-js": { "obs-websocket-js": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-1.2.0.tgz", "resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-1.2.0.tgz",
@@ -544,6 +597,11 @@
"resolved": "https://registry.npmjs.org/snekfetch/-/snekfetch-3.6.4.tgz", "resolved": "https://registry.npmjs.org/snekfetch/-/snekfetch-3.6.4.tgz",
"integrity": "sha512-NjxjITIj04Ffqid5lqr7XdgwM7X61c/Dns073Ly170bPQHLm6jkmelye/eglS++1nfTWktpP6Y2bFXjdPlQqdw==" "integrity": "sha512-NjxjITIj04Ffqid5lqr7XdgwM7X61c/Dns073Ly170bPQHLm6jkmelye/eglS++1nfTWktpP6Y2bFXjdPlQqdw=="
}, },
"sorted-array-functions": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz",
"integrity": "sha512-sWpjPhIZJtqO77GN+LD8dDsDKcWZ9GCOJNqKzi1tvtjGIzwfoyuRH8S0psunmc6Z5P+qfDqztSbwYR5X/e1UTg=="
},
"sshpk": { "sshpk": {
"version": "1.14.2", "version": "1.14.2",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",

View File

@@ -11,6 +11,7 @@
"memcache": "^0.3.0", "memcache": "^0.3.0",
"moment": "^2.22.2", "moment": "^2.22.2",
"node-opus": "^0.2.9", "node-opus": "^0.2.9",
"node-schedule": "^1.3.0",
"obs-websocket-js": "^1.2.0", "obs-websocket-js": "^1.2.0",
"request": "^2.88.0" "request": "^2.88.0"
}, },

222
twitch.js
View File

@@ -2,20 +2,22 @@
* GHBot4Twitch * GHBot4Twitch
*/ */
// @TODO: modularize OBS and Twitch code
// @TODO: Make the bot aware of what video is current active // @TODO: Make the bot aware of what video is current active
// @TODO: Change video playlist source on an interval // @TODO: Change video playlist source on an interval
// @TODO: Rotating background images (leftside)
// Import modules // Import modules
const irc = require('irc'); const irc = require('irc');
const OBSWebSocket = require('obs-websocket-js'); const OBSWebSocket = require('obs-websocket-js');
const schedule = require('node-schedule');
const util = require('./lib/util');
// Read internal configuration // Read internal configuration
let config = require('./config.json'); let config = require('./config.json');
let twitchChat; let currentPlaylist = config.obs.defaultPlaylist;
let twitchChannel = '#' + config.twitch.channels[0].toLowerCase();
const init = (config) => {
let botChannel = '#' + config.twitch.username.toLowerCase();
// Connect to OBS Websocket // Connect to OBS Websocket
const obs = new OBSWebSocket(); const obs = new OBSWebSocket();
@@ -23,8 +25,10 @@ const init = (config) => {
obs.connect({ address: config.obs.websocket.address, password: config.obs.websocket.password }) obs.connect({ address: config.obs.websocket.address, password: config.obs.websocket.password })
.then(() => { .then(() => {
console.log(`Success! We're connected to OBS!`); console.log(`Success! We're connected to OBS!`);
//obs.getSourcesList().then(data => {console.log(data.sources)}).catch(console.error); return twitchInit(config, obs);
twitchInit(config, obs); })
.then(data => {
return streamInit(config, obs, data);
}) })
.catch(err => { .catch(err => {
console.log(err); console.log(err);
@@ -35,6 +39,7 @@ const init = (config) => {
}); });
const twitchInit = (config, obs) => { const twitchInit = (config, obs) => {
return new Promise((resolve, reject) => {
console.log('Connecting to Twitch...'); console.log('Connecting to Twitch...');
// Connect to Twitch IRC server with the Bot // Connect to Twitch IRC server with the Bot
@@ -67,7 +72,9 @@ const init = (config) => {
// Listen for specific commands from admins // Listen for specific commands from admins
if (config.twitch.admins.includes(from) || from === config.twitch.username.toLowerCase()) { if (config.twitch.admins.includes(from) || from === config.twitch.username.toLowerCase()) {
if (commandNoPrefix === 'show' || commandNoPrefix === 'hide') { if (commandNoPrefix === 'show' || commandNoPrefix === 'hide') {
let newVisibility = (commandNoPrefix === 'show'); let newVisibility = (commandNoPrefix === 'show');
let visibleTerm = (newVisibility ? 'visible' : 'hidden'); let visibleTerm = (newVisibility ? 'visible' : 'hidden');
@@ -100,32 +107,76 @@ const init = (config) => {
.catch(err => { .catch(err => {
twitchChat.say(to, JSON.stringify(err)); twitchChat.say(to, JSON.stringify(err));
}); });
} else if (commandNoPrefix === 'auw') {
obs.setSceneItemProperties({"item": "everybody-wow", "visible": true})
.then(res => {
// fade out headphone audio
/*for (i = 100; i >= 0; i--) {
obs.setVolume({"source": "headphones", "volume": i/100});
}*/
// @TODO: send command to mute the songrequest audio
editorChat.say(to, '!volume 0');
twitchChat.say(to, 'Everybody OwenWow'); } else if (commandNoPrefix === 't') {
// hide the source after a certain amount of time (248s in this case) let target = commandParts[1] || false;
setTimeout(() => { if (!target) {
obs.setSceneItemProperties({"item": "everybody-wow", "visible": false}) twitchChat.say(to, `A scene item name is required!`);
return;
}
let sceneItem = {"item": target};
obs.getSceneItemProperties(sceneItem)
.then(data => {
let newVisibility = !data.visible;
let visibleTerm = (newVisibility ? 'visible' : 'hidden');
sceneItem.visible = newVisibility;
obs.setSceneItemProperties(sceneItem)
.then(res => { .then(res => {
// fade in headphone audio twitchChat.say(to, `${target} is now ${visibleTerm}.`);
/*for (i = 1; i <= 100; i++) { })
obs.setVolume({"source": "headphones", "volume": i/100}); .catch(console.error);
}*/ })
// @TODO: send command to unmute the songrequest audio .catch(err => {
twitchChat.say(to, JSON.stringify(err));
});
} else if (commandNoPrefix === 'swap') {
// hide first argument, show second argument
let targetToHide = commandParts[1] || false;
let targetToShow = commandParts[2] || false;
if (targetToHide === false || targetToShow == false) {
twitchChat.say(to, `Format: ${config.twitch.cmdPrefix}swap <item-to-hide> <item-to-show>`);
return
}
obs.setSceneItemProperties({"item": targetToHide, "visible": false})
.then(res => {
obs.setSceneItemProperties({"item": targetToShow, "visible": true});
})
.catch(console.error);
} else if (commandNoPrefix === 'auw') {
// @TODO: switch to 'commercial' scene and show appropriate items, then switch back
// this way, playing a commercial doesn't have to know what's playing in the other scene
obs.setCurrentScene({"scene-name": "commercial"})
.then(res => {
// show the video
return obs.setSceneItemProperties({"item": "everybody-wow", "scene-name": "commercial", "visible": true});
})
.then(res => {
// mute songrequest audio
editorChat.say(to, '!volume 0');
// show owen
obs.setSceneItemProperties({"item": "owen", "scene-name": "commercial", "visible": true});
// tell chat what's up
twitchChat.say(to, 'Everybody OwenWow');
// swap back to fgfm scene after the video ends
setTimeout(() => {
// hide video
obs.setSceneItemProperties({"item": "everybody-wow", "scene-name": "commercial", "visible": false})
// hide owen
obs.setSceneItemProperties({"item": "owen", "scene-name": "commercial", "visible": false});
// unmute songrequest audio
editorChat.say(to, '!volume 75'); editorChat.say(to, '!volume 75');
twitchChat.say(to, 'OwenWow'); // swap back to fgfm
}).catch(console.error); obs.setCurrentScene({"scene-name": "fgfm"});
}, 248000); }, 248000);
}).catch(console.error); })
.catch(console.error);
} else if (commandNoPrefix === 'switch') { } else if (commandNoPrefix === 'switch') {
let target = commandParts[1] || false; let target = commandParts[1] || false;
if (!target) { if (!target) {
twitchChat.say(to, `A scene name is required!`); twitchChat.say(to, `A scene name is required!`);
@@ -182,10 +233,123 @@ const init = (config) => {
twitchChat.addListener('motd', motd => { twitchChat.addListener('motd', motd => {
console.log(`Received MOTD: ${motd}`); console.log(`Received MOTD: ${motd}`);
}); });
}
resolve({"botChat": twitchChat, "editorChat": editorChat});
});
} }
init(config); const streamInit = (config, obs, twitch) => {
return new Promise((resolve, reject) => {
console.log(`Initializing stream timers...`);
// When: Hourly at 55 past
// What: AUW
let auwJob = schedule.scheduleJob({minute: 55}, (fireDate) => {
// AUW
twitch.editorChat.say(twitchChannel, `${config.twitch.cmdPrefix}auw`);
});
console.log(`AUW is scheduled to be shown at ${auwJob.nextInvocation()}`);
let userVotes = [];
let playlistChoices = config.obs.availablePlaylists.map((e, i, a) => {
return `[${i+1}] ${e.chatName}`;
});
setTimeout(() => {
twitch.botChat.say(twitchChannel, `Vote for which video playlist you'd like to see next using ${config.twitch.cmdPrefix}vote #: ${playlistChoices.join(' | ')}`);
}, 5000);
// When: Every 2 Hours
// What: Change the video playlist
let changePlaylistJob = schedule.scheduleJob("*/5 * * * *", () => {
// Base the selection on user votes collected since the last invocation (unless there are 0 votes, then choose randomly)
let newPlaylist;
if (userVotes.length === 0) {
// choose a random item other than currentPlaylist from config.obs.availablePlaylists
let choices = config.obs.availablePlaylists.slice(0);
currentChoice = choices.indexOf(e => e.sceneItem === currentPlaylist);
choices.splice(currentChoice, 1);
newPlaylist = util.randElement(choices);
console.log(`PLAYLIST CHOSEN RANDOMLY: ${newPlaylist.chatName}`);
} else {
// tally and sort votes
let tallied = userVotes.reduce((voteTallies, currentValue) => {
tallyIndex = voteTallies.find(e.id === currentValue.vote);
if (tallyIndex !== -1) {
voteTallies[tallyIndex].count++;
} else {
voteTallies.push({id: currentValue.vote, count: 1});
}
}).sort((a, b) => {
if (a.count < b.count) {
return -1;
}
if (a.count > b.count) {
return 1;
}
// a must be equal to b
return 0;
});
console.log(`[TEST] Voting Results: ${JSON.stringify(tallied)}`);
newPlaylist = config.obs.availablePlaylists[tallied[0].id-1];
console.log(`WINNER OF THE VOTE: ${newPlaylist.chatName}`);
//twitch.botChat.say(twitchChannel, `[TEST] Voting Results: ${JSON.stringify(tallied)}`);
// clear user votes
userVotes = [];
}
/*twitch.botChat.say(twitchChannel, `[TEST] Changing playlist from ${currentPlaylist} to ${newPlaylist.chatName}`);
twitch.editorChat.say(twitchChannel, `[TEST] ${config.twitch.cmdPrefix}swap ${currentPlaylist} ${newPlaylist.sceneItem}`);
twitch.editorChat.say(twitchChannel, `[TEST] !setcurrent NOW SHOWING: ${newPlaylist.activity}`);*/
console.log(`Changing playlist from ${currentPlaylist} to ${newPlaylist.chatName}`);
console.log(`${config.twitch.cmdPrefix}swap ${currentPlaylist} ${newPlaylist.sceneItem}`);
console.log(`!setcurrent NOW SHOWING: ${newPlaylist.activity}`);
currentPlaylist = newPlaylist.sceneItem;
});
console.log(`Playlist will be changed at ${changePlaylistJob.nextInvocation()}`);
// Track user votes for playlist
twitch.botChat.addListener('message', (from, to, message) => {
// Ignore everything from blacklisted users
if (config.twitch.blacklistedUsers.includes(from)) return;
// Listen for commands that start with the designated prefix
if (message.startsWith(config.twitch.cmdPrefix)) {
let commandParts = message.slice(config.twitch.cmdPrefix.length).split(' ');
let commandNoPrefix = commandParts[0] || '';
if (commandNoPrefix === 'vote') {
let userVote = commandParts[1] || false;
if (userVote === false) {
return twitch.botChat.say(to, `Vote for which video playlist you'd like to see next using ${config.twitch.cmdPrefix}vote #: ${playlistChoices.join(' | ')}`);
}
userVote = Number.parseInt(userVote);
if (!Number.isInteger(userVote) || userVote < 1 || userVote > playlistChoices.length) {
return twitch.botChat.say(to, `@${from}, please choose an option from 1 - ${playlistChoices.length}!`);
}
// Check for uniqueness of vote
// if it's not unique, update the vote
let prevVote = userVotes.findIndex(e => e.from === from);
if (prevVote !== -1) {
if (userVotes[prevVote].vote !== userVote) {
// update vote and inform the user
userVotes[prevVote].vote = userVote;
twitch.botChat.say(to, `@${from}, your vote has been updated!`);
}
} else {
// log user vote
userVotes.push({"from": from, "vote": userVote});
twitch.botChat.say(to, `@${from}, your vote has been registered!`);
}
}
}
});
});
}
// catches Promise errors // catches Promise errors
process.on('unhandledRejection', console.error); process.on('unhandledRejection', console.error);