const fs = require('fs'); const tmi = require('tmi.js'); // Twitch bot options const optsFile = fs.readFileSync('twitch.config'); const opts = JSON.parse(optsFile); const client = tmi.client(opts); // Buttsbot options const syllableRegex = /[^aeiouy]*[aeiouy]+(?:[^aeiouy]*$|[^aeiouy](?=[^aeiouy]))?/gi; let configFile = fs.readFileSync('buttsbot.config') let configurations = JSON.parse(configFile); let defaultConfig = { word: "butt", chance: 20, pity: 0, cooldown: 0, limit: 10, syllableCount: 2, ignoredUsers: [] }; let messageCounter = JSON.parse(fs.readFileSync('counter')); let pityTracker = {}; let cooldownTracker = {}; // Client client.on('message', onMessageHandler); client.on('connected', onConnectedHandler); client.connect(); // Helper methods function syllabify(words) { return words.match(syllableRegex); } function randomIntFromInterval(min, max) { // min and max included return Math.floor(Math.random() * (max - min + 1) + min) } function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n); } // Event handlers function onMessageHandler (channel, userstate, message, self) { var message = message.trim(); var messageChannel = "#" + userstate['username']; if (channel === "#butt5b0t") { if (message === "!subscribe") { client.join(messageChannel); client.say(channel, "Joined channel " + messageChannel); if (!opts.channels.includes(messageChannel)) { opts.channels.push(messageChannel); fs.writeFileSync('twitch.config', JSON.stringify(opts)); } if (!(messageChannel in configurations)) { configurations[messageChannel] = defaultConfig; fs.writeFileSync('buttsbot.config', JSON.stringify(configurations)); } } if (message === "!unsubscribe") { client.part(messageChannel); client.say(channel, "Left channel " + messageChannel); if (opts.channels.includes(messageChannel)) { var indexChannel = opts.channels.indexOf(messageChannel); opts.channels.splice(indexChannel, 1); fs.writeFileSync('twitch.config', JSON.stringify(opts)); } if (messageChannel in configurations) { delete configurations[messageChannel]; fs.writeFileSync('buttsbot.config', JSON.stringify(configurations)); } } } else { if (self) return; let config = (channel in configurations) ? configurations[channel] : defaultConfig; if (message.startsWith("!buttsbot")) { if (userstate['room-id'] === userstate['user-id'] || userstate['mod']) { var parts = message.split(' '); var action = parts[1]; var value = parts[2]; switch (action) { case "config": client.say(channel, "Chance: " + config.chance + "% - Cooldown: " + config.cooldown + " seconds - Pity: " + config.pity + " - Word: " + config.word + " - Min. Syllables: " + config.syllableCount + " - Ignored Users: " + config.ignoredUsers.join(', ')); break; case "word": config.word = value; configurations[channel] = config; fs.writeFileSync('buttsbot.config', JSON.stringify(configurations)); client.say(channel, "Word set to " + value); break; case "chance": if (isNumeric(value)) { if (value > 100) value = 100; if (value < 0) value = 0; config.chance = parseInt(value); configurations[channel] = config; fs.writeFileSync('buttsbot.config', JSON.stringify(configurations)); client.say(channel, "Chance set to " + value + "%"); messageCounter[channel] = { total: 0, converted: 0 }; fs.writeFileSync('counter', JSON.stringify(messageCounter)); } else { client.say(channel, "Expected a number, got " + value); } break; case "pity": if (isNumeric(value)) { if (value < 0) value = 0; config.pity = parseInt(value); configurations[channel] = config; fs.writeFileSync('buttsbot.config', JSON.stringify(configurations)); client.say(channel, "Pity set to " + value); } else { client.say(channel, "Expected a number, got " + value); } break; case "ignore": if (value && value !== "") { console.log(`* ignore: value ok`); value = value.replace("@", ""); if (!config.ignoredUsers.includes(value)) { console.log(`* ignore: value not already ignored`); config.ignoredUsers.push(value); configurations[channel] = config; fs.writeFileSync('buttsbot.config', JSON.stringify(configurations)); } client.say(channel, "User " + value + " will be ignored from now on."); } else { client.say(channel, "Excepted username, got empty value"); } break; case "rmignore": if (value && value !== "") { console.log(`* rmignore: value ok`); value = value.replace("@", ""); if (config.ignoredUsers.includes(value)) { console.log(`* rmignore: value ignored`); let index = config.ignoredUsers.indexOf(value); config.ignoredUsers.splice(index, 1); configurations[channel] = config; fs.writeFileSync('buttsbot.config', JSON.stringify(configurations)); } client.say(channel, "User " + value + " will no longer be ignored."); } else { client.say(channel, "Expected username, got empty value"); } break; case "limit": if (isNumeric(value)) { config.limit = parseInt(value); configurations[channel] = config; fs.writeFileSync('buttsbot.config', JSON.stringify(configurations)); client.say(channel, "Set new limit: 1 butt per " + value + " words"); } else { client.say(channel, "Expected a number, got " + value); } break; case "syllables": if (isNumeric(value)) { config.syllableCount = parseInt(value); configurations[channel] = config; fs.writeFileSync('buttsbot.config', JSON.stringify(configurations)); client.say(channel, "Set required syllables to " + value); } else { client.say(channel, "Expected a number, got " + value); } break; case "cooldown": if (isNumeric(value)) { config.cooldown = parseInt(value); configurations[channel] = config; fs.writeFileSync('buttsbot.config', JSON.stringify(configurations)); client.say(channel, "Set cooldown to " + value + " seconds"); } else { client.say(channel, "Expected a number, got " + value); } default: client.say(channel, "Action '" + action + "' not recognized."); } } else { console.log(`* buttsbot: User not authorized`); } } else { let currentTime = Math.floor(new Date().getTime()/1000.0); if (cooldownTracker[channel] && config.cooldown > 0) { if (currentTime - cooldownTracker[channel] < config.cooldown) return; } // ignore commands if (message.startsWith("!")) return; // ignore ignored users if (config.ignoredUsers.includes(userstate['display-name']) || config.ignoredUsers.includes(userstate['username'])) { console.log(`* User ${userstate['username']} is on the ignore list.`); return; } // ignore messages containing URLs var regex = /((http(s)?(\:\/\/))*(www\.)?([\w\-\.\/])*(\.[a-zA-Z]{2,3}\/?))[^\s\b\n|]*/; var matches = message.match(regex); if (matches && matches.length > 0) { console.log(`* Message contained URL - skip`); return; } // split messages into word array and try to determine syllabes var words = message.split(' '); var syllables = words.map(syllabify); // calculate syllabe count var syllableCount = 0; syllables.forEach((s) => { if (s !== null) syllableCount += s.length; }); // ignore message if it doesn't contain enough syllabes if (syllableCount < config.syllableCount) { console.log(`* Message had too few syllables`); return; } var counter = { total: 0, converted: 0 }; if (channel in messageCounter) { console.log(`* Load counter for ${channel}`); counter = messageCounter[channel]; } counter.total++; if (pityTracker[channel]) pityTracker[channel]++; else pityTracker[channel] = 1; // random chance // TODO: find a better alternative var number = Math.random() * 100; if (number <= config.chance || (config.pity != 0 && pityTracker[channel] && pityTracker[channel] >= config.pity)) { pityTracker[channel] = 0; counter.converted++; var buttCount = Math.ceil(words.length / config.limit); var randomNumbersUsed = []; for (var i = 0; i < buttCount; i++) { var random = randomIntFromInterval(1, words.length); while (randomNumbersUsed.includes(random)) random = randomIntFromInterval(1, words.length); randomNumbersUsed.push(random); var word = syllables[random - 1]; if (word) { var randomSyllable = randomIntFromInterval(1, word.length); var firstLetterOfSyllable = word[randomSyllable - 1][0]; word[randomSyllable - 1] = firstLetterOfSyllable == firstLetterOfSyllable.toUpperCase() ? config.word.toUpperCase() : config.word; } } var newMessage = ""; syllables.forEach((s) => { if (s != null) newMessage += s.join('') + ' '; }); client.say(channel, newMessage.trim()); if (config.cooldown > 0) cooldownTracker[channel] = currentTime; } console.log(channel + ": " + JSON.stringify(counter)); messageCounter[channel] = counter; fs.writeFileSync('counter', JSON.stringify(messageCounter)); console.log(JSON.stringify(pityTracker)); } } } function onConnectedHandler (addr, port) { console.log(`* Connected to ${addr}:${port}`); }