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. 
-### 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
-
-
-#### 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"