twitch-js
Version:
Javascript library for the Twitch Messaging Interface.
703 lines • 28.7 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 __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