Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
tmi.js with BTTV emotes
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>BTTV Emotes Gist</title>
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://d2g2wobxbkulb1.cloudfront.net/0.0.18/tmi.min.js"></script>
<script src="js/main.js"></script>
</head>
<body>
<div id="chat"></div>
</body>
</html>
var tmi = null,
twitchEmotes = {
urlTemplate: 'http://static-cdn.jtvnw.net/emoticons/v1/{{id}}/{{image}}',
scales: { 1: '1.0', 2: '2.0', 3: '3.0' }
},
bttvEmotes = {
urlTemplate: 'https://cdn.betterttv.net/emote/{{id}}/{{image}}',
scales: { 1: '1x', 2: '2x', 3: '3x' },
bots: [], // Bots listed by BTTV for a channel { name: 'name', channel: 'channel' }
emoteCodeList: [], // Just the BTTV emote codes
emotes: [], // BTTV emotes
subEmotesCodeList: [], // I don't have a restriction set for Night-sub-only emotes, but the data's here.
allowEmotesAnyChannel: false // Allow all BTTV emotes that are loaded no matter the channel restriction
},
emoteScale = 3;
function htmlEntities(html) { // Custom HTML entity encoder using an array
function it(HTML) {
return HTML.map(function(n, i, arr) { // Iterate
if(n.length == 1) { // Avoid actual HTML
return n.replace(/[\u00A0-\u9999<>\&]/gim, function(i) { // Replace all special characters (Brute force!)
return '&#' + i.charCodeAt(0) + ';'; // Replace with HTML entities
});
}
return n;
});
}
var isArray = Array.isArray(html); // Make sure it's an array
if(!isArray) { // If not
html = html.split(''); // Make it an array
}
html = it(html); // Do it!
if(!isArray) html = html.join(''); // Join back if it wasn't an array
return html; // Return the stuff
}
function get(uri, data, headers, method, cb, json) { // Simplification of jQuery Ajax for my use
return $.ajax({
url: uri || '', data: data || {},
headers: headers || {}, type: method || 'GET',
dataType: json !== true ? json : 'jsonp', // Prefer jsonp
success: cb || function() { console.log('success', arguments); },
error: cb || function() { console.log('error', uri, arguments); }
});
}
// Find occurences of a string
function getIndicesOf(searchStr, str, caseSensitive) { // http://stackoverflow.com/a/3410557
var startIndex = 0, searchStrLen = searchStr.length;
var index, indices = [];
if(!caseSensitive) {
str = str.toLowerCase();
searchStr = searchStr.toLowerCase();
}
while((index = str.indexOf(searchStr, startIndex)) > -1) {
indices.push(index);
startIndex = index + searchStrLen;
}
return indices;
}
// Merge array of objects
function do_merge(roles) { // http://stackoverflow.com/a/21196265
var merger = function (a, b) {
if (_.isObject(a)) {
return _.extend({}, a, b, merger);
}
else {
return a || b;
}
};
var args = _.flatten([{}, roles, merger]);
return _.extend.apply(_, args);
}
function formatEmotes(text, emotes, channel) { // Format the emotes into the text
emotes = _.extend(emotes || {}, do_merge(bttvEmotes.emoteCodeList.map(function(n) { // Add BTTV emotes
var indices = getIndicesOf(n, text, true),
indMap = indices.map(function(m) {
return [m, m + n.length - 1].join('-'); // Create indices for formatEmotes
});
var obj = {};
obj[n] = indMap;
return indMap.length === 0 ? null : obj;
})));
var splitText = text.split(''); // Separate into characters
for(var i in emotes) { // Iterate through the emotes
var e = emotes[i]; // An emote
for(var j in e) { // Loop through this emote's instances
var mote = e[j]; // Indices of this emote instance
if(typeof mote == 'string') { // Make sure we're only getting the indices and not array methods, etc.
mote = mote.split('-'); // Split indices
mote = [parseInt(mote[0]), parseInt(mote[1])]; // Parse to integers
var length = mote[1] - mote[0], // Get emote length
emote = text.substr(mote[0], length + 1), // Get emote text
empty = Array.apply(null, new Array(length + 1)).map(function() { return ''; }); // Empty array to take up space of emote characters
var permToReplace = true, // If it's a BTTV that is allowed to be used, this will still be true ... otherwise true for Twitch emotes
options = { // Emote image options (Twitch emote by default)
template: twitchEmotes.urlTemplate, // Use this URL template
id: i, // Use this image ID
image: twitchEmotes.scales[emoteScale] // Image scale
};
if(bttvEmotes.emoteCodeList.indexOf(emote) > -1) { // Set BTTV emote image options
var bttvEmote = _.findWhere(bttvEmotes.emotes, { code: emote });
if(bttvEmote.restrictions.channels.length > 0 && bttvEmote.restrictions.channels.indexOf(channel.replace(/^#/,'')) == -1) { // Restricted to a channel, but not this one
permToReplace = false;
}
options.template = bttvEmotes.urlTemplate;
options.id = bttvEmote.id;
options.image = bttvEmotes.scales[emoteScale];
}
if(permToReplace || bttvEmotes.allowEmotesAnyChannel) {
var html = '<img class="emoticon" emote="' + emote + '" src="' + options.template
.replace('{{id}}', options.id)
.replace('{{image}}', options.image) + '">';
splitText = splitText.slice(0, mote[0]).concat(empty).concat(splitText.slice(mote[1] + 1, splitText.length)); // Replace emote indices with empty space
splitText.splice(mote[0], 1, html); // Insert emote HTML
}
}
}
}
return htmlEntities(splitText).join(''); // Encode non-images
}
function handleChat(channel, user, message, self) { // Handle le chat
var text = formatEmotes(message, user.emotes, channel); // Format the emotes into the message
$('#chat').append('<div>' + (user['display-name'] || user.username) + ': ' + text + '</div>'); // Display the message
}
function testMessage(channel, user, message, self) { // Throw away when done
handleChat(channel || tmi.opts.channels[0], user || { 'display-name': 'Alca', emotes: null }, message || '(chompy) bttvNice domeHey domeLit splinCreep', self || false);
}
$(document).ready(function(e) {
var channels = ['alca','splinxes']; // Join these channels
console.log('%cThere\'s a function called \'testMessage\' that you can use to manually input messages', 'color:orange;');
tmi = new irc.client({ // A tmi.js client
options: { debug: true },
channels: channels
});
tmi.on('connected', function() { // On connect
testMessage(null, null, 'Open the console!');
testMessage();
testMessage('splinxes', null, '(chompy) bttvNice domeHey domeLit splinCreep');
bttvEmotes.allowEmotesAnyChannel = true;
testMessage('splinxes', null, '(chompy) bttvNice domeHey domeLit splinCreep');
});
tmi.on('message', handleChat); // Received a message
function mergeBTTVEmotes(data, channel) {
console.log('Got BTTV emotes for ' + channel);
bttvEmotes.emotes = bttvEmotes.emotes.concat(data.emotes.map(function(n) {
if(!_.has(n, 'restrictions')) {
n.restrictions = {
channels: [],
games: []
};
}
if(n.restrictions.channels.indexOf(channel) == -1) {
n.restrictions.channels.push(channel);
}
return n;
}));
bttvEmotes.bots = bttvEmotes.bots.concat(data.bots.map(function(n) {
return {
name: n,
channel: channel
};
}));
}
var asyncCalls = [get('https://api.betterttv.net/2/emotes', {}, { Accept: 'application/json' }, 'GET', function(data) {
console.log('Got BTTV global emotes');
bttvEmotes.emotes = bttvEmotes.emotes.concat(data.emotes.map(function(n) {
n.global = true;
return n;
}));
bttvEmotes.subEmotesCodeList = _.chain(bttvEmotes.emotes).where({ global: true }).reject(function(n) { return _.isNull(n.channel); }).pluck('code').value();
}, false)];
function addAsyncCall(channel) {
asyncCalls.push(get('https://api.betterttv.net/2/channels/' + channel, {}, { Accept: 'application/json' }, 'GET', function(data) {
mergeBTTVEmotes(data, channel);
}), false);
}
for(var i in channels) { // Add BTTV emotes for the channels we're connecting to.
addAsyncCall(channels[i]);
}
$.when.apply({}, asyncCalls).always(function() {
bttvEmotes.emoteCodeList = _.pluck(bttvEmotes.emotes, 'code');
tmi.connect();
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment