Skip to content

Instantly share code, notes, and snippets.

@AlcaDesign
Last active December 2, 2018 22:12
Show Gist options
  • Save AlcaDesign/7a5c8bdd3439ca684592 to your computer and use it in GitHub Desktop.
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.
{
"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
}
{
"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
}
}
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