UNPKG

fair-twitch

Version:

Fair's Twitch API and Chat bot library

477 lines (476 loc) 18.6 kB
"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;