better-ttb
Version:
Create Twitch bots gooder
210 lines (151 loc) • 5.4 kB
JavaScript
'use strict'
const tls = require('tls');
const assert = require('assert');
const EventEmitter = require('events').EventEmitter;
const parser = require('./parser');
const TwitchBot = class TwitchBot extends EventEmitter {
constructor({ username, oauth, channels = [], port = 443, silence = false}) {
// Initialize Emitters
super()
try {
assert(username);
assert(oauth);
} catch (err) {
throw new Error('Missing or invalid required arguments');
}
this.username = username;
this.oauth = oauth;
this.channels = channels.map(channel => parser.formatCHANNEL(channel));
this.irc = new tls.TLSSocket();
this.port = port;
this.silence = silence;
this._connect();
}
async _connect() {
this.irc.connect({
host: 'irc.chat.twitch.tv',
port: this.port
});
this.irc.setEncoding('utf8');
this.irc.once('connect', () => { this.afterConnect() });
}
afterConnect() {
this.irc.on('error', err => this.emit('error', err));
this.listen();
this.writeIrcMessage("PASS " + this.oauth);
this.writeIrcMessage("NICK " + this.username);;
this.writeIrcMessage("CAP REQ :twitch.tv/tags");
this.channels.forEach(c => this.join(c));
this.writeIrcMessage("CAP REQ :twitch.tv/membership");
this.writeIrcMessage("CAP REQ :twitch.tv/commands");
this.emit('connected', this);
}
// TODO: Make this parsing better
listen() {
this.irc.on('data', data => {
this.checkForError(data);
/* Twitch sends keep-alive PINGs, need to respond with PONGs */
if (data.includes('PING :tmi.twitch.tv')) {
this.irc.write('PONG :tmi.twitch.tv\r\n');
}
if (data.includes('PRIVMSG')) {
const message = parser.formatPRIVMSG(data);
message.client = this;
//TODO: Move these? IDK
message.reply = (content, callback) => {
this.send(`@${message.username}, ${content}`, message.channel.name, callback);
}
message.channel.send = (content, callback) => {
this.send(content, message.channel.name, callback);
}
this.emit('message', message);
}
if (data.includes('CLEARCHAT')) {
const event = parser.formatCLEARCHAT(data);
if (event.type === 'timeout') this.emit('timeout', event);
if (event.type === 'ban') this.emit('ban', event);
}
if (data.includes('USERNOTICE ')) {
const event = parser.formatUSERNOTICE(data);
if (['sub', 'resub'].includes(event.msg_id)) this.emit('subscription', event);
}
// https://dev.twitch.tv/docs/irc#join-twitch-membership
// TODO: Use code 353 for detecting channel JOIN
if (data.includes(`@${this.username}.tmi.twitch.tv JOIN`)) {
const channel = parser.formatJOIN(data)
if (channel) {
if (!this.channels.includes(channel)) this.channels.push(channel);
this.emit('join', channel);
}
}
if (data.includes(`@${this.username}.tmi.twitch.tv PART`)) {
const channel = parser.formatPART(data)
if (channel) {
if (this.channels.includes(channel)) this.channels.pop(channel);
this.emit('part', channel);
}
}
});
}
checkForError(event) {
/* Login Authentication Failed */
if (event.includes('Login authentication failed')) {
this.irc.emit('error', {
message: 'Login authentication failed'
});
}
/* Auth formatting */
if (event.includes('Improperly formatted auth')) {
this.irc.emit('error', {
message: 'Improperly formatted auth'
});
}
/* Notice about blocked messages */
if (event.includes('Your message was not sent because you are sending messages too quickly')) {
this.irc.emit('error', {
message: 'Your message was not sent because you are sending messages too quickly'
});
}
}
writeIrcMessage(text) {
this.irc.write(text + "\r\n");
}
join(channel) {
channel = parser.formatCHANNEL(channel);
this.writeIrcMessage(`JOIN ${channel}`);
}
part(channel) {
if (!channel && this.channels.length > 0) channel = this.channels[0];
channel = parser.formatCHANNEL(channel);
this.writeIrcMessage(`PART ${channel}`);
}
send(message, channel, callback) {
if (!channel) channel = this.channels[0];
if (message.length >= 500) {
this.cb(callback, {
sent: false,
message: 'Exceeded PRIVMSG character limit (500)'
});
} else {
this.writeIrcMessage('PRIVMSG ' + channel + ' :' + message);
}
}
timeout(username, channel, duration = 600, reason = '') {
if (!channel) channel = this.channels[0];
this.send(`/timeout ${username} ${duration} ${reason}`, channel);
}
ban(username, channel, reason = '') {
if (!channel) channel = this.channels[0];
this.send(`/ban ${username} ${reason}`, channel);
}
close() {
this.irc.destroy();
this.emit('close');
}
cb(callback, obj) {
if (!callback) return;
obj.ts = new Date();
callback(obj);
}
}
module.exports = TwitchBot;