Last active
March 20, 2023 10:01
-
-
Save AlcaDesign/6213ff17d3981c861adf to your computer and use it in GitHub Desktop.
tmi.js with BTTV emotes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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