Last active
December 2, 2018 22:12
-
-
Save AlcaDesign/7a5c8bdd3439ca684592 to your computer and use it in GitHub Desktop.
Code sniffing bot from multiple Twitch channels. Aggregates the codes to a specific channel for more chances at codes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"yourChannel": "your channel", | |
"identity": { | |
"username": "bot name", | |
"password": "oauth token" | |
}, | |
"searchName": "officialparagonbot", | |
"redeemLink": "https://epicgames.com/paragon/redeem", | |
"tmiDebug": false, | |
"botFunctionDebug": true, | |
"gameName": "Paragon", | |
"channelJoinCount": 100, | |
"updateChannelsDelay": 300000, | |
"codesBeforeForceRefresh": 6 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "codesniffer", | |
"version": "1.0.0", | |
"description": "Code sniffing bot from multiple Twitch channels. Aggregates the codes to a specific channel for more chances at codes.", | |
"main": "SniffingBot.js", | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1", | |
"start": "node SniffingBot.js" | |
}, | |
"author": "Jacob \"Alca\" Foster", | |
"license": "ISC", | |
"dependencies": { | |
"cli-color": "^1.1.0", | |
"lodash": "^4.6.1", | |
"request": "^2.69.0", | |
"tmi.js": "0.0.28" | |
}, | |
"jshintConfig": { | |
"esversion": 6, | |
"node": true | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const _request = require('request'), | |
_ = require('lodash'), | |
tmi = require('tmi.js'), | |
clc = require('cli-color'), | |
config = require('./config.json'), | |
yourChannel = config.yourChannel || '', | |
identity = config.identity || {}, | |
searchName = config.searchName || 'officialparagonbot', | |
redeemLink = config.redeemLink || 'https://epicgames.com/paragon/redeem', | |
tmiDebug = config.tmiDebug || false, | |
botFunctionDebug = config.botFunctionDebug || true, | |
gameName = config.gameName || 'Paragon', | |
channelJoinCount = config.channelJoinCount || 100, | |
updateChannelsDelay = config.updateChannelsDelay || 1200000, | |
codesBeforeForceRefresh = config.codesBeforeForceRefresh || 8, | |
options = { | |
debug: tmiDebug | |
}, | |
connection = { | |
cluster: 'aws', | |
reconnect: true, | |
secure: true | |
}, | |
clients = { | |
mainChannel: new tmi.client({ | |
options, identity, connection | |
}), | |
others: new tmi.client({ | |
options, connection | |
}) | |
}, | |
commandDelay = 235; | |
var finishedInitialJoins = false, | |
updateChannelsTimeout = null, | |
currentlyUpdatingChannels = false, | |
codeCache = [], | |
updateChannelsReason = null, | |
lastGameStreamCount = null; | |
function log() { | |
var args = [].slice.call(arguments), | |
timestamp = (new Date()).toTimeString().split(' ')[0]; | |
console.log.apply(console, [timestamp].concat(args)); | |
} | |
function logErr(err) { | |
if(err) { | |
log(clc.redBright(err.message + '\n' + err.stack)); | |
} | |
} | |
function logCall() { | |
if(botFunctionDebug) { | |
var args = [].slice.call(arguments), | |
firstArg = args.shift(), | |
colorFunction = clc.greenBright; | |
if(_.has(clc, firstArg)) { | |
colorFunction = clc[firstArg]; | |
firstArg = args.shift(); | |
} | |
var functionName = colorFunction(firstArg); | |
log.apply(null, [functionName].concat(args)); | |
} | |
} | |
function request(options) { | |
logCall('request', options.url || options.uri); | |
return new Promise((resolve, reject) => | |
_request(options, (err, res, body) => { | |
if(err) { | |
reject({ err, res }); | |
} | |
else { | |
resolve({ res, body }); | |
} | |
}) | |
); | |
} | |
function connect() { | |
logCall('connect'); | |
var connects = _.map(clients, c => c.connect()); | |
return Promise.all(connects); | |
} | |
function on(event, callback) { | |
logCall('on', event, !!callback); | |
_.forEach(clients, c => c.on(event, callback)); | |
return { on }; // Chaining | |
} | |
function setupListeners() { | |
on('join', handleJoin) | |
.on('part', handlePart) | |
.on('message', handleMessage) | |
.on('hosting', handleHosting); | |
} | |
function sendMessage(channel, message) { | |
logCall('sendMessage', channel, message); | |
if(!message) { | |
return Promise.reject(new Error('No valid message argument')); | |
} | |
if(channel.indexOf('#') === -1) { | |
channel = '#' + channel; | |
} | |
var client = _.find(clients, c => _.includes(c.channels, channel)); | |
if(client) { | |
return client.say(channel, message); | |
} | |
else { | |
return Promise.reject(new Error('No client found for #' + channel)); | |
} | |
} | |
function joinChannel(channel) { | |
logCall('joinChannel', channel); | |
return clients.others.join(channel); | |
} | |
function partChannel(channel) { | |
logCall('partChannel', channel); | |
return clients.others.part(channel); | |
} | |
function joinManyChannels(channels) { | |
logCall('joinManyChannels', channels.length); | |
return Promise.all(channels.map((chan, i) => { | |
return new Promise((resolve, reject) => { | |
setTimeout(chan => { | |
joinChannel(chan).then(resolve, reject); | |
}, i * commandDelay, chan); | |
}); | |
})); | |
} | |
function partManyChannels(channels) { | |
logCall('partManyChannels', channels.length); | |
return Promise.all(channels.map((chan, i) => { | |
return new Promise((resolve, reject) => { | |
setTimeout(chan => { | |
partChannel(chan).then(resolve, reject); | |
}, i * commandDelay, chan); | |
}); | |
})); | |
} | |
function joinOursFirst() { | |
logCall('joinOursFirst', yourChannel); | |
clients.mainChannel.join(yourChannel); | |
return Promise.resolve(); | |
} | |
function getStreams() { | |
logCall('getStreams', gameName); | |
return request({ | |
baseUrl: 'https://api.twitch.tv/kraken/', | |
url: 'streams', | |
qs: { | |
game: gameName, | |
stream_type: 'live', | |
limit: Math.min(channelJoinCount, 100) | |
}, | |
json: true | |
}) | |
.then(data => { | |
log(data.body._total, 'total', gameName, 'stream(s)'); | |
var channels = _.chain(data.body.streams) | |
.filter(stream => | |
stream.viewers >= 2 && | |
stream.channel.name !== yourChannel.toLowerCase() | |
) | |
.map('channel.name') | |
.value(); | |
lastGameStreamCount = channels.length; | |
return channels; | |
}); | |
} | |
function setUpdateDelay() { | |
logCall('setUpdateDelay', updateChannelsDelay/1000, 'seconds'); | |
clearTimeout(updateChannelsTimeout); | |
updateChannelsReason = 'updateChannelsDelay'; | |
updateChannelsTimeout = setTimeout(updateChannels, updateChannelsDelay); | |
return updateChannelsTimeout; | |
} | |
function updateChannels() { | |
logCall('updateChannels', updateChannelsReason); | |
if(currentlyUpdatingChannels) { | |
return Promise.reject(new Error('Already updating channels')); | |
} | |
currentlyUpdatingChannels = true; | |
clearTimeout(updateChannelsTimeout); | |
updateChannelsTimeout = null; | |
codeCache = []; | |
return getStreams() | |
.then(newChannels => { | |
var oldChannels = clients.others.channels.map(n => n.replace(/^#/, '')), | |
partThese = _.difference(oldChannels, newChannels), | |
joinThese = _.difference(newChannels, oldChannels); | |
return partManyChannels(partThese) | |
.then(() => joinManyChannels(joinThese)) | |
.then(() => { | |
currentlyUpdatingChannels = false; | |
logCall('updateChannels finished', clients.others.channels.length); | |
return setUpdateDelay(); | |
}); | |
}) | |
.catch(logErr); | |
} | |
function handleJoin(channel, username) { | |
// A little too spammy | |
//logCall('on -> join | handleJoin'); | |
if(username == clients.mainChannel.getUsername()) { | |
log(username, 'joined', channel); | |
} | |
} | |
function handlePart(channel, username) { | |
// A little too spammy | |
//logCall('on -> join | handleJoin'); | |
if(username == clients.mainChannel.getUsername()) { | |
log(username, 'parted', channel); | |
var count = clients.others.channels.length, | |
percent = '(' + (count/lastGameStreamCount*100).toFixed(2) + '%)'; | |
log(count, 'channel(s) remaining of', lastGameStreamCount, percent); | |
if(finishedInitialJoins && count < lastGameStreamCount * 0.7) { | |
updateChannelsReason = '70% part threshold ' + count + ' ' + lastGameStreamCount; | |
updateChannelsTimeout = setTimeout(updateChannels, 1000); | |
} | |
} | |
} | |
function newCodeReceieved(now, message) { | |
var prom = Promise.resolve(), | |
code = message.split(': ')[1]; | |
if(code && !_.includes(codeCache, code)) { | |
log(clc.cyanBright(code)); | |
prom = prom.then(() => sendMessage(yourChannel, code + ' ' + redeemLink)) | |
.catch(logErr); | |
codeCache.push(code); | |
clearTimeout(updateChannelsTimeout); | |
updateChannelsTimeout = null; | |
if(codeCache.length >= codesBeforeForceRefresh) { | |
updateChannelsReason = 'codesBeforeForceRefresh ' + codeCache.length + ' code(s)'; | |
updateChannelsTimeout = setTimeout(updateChannels, 1000); | |
} | |
else { | |
setUpdateDelay(); | |
} | |
} | |
return prom; | |
} | |
function handleMessage(channel, user, message, fromSelf) { | |
// A little too spammy | |
//logCall('on -> message | handleMessage', channel, user.username, message.length); | |
var prom = Promise.resolve(), | |
now = Date.now(); | |
if(!fromSelf && user.username == searchName) { | |
return prom.then(() => newCodeReceieved(now, message)); | |
} | |
return prom; | |
} | |
function handleHosting(channel, target, viewers) { | |
logCall('handleHosting', channel, '->', target); | |
if(_.includes(clients.others.channels, channel)) { | |
partChannel(channel); | |
} | |
} | |
(function init() { | |
logCall('init'); | |
connect() | |
.then(setupListeners) | |
.then(joinOursFirst) | |
.then(getStreams) | |
.then(joinManyChannels) | |
.then(() => finishedInitialJoins = true) | |
.then(setUpdateDelay) | |
.catch(logErr); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment