twitch-toolkit
Version:
A set of tools to integrate with Twitch API, Twitch Chat and Twitch WebHooks.
356 lines (306 loc) • 15.9 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>twitchPubSub.js - Documentation</title>
<script src="scripts/prettify/prettify.js"></script>
<script src="scripts/prettify/lang-css.js"></script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
<label for="nav-trigger" class="navicon-button x">
<div class="navicon"></div>
</label>
<label for="nav-trigger" class="overlay"></label>
<nav>
<li class="nav-link nav-home-link"><a href="index.html">Home</a></li><li class="nav-heading">Classes</li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="TwitchApi.html">TwitchApi</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchApi.html#getAccessToken">getAccessToken</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchApi.html#getGames">getGames</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchApi.html#getStreams">getStreams</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchApi.html#getStreamsMetadata">getStreamsMetadata</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchApi.html#getUsers">getUsers</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchApi.html#getUsersFollows">getUsersFollows</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchApi.html#getVideos">getVideos</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchApi.html#isLive">isLive</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchApi.html#updateUser">updateUser</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchApi.html#validateAccessToken">validateAccessToken</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="TwitchChatEmitter.html">TwitchChatEmitter</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchChatEmitter.html#connect">connect</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="TwitchPubSub.html">TwitchPubSub</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchPubSub.html#connect">connect</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchPubSub.html#disconnect">disconnect</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchPubSub.html#reconnect">reconnect</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchPubSub.html#subscribe">subscribe</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="TwitchWebhook.html">TwitchWebhook</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchWebhook.html#destroy">destroy</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchWebhook.html#handleRequest">handleRequest</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchWebhook.html#subscribe">subscribe</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchWebhook.html#topicStreamUpDownSubscribe">topicStreamUpDownSubscribe</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchWebhook.html#topicUserFollowsSubscribe">topicUserFollowsSubscribe</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="TwitchWebhook.html#unsubscribe">unsubscribe</a></span></li><li class="nav-heading">Events</li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="TwitchChatEmitter.html#event:Chat:chat_parsed">Chat:chat_parsed</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="TwitchWebhook.html#event:Webhook:stream_up_down">Webhook:stream_up_down</a></span></li><li class="nav-heading"><span class="nav-item-type type-event">E</span><span class="nav-item-name"><a href="TwitchWebhook.html#event:Webhook:user_follows">Webhook:user_follows</a></span></li>
</nav>
<div id="main">
<h1 class="page-title">twitchPubSub.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>const eventemitter = require('eventemitter3');
const WebSocket = require('ws');
const util = require('util');
const _ = require('./helpers');
const logger = require('./logger').getLogger();
const WEBSOCKET_ADDRESS = 'wss://pubsub-edge.twitch.tv';
const WEBSOCKET_TIMEOUT = 4 * 60 * 1000;
//TODO Add this param and support
//@param {number} config.maxReconnectAttempts Max number of reconnection attempts (Default: Infinity)
//@param {number} config.maxReconnectInterval Max number of ms to delay a reconnection (Default: 30000)
//@param {number} config.reconnectDecay The rate of increase of the reconnect delay (Default: 1.5)
//@param {number} config.reconnectInterval Number of ms before attempting to reconnect (Default: 1000)
//@param {boolean} config.secure Use secure connection (SSL / HTTPS) (Overrides port to 443)
//@param {number} config.timeout Number of ms to disconnect if no responses from server (Default: 9999)
/**
* @class TwitchPubSub
* The Twitch PubSub implementation, as described in https://dev.twitch.tv/docs/pubsub/ .
* @param {object} config The config object.
* @param {object} config.logger The logger instance. If empty, the default logger will be used.
* @param {string} config.authToken The Twitch with OAuth token.
* @param {boolean} config.reconnect Reconnect to Twitch PubSub when disconnected from server. Default: false
*/
function TwitchPubSub(config) {
eventemitter.call(this);
this.logger = config.logger || logger;
this.waitingResponseMap = new Map();
this.subscriptions = [];
this.config = config;
this.config.reconnect = this.config.reconnect || true;
}
/**
* Connects to the Twitch PubSub
* @returns {Promise} Promise for the connection.
*/
TwitchPubSub.prototype.connect = function() {
this.logger.debug('Connecting Twitch PubSub');
this.ws = new WebSocket(WEBSOCKET_ADDRESS);
this.ws.onopen = _onOpen.bind(this);
this.ws.onclose = _onClose.bind(this);
this.ws.onmessage = _onMessage.bind(this);
this.ws.onerror = _onError.bind(this);
this.ws.on('ping', _onPing.bind(this));
this.ws.on('pong', _onPong.bind(this));
this.refreshIntervalId = setInterval(
_refresh.bind(this),
WEBSOCKET_TIMEOUT
);
return new Promise(
(resolve, reject) =>
(this.connectionPromise = {
resolve,
reject
})
);
};
/**
* Disconnects to the Twitch PubSub
* @returns {Promise} Promise for the disconnection.
*/
TwitchPubSub.prototype.disconnect = function() {
this.logger.debug('Disconnecting Twitch PubSub');
this.ws.close();
this.isConnected = false;
clearInterval(this.refreshIntervalId);
delete this.ws;
delete this.waitingResponseMap;
return new Promise(
(resolve, reject) =>
(this.disconnectionPromise = {
resolve,
reject
})
);
};
/**
* Reconnect to WebSub and resubscribe to the already added topics.
* @returns {Promise}
*/
TwitchPubSub.prototype.reconnect = async function() {
//clear the previous interval
clearInterval(this.refreshIntervalId);
await this.connect();
for (let i in this.subscriptions) {
let subscription = this.subscriptions[i];
this.subscribe(
subscription.types,
subscription.dataId,
subscription.authToken,
true
);
}
};
/**
* Subscribe to a topic based on the type.
* @param {String[]} types The types array. Valid options: 'bits', 'subscription', 'commerce', 'whisper'
* @param {String} id The channel/user ID.
* @param {String} authToken The oauth token with the necessary scopes.
* @returns {Promise} The subscription promise that will be resolved when it receives the response.
*/
TwitchPubSub.prototype.subscribe = function(types, id, authToken, isReconnect) {
return new Promise((resolve, reject) => {
if (!types) reject('Missing types parameter');
if (!id) reject('Missing id parameter');
if (!authToken) reject('Missing authToken parameter');
let topics = [];
for (let i in types) {
let topic;
switch (types[i]) {
case 'bits':
topic = 'channel-bits-events-v1';
break;
case 'subscription':
topic = 'channel-subscribe-events-v1';
break;
case 'commerce':
topic = 'channel-commerce-events-v1';
break;
case 'whisper':
topic = 'whispers';
break;
default:
throw new Error('Unknown type.');
}
topic += '.' + id;
topics.push(topic);
}
let key = _.uuidv4();
let data = {
type: 'LISTEN',
nonce: key,
data: {
topics: topics,
auth_token: authToken
}
};
this.ws.send(JSON.stringify(data));
this.logger.debug('Sending topic subscription for ' + topics);
let subscription = {
types,
id,
authToken,
data: {
resolve,
reject
}
};
this.waitingResponseMap.set(key, subscription);
if (!isReconnect) {
this.subscriptions.push({
types,
id,
authToken
});
}
});
};
function _onOpen() {
this.logger.debug('Twitch PubSub connected.');
this.isConnected = true;
this.connectionPromise.resolve();
delete this.connectionPromise;
}
function _onClose() {
this.logger.debug('Twitch PubSub connection closed.');
if (this.disconnectionPromise) {
this.isConnected = false;
this.disconnectionPromise.resolve();
delete this.disconnectionPromise;
} else {
return this.reconnect();
}
}
function _onPing() {
this.logger.debug('WS PING received.');
}
function _onPong() {
this.logger.debug('WS PONG received.');
}
function _onMessage(event) {
this.logger.debug('New message received: ' + event.data);
let message = JSON.parse(event.data);
if (!message || !message.type) {
throw new Error('Invalid message format.');
} else if (message.type.toUpperCase() === 'PONG') {
this.lastPong = new Date().getTime();
} else if (message.type.toUpperCase() === 'RECONNECT') {
this.logger.debug('Reconnecting in 30 seconds.');
setTimeout(this.reconnect, 30000);
} else if (message.type === 'RESPONSE') {
_handleResponse.call(this, message);
} else if (message.type === 'MESSAGE') {
_handleMessage.call(this, message);
}
}
function _onError(errorEvent) {
logger.warn('Error received: ' + errorEvent.message);
}
function _handleResponse(message) {
if (this.waitingResponseMap.has(message.nonce)) {
if (message.type.toUpperCase() === 'RESPONSE') {
let subscription = this.waitingResponseMap.get(message.nonce);
if (!message.error || message.error.trim().length === 0) {
subscription.data.resolve(message.nonce);
subscription.state === 'connected';
delete subscription.data;
} else {
subscription.data.reject(new Error(message.error));
this.waitingResponseMap.delete(message.nonce);
}
}
} else {
this.logger.warn(
'Connection for RESPONSE with nouce ' +
message.nonce +
' not found.'
);
}
}
function _handleMessage(message) {
let id = message.data.topic.split('.').slice(-1)[0];
let topicArr = message.data.topic.split('.');
let type;
switch (topicArr[0]) {
case 'channel-bits-events-v1':
type = 'bits';
break;
case 'channel-subscribe-events-v1':
type = 'subscription';
break;
case 'channel-commerce-events-v1':
type = 'commerce';
break;
case 'whispers':
type = 'whisper';
break;
default:
this.logger.warn('Unknown topic ' + topicArr[0]);
}
if (type) {
this.emit(type, id, message.data.message);
}
}
function _refresh() {
logger.debug('Refreshing the PubSub with a PING command.');
this.ws.ping();
this.ws.send('{"type": "PING"}');
let pingTime = new Date().getTime();
//check if a PONG will be received in the next 10 seconds, otherwise issue a reconnects
//CLEAR TIMEOUT
let _this = this;
setTimeout(() => {
if (_this.config.reconnect && _this.lastPong < pingTime) {
_this.reconnect();
}
}, 10000);
}
/* test code */
if (process.env.NODE_ENV === 'test') {
TwitchPubSub.prototype._onOpen = _onOpen;
TwitchPubSub.prototype._onMessage = _onMessage;
TwitchPubSub.prototype._handleResponse = _handleResponse;
TwitchPubSub.prototype._refresh = _refresh;
}
/* end-test code */
util.inherits(TwitchPubSub, eventemitter);
module.exports = TwitchPubSub;
</code></pre>
</article>
</section>
</div>
<br class="clear">
<footer>
Generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Mon Nov 12 2018 18:32:59 GMT-0200 (E. South America Daylight Time) using the Minami theme.
</footer>
<script>prettyPrint();</script>
<script src="scripts/linenumber.js"></script>
</body>
</html>