diff --git a/.eslintrc.json b/.eslintrc.json index 51bc361..e4c61d2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,7 +13,6 @@ "module": true, "process": true, "Exception": true, - "console": true, "setInterval": true }, "extends": "eslint:recommended", diff --git a/.gitignore b/.gitignore index 54e7ee9..d32181f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ auth.json -.idea/ \ No newline at end of file +.idea/ +*.log \ No newline at end of file diff --git a/bot.js b/bot.js index 791af9c..887b8b7 100644 --- a/bot.js +++ b/bot.js @@ -1,44 +1,55 @@ -const Discord = require('discord.js') -let twitch = require('./twitch.js') -let fs = require('fs') -const auth = require('./auth.js')() -let config = require('./config.json') +const Discord = require('discord.js'), + twitch2 = require('./twitch.js'), + fs = require('fs'), + auth = require('./auth.js')(), + config = require('./config.json'), + logger = require('./logger.js')(), + cache = require('./cache.js')(100, 10) -const SUPPORTED_STREAMERS = config.streamers +const {STREAMER_NOT_LIVE} = require('./constants') -let live = { - pedropcruz: 0, - drakzOfficial: 0, - ExilePT: 0 -} +// once key is expired set it to be 0 (not live) +cache.on('expired', function(key, value) { + if (value !== STREAMER_NOT_LIVE) { + cache.set(key, STREAMER_NOT_LIVE) + } +}) + +config.streamers.forEach(streamer => { + cache.set(streamer, 0) +}) let bot = new Discord.Client() bot.commands = new Discord.Collection() bot.aliases = new Discord.Collection() fs.readdir('./commands/', (err, files) => { - if (err) console.error(err) + if (err) { + logger.error(err) + return + } let jsfiles = files.filter( f => f.split('.').pop() === 'js' ) + + logger.debug(`${jsfiles.length} commands loaded`) if (jsfiles.length <= 0) { - return console.log('0 commands loaded.') - } else { - console.log(jsfiles.length + ' commands loaded.') + return } jsfiles.forEach(f => { let cmds = require(`./commands/${f}`) - console.log(`Command ${f} loaded.`) + logger.debug(`Command ${f} loaded.`) bot.commands.set(cmds.config.command, cmds) bot.aliases.set(cmds.config.alias, cmds) }) }) bot.login(auth.token) + bot.on('ready', () => { - console.log(`Connected!\nLogged in as ${bot.user.tag}!`) + logger.debug(`Logged in as ${bot.user.tag}!`) bot.user.setGame('www.drakz.pt') const twitchOnline = @@ -48,36 +59,45 @@ bot.on('ready', () => { 'id', config.channel_announces ) - setInterval(() => { - live = twitch.checkTwitchStreams( - SUPPORTED_STREAMERS, - announce_channel, - live, - auth.twitch_clientId - ) - }, config.twitch_checktime * 1000) + + // assign new function with already bound parameters + let renderLiveStreams = twitch2.renderLiveStreams.bind( + null, + auth.twitch_clientId, + announce_channel, + cache, + config.streamers + ) + + setInterval( + renderLiveStreams, + config.twitch_checktime * 1000 + ) } }) +const prefix = config.command_prefix + bot.on('message', message => { if (message.author.bot) return - let prefix = config.command_prefix - // let sender = message.author let msg = message.content.toLowerCase() - let cont = message.content.slice(prefix.length).split(' ') - let args = cont.slice(1) if (msg === 'poop') { message.channel.send(':poop:') + return } if (msg.startsWith('hey drakzbot')) { message.reply('hey!') + return } if (!msg.startsWith(prefix)) return + let cont = message.content.slice(prefix.length).split(' ') + let args = cont.slice(1) + let cmd if (bot.commands.has(cont[0])) cmd = bot.commands.get(cont[0]) diff --git a/cache.js b/cache.js new file mode 100644 index 0000000..f131df8 --- /dev/null +++ b/cache.js @@ -0,0 +1,8 @@ +const NodeCache = require('node-cache') + +module.exports = function(ttl = 100, checkPeriod = 120) { + return new NodeCache({ + stdTTL: ttl, + checkperiod: checkPeriod + }) +} diff --git a/constants.js b/constants.js new file mode 100644 index 0000000..00aa395 --- /dev/null +++ b/constants.js @@ -0,0 +1,4 @@ +/* eslint-disable */ + +const STREAMER_NOT_LIVE = 0 +const STREAMER_LIVE = 1 diff --git a/logger.js b/logger.js new file mode 100644 index 0000000..feccf21 --- /dev/null +++ b/logger.js @@ -0,0 +1,39 @@ +const { + createLogger, + format, + transports +} = require('winston') +const {combine, timestamp, printf} = format +const customFormat = printf(info => { + return `${info.timestamp} ${info.level}: ${info.message}` +}) + +module.exports = () => { + const logger = createLogger({ + level: 'error', + format: combine( + format.splat(), + timestamp(), + customFormat + ), + transports: [ + new transports.File({ + filename: 'error.log', + level: 'error' + }) + ], + exceptionHandlers: [ + new transports.File({filename: 'exceptions.log'}) + ] + }) + + if (process.env.NODE_ENV !== 'production') { + logger.add( + new transports.Console({ + level: 'debug' + }) + ) + } + + return logger +} diff --git a/package-lock.json b/package-lock.json index 974c987..b3d5c24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -149,10 +149,10 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "async": { - "version": "1.0.0", + "version": "1.5.2", "resolved": - "https://registry.npmjs.org/async/-/async-1.0.0.tgz", - "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" }, "async-limiter": { "version": "1.0.0", @@ -499,6 +499,24 @@ "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "color": { + "version": "0.8.0", + "resolved": + "https://registry.npmjs.org/color/-/color-0.8.0.tgz", + "integrity": "sha1-iQwHw/1OZJU3Y4kRz2keVFi2/KU=", + "requires": { + "color-convert": "0.5.3", + "color-string": "0.3.0" + }, + "dependencies": { + "color-convert": { + "version": "0.5.3", + "resolved": + "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" + } + } + }, "color-convert": { "version": "1.9.1", "resolved": @@ -514,14 +532,38 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "0.3.0", + "resolved": + "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "requires": { + "color-name": "1.1.3" + } + }, + "colornames": { + "version": "0.0.2", + "resolved": + "https://registry.npmjs.org/colornames/-/colornames-0.0.2.tgz", + "integrity": "sha1-2BH9bIT1kClJmorEQ2ICk1uSvjE=" }, "colors": { - "version": "1.0.3", + "version": "1.1.2", + "resolved": + "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" + }, + "colorspace": { + "version": "1.0.1", "resolved": - "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + "https://registry.npmjs.org/colorspace/-/colorspace-1.0.1.tgz", + "integrity": "sha1-yZx5btMRKLmHalLh7l7gOkpxl0k=", + "requires": { + "color": "0.8.0", + "text-hex": "0.0.0" + } }, "combined-stream": { "version": "1.0.5", @@ -661,12 +703,6 @@ } } }, - "cycle": { - "version": "1.0.3", - "resolved": - "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" - }, "dashdash": { "version": "1.14.1", "resolved": @@ -749,6 +785,17 @@ "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "diagnostics": { + "version": "1.1.0", + "resolved": + "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.0.tgz", + "integrity": "sha1-4QkJALSVI+hSe+IPCBJ1IF8q42o=", + "requires": { + "colorspace": "1.0.1", + "enabled": "1.0.2", + "kuler": "0.0.0" + } + }, "discord.js": { "version": "11.2.1", "resolved": @@ -791,6 +838,15 @@ "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", "dev": true }, + "enabled": { + "version": "1.0.2", + "resolved": + "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "requires": { + "env-variable": "0.0.3" + } + }, "end-of-stream": { "version": "1.4.0", "resolved": @@ -800,6 +856,20 @@ "once": "1.4.0" } }, + "env-variable": { + "version": "0.0.3", + "resolved": + "https://registry.npmjs.org/env-variable/-/env-variable-0.0.3.tgz", + "integrity": "sha1-uGwWQb5WECZ9UG8YBx6nbXBwl8s=" + }, + "erlpack": { + "version": + "github:hammerandchisel/erlpack#674ebfd3439ba4b7ce616709821d27630f7cdc61", + "requires": { + "bindings": "1.3.0", + "nan": "2.7.0" + } + }, "error-ex": { "version": "1.3.1", "resolved": @@ -1206,12 +1276,6 @@ "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, - "eyes": { - "version": "0.1.8", - "resolved": - "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" - }, "fast-deep-equal": { "version": "1.0.0", "resolved": @@ -1239,6 +1303,12 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fecha": { + "version": "2.3.2", + "resolved": + "https://registry.npmjs.org/fecha/-/fecha-2.3.2.tgz", + "integrity": "sha1-Ng8DXdbt2VS8lYH5XypKfyo1BcE=" + }, "ffmpeg-binaries": { "version": "3.2.2-3", "resolved": @@ -2021,6 +2091,15 @@ "verror": "1.10.0" } }, + "kuler": { + "version": "0.0.0", + "resolved": + "https://registry.npmjs.org/kuler/-/kuler-0.0.0.tgz", + "integrity": "sha1-tmu0a5NOVQ9Z2BiEjgq7pPf1VTw=", + "requires": { + "colornames": "0.0.2" + } + }, "leven": { "version": "2.1.0", "resolved": @@ -2427,6 +2506,17 @@ } } }, + "logform": { + "version": "1.2.2", + "resolved": + "https://registry.npmjs.org/logform/-/logform-1.2.2.tgz", + "integrity": + "sha512-a0TCbuqQWYhVdLie9f0tEP33bMxniAuw2StG1c5KhiTANm+RBRNpbSiGrNGpaiTZeoCiVWVsL+V5F0fpy7Q2Og==", + "requires": { + "colors": "1.1.2", + "fecha": "2.3.2" + } + }, "long": { "version": "3.2.0", "resolved": @@ -2713,6 +2803,12 @@ "wrappy": "1.0.2" } }, + "one-time": { + "version": "0.0.4", + "resolved": + "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" + }, "onetime": { "version": "2.0.1", "resolved": @@ -3799,6 +3895,12 @@ "tar": "2.2.1" } }, + "text-hex": { + "version": "0.0.0", + "resolved": + "https://registry.npmjs.org/text-hex/-/text-hex-0.0.0.tgz", + "integrity": "sha1-V4+8haapJjbkLdF7QdAhjM6esrM=" + }, "text-table": { "version": "0.2.0", "resolved": @@ -3847,6 +3949,12 @@ "punycode": "1.4.1" } }, + "triple-beam": { + "version": "1.1.0", + "resolved": + "https://registry.npmjs.org/triple-beam/-/triple-beam-1.1.0.tgz", + "integrity": "sha1-KsOHyMS9BL0mxh34kaYHn4WS/hA=" + }, "tunnel-agent": { "version": "0.6.0", "resolved": @@ -3981,19 +4089,28 @@ } }, "winston": { - "version": "2.4.0", + "version": "3.0.0-rc1", "resolved": - "https://registry.npmjs.org/winston/-/winston-2.4.0.tgz", - "integrity": "sha1-gIBQuT1SZh7Z+2wms/DIJnCLCu4=", + "https://registry.npmjs.org/winston/-/winston-3.0.0-rc1.tgz", + "integrity": + "sha512-aNtKirnK2UEe5v56SK0TIEr5ylyYdXyjAaIJXZTk21UlNx7FQclTkVU2T1ZzMtdDM+Rk2b7vrI/e/4n8U84XaQ==", "requires": { - "async": "1.0.0", - "colors": "1.0.3", - "cycle": "1.0.3", - "eyes": "0.1.8", + "async": "1.5.2", + "diagnostics": "1.1.0", "isstream": "0.1.2", - "stack-trace": "0.0.10" + "logform": "1.2.2", + "one-time": "0.0.4", + "stack-trace": "0.0.10", + "triple-beam": "1.1.0", + "winston-transport": "3.0.1" } }, + "winston-transport": { + "version": "3.0.1", + "resolved": + "https://registry.npmjs.org/winston-transport/-/winston-transport-3.0.1.tgz", + "integrity": "sha1-gAixXu9WYMT7P6CU1YzL0IUoxY0=" + }, "wordwrap": { "version": "1.0.0", "resolved": diff --git a/package.json b/package.json index 6babcdc..f528ccb 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "urban": "^0.3.1", "uws": "^0.14.5", "weather-js": "^2.0.0", - "winston": "^2.4.0" + "winston": "^3.0.0-rc1" }, "scripts": { "start": "node bot.js", diff --git a/twitch.js b/twitch.js index cf95fef..c516712 100644 --- a/twitch.js +++ b/twitch.js @@ -1,75 +1,86 @@ -const Discord = require('discord.js') -const https = require('https') +const Discord = require('discord.js'), + https = require('https'), + logger = require('./logger.js')() -function checkTwitchStreams( - streamers, +const { + STREAMER_LIVE, + STREAMER_NOT_LIVE +} = require('./constants') + +module.exports.renderLiveStreams = function( + clientId, channel, - liveStatus, - clientId + cache, + streamers ) { + const activeStreams = getActiveStreams( + clientId, + cache, + streamers + ) + activeStreams.forEach(stream => + renderLiveStream(channel, stream) + ) +} + +function renderLiveStream(channel, stream) { + // TODO: the following constants need + // to be refactored into localizable strings + + const title = `O streamer ${ + stream.channel.display_name + } acabou de entrar em direto na Twitch!` + + const description = + 'Acompanha já a transmissão em direto!' + + const url = `https://www.twitch.tv/${ + stream.channel.display_name + }` + + const titleField = 'Título' + const viewersField = 'Viewers: ' + + const embed = new Discord.RichEmbed() + .setTitle(title) + .setDescription(description) + .setThumbnail(stream.preview.small) + .addField(titleField, stream.channel.status, true) + .addField(viewersField, stream.viewers, true) + .setURL(url) + .setColor('#6034b1') + .setFooter('João Rodrigues © 2018') + + channel.send({embed}) +} + +function getActiveStreams(clientId, cache, streamers) { + let activeStreamers = [] + streamers.forEach(name => { + const url = `https://api.twitch.tv/kraken/streams/${name}?client_id=${clientId}` https - .get( - `https://api.twitch.tv/kraken/streams/${name}?client_id=${clientId}`, - res => { - res.on('data', chunk => { - let result - try { - result = JSON.parse(chunk) - } catch (e) { - result = false - console.log(e) - } + .get(url, res => { + res.on('data', body => { + const payload = JSON.parse(body) + const isLive = + payload.stream && + payload.stream.channel.display_name && + cache.get(name) === STREAMER_NOT_LIVE - if (result) { - if (result.stream !== null) { - if (liveStatus[name] === 0) { - if ( - result.stream.channel.display_name !== - null - ) { - const embed = new Discord.RichEmbed() - .setTitle( - 'O streamer ' + - result.stream.channel - .display_name + - ' acabou de entrar em direto na Twitch!' - ) - .setDescription( - 'Acompanha já a transmissão em direto!' - ) - .setThumbnail( - result.stream.preview.small - ) - .addField( - 'Título', - result.stream.channel.status, - true - ) - .addField( - 'Viewers: ', - result.stream.viewers, - true - ) - .setURL( - 'https://www.twitch.tv/' + - result.stream.channel.display_name - ) - .setColor('#6034b1') - .setFooter('João Rodrigues © 2018') - channel.send({embed}) - liveStatus[name] = 1 - return liveStatus - } - } - } - } - }) - } - ) - .on('error', e => console.log('Erro: ', e.message)) + if (isLive) { + activeStreamers.push(payload.stream) + cache.set(name, STREAMER_LIVE) + logger.debug(`Streamer ${name} is live`) + } + }) + }) + .on('error', err => parseError(err)) }) - return liveStatus + + return activeStreamers } -module.exports.checkTwitchStreams = checkTwitchStreams +function parseError(err) { + logger.error('Error while using Twitch API:', err.message) +}