twitchps
Version:
Library to easily interact with Twitch PubSub System
926 lines (891 loc) • 41 kB
JavaScript
let WebSocket = require('ws'),
shortid = require('shortid'),
EventEmitter = require('events');
/**
* Connection to Twitch Pubsub System
*/
class TwitchPS extends EventEmitter {
/**
* Constructor
* @constructor
* @param {Object} options - JSON object of required options
* @param {boolean} [options.reconnect=true] - True to try to reconnect, false to not
* @param {Object[]} options.init_topics - JSON Object array of initial topic
* @param {string} options.init_topics[].topic - Topic to listen too
* @param {string} options.init_topics[].token - Authentication token
* @param {boolean} [options.debug=false] - Turns debug console output on and off
* @param {string} [options.url='wss://pubsub-edge.twitch.tv'] - URL of WS to connect too. DEFAULT: Twitch {"wss://pubsub-edge.twitch.tv"}
*/
constructor(options = {
reconnect: true,
init_topics: {},
debug: false,
url: 'wss://pubsub-edge.twitch.tv'
}) {
super();
if (Object.prototype.toString.call(options.init_topics) != '[object Array]' || options.init_topics.length == 0) throw new Error('Missing initial topics');
this._recon = options.reconnect;
this._debug = options.debug;
this._url = (options.url) ? options.url : 'wss://pubsub-edge.twitch.tv';
this._init_topics = options.init_topics;
this._topics = [];
this._pending = [];
this._interval = null;
this._timeout = null;
this._tries = 0;
this._init_nonce = null;
this._ws = null;
this._connect();
}
/**
* Initial connection function -- Sets up connection, and websocket listeners
*/
_connect() {
this._ws = new WebSocket(this._url);
const self = this;
this._ws.on('open', () => {
self.addTopic(self._init_topics, true);
self.emit('connected');
});
/**
* MSG TYPES HANDLED:
* PONG - response to send type ping
* RECONNECT - sent when server restarting - reconnect to server
* MESSAGE - sent from server with message data - different topics - See topic handler section for emit details
* Types of topics:
* channel-bits-events-v1 - Bits - Sent on cheer events
* whispers - Whisper - Sent on whisper events
* video-playback - Sent on update to stream -
* stream-up - Sent when stream starts
* stream-down - Sent when stream ends
* viewcount - Sent on update to viewer count
* RESPONSE - sent from server after receiving listen message -- if error is empty string then it is good -
* Types of errors:
* ERR_BADMESSAGE
* ERR_BADAUTH
* ERR_SERVER
* ERR_BADTOPIC
*/
this._ws.on('message', (mess) => {
try {
let message = JSON.parse(mess);
// Emit 'raw' event on every message received
self.emit('raw', message);
self._sendDebug('_connect()', message);
if (message.type === 'RESPONSE') {
if (message.nonce === self._init_nonce) {
self._init_nonce = null;
if (message.error !== "") {
self._pending[message.nonce].reject(message.error);
self._handleError('MESSAGE RESPONSE - Error while listening to initial topics', message.error);
} else {
self._pending[message.nonce].resolve();
}
} else {
if (self._pending[message.nonce]) {
if (message.error !== "") {
self._pending[message.nonce].reject(message.error);
} else {
self._pending[message.nonce].resolve();
}
} else {
self._handleError('MESSAGE RESPONSE', 'Received message with unknown nonce');
}
}
} else if (message.type === 'MESSAGE') {
if (typeof message.data.message === 'string') message.data.message = JSON.parse(message.data.message);
let split = message.data.topic.split('.'),
channel = split[1];
switch (message.data.topic.substr(0, message.data.topic.indexOf('.'))) {
case 'channel-bits-events-v1':
case 'channel-bits-events-v2':
self._onBits(message);
break;
case 'channel-bits-badge-unlocks':
self._onBitsBadgeUnlocks(message);
break;
case 'channel-points-channel-v1':
self._onChannelPoints(message);
break;
case 'community-points-channel-v1':
self._onCommunityPoints(message);
break;
case 'channel-subscribe-events-v1':
self._onSub(message);
break;
case 'whispers':
self._onWhisper(message);
break;
case 'video-playback':
self._onVideoPlayback(message, channel);
break;
case 'chat_moderator_actions':
self._onModeratorAction(message);
break;
}
} else if (message.type === 'RECONNECT') {
self._reconnect();
} else if (message.type === 'PONG') {
self._sendDebug('In messageType Pong', 'Received pong');
clearTimeout(self._timeout);
self._timeout = null;
} else {
self._handleError('MESSAGE RESPONSE - Unknown message type', message);
}
} catch (e) {
self._handleError('Error caught in _connect() on message', e);
}
});
this._ws.on('close', () => {
self._sendDebug('In websocket close', '');
self.emit('disconnected');
if (self._recon) {
self.emit('reconnect');
setTimeout(() => {
self._ws = new WebSocket(self._url);
}, 1000 * self._tries);
self._tries += 1;
}
clearTimeout(self._timeout);
clearInterval(self._interval);
self._timeout = null;
self._interval = null;
});
self._interval = setInterval(() => {
if (self._ws.readyState === 1) {
self._ws.send(JSON.stringify({
type: 'PING'
}));
self._sendDebug('In setInterval', 'Sent ping');
self._timeout = setTimeout(() => self._reconnect(), 15000);
}
}, 300000);
}
/**
* Reconnect function - Terminates current websocket connection and reconnects
*/
_reconnect() {
const self = this;
self._ws.terminate();
self._sendDebug('_reconnect()', 'Websocket has been terminated');
self.emit('reconnect');
setTimeout(() => {
self._connect();
}, 5000);
}
/*****
**** Message Handler Functions
****/
/**
* Handles Bits Message
* @param message - {object} - Message object received from pubsub-edge
* @param message.type - {string} - Type of message - Will always be 'MESSAGE' - Handled by _connect()
* @param message.data - {JSON} - JSON wrapper of topic/message fields
* @param message.data.topic - {string} - Topic that message pertains too - Will always be 'channel-bits-events-v1.<CHANNEL_ID>' - Handled by _connect()
* @param message.data.message - {JSON} - Parsed into JSON in _connect() - Originally received as string from Twitch
* @emits bits - {event} -
* JSON object -
* bits_used - {integer} - Number of bits used
* channel_id - {string} - User ID of the channel on which bits were used
* channel_name - {string} - Name of the channel on which bits were used
* chat_message - {string} - Chat message sent with the cheer
* context - {string} - Event type associated with this use of bits
* message_id - {string} - Message ID
* message_type - {string} - Message type
* time - {string} - Time when the bits were used. RFC 3339 format
* total_bits_used - {integer} - All-time total number of bits used on this channel by the specified user
* user_id - {string} - User ID of the person who used the bits
* user_name - {string} - Login name of the person who used the bits
* version - {string} - Message version
*/
_onBits(message) {
// TODO ADD VERSION CHECK/EMIT
this.emit('bits', {
"badge_entitlement": message.data.message.data.badge_entitlement, // v2 only
"bits_used": message.data.message.data.bits_used,
"channel_id": message.data.message.data.channel_id,
"channel_name": message.data.message.data.channel_name,
"chat_message": message.data.message.data.chat_message,
"context": message.data.message.data.context,
"is_anonymous": message.data.message.data.is_anonymous, // v2 only
"message_id": message.data.message.message_id,
"message_type": message.data.message.message_type,
"time": message.data.message.data.time,
"total_bits_used": message.data.message.data.total_bits_used,
"user_id": message.data.message.data.user_id,
"user_name": message.data.message.data.user_name,
"version": message.data.message.version
});
}
/**
* Handles Bits Badge Notification Message
* @param message - {object} - Message object received from pubsub-edge
* @param message.type - {string} - Type of message - Will always be 'MESSAGE' - Handled by _connect()
* @param message.data - {JSON} - JSON wrapper of topic/message fields
* @param message.data.topic - {string} - Topic that message pertains too - Will always be 'channel-bits-badge-unlocks.<CHANNEL_ID>' - Handled by _connect()
* @param message.data.message - {JSON} - Parsed into JSON in _connect() - Originally received as string from Twitch
* @emits bits-badge - {event} -
* JSON object -
* user_id - {string} - ID of user who earned the new Bits badge
* user_name - {string} - Login of user who earned the new Bits badge
* channel_id - {string} - ID of channel where user earned the new Bits badge
* channel_name - {string} - Login of channel where user earned the new Bits badge
* badge_tier - {int} - Value of Bits badge tier that was earned (1000, 10000, etc.)
* chat_message - {string} - [Optional] Custom message included with share
* time - {string} - Time when the bits were used. RFC 3339 format
*/
_onBitsBadgeUnlocks(message) {
this.emit('bits-badge', {
"user_id": message.data.message.data.user_id,
"user_name": message.data.message.data.user_name,
"channel_id": message.data.message.data.channel_id,
"channel_name": message.data.message.data.channel_name,
"chat_message": message.data.message.data.chat_message,
"badge_tier": message.data.message.data.badge_tier,
"time": message.data.message.data.time
});
}
/**
* Handles Channel Points Event Message
* @param message - {object} - Message object received from pubsub-edge
* @param message.type - {string} - Type of message - Will always be 'MESSAGE' - Handled by _connect()
* @param message.data - {JSON} - JSON wrapper of topic/message fields
* @param message.data.topic - {string} - Topic that message pertains too - Will always be 'channel-points-channel-v1.<CHANNEL_ID>' - Handled by _connect()
* @param message.data.message - {JSON} - Parsed into JSON in _connect() - Originally received as string from Twitch
* @emits channel-points - {event} -
* JSON object -
* timestamp - {string} - Time the pubsub message was sent
* redemption - {object} - Data about the redemption, includes unique id and user that redeemed it
* channel_id - {string} - ID of the channel in which the reward was redeemed.
* redeemed_at - {string} - Timestamp in which a reward was redeemed
* reward - {object} - Data about the reward that was redeemed
* user_input - {string} - [Optional] A string that the user entered if the reward requires input
* status - {string} - reward redemption status, will be FULFULLED if a user skips the reward queue, UNFULFILLED otherwise
*/
_onChannelPoints(message) {
this.emit('channel-points', {
"timestamp": message.data.message.data.timestamp,
"redemption": message.data.message.data.redemption,
"channel_id": message.data.message.data.redemption.channel_id,
"redeemed_at": message.data.message.data.redemption.redeemed_at,
"reward": message.data.message.data.redemption.reward,
"user_input": message.data.message.data.redemption.user_input,
"status": message.data.message.data.redemption.status
});
}
/**
* Handles Community Channel Points Event Message
* @param message - {object} - Message object received from pubsub-edge
* @param message.type - {string} - Type of message - Will always be 'MESSAGE' - Handled by _connect()
* @param message.data - {JSON} - JSON wrapper of topic/message fields
* @param message.data.topic - {string} - Topic that message pertains too - Will always be 'community-points-channel-v1.<CHANNEL_ID>' - Handled by _connect()
* @param message.data.message - {JSON} - Parsed into JSON in _connect() - Originally received as string from Twitch
* @emits community-points-all, community-custom-reward-created, community-custom-reward-updated
* community-custom-reward-deleted, community-goal-created, community-goal-updated,
* community-goal-deleted, channel-points
* community-points-all
* JSON object -
* type - {string} - Type of the event
* timestamp - {string} - Time the pubsub message was sent
* event - {object} - All data from all points events
* community-reward-created
* JSON object -
* timestamp - {string} - Time the pubsub message was sent
* event - {object} - All data from the event
* community-reward-updated
* JSON object -
* timestamp - {string} - Time the pubsub message was sent
* event - {object} - All data from the event
* community-reward-deleted
* JSON object -
* timestamp - {string} - Time the pubsub message was sent
* event - {object} - All data from the event
* community-goal-created
* JSON object -
* timestamp - {string} - Time the pubsub message was sent
* event - {object} - All data from the event
* community-goal-updated
* JSON object -
* timestamp - {string} - Time the pubsub message was sent
* event - {object} - All data from the event
* community-goal-deleted
* JSON object -
* timestamp - {string} - Time the pubsub message was sent
* event - {object} - All data from the event
* channel-points
* JSON object -
* timestamp - {string} - Time the pubsub message was sent
* redemption - {object} - Data about the redemption, includes unique id and user that redeemed it
* channel_id - {string} - ID of the channel in which the reward was redeemed.
* redeemed_at - {string} - Timestamp in which a reward was redeemed
* reward - {object} - Data about the reward that was redeemed
* user_input - {string} - [Optional] A string that the user entered if the reward requires input
* status - {string} - reward redemption status, will be FULFULLED if a user skips the reward queue, UNFULFILLED otherwise
*/
_onCommunityPoints(message) {
this.emit('community-points-all', {
"type": message.data.message.type,
"timestamp": message.data.message.data.timestamp,
"event": message.data.message.data[`${Object.keys(message.data.message.data)[1]}`]
});
switch(message.data.message.type){
case "reward-redeemed":
this.emit('reward-redeemed', {
"timestamp": message.data.message.data.timestamp,
"redemption": message.data.message.data.redemption,
"channel_id": message.data.message.data.redemption.channel_id,
"redeemed_at": message.data.message.data.redemption.redeemed_at,
"reward": message.data.message.data.redemption.reward,
"user_input": message.data.message.data.redemption.user_input,
"status": message.data.message.data.redemption.status
});
break;
case "custom-reward-created":
this.emit('community-reward-created', {
"timestamp": message.data.message.data.timestamp,
"event": message.data.message.data.new_reward
});
break;
case "custom-reward-updated":
this.emit('community-reward-updated', {
"timestamp": message.data.message.data.timestamp,
"event": message.data.message.data.updated_reward
});
break;
case "custom-reward-deleted":
this.emit('community-reward-deleted', {
"timestamp": message.data.message.data.timestamp,
"event": message.data.message.data.deleted_reward
});
break;
case "community-goal-created":
this.emit('community-goal-created', {
"timestamp": message.data.message.data.timestamp,
"event": message.data.message.data.community_goal
});
break;
case "community-goal-updated":
this.emit('community-goal-updated', {
"timestamp": message.data.message.data.timestamp,
"event": message.data.message.data.community_goal
});
break;
case "community-goal-deleted":
this.emit('community-goal-deleted', {
"timestamp": message.data.message.data.timestamp,
"event": message.data.message.data.community_goal
});
break;
default:
// Do Nothing
}
}
/**
* Handles Subscription Message
* @param message - {object} - Message object received from pubsub-edge
* @param message.type - {string} - Type of message - Will always be 'MESSAGE' - Handled by _connect()
* @param message.data - {JSON} - JSON wrapper of topic/message fields
* @param message.data.topic - {string} - Topic that message pertains too - Will always be 'channel-subscribe-events-v1.<CHANNEL_ID>' - Handled by _connect()
* @param message.data.message - {JSON} - Parsed into JSON in _connect() - Originally received as string from Twitch
* @emits subscribe - {event} -
* JSON object -
* user_name - {string} - Username of subscriber
* display_name - {string} - Display name of subscriber
* channel_name - {string} - Name of the channel subscribed too
* user_id - {string} - UserID of subscriber
* channel_id - {string} - Channel ID of channel subscribed too
* time - {string} - Time of subscription event RFC 3339 format
* sub_plan - {string} - Type of sub plan (ie. Prime, 1000, 2000, 3000)
* sub_plan_name - {string} - Name of subscription plan
* months - {integer} - Months subscribed to channel
* cumulative_months - {integer} - Cumulative months subscribed to channel
* context - {string} - Context of sub -- (ie. sub, resub)
* sub_message - {object} - Object containing message
* sub_message.message - {string} - Message sent in chat on resub
* sub_message.emotes - {array} - Array of emotes
*/
_onSub(message) {
this.emit('subscribe', {
"user_name": message.data.message.user_name,
"benefit_end_month": message.data.message.benefit_end_month,
"display_name": message.data.message.display_name,
"channel_name": message.data.message.channel_name,
"user_id": message.data.message.user_id,
"channel_id": message.data.message.channel_id,
"time": message.data.message.time,
"sub_plan": message.data.message.sub_plan,
"sub_plan_name": message.data.message.sub_plan_name,
"months": message.data.message.months, // Depecrecated, but still used by gift subs
"cumulative_months": message.data.message.cumulative_months,
"streak_months": message.data.message.streak_months,
"context": message.data.message.context,
"sub_message": {
"message": message.data.message.sub_message.message,
"emotes": message.data.message.sub_message.emotes
},
"recipient_id": message.data.message.recipient_id,
"recipient_user_name": message.data.message.recipient_user_name,
"recipient_display_name": message.data.message.recipient_display_name
});
}
/**
* Handles Whisper Message
* @param message - {object} - Message object received from pubsub-edge
* @param message.type - {string} - Type of message - Will always be 'MESSAGE' - Handled by _connect()
* @param message.data - {JSON} - JSON wrapper of topic/message fields
* @param message.data.topic - {string} - Topic that message pertains too - Will always be 'whispers.<CHANNEL_ID>' - Handled by _connect()
* @param message.data.message - {JSON} - Parsed into JSON in _connect() - Originally received as string from Twitch
* @emits whisper_sent, whisper_received - {event} -
* JSON object -
* id - {integer} - Message ID
* body - {string} - Body of message sent
* thread_id - {string} - Thread ID
* sender - {JSON} - Object containing message sender's Information
* sender.id - {integer} - User ID of sender
* sender.username - {string} - Username of sender
* sender.display_name - {string} - Display name of sender (Usually only differs in letter case)
* sender.color - {string} - Color hex-code of sender username in chat
* sender.badges - {Array} - Array of sender badges
* sender.emotes - {Array} - Array of emotes usable by sender
* recipient - {JSON} - Object containing message recipient's Information
* recipient.id - {integer} - User ID of recipient
* recipient.username - {string} - Username of recipient
* recipient.display_name - {string} - Display name of recipient(Usually only differs in letter case)
* recipient.color - {string} - Color hex-code of recipient username in chat
* recipient.badges - {Array} - Array of recipient badges
* sent_ts - {integer} - Timestamp of when message was sent
* nonce - {string} - Nonce associated with whisper message
*/
_onWhisper(message) {
if (typeof message.data.message.tags === 'string') message.data.message.tags = JSON.parse(message.data.message.tags);
if (typeof message.data.message.recipient === 'string') message.data.message.recipient = JSON.parse(message.data.message.recipient);
switch (message.data.message.type) {
case 'whisper_sent':
this.emit('whisper_sent', {
id: message.data.message.data_object.id,
body: message.data.message.data_object.body,
thread_id: message.data.message.data_object.thread_id,
sender: {
id: message.data.message.data.from_id,
username: message.data.message.data_object.tags.login,
display_name: message.data.message.data_object.tags.display_name,
color: message.data.message.data_object.tags.color,
badges: message.data.message.data_object.tags.badges,
emotes: message.data.message.data_object.tags.emotes
},
recipient: {
id: message.data.message.data_object.recipient.id,
username: message.data.message.data_object.recipient.username,
display_name: message.data.message.data_object.recipient.display_name,
color: message.data.message.data_object.recipient.color,
badges: message.data.message.data_object.recipient.badges
},
sent_ts: message.data.message.data_object.sent_ts,
nonce: message.data.message.data_object.nonce
});
break;
case 'whisper_received':
this.emit('whisper_received', {
id: message.data.message.data_object.id,
body: message.data.message.data_object.body,
thread_id: message.data.message.data_object.thread_id,
sender: {
id: message.data.message.data.from_id,
username: message.data.message.data_object.tags.login,
display_name: message.data.message.data_object.tags.display_name,
color: message.data.message.data_object.tags.color,
badges: message.data.message.data_object.tags.badges,
emotes: message.data.message.data_object.tags.emotes
},
recipient: {
id: message.data.message.data_object.recipient.id,
username: message.data.message.data_object.recipient.username,
display_name: message.data.message.data_object.recipient.display_name,
color: message.data.message.data_object.recipient.color,
badges: message.data.message.data_object.recipient.badges
},
sent_ts: message.data.message.data_object.sent_ts,
nonce: message.data.message.data_object.nonce
});
break;
case 'thread':
this.emit('thread', {
thread_id: message.data.message.data_object.thread_id
});
break;
}
}
/**
* Handles Video-Playback Message
* @param message - {object} - Message object received from pubsub-edge
* @param message.type - {string} - Type of message - Will always be 'MESSAGE' - Handled by _connect()
* @param message.data - {JSON} - JSON wrapper of topic/message fields
* @param message.data.topic - {string} - Topic that message pertains too - Will always be 'whispers.<CHANNEL_ID>' - Handled by _connect()
* @param message.data.message - {JSON} - Parsed into JSON in _connect() - Originally received as string from Twitch
* @param channel - {string} - Channel name from
* @emits stream-up, stream-down, viewcount
* stream-up -
* JSON object -
* time - {integer} - Server time. RFC 3339 format
* channel_name - {string} - Channel name
* play_delay - {string} - Delay of stream
* stream-down -
* JSON object -
* time - {integer} - Server time. RFC 3339 format
* channel_name - {string} - Channel name
* viewcount -
* JSON object -
* time - {integer} - Server time. RFC 3339 format
* channel_name - {string} - Channel name
* viewers - {integer} - Number of viewers currently watching
*/
_onVideoPlayback(message, channel) {
if (message.data.message.type === 'stream-up') {
this.emit('stream-up', {
time: message.data.message.server_time,
channel_name: channel,
play_delay: message.data.message.play_delay
});
} else if (message.data.message.type === 'stream-down') {
this.emit('stream-down', {
time: message.data.message.server_time,
channel_name: channel
});
} else if (message.data.message.type === 'viewcount') {
this.emit('viewcount', {
time: message.data.message.server_time,
channel_name: channel,
viewers: message.data.message.viewers
});
}
}
/**
* Handles Moderator Actions (Ban/Unban/Timeout/Clear)
* @param message - {object} - Message object received from pubsub-edge
* @param message.type - {string} - Type of message - Will always be 'MESSAGE' - Handled by _connect()
* @param message.data - {JSON} - JSON wrapper of topic/message fields
* @param message.data.topic - {string} - Topic that message pertains too - Will always be 'chat_moderator_actions.<USER_ID><ROOM_ID>' - Handled by _connect()
* @param message.data.message - {JSON} - Parsed into JSON in _connect() - Originally received as string from Twitch
* @emits ban, unban, timeout, clear
* ban -
* JSON object -
* target - {string} - The banee's username
* target_user_id - {string} - The banee's user ID
* created_by - {string} - The banear's username
* created_by_user_id - {string} - The banear's user ID
* reason - {string} - The reason provided by the banear - Null if no reason was given
* unban -
* JSON object -
* target - {string} - The banee's username
* target_user_id - {string} - The banee's user ID
* created_by - {string} - The banear's username
* created_by_user_id - {string} - The banear's user ID
* timeout -
* JSON object -
* target - {string} - The timeout-ee's username
* target_user_id - {string} - The timeout-ee's user ID
* created_by - {string} - The timeout-ear's username
* created_by_user_id - {string} - The timeout-ear's user ID
* duration - {string} - The timeout duration in seconds
* clear -
* JSON object -
* created_by - {string} - The username of who cleared the chat
* created_by_user_id - {string} - The user ID of who cleared the chat
*/
_onModeratorAction(message) {
switch (message.data.message.data.moderation_action) {
case 'ban':
this.emit('ban', {
target: message.data.message.data.args[0],
target_user_id: message.data.message.data.target_user_id,
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
reason: message.data.message.data.args[1] || null,
});
break;
case 'unban':
this.emit('unban', {
target: message.data.message.data.args[0],
target_user_id: message.data.message.data.target_user_id,
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'timeout':
this.emit('timeout', {
target: message.data.message.data.args[0],
target_user_id: message.data.message.data.target_user_id,
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
duration: message.data.message.data.args[1], // in seconds
reason: message.data.message.data.args[2] || null,
});
break;
case 'unhost':
this.emit('unhost', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'emoteonly':
this.emit('emoteonly', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'emoteonlyoff':
this.emit('emoteonlyoff', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'followers':
this.emit('followers', {
length: message.data.message.data.args[0], // in minutes
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'followersoff':
this.emit('followersoff', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'subscribers':
this.emit('subscribers', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'subscribersoff':
this.emit('subscribersoff', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'r9kbeta':
this.emit('r9kbeta', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'r9kbetaoff':
this.emit('r9kbetaoff', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'slow':
this.emit('slow', {
length: message.data.message.data.args[0], // in seconds
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'slowoff':
this.emit('slowoff', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'clear':
this.emit('clear', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
});
break;
case 'automod_rejected':
this.emit('automod_rejected', {
user: message.data.message.data.target_user_login,
user_id: message.data.message.data.target_user_id,
message_id: message.data.message.data.msg_id,
message: message.data.message.data.args[1],
reason: message.data.message.data.args[2],
});
break;
case 'approved_automod_message':
this.emit('approved_automod_message', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
message_id: message.data.message.data.msg_id,
target_user_login: message.data.message.target_user_login,
target_user_id: message.data.message.data.target_user_id,
});
break;
case 'denied_automod_message':
this.emit('denied_automod_message', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
message_id: message.data.message.data.msg_id,
target_user_login: message.data.message.data.target_user_login,
target_user_id: message.data.message.data.target_user_id,
});
break;
case 'add_permitted_term':
this.emit('add_permitted_term', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
approved_term: message.data.message.data.args[0],
});
break;
case 'delete_permitted_term':
this.emit('delete_permitted_term', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
deleted_term: message.data.message.data.args[0],
});
break;
case 'add_blocked_term':
this.emit('add_blocked_term', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
approved_term: message.data.message.data.args[0],
});
break;
case 'delete_blocked_term':
this.emit('delete_blocked_term', {
created_by: message.data.message.data.created_by,
created_by_user_id: message.data.message.data.created_by_user_id,
blocked_term: message.data.message.data.args[0],
});
break;
default:
// Do Nothing
}
}
/***** End Message Handler Functions *****/
/*****
**** Helper Functions
****/
/**
* Handles error
* @param {string} origin - Name of what callback function error originates from
* @param {string} error - Error message to emit
*/
_handleError(origin, error, topic = null) {
this.emit('error', {
origin,
error,
topic,
});
}
/**
* Debug
* @param {string} origin - Name of what callback function error originates from
* @param {string} mess - Status message to emit
*/
_sendDebug(origin, mess) {
if (this._debug) {
let d = new Date();
console.log('TwitchPS -- ' + d.toLocaleString() + ' -- in ' + origin + ' -- ', mess);
}
}
/**
* Wait for websocket
*/
_wait(callback) {
setTimeout(() => {
if (this._ws.readyState === 1) {
this._sendDebug('_wait()', 'Connected');
if (callback != null) {
callback();
}
return;
}
this._sendDebug('_wait()', 'Waiting for connection');
this._wait(callback);
}, 500);
}
/***** End Helper Functions *****/
/*****
**** External Functions
****/
/**
* Add new topics to listen too
* @param {Object} topics - JSON Object array of topic(s)
* @param {string} topics[].topic - Topic to listen too
* @param {string} [token=Default Token] topics[].token - Authentication token
* @param {Boolean} init - Boolean for if first topics to listen
*/
addTopic(topics, init = false) {
return new Promise((res, rej) => {
this._wait(() => {
for (let i = 0; i < topics.length; i += 1) {
let top = topics[i].topic;
let tok = topics[i].token;
let nonce = shortid.generate();
if (init) {
this._init_nonce = nonce;
init = false;
}
this._pending[nonce] = {
resolve: () => {
this._topics.push(top);
this._sendDebug('addTopic()', `Topic added successfully: ${top}`);
delete this._pending[nonce];
return res();
},
reject: (err) => {
this._handleError('addTopic()', err, top);
delete this._pending[nonce];
return rej(err);
}
};
this._ws.send(JSON.stringify({
type: 'LISTEN',
nonce,
data: {
topics: [top],
auth_token: tok
}
}));
setTimeout(() => {
if (this._pending[nonce]) {
this._pending[nonce].reject('timeout');
}
}, 10000);
}
});
});
}
/**
* Remove topic(s) from list of topics and unlisten
* @param {Object} topics - JSON object array of topics
* @param {string} topics.topic - Topic to unlisten
*
*/
removeTopic(topics) {
return new Promise((resolve, reject) => {
this._wait(() => {
for (let i = 0; i < topics.length; i += 1) {
let top = topics[i].topic;
let nonce = shortid.generate();
this._pending[nonce] = {
resolve: () => {
let removeTopic = () => {
delete this._topics[nonce];
};
topics.map(removeTopic);
delete this._pending[nonce];
return resolve();
},
reject: (err) => {
delete this._pending[nonce];
return reject(err);
}
};
this._ws.send(JSON.stringify({
type: 'UNLISTEN',
nonce,
data: {
topics: [top]
}
}));
}
});
});
}
/***** End External Functions *****/
}
module.exports = TwitchPS;