fair-twitch
Version:
Fair's Twitch API and Chat bot library
477 lines (476 loc) • 18.6 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var net_1 = __importDefault(require("net"));
var ExpandedEventEmitter_1 = __importDefault(require("./ExpandedEventEmitter"));
var NotificationsEmitter_1 = __importDefault(require("./NotificationsEmitter"));
var RoomTracker_1 = __importDefault(require("./RoomTracker"));
var TwitchIRC = /** @class */ (function (_super) {
__extends(TwitchIRC, _super);
/**
* @param options The constructor options
*/
function TwitchIRC(options) {
var _this = _super.call(this) || this;
// Default options
_this.options = {
login: null,
token: null,
autoReconnect: true,
requestCAP: true,
autoConnect: true,
url: 'irc.chat.twitch.tv',
port: 6667,
successMessages: [
'Welcome, GLHF!',
'You are in a maze of twisty passages',
'>'
]
};
if (options) {
for (var key in options) {
if (_this.options.hasOwnProperty(key)) {
_this.options[key] = options[key];
}
}
}
if (_this.options.login === null) {
var anoLogin = 'justinfan';
for (var i = 0; i < 5; i++) {
var num = Math.floor(Math.random() * 10);
anoLogin += num;
}
_this.options.login = anoLogin;
}
else {
// If token is null and login doesn't match justinfan regex
if (_this.options.token === null && !/justinfan[\d]+/.test(_this.options.login)) {
throw new Error('Missing token option');
}
}
_this.ready = false;
_this.sendQueue = []; // Used to store messages that needs to be sent when ready
_this.closeCalled = false;
_this.socket = null;
_this.dataBuffer = '';
if (_this.options.autoConnect) {
_this.connect();
}
return _this;
}
/**
* Starts a connection to Twitch IRC servers using the options given in constructor
* Will reconnect if already connected
* @param callback Callback for when connection was successfully created (not ready to be used)
*/
TwitchIRC.prototype.connect = function (callback) {
var _this = this;
// If already connecting/connected clear that
if (this.socket !== null) {
this.socket.end();
this.socket.removeAllListeners();
this.socket.unref();
this.socket = null;
}
this.readyList = this.options.successMessages.slice(); // A list of required messages for successful login
this.socket = net_1.default.createConnection(this.options.port, this.options.url, callback);
// Handle errors
this.socket.addListener('error', function (err) { return _this.emit('error', err); });
// Handle data
this.socket.addListener('data', function (data) {
if (typeof (data) !== 'string') {
data = data.toString();
}
// We split the data up into lines.
// It's possible that the data we receive doesn't end with a new line, and we have to store that and wait for an ending
data = _this.dataBuffer + data;
if (!data.endsWith('\n')) {
var lastNl = data.lastIndexOf('\n');
// console.log('Storing last part (' + lastNl + ',' + data.length + '):');
if (lastNl === -1) {
_this.dataBuffer = data;
return; // Don't process data
}
else {
_this.dataBuffer = data.substring(lastNl + 1);
data = data.substring(0, lastNl);
}
}
else {
_this.dataBuffer = '';
}
var lines = data.split('\n');
for (var i = 0; i < lines.length; i++) {
var rawLine = lines[i].trim();
if (rawLine.length === 0)
continue;
// console.log(rawLine); // Debug printing
if (rawLine.startsWith('PING')) {
_this.send('PONG :tmi.twitch.tv');
continue;
}
try {
var pl = parseTwitchMessage(rawLine);
if (!_this.ready) {
if (pl.msg.includes(_this.readyList[0])) {
_this.readyList.splice(0, 1);
if (_this.readyList.length === 0) {
_this.ready = true;
while (_this.sendQueue.length > 0) {
_this.send(_this.sendQueue[0]);
_this.sendQueue.splice(0, 1);
}
_this.emit('ready');
}
}
// A notice means there was a problem with connecting or login
if (pl.cmd === 'NOTICE') {
_this.emit('error', new Error(pl.msg));
_this.close();
}
}
else {
_this.emit('raw', rawLine, pl);
switch (pl.cmd) {
case 'JOIN':
var joinLogin = pl.url.substring(0, pl.url.indexOf('!'));
if (joinLogin === _this.options.login) {
_this.emit('join', pl.channel);
}
else {
_this.emit('userjoin', pl.channel, joinLogin);
}
break;
case 'PART':
var partLogin = pl.url.substring(0, pl.url.indexOf('!'));
if (partLogin === _this.options.login) {
_this.emit('part', pl.channel);
}
else {
_this.emit('userpart', pl.channel, partLogin);
}
break;
case 'PRIVMSG':
_this.emit('msg', pl.channel, pl.url.substring(0, pl.url.indexOf('!')), pl.msg, pl.tags);
break;
case 'ROOMSTATE':
_this.emit('roomstate', pl.channel, pl.tags);
break;
case 'USERNOTICE':
// console.log(rawLine);
// console.log(pl);
_this.emit('usernotice', pl.channel, pl.tags.login, pl.msg, pl.tags);
break;
case 'NOTICE':
_this.emit('notice', pl.channel, pl.msg, pl.tags);
break;
case 'CLEARCHAT':
if (pl.msg && pl.msg.length > 0) {
_this.emit('clearchat', pl.channel);
}
else {
_this.emit('userban', pl.channel, pl.msg, pl.tags);
}
break;
case 'CLEARMSG':
_this.emit('clearmsg', pl.channel, pl.tags);
break;
case 'GLOBALUSERSTATE':
_this.emit('globaluserstate', pl.tags);
break;
case 'USERSTATE':
_this.emit('userstate', pl.channel, pl.tags);
break;
case 'HOSTTARGET':
var msgSplit = pl.msg.split(' ');
var target = msgSplit[0];
var viewers = msgSplit.length > 1 ? Number(msgSplit[1]) : undefined;
if (isNaN(viewers))
viewers = undefined;
_this.emit('host', pl.channel, target, viewers);
break;
default: {
// Do nothing on default
}
}
}
}
catch (err) {
_this.emit('parseerror', rawLine, err);
}
}
});
this.socket.once('ready', function () {
// Login once the connection is ready
if (_this.options.requestCAP) {
_this.socket.write('CAP REQ twitch.tv/membership\r\n');
_this.socket.write('CAP REQ twitch.tv/tags\r\n');
_this.socket.write('CAP REQ twitch.tv/commands\r\n');
}
if (_this.options.token) {
if (_this.options.token.startsWith('oauth:')) {
_this.socket.write('PASS ' + _this.options.token + '\r\n');
}
else {
_this.socket.write('PASS oauth:' + _this.options.token + '\r\n');
}
}
_this.socket.write('NICK ' + _this.options.login + '\r\n');
});
this.socket.once('close', function () {
_this.ready = false;
if (!_this.closeCalled && _this.options.autoReconnect) {
// Try and reconnect after 5 seconds
setTimeout(function () {
_this.connect();
}, 5000);
}
});
};
/**
* @returns If connection is ready or not
*/
TwitchIRC.prototype.isReady = function () {
return this.ready;
};
TwitchIRC.prototype.createNotificationsEmitter = function () {
return new NotificationsEmitter_1.default(this);
};
TwitchIRC.prototype.createRoomTracker = function () {
return new RoomTracker_1.default(this);
};
/**
* Same as once, but will remove the listener if the callback returns true
* @param eventName The name of the event
* @param callback Event callback
* @param timeout Timeout to remove listener
*/
TwitchIRC.prototype.onceIf = function (eventName, callback, timeout) {
var _this = this;
var listener = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
if (callback.apply(void 0, args)) {
removeListener();
}
};
var removeListener = function () {
_this.removeListener(eventName, listener);
};
this.addListener(eventName, listener);
if (typeof (timeout) == 'number') {
setTimeout(removeListener, timeout);
}
return this;
};
/**
* Joins a channel, callback is optional and has no parameters
* @param channel The channel to join (without #)
* @param callback Channel joined callback. Times out after 5 seconds if still not joined
*/
TwitchIRC.prototype.join = function (channel, callback) {
if (!channel.startsWith('#'))
channel = '#' + channel;
this.send('JOIN ' + channel);
if (typeof (callback) === 'function') {
this.onceIf('join', function (chn) {
if (!chn.startsWith('#'))
chn = '#' + chn;
if (chn === channel) {
callback();
return true;
}
return false;
}, 5000);
}
};
/**
* Leaves a channel, callback is optional and has no parameters
* @param channel The channel to join (without #)
* @param callback Channel joined callback. Times out after 5 seconds if still not left
*/
TwitchIRC.prototype.part = function (channel, callback) {
if (!channel.startsWith('#'))
channel = '#' + channel;
this.send('PART ' + channel);
if (typeof (callback) === 'function') {
this.onceIf('part', function (chn) {
if (!chn.startsWith('#'))
chn = '#' + chn;
if (chn === channel) {
callback();
return true;
}
return false;
}, 5000);
}
};
/**
* Sends a chat message in a channel
* @param channel The channel to talk in (without #)
* @param msg The message to send
*/
TwitchIRC.prototype.say = function (channel, msg) {
if (!channel.startsWith('#'))
channel = '#' + channel;
this.send('PRIVMSG ' + channel + ' :' + msg);
};
/**
* Sends data to Twitch. Use @function Say() to send chat messages
* @param data The data to send
*/
TwitchIRC.prototype.send = function (data) {
var _this = this;
if (data.length > 500) {
this.emit('error', new Error('Cannot send more than 500 characters'));
return;
}
if (!this.ready) {
this.sendQueue.push(data);
}
else {
this.socket.write(data + '\r\n', function () {
_this.emit('rawSend', data);
});
}
};
/**
* Tries to close the connection.
* @param callback Callback when close happened
*/
TwitchIRC.prototype.close = function (callback) {
this.closeCalled = true;
var socket = this.socket;
if (socket !== null) {
socket.end();
socket.once('close', function (err) {
if (callback)
callback(err);
socket.removeAllListeners();
socket.unref();
});
this.socket = null;
}
};
return TwitchIRC;
}(ExpandedEventEmitter_1.default));
// This is iterated through to parse each part of a Twitch message
// Each function is called and should return the remaining of the unparsed data
var parserActions = [
// Look for tags first
function (data, obj) {
if (data.charAt(0) === ':') {
return data.substring(1);
}
var endIndex = data.indexOf(' :');
if (endIndex !== -1) {
var tagsData = data.substring(0, endIndex);
if (tagsData.length > 0 && tagsData.charAt(0) === '@') {
var tagsSplit = tagsData.substring(1).split(';');
var tags = {};
for (var i = 0; i < tagsSplit.length; i++) {
var tagSplit = tagsSplit[i].split('=');
if (tagSplit.length > 1) {
tags[tagSplit[0]] = tagSplit[1];
}
else {
tags[tagSplit[0]] = null;
}
}
obj.tags = tags;
}
return data.substring(endIndex + 1);
}
else {
return data;
}
},
// Look for the url kind of part
function (data, obj) {
if (data.charAt(0) === ':')
data = data.substring(1);
var endIndex = data.indexOf(" ");
if (endIndex !== -1) {
var urlData = data.substring(0, endIndex);
if (urlData.length > 0) {
obj.url = urlData;
}
return data.substring(endIndex + 1);
}
else {
return data;
}
},
// Look for the command
function (data, obj) {
var endIndex = data.indexOf(" ");
var cmdData = endIndex === -1 ? data : data.substring(0, endIndex);
var out = endIndex === -1 ? null : data.substring(endIndex + 1);
if (cmdData.length > 0) {
obj.cmd = cmdData;
}
return out;
},
// Extra and channel
function (data, obj) {
var endIndex = data.indexOf(" :");
var extraData = endIndex === -1 ? data : data.substring(0, endIndex);
var out = endIndex === -1 ? null : data.substring(endIndex + 1);
if (extraData.length > 0) {
var extraSplit = extraData.split(' ');
// Look for channel
for (var i = 0; i < extraSplit.length; i++) {
if (extraSplit[i].length > 0 && extraSplit[i].charAt(0) === '#') {
obj.channel = extraSplit[i].substring(1);
}
}
obj.extra = extraData.trim();
}
return out;
},
// Look for the msg
function (data, obj) {
if (data.charAt(0) === ':')
data = data.substring(1);
obj.msg = data.trim();
return null;
}
];
function parseTwitchMessage(msg) {
var out = {
tags: null,
url: null,
cmd: null,
channel: null,
extra: null,
msg: null,
};
for (var i = 0; i < parserActions.length; i++) {
var parser = parserActions[i];
msg = parser(msg, out);
if (msg === null || msg.length === 0)
break;
}
if (msg !== null && msg.length > 0) {
console.log('Could not parse part of message:');
console.log(msg);
}
return out;
}
module.exports = TwitchIRC;