Skip to content
Permalink
Browse files

Initial commit

  • Loading branch information...
soatok committed Jul 16, 2019
0 parents commit 290c35606e1c4a0bfdb5b95483e64bf54863d48f
Showing with 908 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +133 −0 FloofState.js
  3. +247 −0 FloofWorker.js
  4. +12 −0 LICENSE.md
  5. +3 −0 README.md
  6. +47 −0 csprng.js
  7. +8 −0 docs/README.md
  8. +35 −0 docs/sketch.md
  9. +63 −0 index.html
  10. +65 −0 main.js
  11. +28 −0 package.json
  12. +14 −0 packaging/README.md
  13. +22 −0 packaging/build-linux.sh
  14. +22 −0 packaging/build-mac.sh
  15. +21 −0 packaging/build-windows.sh
  16. +12 −0 packaging/build.sh
  17. +41 −0 packaging/public-key.asc
  18. +5 −0 preload.js
  19. +10 −0 prepare-sandbox.sh
  20. +96 −0 renderer.js
  21. +20 −0 style.css
@@ -0,0 +1,4 @@
node_modules
package-lock.json
active.json

@@ -0,0 +1,133 @@
const fs = require('fs');

module.exports = class FloofState {
constructor(obj) {
let config = {};
if (typeof(obj['twitch']) === 'undefined') {
config['twitch'] = '';
} else {
config['twitch'] = obj['twitch'];
}

if (typeof(obj.sourceImage) === 'undefined') {
config['sourceImage'] = '';
} else {
config['sourceImage'] = obj['sourceImage'];
}

if (typeof(obj.sourceText) === 'undefined') {
config['sourceText'] = '';
} else {
config['sourceText'] = obj['sourceText'];
}

if (typeof(obj.restImage) === 'undefined') {
config['restImage'] = '';
} else {
config['restImage'] = obj['restImage'];
}

if (typeof(obj.activeImage) === 'undefined') {
config['activeImage'] = '';
} else {
config['activeImage'] = obj['activeImage'];
}

if (typeof(obj.timeActive) === 'undefined') {
config['timeActive'] = 3000;
} else {
config['timeActive'] = obj['timeActive'];
}

if (typeof(obj.speechTemplate) === 'undefined') {
config['speechTemplate'] = "Hi ${username}!";
} else {
config['speechTemplate'] = obj['speechTemplate'];
}
this.config = config;
}

set(key, value) {
this.config[key] = value;
return this;
}

/**
* @param {String} key
* @returns {String|Number|null}
*/
get(key) {
if (typeof this.config[key] === "undefined") {
return null;
}
return this.config[key];
}

/**
* @return {boolean}
*/
isValid() {
if (this.config['twitch'].length < 1) {
return false;
}
if (this.config['sourceImage'].length < 1) {
return false;
}
if (this.config['sourceText'].length < 1) {
return false;
}
if (this.config['restImage'].length < 1) {
return false;
}
if (this.config['activeImage'].length < 1) {
return false;
}
if (this.config['timeActive'] < 1) {
return false;
}
if (this.config['speechTemplate'].length < 1) {
return false;
}
return true;
}

/**
* @param {String} path
*/
save(path) {
fs.writeFile(
path,
JSON.stringify(this.config),
() => {}
);
}

/**
*
* @returns {module.FloofState}
*/
static default() {
return new FloofState({
"twitch": "",
"sourceImage": "",
"sourceText": "",
"restImage": "",
"activeImage": "",
"timeActive": 2500,
"speechTemplate": "Hi ${username}!"
});
}

/**
* @param {String} path
* @return {module.FloofState}
*/
static load(path) {
try {
let settings = JSON.parse(fs.readFileSync(path).toString());
return new FloofState(settings);
} catch (e) {
return FloofState.default();
}
}
};
@@ -0,0 +1,247 @@
const changeTime = require('change-file-time');
const fs = require('fs').promises;
const fsc = require('fs').constants;
const nodeConsole = require('console');
const randomInt = require('./csprng');
const request = require('request-promise');
let myConsole = new nodeConsole.Console(process.stdout, process.stderr);

let isWindowsAdmin = false;
if (process.platform === "win32") {
let exec = require('child_process').exec;
exec('NET SESSION', function (err, so, se) {
isWindowsAdmin = se.length === 0;
});
}

module.exports = class FloofWorker {
constructor(state) {
isWindowsAdmin = false;
if (process.platform === "win32") {
let exec = require('child_process').exec;
exec('NET SESSION', function (err, so, se) {
isWindowsAdmin = se.length === 0;
});
}
this.animationInProgress = false;
this.chatters = [];
this.canary = FloofWorker.randomString(16);
this.floofState = state;
this.greetQueue = [];
this.init = false;
this.isWindowsAdmin = isWindowsAdmin;
this.tickTimer = 1000;
}

/**
* @returns {module.FloofWorker}
*/
begin() {
let that = this;
// Kick off a first request/timeout loop
setInterval(async () => { return await that.doSomeWork();}, this.tickTimer);
}

async kickoffAnimations() {
if (this.animationInProgress) {
return false;
}
if (this.greetQueue.length < 1) {
// No animation to speak of...
await this.restState();
} else {
this.animationInProgress = true;
let chatter = this.greetQueue.shift();
await this.greetNextChatter(chatter);
}
}

getSpeechBubble(username) {
return this.floofState.get('speechTemplate')
.split('${username}').join(username);
}

async greetNextChatter(chatter) {
if (this.canary !== chatter.canary) {
return; // Abort
}
// Write to speech bubble file
let message = this.getSpeechBubble(chatter.chatter);
myConsole.log(message);
await fs.writeFile(
this.floofState.get('sourceText'),
message
);
// Set active Image
await this.selectImage(
this.floofState.get('sourceImage'),
this.floofState.get('activeImage')
);
let that = this;
// Kickoff the cleanup
setTimeout(
async function () {
await that.restState();
that.animationInProgress = false;
},
this.floofState.get('timeActive')
);
}

async restState() {
await fs.writeFile(
this.floofState.get('sourceText'),
""
);
await this.selectImage(
this.floofState.get('sourceImage'),
this.floofState.get('restImage')
);
}

async getChatters() {
let streamer = this.floofState.get('twitch');
let url = `https://tmi.twitch.tv/group/user/${streamer}/chatters`;
let canary = this.canary;
let that = this;
request.get(url).then(async function (e) {
if (that.canary !== canary) {
return; // Abort
}
let chatters = await FloofWorker.flatten(JSON.parse(e).chatters);
await that.processActiveChatters(chatters);
that.init = true;
that.chatters = chatters;
});
}

/**
* @param {object} obj
* @returns {Promise<Array>}
*/
static async flatten(obj) {
let flat = [];
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
for (let i = 0; i < obj[k].length; i++) {
flat.push(obj[k][i]);
}
}
}
return flat;
}

async processActiveChatters(current) {
if (!this.init) {
// First run doesn't need to queue animations
return;
}
let diff = current.filter(x => !this.chatters.includes(x));
let canary = this.canary;
for (let i = 0; i < diff.length; i++) {
this.greetQueue.push({"canary": canary, "chatter": diff[i]});
}
}

/**
* @param {module.FloofState} state
* @return {module.FloofWorker}
*/
setFloofState(state) {
this.floofState = state;
this.canary = FloofWorker.randomString(16);
return this;
}

/**
* Select the image for display on stream.
*
* @param {string} symlink
* @param {string} activeImage
*/
async selectImage(symlink, activeImage) {
try {
let bail = false;

// If symlink is wrong, erase it.
await fs.access(symlink, fsc.F_OK).then(async () => {
let link = await fs.readlink(symlink);
if (link === activeImage) {
bail = true;
return null;
}
return await fs.unlink(symlink);
}).catch(()=>{});
if (bail) {
// Bail out. We're pointed at the right file.
return null;
}

// If empty, bail out.
if (activeImage === "") {
return null;
}

// Now do the switch.
if (process.platform === "win32") {
/*
On Windows, if you don't have permission to create a symlink
(i.e. you're not running this as Administrator), we have to
delete and copy the file instead. This is much slower, but it
serves the same purpose.
*/
if (this.isWindowsAdmin) {
return await fs.symlink(activeImage, symlink, ()=>{
changeTime(activeImage);
changeTime(symlink);
});
} else {
return await fs.copyFile(
activeImage,
symlink,
function () {
changeTime(activeImage);
changeTime(symlink);
}
);
}
} else {
return await fs.symlink(activeImage, symlink, ()=>{
changeTime(activeImage);
changeTime(symlink);
});
}
} catch (e) {
myConsole.log(e.message);
}
}

/**
* Calculate a randomly-generated filename
*
* @param {number} len
* @returns {string}
*/
static randomString(len = 16) {
let str = "";
let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let charLen = 62;
for (let i = 0; i < len; i++) {
let r = randomInt(0, charLen - 1);
str += chars.charAt(r);
}
return str;
}

async doSomeWork() {
try {
if (this.floofState.isValid()) {
await this.getChatters();
await this.kickoffAnimations();
}
} catch (e) {
myConsole.log(this);
myConsole.log(e.message);
}
}
};

0 comments on commit 290c356

Please sign in to comment.
You can’t perform that action at this time.