diff --git a/.gitignore b/.gitignore index 32407b4..bd81c93 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ jspm_packages config.js npm_test.js local_test.js +expected.js .vscode/ \ No newline at end of file diff --git a/README.md b/README.md index 0681d2d..d82e0b5 100644 --- a/README.md +++ b/README.md @@ -2,114 +2,138 @@ Note: This package is not to be used for Twitch botting (inflating live viewer counts) and should only be used for chatbots. Attempting to 'bot' a Twitch channel can lead to your account being permanently banned. ![](https://static-cdn.jtvnw.net/emoticons/v1/91/1.0) -### Installation -Version 2.0.0^ (Recommended): +# Install ```bash $ npm install node-twitchbot ``` -Version 1 (Deprecated): -```bash -$ npm install node-twitchbot@1.2.2 -``` -## V2 DOCS -### Example +# Docs +## Basic example ```javascript const TwitchBot = require('node-twitchbot') const Bot = new TwitchBot({ - username : 'GLADOS', - oauth : 'oauth:secret-oauth-pass', - channel : 'Aperture' + username: 'your_twitch_bot', + oauth: 'oauth:your-oauth-key', + channel: 'your_channel' }) -/* Connect bot to Twitch IRC */ Bot.connect() .then(() => { - - /* Listen for all messages in channel */ - Bot.listen((err, chatter) => { - if(err) { - console.log(err) - } else { - console.log(chatter.msg) // 'Hello World!' + + Bot.on('message', chatter => { + if(chatter.msg === '!command') { + Bot.msg('Command executed PogChamp') } }) - /* Listen for an exact messages match */ - Bot.listenFor('KKona', (err, chatter) => { - console.log(chatter) - }) - - /* Send a message in the channel */ - Bot.msg('this is the message text PogChamp') - - /* Listen for raw IRC events */ - Bot.raw((err, event) => { - console.log(event) + Bot.on('error', err => { + console.log('twitch irc error', err) }) }) -.catch(err => { - console.log('Connection error!') - console.log(err) -}) +.catch(err => console.log(err)) ``` -### Chatter Object -Most callbacks return a `chatter` object which contains the following attributes: +## `TwitchBot()` options +| parameter | type | description | required | +| - | - | - | - | +| `username` | `String` | Bot username | ✔️ | +| `oauth` | `String` | Twitch chat oauth token | ✔️ | +| `channel` | `String` | Channel, `#` can be included e.g. `#channel` or `channel` | ✔️ | +| `port` | `int` | Twitch IRC port, usually `443` or `6777`. Defaults to `443`| ❌ | +| `silence` | `boolean` | Prevent bot from sending messages in chat. Outbound messages logged in console - useful for development. Defaults to `false` | ❌ | +| `limit` | `int` | Limit number of raw messages sent to IRC. Defaults to `19`. Use `30` for moderators. | ❌ | +| `period` | `int` | Message rate limit period (milliseconds). Defaults to `30000` | ❌ | + +## Events +This package makes use of an `EventEmitter` to emit messages on certain Twitch IRC events. Events can be listened by using the `on` method, for example: ```javascript -{ - user: 'kritzware', - msg: 'Hello world! Kappa', - channel: 'kritzware', - twitch_id: '44667418', - level: 'mod', - sub: 0, - turbo: 0 -} +Bot.on('event', message => { + // ... +}) ``` -## V1 DOCS -### Example -```javascript -const Bot = require('node-twitchbot') +The available events are listed below: -Bot.run({ -username: 'bot_username', - oauth: 'oauth:twitch_oauth_key', - channel: 'channel' +### `join` - `(connected)` +Example +```javascript +Bot.on('join', connected => { + // ... }) +``` +Response +```javascript +connected { joined: true, ts: timestamp } +``` -/* Exact message match */ -Bot.listenFor('Kappa', (err, chatter) => { - if(err) { - console.log(err) - } else { - console.log(chatter) - } +### `message` - `(chatter)` +Example +```javascript +Bot.on('message', chatter => { + // ... }) +``` +Response +```javascript +chatter { + badges: 'subscriber/6,premium/1', + color: '#00FF6A', + 'display-name': 'AceSlash', + emotes: true, + id: '73358dc0-e898-4cd2-b5ae-f647893b64b3', + mod: '0', + 'room-id': '23161357', + 'sent-ts': '1496436125243', + subscriber: '1', + 'tmi-sent-ts': '1496436125849', + turbo: '0', + 'user-id': '40705354', + 'user-type': true +} +``` -/* Return all user message in channel */ -Bot.listenFor('*', (err, chatter) { - // Returns all viewer messages in channel -}) +## Methods -/* String is included in message */ -Bot.listen('PogChamp', (err, chatter) => { - console.log(chatter) +### `connect()` +The `connect` method creates a connection to `"irc.chat.twitch.tv"` and resolves once the connection is established. +#### Usage +```javascript +Bot.connect() +.then(() => { + // ... }) +.catch(err => console.log(err)) +``` -/* Sub/resub event in chat */ -Bot.resub((err, chatter, sub) => { - console.log(sub) -}) +#### Usage with async/await +```javascript +async function start() { + await Bot.connect() + // ... +} + +try { + start() +} catch(err) { + console.log(err) +} +``` -/* Say messages in chat */ -Bot.msg('Hello chat!') +### `msg(text, callback)` +Sends a chat message to the connected Twitch channel. If `silence` was set to `true` the message will be printed in the console and not sent to IRC. A callback is provided if you wish to validate if the message was correctly sent. +#### Usage +```javascript +Bot.msg('This is a message from the bot! PogChamp') +Bot.msg('Kappa 123') -/* Private message user */ -Bot.whisper('kritzware', 'This is a private message Kappa') +Bot.msg('Did this message send? :thinking:', err => { + if(err) console.log(err) +}) +``` +OLD DOCS (REMOVED BEFORE PUBLISHING) +```javascript /* Setting commands instead of checking via string match */ const commands = { help : 'For help using this bot, contact kritzware', @@ -132,14 +156,4 @@ Bot.commands('!', commands, (err, chatter, command) => { console.log(chatter) } }) -``` - -#### Output for example '!goodnight' command above -![](http://i.imgur.com/buPqiaK.gif) - -#### Example of a command -```javascript -Bot.listenFor('!command', (err, chatter) => { - Bot.msg('This is the command response') -}) -``` +``` \ No newline at end of file diff --git a/index.js b/index.js index 070edaf..d34f87e 100644 --- a/index.js +++ b/index.js @@ -1,105 +1 @@ -"use strict" - -const IRC = require('irc') -const _ = require('lodash') -const parser = require('./src/parser') - -function Bot({ - username=null, - oauth=null, - channel=null -}) { - if(!username || !oauth || !channel) { - throw new Error('Bot() requires options argument') - } - this.username = username - this.oauth = oauth - this.channel = channel.toLowerCase() - this.client = null -} - -Bot.prototype = { - - connect() { - return new Promise((resolve, reject) => { - this.client = new IRC.Client('irc.chat.twitch.tv', this.username, { - port: 443, - password: this.oauth, - channels: ['#' + this.channel], - debug: false, - secure: true, - autoConnect: false - }) - this.client.connect(connected => { - if(!connected) reject() - if(connected.rawCommand === '001') { - this.client.send('CAP REQ', 'twitch.tv/membership') - this.client.send('CAP REQ', 'twitch.tv/tags') - this.client.send('CAP REQ', 'twitch.tv/commands') - resolve() - } - }) - this.client.addListener('error', err => { - console.log('CONNECTION ERROR') - console.log(err) - reject(err) - }) - }) - }, - - listen(callback) { - return new Promise((resolve, reject) => { - this.raw((err, event) => { - if(err) { - resolve(callback(err)) - } else { - if(event.commandType === 'normal') { - const split = event.command.split(';') - if(_.includes(split[2], 'display-name=') && !_.includes(event.args[0], 'USERSTATE')) { - parser.createChatter(event) - .then(chatter => resolve(callback(null, chatter))) - .catch(err => resolve(callback(err))) - } - } - } - }) - }) - }, - - listenFor(word, callback) { - return new Promise((resolve, reject) => { - this.raw((err, event) => { - if(err) { - resolve(callback(err)) - } else { - if(event.commandType === 'normal') { - const split = event.command.split(';') - if(_.includes(split[2], 'display-name=')) { - parser.exactMatch(event, word) - .then(chatter => resolve(callback(null, chatter))) - .catch(err => resolve(callback(err))) - } - } - } - }) - }) - }, - - raw(cb_event) { - return new Promise((resolve, reject) => { - this.client.addListener('raw', event => { - resolve(cb_event(null, event)) - }) - this.client.addListener('error', err => { - resolve(cb_event(err)) - }) - }) - }, - - msg(text) { - this.client.send('PRIVMSG #' + this.channel, text) - } - -} - -module.exports = Bot \ No newline at end of file +module.exports = require('./src/bot') \ No newline at end of file diff --git a/package.json b/package.json index 3a6f3bc..f4dbbb6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Easily create chat bots for Twitch.tv", "main": "index.js", "scripts": { - "test": "mocha test/test.js --timeout 6000" + "test": "mocha test/test.js --timeout 10000" }, "repository": { "type": "git", @@ -24,7 +24,8 @@ }, "homepage": "https://github.com/kritzware/node-twitchbot#readme", "dependencies": { - "irc": "kritzware/node-irc", + "irc-message": "^3.0.2", + "limiter": "^1.1.0", "lodash": "^4.16.2" }, "devDependencies": { diff --git a/src/bot.js b/src/bot.js new file mode 100644 index 0000000..f668715 --- /dev/null +++ b/src/bot.js @@ -0,0 +1,212 @@ +"use strict" + +const tls = require('tls') +const events = require('events') + +const ircMsg = require('irc-message') +const RateLimiter = require('limiter').RateLimiter + +const parser = require('./parser') + +const Bot = class Bot { + + constructor({ + username, + oauth, + channel, + port = 443, + silence = false, + limit = 19, + period = 30000, + command_prefix = '!' + }) { + this.username = username + this.oauth = oauth + this.channel = channel ? channel.toLowerCase() : channel + + if(!username || !oauth || !channel) { + throw ({ + name: 'missing required arguments', + message: 'You must provide all the default arguments [username, oauth, channel]' + }) + } + + this.port = port + this.silence = silence + this.message_rate_limit = limit + this.message_rate_period = period + this.command_prefix = command_prefix + + if(!this.channel.includes('#')) this.channel = '#' + this.channel + + this.irc = new tls.TLSSocket() + this.messageRateLimiter = new RateLimiter(this.message_rate_limit, this.message_rate_period) + events.EventEmitter.call(this) + } + + async connect() { + return new Promise((resolve, reject) => { + this.irc.connect({ + host: "irc.chat.twitch.tv", + port: this.port + }) + this.irc.setEncoding("utf8") + this.irc.once("connect", () => { + + this.write("PASS " + this.oauth) + this.write("NICK " + this.username) + this.write("JOIN " + this.channel) + + this.write("CAP REQ :twitch.tv/membership") + this.write("CAP REQ :twitch.tv/tags") + this.write("CAP REQ :twitch.tv/commands") + + this.listen() + resolve() + }) + this.irc.on("error", err => { + // reject(err) + this.emit('error', err) + }) + }) + } + + listen(callback) { + const events_to_ignore = [ + '001', '002', '003', '004', + '372', '375', '376', // MOTD + 'CAP', // CAP + '353', '366' // NAMES LIST + ] + this.raw((event, err) => { + if(err) this.emit('error', err) + else { + + if(!events_to_ignore.includes(event.command)) { + switch(event.command) { + + case 'PRIVMSG': + const chatter = parser.getChatter(event, { + command_prefix: this.command_prefix + }) + this.emit('message', chatter) + break + + case 'NOTICE': + if(event.params.includes('Login authentication failed')) { + this.emit('error', event.params[1] + ' for ' + this.username) + } + break + + case 'ROOMSTATE': + if(!event.tags['broadcaster-lang']) { + const roomstate_event = parser.getRoomstate(event) + this.emit('roomstate', roomstate_event) + } + break + + case 'USERNOTICE': // resubs + if(event.tags['msg-id'] === 'resub') { + this.emit('resub', event.tags) + } + if(event.tags['msg-id'] === 'sub') { + this.emit('sub', event.tags) + } + break + + case 'NOTICE': + console.log(event) + break + + case 'USERSTATE': + break + + case 'JOIN': + // console.log(event) + this.emit('join', { joined: true, ts: new Date() }) + break + + case 'MODE': + break + + case 'CLEARCHAT': + console.log(event.tags) // /ban, /clear + break + + case 'HOSTTARGET': + const host = parser.getHost(event) + this.emit('host', host) + break + + case 'PING': + this.raw('PONG :tmi.twitch.tv') + this.emit('ping', { + sent: true, + ts: new Date() + }) + break + + case '421': + this.emit('error', event.params[2] + ' ' + event.params[1]) + break + + default: + console.log('something new!') + console.log(event) + } + } + } + }) + } + + write(text, callback) { + this.messageRateLimiter.removeTokens(1, (err, requests) => { + if(err) callback(err, false) + if(requests < 1) { + callback(new Error('Twitch IRC rate limit reached'), false) + } else { + this.irc.write(text + "\r\n") + if(callback) callback(null, true) + } + }) + } + + raw(callback) { + this.irc.pipe(ircMsg.createStream()).on('data', data => { + if(callback) callback(data, null) + }) + this.irc.on('error', err => callback(null, err)) + } + + say(text, callback) { + if(!this.silence) { + this.write("PRIVMSG " + this.channel + " :" + text, (err, sent) => { + if(callback) callback(err, { + sent: true, + ts: new Date() + }) + }) + } else { + console.log('SILENCED MSG: ' + text) + if(callback) callback(null, 'silenced message') + } + } + + whisper(user, text, callback) { + this.write("PRIVMSG #jtv : /w " + user + " " + text, (err, sent) => { + if(callback) callback(err, { + sent: true, + ts: new Date() + }) + }) + } + + close() { + this.irc.destroy() + } + +} + +Bot.prototype.__proto__ = events.EventEmitter.prototype + +module.exports = Bot \ No newline at end of file diff --git a/src/parser.js b/src/parser.js index 227d4b3..766c719 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1,78 +1,96 @@ -"use strict"; - const _ = require('lodash') module.exports = { - createChatter : function(msg) { - return new Promise(resolve => { - resolve({ - user: this.getElement(msg, 'display-name'), - msg: this.getMessage(msg), - channel: this.getChannel(msg), - twitch_id: this.getElement(msg, 'user-id'), - level: this.getElement(msg, 'user-type'), - sub: +this.getElement(msg, 'subscriber'), - turbo: +this.getElement(msg, 'turbo'), - }) - }) - }, + getChatter(event, { command_prefix }) { - getElement : function(msg, el) { - let temp; - const s = msg.rawCommand.split(';') + const chatter = event.tags - s.some((m) => { - if(_.includes(m, el)) { - temp = m.split('=')[1] - } - }) - return temp; - }, + chatter.mod = !!+chatter.mod + chatter['room-id'] = +chatter['room-id'] + chatter.subscriber = !!+chatter.subscriber + chatter['tmi-sent-ts'] = +chatter['tmi-sent-ts'] + chatter['sent-ts'] = +chatter['sent-ts'] + chatter.turbo = !!+chatter.turbo + chatter['user-id'] = +chatter['user-id'] - getMessage : function(msg) { - return msg.args[0].split(':')[1] - }, + /* ircMsg parser module returns empty vals as bools */ + if(typeof chatter.emotes === 'boolean') { + chatter.emotes = false + } + + chatter.msg = event.params[1] + if(chatter.msg.includes('\u0001ACTION')) { + chatter.msg = chatter.msg.split('\u0001')[1].replace('ACTION ', '') + chatter.color_message = true + } - getChannel : function(msg) { - if(_.includes(msg.args[0], '#')) { - return msg.args[0].split('#')[1].split(' ')[0] + if(command_prefix) { + chatter.is_command = chatter.msg.split(' ')[0][0] === command_prefix ? true : false } - return 'IRC' - }, - exactMatch : function(msg, word) { - return new Promise(resolve => { - if(word === '*') { - resolve(this.createChatter(msg)) - } else { - if(msg.args[0].split(':')[1] === word) { - resolve(this.createChatter(msg)) - } - } - }) + if(chatter.is_command) { + const split_msg = chatter.msg.split(' ') + chatter.args = split_msg.splice(1, split_msg.length - 1) + chatter.msg_without_command = chatter.args.join(' ') + chatter.command = split_msg[0].replace('!', '') + } + + if(typeof chatter['display-name'] === 'boolean') { + chatter.username = event.prefix.split('!')[0] + } else { + chatter.username = chatter['display-name'].toLowerCase() + } + + if(chatter.bits) { + + } + + return chatter }, - includesMatch : function(msg, word) { - return new Promise(resolve => { - if(_.includes(msg.args[0].split(':')[1], word)) { - resolve(this.createChatter(msg)) + getHost(event) { + const host = event.params[1] + const is_hosting = host.split(' ') + + if(is_hosting[0] === '-') { + return { + hosting: false, + ts: new Date() } - }) + } + return { + hosting: true, + channel: is_hosting[0], + viewers: is_hosting[1], + ts: new Date() + } }, - resub : function(msg) { - const _this = this; - return new Promise(resolve => { - if(_.includes(msg.command, 'msg-id=resub')) { - var split_raw = _.split(msg.command, ';') - split_raw.forEach(function(msg) { - if(_.includes(msg, 'msg-param-months')) { - resolve(_this.createChatter(msg), _.split(msg, '=')[1]) - } - }) - } - }) + getRoomstate(event) { + const state = event.tags + + state['room-id'] = +state['room-id'] + + if(state['subs-only']) { + state['subs-only'] = !!+state['subs-only'] + } + if(state['r9k']) { + // console.log(event.tags) + // delete state['r9k'] + // state['rk9'] = !!+state['r9k'] + } + if(state.slow) { + state.slow = +state.slow + } + if(state['emote-only']) { + state['emote-only'] = !!+state['emote-only'] + } + if(state['followers-only']) { + state['followers-only'] = +state['followers-only'] + } + + return state } } \ No newline at end of file diff --git a/test/events.js b/test/events.js new file mode 100644 index 0000000..bd40429 --- /dev/null +++ b/test/events.js @@ -0,0 +1,74 @@ +module.exports = { + + 'sub': { + badges: 'subscriber/0,premium/1', + color: true, + 'display-name': 'Chezfez14', + emotes: true, + id: 'a9224894-7c4d-4868-8e50-5505f3cf410e', + login: 'chezfez14', + mod: '0', + 'msg-id': 'sub', + 'msg-param-months': '1', + 'msg-param-sub-plan-name': 'Channel\\sSubscription\\s(LIRIK)', + 'msg-param-sub-plan': 'Prime', + 'room-id': '23161357', + subscriber: '1', + 'system-msg': 'Chezfez14\\sjust\\ssubscribed\\swith\\sTwitch\\sPrime!', + 'tmi-sent-ts': '1496435693948', + turbo: '0', + 'user-id': '83703386', + 'user-type': true + }, + + 'resub': { + + }, + + 'chatter': { + badges: 'subscriber/6,premium/1', + color: '#00FF6A', + 'display-name': 'AceSlash', + emotes: true, + id: '73358dc0-e898-4cd2-b5ae-f647893b64b3', + mod: '0', + 'room-id': '23161357', + 'sent-ts': '1496436125243', + subscriber: '1', + 'tmi-sent-ts': '1496436125849', + turbo: '0', + 'user-id': '40705354', + 'user-type': true + }, + + 'chatter-mod': { + badges: 'moderator/1,subscriber/12', + color: '#69E600', + 'display-name': 'hnlBot', + emotes: '50741:0-9/89011:35-43', + id: '8c88735a-2d24-4761-ada5-cc01c6691c3d', + mod: '1', + 'room-id': '23161357', + subscriber: '1', + 'tmi-sent-ts': '1496436178173', + turbo: '0', + 'user-id': '78917118', + 'user-type': 'mod' + }, + + 'host': { + tags: {}, + prefix: 'tmi.twitch.tv', + command: 'HOSTTARGET', + params: [ '#kritzware', 'blarev 0'] + }, + + 'unhost': { + raw: ':tmi.twitch.tv HOSTTARGET #kritzware :- 0', + tags: {}, + prefix: 'tmi.twitch.tv', + command: 'HOSTTARGET', + params: [ '#kritzware', '- 0'] + } + +} \ No newline at end of file diff --git a/test/test.js b/test/test.js index 4222a65..dce5eab 100644 --- a/test/test.js +++ b/test/test.js @@ -1,169 +1,157 @@ const assert = require('assert') const expect = require('chai').expect -const Bot = require('../index') -const test_options = { - username : process.env.username, - oauth : process.env.oauth, - channel : process.env.channel +const TwitchBot = require('../index') +const parser = require('../src/parser') + +const conf = { + username: process.env.BOT_USERNAME, + oauth: process.env.BOT_OAUTH, + channel: !process.env.BOT_CHANNEL.includes('#') ? '#' + process.env.BOT_CHANNEL : process.env.BOT_CHANNEL +} +const sender_conf = { + username: process.env.USERNAME, + oauth: process.env.OAUTH, + channel: !process.env.CHANNEL.includes('#') ? '#' + process.env.CHANNEL : process.env.CHANNEL } -/* TODO: Make all tests better */ +async function newBot(options) { + const bot = new TwitchBot(options) + await bot.connect() + return bot +} +function destroy(reciever, sender) { + reciever.close() + sender.close() +} describe('Bot', () => { - describe('init', () => { - it('should create a new bot instance when given valid arguments', () => { - const TwitchBot = new Bot(test_options) - }) - it('should fail when no options given', () => { - expect(() => new Bot()).to.throw(Error) - }) - it('should fail when given no username, oauth or channel arguments', () => { - expect(() => new Bot({}).to.throw(Error)) - expect(() => new Bot({username: 'hal',}).to.throw(Error)) - }) - }) + describe('constructor', () => { - describe('connect', () => { - let TwitchBot - - afterEach(done => { - TwitchBot.client.disconnect() - done() - }) - - it('should connect to twitch irc', done => { - TwitchBot = new Bot(test_options) - TwitchBot.connect() - .then(() => { - assert.equal(true, TwitchBot.client.conn.connected) - done() + it('should create a bot with default settings', () => { + const bot = new TwitchBot({ + username: conf.username, + oauth: conf.oauth, + channel: conf.channel }) - }) - it('should connect to the correct twitch channel', done => { - TwitchBot = new Bot(test_options) - TwitchBot.connect() - .then(() => { - assert.equal('#'+test_options.channel, TwitchBot.client.opt.channels[0]) - done() + expect(bot).to.include({ username: conf.username }) + expect(bot).to.include({ oauth: conf.oauth }) + expect(bot).to.include({ channel: conf.channel }) + expect(bot).to.include({ port: 443 }) + expect(bot).to.include({ silence: false }) + expect(bot).to.include({ message_rate_limit: 19 }) + expect(bot).to.include({ message_rate_period: 30000 }) + expect(bot).to.include({ command_prefix: '!' }) + }) + + it('should create a bot with optional settings', () => { + const bot = new TwitchBot({ + username: conf.username, + oauth: conf.oauth, + channel: conf.channel, + port: 6667, + silence: true, + limit: 10, + period: 25000, + command_prefix: '#' }) - }) - }) - - /* TODO: Make this test better */ - describe('raw', () => { - let TwitchBot - - before(done => { - TwitchBot = new Bot(test_options) - TwitchBot.connect().then(() => done()) - }) - - it('should return all irc events', (done) => { - TwitchBot.raw((err, event) => { - if(!err && event.rawCommand === 'JOIN') { - expect(event).to.have.any.keys('prefix', 'nick', 'user', 'host', 'args') - expect(event.args[0]).to.equal('#'+test_options.channel) - done() - } + expect(bot).to.include({ username: conf.username }) + expect(bot).to.include({ oauth: conf.oauth }) + expect(bot).to.include({ channel: conf.channel }) + expect(bot).to.include({ port: 6667 }) + expect(bot).to.include({ silence: true }) + expect(bot).to.include({ message_rate_limit: 10 }) + expect(bot).to.include({ message_rate_period: 25000 }) + expect(bot).to.include({ command_prefix: '#' }) + }) + + it('should fail when default arguments not provided', () => { + try { + const bot = new TwitchBot({ + username: '', + oauth: '' }) - }) - }) - - describe('msg', () => { - let TwitchBotSender, TwitchBotListener - - before(done => { - TwitchBotSender = new Bot(test_options) - TwitchBotListener = new Bot(test_options) - Promise.all([ - TwitchBotSender.connect(), - TwitchBotListener.connect() - ]) - .then(() => done()) + } catch(err) { + expect(err.name).to.equal('missing required arguments') + } }) - after(() => { - TwitchBotSender.client.disconnect() - TwitchBotListener.client.disconnect() - }) - - it('should send a message to the correct twitch irc channel', done => { - TwitchBotListener.listen((err, chatter) => { - if(!err) { - expect(chatter.msg).to.equal('message test FeelsGoodMan') - expect(chatter.user).to.equal(test_options.username) - expect(chatter.channel).to.equal(test_options.channel) - done() - } + it('should create a new socket', () => { + const bot = new TwitchBot({ + username: conf.username, + oauth: conf.oauth, + channel: conf.channel }) - TwitchBotSender.msg('message test FeelsGoodMan') + expect(bot.irc).to.include({ domain: null }) + expect(bot.irc).to.include({ connecting: true }) }) + }) - describe('listen', () => { - let TwitchBotSender, TwitchBotListener - - before(done => { - TwitchBotSender = new Bot(test_options) - TwitchBotListener = new Bot(test_options) - Promise.all([ - TwitchBotSender.connect(), - TwitchBotListener.connect() - ]) - .then(() => done()) + describe('connect', () => { + + it('should connect to Twitch IRC', async () => { + const bot = await newBot(conf) + expect(bot.irc).to.include({ connecting: false }) + expect(bot.irc).to.include({ _host: 'irc.chat.twitch.tv' }) }) - after(() => { - TwitchBotSender.client.disconnect() - TwitchBotListener.client.disconnect() - }) + }) - it('should listen for all messages and return chatter objects', (done) => { - const messages = [] - TwitchBotSender.msg('chatter test 1') - TwitchBotSender.msg('chatter test 2') - TwitchBotListener.listen((err, chatter) => { - if(!err) { - messages.push(chatter) - if(messages.length > 1) { - expect(messages[0]).to.have.keys('user', 'msg', 'channel', 'twitch_id', 'level', 'sub', 'turbo') - expect(messages[0].msg).to.equal('chatter test 1') - expect(messages[1]).to.have.keys('user', 'msg', 'channel', 'twitch_id', 'level', 'sub', 'turbo') - expect(messages[1].msg).to.equal('chatter test 2') - done() - } - } + describe('events', () => { + + describe('join', () => { + it('should emit a join message when connecting to the channel room', async () => { + const bot = await newBot(conf) + return new Promise(resolve => { + bot.on('join', event => { + expect(event.joined).to.equal(true) + resolve() + }) + }) }) }) - }) - describe('listenFor', () => { - let TwitchBotSender, TwitchBotListener - - before(done => { - TwitchBotSender = new Bot(test_options) - TwitchBotListener = new Bot(test_options) - Promise.all([ - TwitchBotSender.connect(), - TwitchBotListener.connect() - ]) - .then(() => done()) + describe('roomstate', () => { + it('should emit roomstate event when a room mode is toggled', async () => { + const bot = await newBot(conf) + return new Promise(resolve => { + bot.on('roomstate', state => { + if(state['subs-only']) { + expect(state['subs-only']).to.equal(true) + resolve() + } + }) + bot.say('/subscribersoff') + bot.say('/subscribers') + bot.say('/subscribersoff') + }) + }) }) - it('should listen for an exact match and return a chatter object', (done) => { - TwitchBotSender.msg('exact match test NaM') - TwitchBotSender.msg('exact match test KKona') - TwitchBotListener.listenFor('exact match test KKona', (err, chatter) => { - if(!err) { - expect(chatter).to.not.be.null - expect(chatter.user).to.equal(test_options.username) - expect(chatter.msg).to.equal('exact match test KKona') - done() - } + describe('message', () => { + it('should emit a chatter when a message is sent in the channel room', async () => { + const msg = 'unit test message 1 KKona' + const [ + bot, + sender + ] = await Promise.all([ + newBot(conf), + newBot(sender_conf) + ]) + return new Promise(resolve => { + bot.on('message', chatter => { + expect(chatter.msg).to.equal(msg) + expect(chatter.username).to.equal(sender_conf.username) + expect(chatter.emotes).to.equal(false) + resolve() + }) + sender.say(msg) + }) }) }) - }) + }) + }) \ No newline at end of file diff --git a/v1/index.js b/v1/index.js deleted file mode 100644 index 696fcd3..0000000 --- a/v1/index.js +++ /dev/null @@ -1,140 +0,0 @@ -"use strict"; - -const irc = require('irc') -const Q = require('q') -const _ = require('lodash') - -const parser = require('./src/parser') - -let _conf; -let client; -let _commands; - -module.exports = { - - run : function(conf) { - conf.hashedChannel = '#' + conf.channel - _conf = conf - - client = new irc.Client('irc.chat.twitch.tv', conf.username, { - port: 6667, - password: conf.oauth, - channels: [conf.hashedChannel] - }) - - client.addListener('error', err => { - console.log(err) - }) - - client.send('CAP REQ', 'twitch.tv/membership') - client.send('CAP REQ', 'twitch.tv/tags') - client.send('CAP REQ', 'twitch.tv/commands') - }, - - raw : function(callback) { - const deferred = Q.defer() - - client.addListener('raw', (msg) => { - if(msg.commandType === 'normal') { - var s = msg.command.split(';') - if(_.includes(s[2], 'display-name=')) { - deferred.resolve(callback(msg, s)) - } - } - }) - - client.addListener('error', (err) => { - deferred.reject(err) - }) - - return deferred.promise; - }, - - /* Exact string match */ - listenFor : function(word, callback) { - const deferred = Q.defer() - - this.raw((msg) => { - parser.exactMatch(msg, word) - .then((chatter) => { - deferred.resolve(callback(null, chatter)) - }).catch((err) => { - deferred.reject(callback(err)) - }) - }).catch((err) => { - deferred.reject(callback(err)) - }) - return deferred.promise; - }, - - /* Includes string match */ - listen : function(word, callback) { - const deferred = Q.defer() - - this.raw((msg) => { - parser.includesMatch(msg, word) - .then((chatter) => { - deferred.resolve(callback(null, chatter)) - }).catch((err) => { - deferred.reject(callback(err)) - }) - }).catch((err) => { - deferred.reject(callback(err)) - }) - return deferred.promise; - }, - - resub : function(callback) { - const deferred = Q.defer() - - this.raw((msg) => { - parser.resub(msg) - .then((chatter, sub) => { - deferred.resolve(callback(null, chatter, sub)) - }).catch((err) => { - deferred.reject(callback(err)) - }) - }).catch((err) => { - deferred.reject(callback(err)) - }) - return deferred.promise; - }, - - msg : function(message) { - client.send('PRIVMSG ' + _conf.hashedChannel, message) - }, - - whisper : function(user, message) { - client.send('PRIVMSG ' + _conf.hashedChannel, '/w ' + user + ' ' + message) - }, - - commands : function(prefix, commands, callback) { - _commands = commands - const _this = this - const deferred = Q.defer() - - this.raw((msg) => { - _.keys(_commands).forEach((cmd) => { - parser.exactMatch(msg, prefix + cmd) - .then((chatter) => { - if(Object.prototype.toString.call(_commands[cmd]) == '[object Function]') { - try { - const out = _commands[cmd](chatter) - _this.msg(out.toString()) - deferred.resolve(callback(null, chatter, cmd)) - } catch(err) { - deferred.reject(callback(err)) - } - } else { - _this.msg(_commands[cmd]) - deferred.resolve(callback(null, chatter, cmd)) - } - }) - }) - }) - .catch((err) => { - deferred.reject(callback(err)) - }) - return deferred.promise; - } -} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 03ff0bc..224bd31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -43,6 +43,10 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + debug@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" @@ -90,12 +94,6 @@ has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" -iconv@~2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/iconv/-/iconv-2.2.1.tgz#39b13fdd98987d26aef26c0a2f2a900911fa4584" - dependencies: - nan "^2.3.5" - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -103,27 +101,38 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@~2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" -irc-colors@^1.1.0: - version "1.3.3" - resolved "https://registry.yarnpkg.com/irc-colors/-/irc-colors-1.3.3.tgz#16f8fa6130a3882fdf4fab801c5b4f58328a3391" - -irc@kritzware/node-irc: - version "0.5.2" - resolved "https://codeload.github.com/kritzware/node-irc/tar.gz/cab82a1a4d7fe16484ca8bddddefb89418d4c8b8" +irc-message@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/irc-message/-/irc-message-3.0.2.tgz#5377b235c54c4e3ce56536c9d40c5291798df9df" dependencies: - irc-colors "^1.1.0" - optionalDependencies: - iconv "~2.2.1" - node-icu-charset-detector "~0.2.0" + irc-prefix-parser "^1.0.1" + iso8601-convert "^1.0.0" + through2 "^0.6.3" + +irc-prefix-parser@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/irc-prefix-parser/-/irc-prefix-parser-1.0.1.tgz#56ce60d78f30e9b80b0fcf123f4ef9f8f2e62e0b" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +iso8601-convert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/iso8601-convert/-/iso8601-convert-1.0.0.tgz#8a709807c98e60466d9e794d811265c90a415635" json3@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" +limiter@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.0.tgz#6e2bd12ca3fcdaa11f224e2e53c896df3f08d913" + lodash._baseassign@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" @@ -211,16 +220,6 @@ ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" -nan@^2.3.3, nan@^2.3.5: - version "2.5.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" - -node-icu-charset-detector@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/node-icu-charset-detector/-/node-icu-charset-detector-0.2.0.tgz#c2320da374ddcb671fc54cb4a0e041e156ffd639" - dependencies: - nan "^2.3.3" - once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -231,12 +230,32 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" +"readable-stream@>=1.0.33-1 <1.1.0-0": + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + supports-color@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" dependencies: has-flag "^1.0.0" +through2@^0.6.3: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + type-detect@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" @@ -248,3 +267,7 @@ type-detect@^1.0.0: wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +"xtend@>=4.0.0 <4.1.0-0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"