UNPKG

twitch-js

Version:

Javascript library for the Twitch Messaging Interface.

703 lines 28.7 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 __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArrays = (this && this.__spreadArrays) || function () { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j]; return r; }; function __export(m) { for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; } var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; var _a; Object.defineProperty(exports, "__esModule", { value: true }); var eventemitter3_1 = __importDefault(require("eventemitter3")); var get_1 = __importDefault(require("lodash/get")); var toLower_1 = __importDefault(require("lodash/toLower")); var uniq_1 = __importDefault(require("lodash/uniq")); var twitch_1 = require("../twitch"); var logger_1 = __importDefault(require("../utils/logger")); var utils = __importStar(require("../utils")); var chatUtils = __importStar(require("./utils")); var Client_1 = __importDefault(require("./Client")); var Errors = __importStar(require("./Errors")); var constants = __importStar(require("./constants")); var commands = __importStar(require("./utils/commands")); var parsers = __importStar(require("./utils/parsers")); var sanitizers = __importStar(require("./utils/sanitizers")); var validators = __importStar(require("./utils/validators")); var types_1 = require("./types"); __export(require("./types")); /** * Interact with Twitch chat. * * ## Connecting * * ```js * const token = 'cfabdegwdoklmawdzdo98xt2fo512y' * const username = 'ronni' * const { chat } = new TwitchJs({ token, username }) * * chat.connect().then(globalUserState => { * // Do stuff ... * }) * ``` * * **Note:** Connecting with a `token` and a `username` is optional. * * Once connected, `chat.userState` will contain * [[GlobalUserStateTags|global user state information]]. * * ## Joining a channel * * ```js * const channel = '#dallas' * * chat.join(channel).then(channelState => { * // Do stuff with channelState... * }) * ``` * * After joining a channel, `chat.channels[channel]` will contain * [[ChannelState|channel state information]]. * * ## Listening for events * * ```js * // Listen to all events * chat.on('*', message => { * // Do stuff with message ... * }) * * // Listen to private messages * chat.on('PRIVMSG', privateMessage => { * // Do stuff with privateMessage ... * }) * ``` * * Events are nested; for example: * * ```js * // Listen to subscriptions only * chat.on('USERNOTICE/SUBSCRIPTION', userStateMessage => { * // Do stuff with userStateMessage ... * }) * * // Listen to all user notices * chat.on('USERNOTICE', userStateMessage => { * // Do stuff with userStateMessage ... * }) * ``` * * For added convenience, TwitchJS also exposes event constants. * * ```js * const { chat } = new TwitchJs({ token, username }) * * // Listen to all user notices * chat.on(chat.events.USER_NOTICE, userStateMessage => { * // Do stuff with userStateMessage ... * }) * ``` * * ## Sending messages * * To send messages, [Chat] must be initialized with a `username` and a * [`token`](../#authentication) with `chat_login` scope. * * All messages sent to Twitch are automatically rate-limited according to * [Twitch Developer documentation](https://dev.twitch.tv/docs/irc/guide/#command--message-limits). * * ### Speak in channel * * ```js * const channel = '#dallas' * * chat * .say(channel, 'Kappa Keepo Kappa') * // Optionally ... * .then(userStateMessage => { * // ... do stuff with userStateMessage on success ... * }) * ``` * * ### Send command to channel * * All chat commands are currently supported and exposed as camel-case methods. For * example: * * ```js * const channel = '#dallas' * * // Enable followers-only for 1 week * chat.followersOnly(channel, '1w') * * // Ban ronni * chat.ban(channel, 'ronni') * ``` * * **Note:** `Promise`-resolves for each commands are * [planned](https://github.com/twitch-devs/twitch-js/issues/87). * * ## Joining multiple channels * * ```js * const channels = ['#dallas', '#ronni'] * * Promise.all(channels.map(channel => chat.join(channel))).then(channelStates => { * // Listen to all messages from #dallas only * chat.on('#dallas', message => { * // Do stuff with message ... * }) * * // Listen to private messages from #dallas and #ronni * chat.on('PRIVMSG', privateMessage => { * // Do stuff with privateMessage ... * }) * * // Listen to private messages from #dallas only * chat.on('PRIVMSG/#dallas', privateMessage => { * // Do stuff with privateMessage ... * }) * * // Listen to all private messages from #ronni only * chat.on('PRIVMSG/#ronni', privateMessage => { * // Do stuff with privateMessage ... * }) * }) * ``` * * ### Broadcasting to all channels * * ```js * chat * .broadcast('Kappa Keepo Kappa') * // Optionally ... * .then(userStateMessages => { * // ... do stuff with userStateMessages on success ... * }) * ``` */ var Chat = /** @class */ (function (_super) { __extends(Chat, _super); /** * Chat constructor. */ function Chat(maybeOptions) { var _this = _super.call(this) || this; _this._readyState = types_1.ChatReadyStates.NOT_READY; _this._connectionAttempts = 0; _this._channelState = {}; _this._isDisconnecting = false; /** * Connect to Twitch. */ _this.connect = function () { _this._isDisconnecting = false; if (_this._connectionInProgress) { return _this._connectionInProgress; } _this._connectionInProgress = Promise.race([ utils.rejectAfter(_this.options.connectionTimeout, new Errors.TimeoutError(constants.ERROR_CONNECT_TIMED_OUT)), _this._handleConnectionAttempt(), ]) .then(_this._handleConnectSuccess.bind(_this)) .catch(_this._handleConnectRetry.bind(_this)); return _this._connectionInProgress; }; /** * Send a raw message to Twitch. */ _this.send = function (message, options) { return _this._client.send(message, options); }; /** * Disconnected from Twitch. */ _this.disconnect = function () { _this._isDisconnecting = true; _this._readyState = types_1.ChatReadyStates.DISCONNECTING; _this._clearChannelState(); _this._client.disconnect(); }; /** * Reconnect to Twitch, providing new options to the client. */ _this.reconnect = function (newOptions) { if (newOptions) { _this.options = __assign(__assign({}, _this.options), newOptions); } _this._connectionInProgress = null; _this._readyState = types_1.ChatReadyStates.RECONNECTING; var channels = _this._getChannels(); _this.disconnect(); return _this.connect().then(function () { return Promise.all(channels.map(function (channel) { return _this.join(channel); })); }); }; /** * Join a channel. * * @example <caption>Joining #dallas</caption> * const channel = '#dallas' * * chat.join(channel).then(channelState => { * // Do stuff with channelState... * }) * * @example <caption>Joining multiple channels</caption> * const channels = ['#dallas', '#ronni'] * * Promise.all(channels.map(channel => chat.join(channel))) * .then(channelStates => { * // Listen to all PRIVMSG * chat.on('PRIVMSG', privateMessage => { * // Do stuff with privateMessage ... * }) * * // Listen to PRIVMSG from #dallas ONLY * chat.on('PRIVMSG/#dallas', privateMessage => { * // Do stuff with privateMessage ... * }) * // Listen to all PRIVMSG from #ronni ONLY * chat.on('PRIVMSG/#ronni', privateMessage => { * // Do stuff with privateMessage ... * }) * }) */ _this.join = function (maybeChannel) { var channel = sanitizers.channel(maybeChannel); var joinProfiler = _this._log.profile("Joining " + channel); var connect = _this.connect(); var roomStateEvent = utils.resolveOnEvent(_this, twitch_1.Commands.ROOM_STATE + "/" + channel); var userStateEvent = !chatUtils.isUserAnonymous(_this.options.username) ? utils.resolveOnEvent(_this, twitch_1.Commands.USER_STATE + "/" + channel) : Promise.resolve(); var join = Promise.all([connect, roomStateEvent, userStateEvent]).then(function (_a) { var roomState = _a[1], userState = _a[2]; var channelState = { roomState: roomState.tags, userState: userState ? userState.tags : null, }; _this._setChannelState(roomState.channel, channelState); joinProfiler.done("Joined " + channel); return channelState; }); var send = _this.send(twitch_1.Commands.JOIN + " " + channel); return send.then(function () { return Promise.race([ utils.rejectAfter(_this.options.joinTimeout, new Errors.TimeoutError(constants.ERROR_JOIN_TIMED_OUT)), join, ]); }); }; /** * Depart from a channel. */ _this.part = function (maybeChannel) { var channel = sanitizers.channel(maybeChannel); _this._log.info("Parting " + channel); _this._removeChannelState(channel); _this.send(twitch_1.Commands.PART + " " + channel); }; /** * Send a message to a channel. */ _this.say = function (maybeChannel, message) { var messageArgs = []; for (var _i = 2; _i < arguments.length; _i++) { messageArgs[_i - 2] = arguments[_i]; } var channel = sanitizers.channel(maybeChannel); var args = messageArgs.length ? __spreadArrays([''], messageArgs).join(' ') : ''; var info = "PRIVMSG/" + channel + " :" + message + args; var isModerator = get_1.default(_this, ['_channelState', channel, 'isModerator']); // const timeout = utils.rejectAfter( // this.options.joinTimeout, // new Errors.TimeoutError(constants.ERROR_SAY_TIMED_OUT), // ) var commandResolvers = commands.resolvers(_this)(channel, message); var resolvers = function () { return Promise.race(__spreadArrays(commandResolvers)); }; return utils .resolveInSequence([ _this._isUserAuthenticated.bind(_this), _this.send.bind(_this, twitch_1.Commands.PRIVATE_MESSAGE + " " + channel + " :" + message + args, { isModerator: isModerator }), resolvers, ]) .then(function (resolvedEvent) { _this._log.info(info); return resolvedEvent; }) .catch(function (err) { _this._log.error(info, err); throw err; }); }; /** * Whisper to another user. */ _this.whisper = function (user, message) { return utils.resolveInSequence([ _this._isUserAuthenticated.bind(_this), _this.send.bind(_this, twitch_1.Commands.WHISPER + " :/w " + user + " " + message), ]); }; /** * Broadcast message to all connected channels. */ _this.broadcast = function (message) { return utils.resolveInSequence([ _this._isUserAuthenticated.bind(_this), function () { return Promise.all(_this._getChannels().map(function (channel) { return _this.say(channel, message); })); }, ]); }; _this.options = maybeOptions; // Create logger. _this._log = logger_1.default(__assign({ name: 'Chat' }, _this.options.log)); // Create commands. Object.assign(_this, commands.factory(_this)); return _this; } Object.defineProperty(Chat.prototype, "options", { /** * Retrieves the current options */ get: function () { return this._options; }, /** * Validates the passed options before changing `_options` */ set: function (maybeOptions) { this._options = validators.chatOptions(maybeOptions); }, enumerable: true, configurable: true }); /** * Updates the client options after instantiation. * To update `token` or `username`, use `reconnect()`. */ Chat.prototype.updateOptions = function (options) { var _a = this.options, token = _a.token, username = _a.username; this.options = __assign(__assign({}, options), { token: token, username: username }); }; Chat.prototype._handleConnectionAttempt = function () { var _this = this; return new Promise(function (resolve, reject) { var connectProfiler = _this._log.profile('Connecting ...'); // Connect ... _this._readyState = types_1.ChatReadyStates.CONNECTING; // Increment connection attempts. _this._connectionAttempts += 1; if (_this._client) { // Remove all listeners, just in case. _this._client.removeAllListeners(); } // Create client and connect. _this._client = new Client_1.default(_this.options); // Handle messages. _this._client.on(twitch_1.Events.ALL, _this._handleMessage, _this); // Handle disconnects. _this._client.on(twitch_1.Events.DISCONNECTED, _this._handleDisconnect, _this); // Listen for reconnects. _this._client.once(twitch_1.Events.RECONNECT, function () { return _this.reconnect(); }); // Listen for authentication failures. _this._client.once(twitch_1.Events.AUTHENTICATION_FAILED, reject); // Once the client is connected, resolve ... _this._client.once(twitch_1.Events.CONNECTED, function (e) { _this._handleJoinsAfterConnect(); connectProfiler.done('Connected'); resolve(e); }); }); }; Chat.prototype._handleConnectSuccess = function (globalUserState) { this._readyState = types_1.ChatReadyStates.CONNECTED; this._connectionAttempts = 0; return parsers.globalUserStateMessage(globalUserState); }; Chat.prototype._handleJoinsAfterConnect = function () { return __awaiter(this, void 0, void 0, function () { var channels; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: channels = this._getChannels(); return [4 /*yield*/, Promise.all(channels.map(function (channel) { return _this.join(channel); }))]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; Chat.prototype._handleConnectRetry = function (errorMessage) { return __awaiter(this, void 0, void 0, function () { var token, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: this._connectionInProgress = null; if (this._isDisconnecting) { // .disconnect() was called; do not retry to connect. return [2 /*return*/, Promise.resolve()]; } this._readyState = types_1.ChatReadyStates.CONNECTING; this._log.info('Retrying ...'); if (!(errorMessage.event === twitch_1.Events.AUTHENTICATION_FAILED)) return [3 /*break*/, 6]; _a.label = 1; case 1: _a.trys.push([1, 5, , 6]); return [4 /*yield*/, this.options.onAuthenticationFailure()]; case 2: token = _a.sent(); if (!token) return [3 /*break*/, 4]; this.options = __assign(__assign({}, this.options), { token: token }); return [4 /*yield*/, utils.resolveAfter(this.options.connectionTimeout)]; case 3: _a.sent(); return [2 /*return*/, this.connect()]; case 4: return [3 /*break*/, 6]; case 5: error_1 = _a.sent(); this._log.error('Connection failed'); throw new Errors.AuthenticationError(error_1, errorMessage); case 6: return [2 /*return*/, this.connect()]; } }); }); }; Chat.prototype._isUserAuthenticated = function () { var _this = this; return new Promise(function (resolve, reject) { if (chatUtils.isUserAnonymous(_this.options.username)) { reject(new Error('Not authenticated')); } else { resolve(); } }); }; Chat.prototype._emit = function (eventName, message) { var _this = this; if (eventName) { var events = uniq_1.default(eventName.split('/')); var displayName = get_1.default(message, 'tags.displayName') || get_1.default(message, 'username') || ''; var info = get_1.default(message, 'message') || ''; this._log.info("" + events.join('/'), "" + displayName + (info ? ':' : ''), info); events .filter(function (part) { return part !== '#'; }) .reduce(function (parents, part) { var eventParts = __spreadArrays(parents, [part]); if (eventParts.length > 1) { _super.prototype.emit.call(_this, part, message); } _super.prototype.emit.call(_this, eventParts.join('/'), message); return eventParts; }, []); } /** * All events are also emitted with this event name. * @event Chat#* */ _super.prototype.emit.call(this, twitch_1.Events.ALL, message); }; Chat.prototype._getChannels = function () { return Object.keys(this._channelState); }; Chat.prototype._getChannelState = function (channel) { return this._channelState[channel]; }; Chat.prototype._setChannelState = function (channel, state) { this._channelState[channel] = state; }; Chat.prototype._removeChannelState = function (channel) { this._channelState = Object.entries(this._channelState).reduce(function (channelStates, _a) { var _b; var name = _a[0], state = _a[1]; return name === channel ? channelStates : __assign(__assign({}, channelStates), (_b = {}, _b[name] = state, _b)); }, {}); }; Chat.prototype._clearChannelState = function () { this._channelState = {}; }; Chat.prototype._handleMessage = function (baseMessage) { var channel = sanitizers.channel(baseMessage.channel); var preMessage = baseMessage; var eventName = preMessage.command; var message = preMessage; switch (preMessage.command) { case twitch_1.Events.JOIN: { message = parsers.joinMessage(preMessage); eventName = message.command + "/" + channel; break; } case twitch_1.Events.PART: { message = parsers.partMessage(preMessage); eventName = message.command + "/" + channel; break; } case twitch_1.Events.NAMES: { message = parsers.namesMessage(preMessage); eventName = message.command + "/" + channel; break; } case twitch_1.Events.NAMES_END: { message = parsers.namesEndMessage(preMessage); eventName = message.command + "/" + channel; break; } case twitch_1.Events.CLEAR_CHAT: { message = parsers.clearChatMessage(preMessage); eventName = message.event ? message.command + "/" + message.event + "/" + channel : message.command + "/" + channel; break; } case twitch_1.Events.HOST_TARGET: { message = parsers.hostTargetMessage(preMessage); eventName = message.command + "/" + channel; break; } case twitch_1.Events.MODE: { message = parsers.modeMessage(preMessage); eventName = message.command + "/" + channel; if (toLower_1.default(this.options.username) === toLower_1.default(message.username)) { var channelState = this._getChannelState(channel); this._setChannelState(channel, __assign(__assign({}, channelState), { userState: __assign(__assign({}, channelState.userState), { isModerator: message.isModerator }) })); } break; } case twitch_1.Events.GLOBAL_USER_STATE: { message = parsers.globalUserStateMessage(preMessage); this._userState = message.tags; break; } case twitch_1.Events.USER_STATE: { message = parsers.userStateMessage(preMessage); eventName = message.command + "/" + channel; this._setChannelState(channel, __assign(__assign({}, this._getChannelState(channel)), { userState: message.tags })); break; } case twitch_1.Events.ROOM_STATE: { message = parsers.roomStateMessage(preMessage); eventName = message.command + "/" + channel; this._setChannelState(channel, __assign(__assign({}, this._getChannelState(channel)), { roomState: message.roomState })); break; } case twitch_1.Events.NOTICE: { message = parsers.noticeMessage(preMessage); eventName = message.command + "/" + message.event + "/" + channel; break; } case twitch_1.Events.USER_NOTICE: { message = parsers.userNoticeMessage(preMessage); eventName = message.command + "/" + message.event + "/" + channel; break; } case twitch_1.Events.PRIVATE_MESSAGE: { message = parsers.privateMessage(preMessage); eventName = message.event ? message.command + "/" + message.event + "/" + channel : message.command + "/" + channel; break; } default: { var command = chatUtils.getEventNameFromMessage(preMessage); eventName = channel === '#' ? command : command + "/" + channel; } } this._emit(eventName, message); }; Chat.prototype._handleDisconnect = function () { this._connectionInProgress = null; this._readyState = types_1.ChatReadyStates.DISCONNECTED; this._isDisconnecting = false; }; Chat.Commands = twitch_1.Commands; Chat.Events = twitch_1.Events; Chat.CompoundEvents = (_a = {}, _a[twitch_1.Events.NOTICE] = types_1.NoticeCompounds, _a[twitch_1.Events.PRIVATE_MESSAGE] = types_1.PrivateMessageCompounds, _a[twitch_1.Events.USER_NOTICE] = types_1.UserNoticeCompounds, _a); return Chat; }(eventemitter3_1.default)); exports.default = Chat; //# sourceMappingURL=index.js.map