mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
1007 lines
No EOL
25 KiB
JavaScript
1007 lines
No EOL
25 KiB
JavaScript
(function wrapper(window, injectNeeded) {
|
|
'use strict';
|
|
|
|
// Script injection as necessary.
|
|
if ( injectNeeded ) {
|
|
var script = document.createElement('script');
|
|
script.textContent = '(' + wrapper + ')(window, false)';
|
|
document.body.appendChild(script);
|
|
document.body.removeChild(script);
|
|
return;
|
|
}
|
|
|
|
// -----------------
|
|
// Global Variables
|
|
// -----------------
|
|
|
|
var CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)[^}]*\}/mg,
|
|
IMGUR_KEY = 'e48d122e3437051', CACHE_LENGTH = 10800000,
|
|
SERVER = '//commondatastorage.googleapis.com/frankerfacez/',
|
|
DEBUG = location.search.indexOf('frankerfacez') !== -1;
|
|
|
|
|
|
// -----------------
|
|
// The Constructor
|
|
// -----------------
|
|
|
|
var ffz = function() {
|
|
this.alive = true;
|
|
this.donors = {};
|
|
this.getting = {};
|
|
|
|
// Master Emoticon Storage
|
|
this.emoticons = [];
|
|
this.emotesets = {};
|
|
|
|
// Channel Storage
|
|
this.channels = {};
|
|
|
|
// Global Sets Storage
|
|
this.collections = {};
|
|
this.globals = {};
|
|
this.global_sets = [];
|
|
this.styles = {};
|
|
|
|
// Pending Styles -- For Super Early Initialization
|
|
this.pending_styles = [];
|
|
|
|
// Keep track of all logging too.
|
|
this._log = [];
|
|
this._log2 = [];
|
|
|
|
// Now, let's do this!
|
|
this.init(10);
|
|
};
|
|
|
|
ffz.prototype.last_set = 0;
|
|
ffz.prototype.last_emote = 0;
|
|
ffz.prototype.manger = null;
|
|
ffz.prototype.has_bttv = false;
|
|
|
|
ffz.commands = {};
|
|
|
|
|
|
// -----------------
|
|
// Logging
|
|
// -----------------
|
|
|
|
ffz.prototype.log = function(msg) {
|
|
this._log.push(msg);
|
|
msg = "FFZ" + (this.alive ? ": " : " (Dead): ") + msg;
|
|
console.log(msg);
|
|
|
|
// Don't echo to chat if we're not debugging.
|
|
if ( !DEBUG ) return;
|
|
|
|
var chan;
|
|
for(var name in this.channels) {
|
|
if ( this.channels[name] && this.channels[name].room ) {
|
|
chan = this.channels[name];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( chan )
|
|
chan.room.addTmiMessage(msg);
|
|
else
|
|
this._log2.push(msg);
|
|
};
|
|
|
|
|
|
// -----------------
|
|
// Initialization
|
|
// -----------------
|
|
|
|
ffz.prototype.init = function(increment, delay) {
|
|
// This function exists to ensure FFZ doesn't run until it can properly
|
|
// hook into the Twitch Ember application.
|
|
if ( !this.alive ) return;
|
|
|
|
var loaded = window.Ember != undefined &&
|
|
window.App != undefined &&
|
|
App.EmoticonsController != undefined &&
|
|
App.Room != undefined;
|
|
|
|
if ( !loaded ) {
|
|
// Only try loading for 60 seconds.
|
|
if ( delay >= 60000 )
|
|
this.log("Twitch API not detected in \"" + location.toString() + "\". Aborting.");
|
|
else
|
|
setTimeout(this.init.bind(this, increment, (delay||0) + increment),
|
|
increment);
|
|
return;
|
|
}
|
|
|
|
this.setup();
|
|
};
|
|
|
|
ffz.prototype.setup = function() {
|
|
if ( !this.alive ) return;
|
|
|
|
// Hook into the Ember application.
|
|
this.log("Hooking Ember application.");
|
|
this.modify_room();
|
|
this.modify_viewers();
|
|
this.modify_emotes();
|
|
this.modify_lines();
|
|
|
|
this.log("Loading data.");
|
|
this.load_donors();
|
|
this.load_emotes('global');
|
|
|
|
if ( ! document.body ) {
|
|
// We need to listen for the DOM to load in case any style elements
|
|
// get created before we can add them.
|
|
this.listen_dom = this.listen_dom.bind(this);
|
|
document.addEventListener("DOMContentLoaded", this.listen_dom, false);
|
|
}
|
|
|
|
// Detect BetterTTV
|
|
this.find_bttv(10);
|
|
|
|
this.log("Initialization complete.");
|
|
};
|
|
|
|
ffz.prototype.destroy = function() {
|
|
if ( !this.alive ) return;
|
|
|
|
// TODO: Teardown stuff.
|
|
|
|
// Mark us as dead and remove our reference.
|
|
alive = false;
|
|
if ( window.ffz === this )
|
|
window.ffz = undefined;
|
|
|
|
// And, before the door hits us... delete the log.
|
|
delete this._log;
|
|
delete this._log2;
|
|
}
|
|
|
|
|
|
// -----------------
|
|
// DOM Listening
|
|
// -----------------
|
|
|
|
ffz.prototype.listen_dom = function() {
|
|
document.removeEventListener("DOMContentLoaded", this.listen_dom, false);
|
|
|
|
// Check for waiting styles.
|
|
while ( this.pending_styles.length )
|
|
document.body.appendChild(this.pending_styles.pop());
|
|
}
|
|
|
|
|
|
// -----------------
|
|
// Commands
|
|
// -----------------
|
|
|
|
ffz.prototype._msg = function(room, out) {
|
|
if ( this.has_bttv )
|
|
return BetterTTV.chat.helpers.serverMessage(out.replace(/\n/g, "<br>"));
|
|
|
|
out = out.split("\n");
|
|
for(var i=0; i < out.length; i++)
|
|
room.addMessage({style: 'ffz admin', from: 'FFZ', message: out[i]});
|
|
}
|
|
|
|
ffz.prototype.run_command = function(room, m) {
|
|
var args = (m.substr(5) || "list").split(' '),
|
|
cmd = args.shift().toLowerCase();
|
|
|
|
this.log("Got FFZ Command: " + cmd + " " + JSON.stringify(args));
|
|
|
|
var c = ffz.commands[cmd], out;
|
|
if ( c )
|
|
out = c.bind(this)(room, args);
|
|
else
|
|
out = "No such sub-command.";
|
|
|
|
if ( out ) this._msg(room, out);
|
|
}
|
|
|
|
ffz.commands['help'] = function(room, args) {
|
|
if ( args && args.length > 0 ) {
|
|
var c = ffz.commands[args[0].toLowerCase()];
|
|
if ( !c )
|
|
return "No such sub-command: " + args[0];
|
|
else if ( c && c.help == undefined )
|
|
return "No help available for: " + args[0];
|
|
else
|
|
return c.help;
|
|
}
|
|
|
|
var l = [];
|
|
for (var c in ffz.commands)
|
|
ffz.commands.hasOwnProperty(c) ? l.push(c) : false;
|
|
|
|
return "Available sub-commands are: " + l.join(", ");
|
|
}
|
|
ffz.commands['help'].help = "Usage: /ffz help [command]\nList available commands, or show help for a specific command.";
|
|
|
|
ffz.commands['log'] = function(room, args) {
|
|
var out = "FrankerFaceZ Session Log\n\n" + this._log.join("\n");
|
|
|
|
out += "\n\n--------------------------------------------------------------------------------\n" +
|
|
"Internal State\n\n";
|
|
|
|
out += "Channels:\n";
|
|
for(var id in this.channels) {
|
|
if ( !this.channels.hasOwnProperty(id) )
|
|
continue;
|
|
|
|
var chan = this.channels[id];
|
|
if ( !chan ) {
|
|
out += " " + id + " (Unloaded)\n";
|
|
continue;
|
|
}
|
|
|
|
out += " " + id + ":\n";
|
|
out += " set_id: " + chan.set_id + "\n";
|
|
|
|
if ( ! chan.set ) {
|
|
out += " set (Unloaded)\n";
|
|
} else {
|
|
out += " set:\n";
|
|
for(var i=0; i < chan.set.length; i++) {
|
|
var e = chan.set[i];
|
|
out += " isEmoticon: " + e.isEmoticon + ", cls: " + JSON.stringify(e.cls) + ", regex: " + e.regex.toString() + "\n";
|
|
}
|
|
out += "\n";
|
|
}
|
|
|
|
if ( ! chan.style) {
|
|
out += " style (Unloaded)";
|
|
} else {
|
|
var s = chan.style.innerHTML.split("\n");
|
|
out += " style:\n";
|
|
for (var i=0; i < s.length; i++)
|
|
out += " " + s[i] + "\n";
|
|
out += "\n";
|
|
}
|
|
}
|
|
|
|
out += "\nGlobal Sets:\n";
|
|
for(var id in this.globals) {
|
|
if ( !this.globals.hasOwnProperty(id) )
|
|
continue;
|
|
|
|
var set_id = this.globals[id];
|
|
if ( !set_id ) {
|
|
out += " " + id + " (Unloaded)\n";
|
|
continue;
|
|
}
|
|
|
|
out += " " + id + ":\n";
|
|
out += " set_id: " + set_id + "\n";
|
|
|
|
var set = this.emotesets[set_id];
|
|
if ( !set ) {
|
|
out += " set (Unloaded)\n";
|
|
} else {
|
|
out += " set:\n";
|
|
for(var i=0; i < set.length; i++) {
|
|
var e = set[i];
|
|
out += " isEmoticon: " + e.isEmoticon + ", cls: " + JSON.stringify(e.cls) + ", regex: " + e.regex.toString() + "\n";
|
|
}
|
|
out += "\n";
|
|
}
|
|
|
|
var style = this.styles[id];
|
|
if ( !style ) {
|
|
out += " style (Unloaded)\n";
|
|
} else {
|
|
var s = style.innerHTML.split("\n");
|
|
out += " style:\n";
|
|
for (var i=0; i < s.length; i++)
|
|
out += " " + s[i] + "\n";
|
|
out += "\n";
|
|
}
|
|
}
|
|
|
|
out += "\nEmotes:\n";
|
|
for(var i=0; i < this.emoticons.length; i++) {
|
|
var e = this.emoticons[i];
|
|
out += " " + e.text + " (" + e.image.id + ")\n";
|
|
out += " ffzset: " + e.ffzset + " (" + e.image.emoticon_set + ")\n";
|
|
out += " channel: " + e.channel + "\n";
|
|
out += " regex: " + e.regex.toString() + "\n";
|
|
out += " height: " + e.image.height + ", width: " + e.image.width + "\n";
|
|
out += " url: " + e.image.url + "\n"
|
|
out += " html: " + e.image.html + "\n\n";
|
|
}
|
|
|
|
window.open("data:text/plain," + encodeURIComponent(out), "_blank");
|
|
}
|
|
ffz.commands['log'].help = "Usage: /ffz log\nOpen a window with FFZ's debugging output.";
|
|
|
|
ffz.commands['list'] = function(room, args) {
|
|
var output = '', filter, html = this.has_bttv;
|
|
|
|
if ( args && args.length > 0 )
|
|
filter = args.join(" ").toLowerCase();
|
|
|
|
if ( html ) output += "<table style=\"width:100%\">";
|
|
|
|
for(var name in this.collections) {
|
|
if ( ! this.collections.hasOwnProperty(name) )
|
|
return;
|
|
|
|
var include;
|
|
if ( filter )
|
|
include = name.toLowerCase().indexOf(filter) !== -1;
|
|
else
|
|
include = name !== "FFZ Global Emotes";
|
|
|
|
if ( !include )
|
|
continue;
|
|
|
|
if ( html )
|
|
output += "<thead><th colspan=\"2\">" + name + "</th></thead><tbody>";
|
|
else
|
|
output += name + "\n";
|
|
|
|
var em = this.collections[name];
|
|
for(var e in em) {
|
|
if ( em.hasOwnProperty(e) ) {
|
|
var emote = em[e], t = emote.text;
|
|
if ( html )
|
|
output += "<tr style=\"line-height:" + emote.image.height + "px\"><td>" + t + "</td><td>" + emote.image.html + "</td></tr>";
|
|
else {
|
|
t = t[0] + "\u200B" + t.substr(1);
|
|
output += " " + t + " = " + emote.text + "\n";
|
|
}
|
|
}
|
|
}
|
|
if ( html ) output += "</tbody>";
|
|
}
|
|
|
|
if ( html ) output += "</table>";
|
|
|
|
// Make sure we actually have output.
|
|
if ( output.indexOf(html ? '<td>' : '\u200B') === -1 )
|
|
return "There are no available FFZ channel emoticons. If this is in error, please try the /ffz reload command.";
|
|
else
|
|
return "The following emotes are available:\n" + output;
|
|
}
|
|
ffz.commands['list'].help = "Usage: /ffz list [global]\nList available FFZ emoticons. Use the global parameter to list ALL FFZ emoticons, or filter for a specific set.";
|
|
|
|
ffz.commands['global'] = function(room, args) {
|
|
return ffz.commands['list'].bind(this)(room, ['global']); }
|
|
ffz.commands['global'].help = "Usage: /ffz global\nShorthand for /ffz list global. List ALL FFZ emoticons, including FFZ global emoticons.";
|
|
|
|
ffz.commands['reload'] = function(room, args) {
|
|
for(var id in this.channels)
|
|
if ( this.channels.hasOwnProperty(id) && this.channels[id] )
|
|
this.load_emotes(id, true);
|
|
|
|
this.load_emotes('global');
|
|
this.load_donors();
|
|
|
|
return "Attempting to reload FFZ data from the server.";
|
|
}
|
|
ffz.commands['reload'].help = "Usage: /ffz reload\nAttempt to reload FFZ emoticons and donors.";
|
|
|
|
ffz.commands['inject'] = function(room, args) {
|
|
if ( !args || args.length !== 1 )
|
|
return "/ffz inject requires exactly 1 argument.";
|
|
|
|
var album = args[0].split('/').pop().split('?').shift().split('#').shift();
|
|
this._msg(room, "Attempting to load test emoticons from imgur album \"" + album + "\"...");
|
|
|
|
// Make sure there's no cache hits.
|
|
var res = "https://api.imgur.com/3/album/" + album;
|
|
if ( window.localStorage )
|
|
localStorage.removeItem("ffz_" + res);
|
|
|
|
this.get(res, this.do_imgur.bind(this, room, album), 1,
|
|
{'Accept': 'application/json', 'Authorization': 'Client-ID ' + IMGUR_KEY},
|
|
5);
|
|
}
|
|
ffz.commands['inject'].help = "Usage: /ffz inject [album-id]\nLoads emoticons from an imgur album for testing. album-id can simply be the album URL. Ex: /ffz inject http://imgur.com/a/v4aZr";
|
|
|
|
ffz.prototype.do_imgur = function(room, album, data) {
|
|
if ( data === undefined )
|
|
return this._msg(room, "An error occurred communicating with Imgur.");
|
|
else if ( !data )
|
|
return this._msg(room, "The named album does not exist or is private.");
|
|
|
|
// Get our data structure.
|
|
data = JSON.parse(data).data;
|
|
|
|
var images = data.images, css = "";
|
|
for (var i=0; i < images.length; i++) {
|
|
var im = images[i],
|
|
name = im.title ? im.title : album + (i+1),
|
|
marg = im.height > 18 ? (im.height - 18) / -2 : 0,
|
|
desc = im.description ? im.description.trim().split(/(?:\W*\n\W*)+/) : undefined,
|
|
extra_css = '';
|
|
|
|
if ( desc ) {
|
|
for (var q=0; q < desc.length; q++) {
|
|
if ( desc[q].substr(0, 5).toLowerCase() === "css: " ) {
|
|
extra_css = desc[q].substr(5);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
css += ".imgur-" + album + "-" + (i+1) + ' {content: "' + name +
|
|
'"; background-image: url("' + im.link + '"); height: ' + im.height +
|
|
'px; width: ' + im.width + 'px; margin: ' + marg + 'px 0px; ' + extra_css + '}\n';
|
|
}
|
|
|
|
var count = this.process_css('imgur-' + album, 'FFZ Global Emotes - Imgur Album: ' + album, css);
|
|
this._msg(room, "Loaded " + count + " emoticons from Imgur.");
|
|
this._msg(room, ffz.commands['list'].bind(this)(room, [album]));
|
|
}
|
|
|
|
|
|
// -----------------
|
|
// BetterTTV Hooks
|
|
// -----------------
|
|
|
|
ffz.prototype.find_bttv = function(increment, delay) {
|
|
if ( !this.alive ) return;
|
|
|
|
if ( window.BTTVLOADED )
|
|
return this.setup_bttv();
|
|
|
|
else if ( delay === undefined )
|
|
this.log("BetterTTV not yet loaded. Waiting...");
|
|
|
|
if ( delay >= 60000 )
|
|
this.log("BetterTTV not detected in \"" + location.toString() + "\". Giving up.");
|
|
else
|
|
setTimeout(this.find_bttv.bind(this, increment, (delay||0) + increment),
|
|
increment);
|
|
}
|
|
|
|
var donor_badge = {type: 'ffz-donor', name: '', description: 'FFZ Donor'};
|
|
|
|
ffz.prototype.setup_bttv = function() {
|
|
this.log("BetterTTV was detected. Installing hook.");
|
|
this.has_bttv = true;
|
|
|
|
// Add badge handling to BetterTTV chat.
|
|
var privmsg = BetterTTV.chat.templates.privmsg, f = this;
|
|
BetterTTV.chat.templates.privmsg = function(highlight, action, server, isMod, data) {
|
|
if ( f.check_donor(data.sender) ) {
|
|
var inserted = false;
|
|
for(var i=0; i < data.badges.length; i++) {
|
|
var t = data.badges[i].type;
|
|
if ( t != 'turbo' && t != 'subscriber' )
|
|
continue;
|
|
data.badges.insertAt(i, donor_badge);
|
|
inserted = true;
|
|
break;
|
|
}
|
|
if ( ! inserted )
|
|
data.badges.push(donor_badge);
|
|
}
|
|
|
|
return privmsg(highlight, action, server, isMod, data);
|
|
}
|
|
}
|
|
|
|
|
|
// -----------------
|
|
// Ember Hooks
|
|
// -----------------
|
|
|
|
ffz.prototype.add_badge = function(sender, badges) {
|
|
// Is the sender a donor?
|
|
if ( ! this.check_donor(sender) )
|
|
return;
|
|
|
|
// Create the FFZ Donor badge.
|
|
var c = document.createElement('span');
|
|
c.className = 'badge-container tooltip';
|
|
c.setAttribute('title', 'FFZ Donor');
|
|
|
|
var b = document.createElement('div');
|
|
b.className = 'badge ffz-donor';
|
|
c.appendChild(b);
|
|
c.appendChild(document.createTextNode(' '));
|
|
|
|
// Figure out where to place the badge.
|
|
var before = badges.find('.badge-container').filter(function(i) {
|
|
var t = this.title.toLowerCase();
|
|
return t == "subscriber" || t == "turbo";
|
|
}).first();
|
|
|
|
if ( before.length )
|
|
before.before(c);
|
|
else
|
|
badges.append(c);
|
|
}
|
|
|
|
ffz.prototype.modify_lines = function() {
|
|
var f = this;
|
|
App.LineView.reopen({
|
|
didInsertElement: function() {
|
|
this._super();
|
|
f.add_badge(this.get('context.model.from'), this.$('.badges'));
|
|
}
|
|
});
|
|
}
|
|
|
|
ffz.prototype._modify_room = function(room) {
|
|
var f = this;
|
|
room.reopen({
|
|
init: function() {
|
|
this._super();
|
|
if ( f.alive )
|
|
f.add_channel(this.id, this);
|
|
},
|
|
|
|
willDestroy: function() {
|
|
this._super();
|
|
if ( f.alive )
|
|
f.remove_channel(this.id);
|
|
},
|
|
|
|
send: function(e) {
|
|
if ( f.alive && (e.substr(0,5) == '/ffz ' || e == '/ffz') ) {
|
|
// Clear the input box.
|
|
this.set("messageToSend", "");
|
|
f.run_command(this, e);
|
|
} else
|
|
return this._super(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
ffz.prototype.modify_room = function() {
|
|
this._modify_room(App.Room);
|
|
|
|
var inst = App.Room.instances;
|
|
for(var n in inst) {
|
|
if ( ! inst.hasOwnProperty(n) ) continue;
|
|
var i = inst[n];
|
|
if ( this.alive )
|
|
this.add_channel(i.id, i);
|
|
|
|
if ( i.tmiRoom && this.alive )
|
|
this.alter_tmi(i.id, i.tmiRoom);
|
|
else if ( i.viewers )
|
|
this._modify_viewers(i.viewers);
|
|
|
|
this._modify_room(i);
|
|
}
|
|
};
|
|
|
|
|
|
ffz.prototype._modify_viewers = function(vwrs) {
|
|
var f = this;
|
|
vwrs.reopen({
|
|
tmiRoom: Ember.computed(function(key, val) {
|
|
if ( arguments.length > 1 ) {
|
|
this.tmiRoom = val;
|
|
if ( f.alive )
|
|
f.alter_tmi(this.id, val);
|
|
}
|
|
return undefined;
|
|
})
|
|
});
|
|
}
|
|
|
|
ffz.prototype.modify_viewers = function() {
|
|
this._modify_viewers(App.Room.Viewers);
|
|
};
|
|
|
|
|
|
ffz.prototype._modify_emotes = function(ec) {
|
|
var f = this;
|
|
ec.reopen({
|
|
_emoticons: ec.emoticons || [],
|
|
|
|
init: function() {
|
|
this._super();
|
|
if ( f.alive )
|
|
f.get_manager(this);
|
|
},
|
|
|
|
emoticons: Ember.computed(function(key, val) {
|
|
if ( arguments.length > 1 ) {
|
|
this._emoticons = val;
|
|
f.log("Twitch standard emoticons loaded.");
|
|
}
|
|
return f.alive ? _.union(this._emoticons, f.emoticons) : this._emoticons;
|
|
})
|
|
});
|
|
}
|
|
|
|
ffz.prototype.modify_emotes = function() {
|
|
this._modify_emotes(App.EmoticonsController);
|
|
|
|
var ec = App.__container__.lookup("controller:emoticons");
|
|
if ( ! ec ) return;
|
|
|
|
this._modify_emotes(ec);
|
|
this.get_manager(ec);
|
|
};
|
|
|
|
ffz.prototype.get_manager = function(manager) {
|
|
this.manager = manager;
|
|
for(var key in this.emotesets) {
|
|
if ( this.emotesets.hasOwnProperty(key) )
|
|
manager.emoticonSets[key] = this.emotesets[key];
|
|
}
|
|
}
|
|
|
|
|
|
// -----------------
|
|
// Channel Management
|
|
// -----------------
|
|
|
|
ffz.prototype.add_channel = function(id, room) {
|
|
if ( !this.alive ) return;
|
|
this.log("Registered channel: " + id);
|
|
var chan = this.channels[id] = {id: id, room: room, tmi: null, style: null};
|
|
|
|
// Do we have log messages?
|
|
if ( this._log2.length > 0 ) {
|
|
var func = this.has_bttv ? BetterTTV.chat.helpers.serverMessage : room.addTmiMessage;
|
|
while ( this._log2.length )
|
|
func(this._log2.shift());
|
|
}
|
|
|
|
// Load the emotes for this channel.
|
|
this.load_emotes(id);
|
|
}
|
|
|
|
ffz.prototype.remove_channel = function(id) {
|
|
var chan = this.channels[id];
|
|
if ( !chan ) return;
|
|
|
|
this.log("Removing channel: " + id);
|
|
|
|
// Unload the associated emotes.
|
|
this.unload_emotes(id);
|
|
|
|
// If we have a tmiRoom for this channel, restore its getEmotes function.
|
|
if ( chan.tmi )
|
|
delete chan.tmi.getEmotes;
|
|
|
|
// Delete this channel.
|
|
this.channels[id] = false;
|
|
}
|
|
|
|
ffz.prototype.alter_tmi = function(id, tmi) {
|
|
var chan = this.channels[id], f = this;
|
|
if ( !chan || !this.alive ) return;
|
|
|
|
// Store the TMI instance.
|
|
if ( chan.tmi) return;
|
|
chan.tmi = tmi;
|
|
|
|
var tp = tmi.__proto__.getEmotes.bind(tmi);
|
|
tmi.getEmotes = function(name) {
|
|
return _.union([chan.set_id], f.global_sets, tp(name)||[]);
|
|
}
|
|
}
|
|
|
|
|
|
// -----------------
|
|
// Emote Handling
|
|
// -----------------
|
|
|
|
ffz.prototype.load_emotes = function(group, refresh) {
|
|
// TEMPORARY GROUP CHAT
|
|
var m = /^_(.+)_\d+$/.exec(group), name = group;
|
|
if ( m != null )
|
|
name = m[1];
|
|
|
|
this.get(SERVER + name + ".css",
|
|
this.process_css.bind(this, group, undefined), refresh ? 1 : CACHE_LENGTH);
|
|
}
|
|
|
|
ffz.prototype.process_css = function(group, channel, data) {
|
|
if ( !this.alive ) return 0;
|
|
if ( data === undefined ) return 0;
|
|
|
|
// Before we go anywhere, let's start clean.
|
|
this.unload_emotes(group);
|
|
|
|
// If data is null, we've got no emotes.
|
|
if ( data == null )
|
|
return 0;
|
|
|
|
// Let's look up this group to see where it goes! Is it a channel?
|
|
var chan = this.channels[group];
|
|
if ( chan === false )
|
|
// It's for an unloaded channel. Stop here.
|
|
return;
|
|
|
|
// Get our new stuff.
|
|
var set_id = --this.last_set, set = [], channel,
|
|
style = document.createElement('style');
|
|
|
|
// Let's store our things right now.
|
|
if ( chan ) {
|
|
chan.set_id = set_id;
|
|
chan.set = set;
|
|
chan.style = style;
|
|
channel = "FFZ Channel Emotes: " + group;
|
|
|
|
} else {
|
|
this.globals[group] = set_id;
|
|
this.global_sets.push(set_id);
|
|
this.styles[group] = style;
|
|
|
|
if ( !channel )
|
|
channel = "FFZ Global Emotes" + (group != "global" ? ": " + group : "");
|
|
}
|
|
|
|
// Register this set with the manager.
|
|
this.emotesets[set_id] = set;
|
|
if ( this.manager )
|
|
this.manager.emoticonSets[set_id] = set;
|
|
|
|
// Update the style.
|
|
style.type = 'text/css';
|
|
style.innerHTML = data;
|
|
if ( document.body )
|
|
document.body.appendChild(style);
|
|
else
|
|
this.pending_styles.push(style);
|
|
|
|
// Parse out the usable emoticons.
|
|
var count = 0, f = this;
|
|
|
|
// Store our emotes in an extra place.
|
|
var col = this.collections[channel] = [];
|
|
|
|
data.replace(CSS, function(match, klass, name, path, height, width) {
|
|
height = parseInt(height); width = parseInt(width);
|
|
var image_data = {
|
|
emoticon_set: set_id, height: height, width: width, url: path,
|
|
html: '<span class="' + klass + ' emoticon" title="' + name + '"></span>',
|
|
id: --f.last_emote}, regex;
|
|
|
|
if ( name[name.length-1] === '!' )
|
|
regex = new RegExp('\\b' + name + '(?=\\W|$)', 'g');
|
|
else
|
|
regex = new RegExp('\\b' + name + '\\b', 'g');
|
|
|
|
var emote = {
|
|
image: image_data, images: [image_data], text: name,
|
|
channel: channel, hidden: false, regex: regex, ffzset: group};
|
|
|
|
col.push(emote);
|
|
f.emoticons.push(emote);
|
|
set.push({isEmoticon: !0, cls: klass, regex: regex});
|
|
count++;
|
|
});
|
|
|
|
this.log("Loaded " + count + " emotes from collection: " + group);
|
|
|
|
// Notify the manager that we've added emotes.
|
|
// Don't notify the manager for now because of BTTV.
|
|
//if ( this.manager && ! this.has_bttv )
|
|
// this.manager.notifyPropertyChange('emoticons');
|
|
|
|
return count;
|
|
}
|
|
|
|
|
|
ffz.prototype.unload_emotes = function(group) {
|
|
if ( !this.alive ) return;
|
|
|
|
// Is it a channel?
|
|
var chan = this.channels[group], set, set_id, style, channel;
|
|
if ( chan === false )
|
|
return;
|
|
else if ( chan ) {
|
|
// It's a channel.
|
|
set = chan.set;
|
|
set_id = chan.set_id;
|
|
style = chan.style;
|
|
channel = "FFZ Channel Emotes: " + group;
|
|
|
|
// Clear it out.
|
|
delete chan.set;
|
|
delete chan.set_id;
|
|
delete chan.style;
|
|
|
|
} else {
|
|
// It must be global.
|
|
set_id = this.globals[group];
|
|
set = this.emotesets[set_id];
|
|
style = this.styles[group];
|
|
channel = "FFZ Global Emotes" + (group != "global" ? ": " + group : "");
|
|
|
|
// Clear out the basics.
|
|
delete this.globals[group];
|
|
delete this.styles[group];
|
|
|
|
var ind = this.global_sets.indexOf(set_id);
|
|
if ( ind !== -1 )
|
|
this.global_sets.splice(ind, 1);
|
|
}
|
|
|
|
// Do we have a collection?
|
|
if ( this.collections[channel] )
|
|
delete this.collections[channel];
|
|
|
|
// Do we have a style?
|
|
if ( style )
|
|
// Remove it from its parent.
|
|
try { style.parentNode.removeChild(style); } catch(err) {}
|
|
|
|
// Remove the emoteset from circulation.
|
|
delete this.emotesets[set_id];
|
|
if ( this.manager )
|
|
delete this.manager.emoticonSets[set_id];
|
|
|
|
// Remove every emote from this group.
|
|
var filt = function(e) { return e.ffzgroup !== group; }
|
|
this.emoticons = this.emoticons.filter(filt);
|
|
|
|
// Update the emoticons with the manager.
|
|
// Don't notify the manager for now for BTTV.
|
|
//if ( this.manager && ! this.has_bttv )
|
|
// this.manager.notifyPropertyChange('emoticons');
|
|
}
|
|
|
|
|
|
// -----------------
|
|
// Donor Processing
|
|
// -----------------
|
|
|
|
ffz.prototype.check_donor = function(username) { return this.donors[username] || false; }
|
|
|
|
ffz.prototype.load_donors = function(refresh) {
|
|
this.get(SERVER + "donors.txt",
|
|
this.process_donors.bind(this), refresh ? 1 : CACHE_LENGTH);
|
|
}
|
|
|
|
ffz.prototype.process_donors = function(text) {
|
|
if ( !this.alive ) return;
|
|
this.donors = {};
|
|
var count = 0;
|
|
|
|
if ( text != null ) {
|
|
var l = text.trim().split(/\W+/);
|
|
for (var i=0; i < l.length; i++)
|
|
this.donors[l[i]] = true;
|
|
count += l.length;
|
|
}
|
|
|
|
this.log("Loaded " + count + " donors.");
|
|
}
|
|
|
|
|
|
// -----------------
|
|
// Networking
|
|
// -----------------
|
|
|
|
ffz.prototype.get = function(resource, callback, expires, headers, max_attempts) {
|
|
if ( !this.alive ) return;
|
|
if ( this.getting[resource] ) {
|
|
this.log("Already getting resource: " + resource);
|
|
return;
|
|
}
|
|
this.getting[resource] = true;
|
|
|
|
max_attempts = max_attempts || 10;
|
|
var age = 0, now = new Date().getTime();
|
|
|
|
// First, immediately try using the resource from cache.
|
|
if ( window.localStorage ) {
|
|
var res = localStorage.getItem("ffz_" + resource);
|
|
if ( res != null ) {
|
|
this.log("Found resource in localStorage: " + resource);
|
|
try {
|
|
callback(JSON.parse(res));
|
|
} catch(err) { this.log("Error in callback: " + err); }
|
|
|
|
// Also, get the age to see if we need to fetch it again.
|
|
age = parseInt(localStorage.getItem("ffz_age_" + resource)||0);
|
|
}
|
|
}
|
|
|
|
if ( DEBUG || !age || (expires !== undefined && expires !== null && (now-age) > expires) ) {
|
|
// Try getting it again.
|
|
this.log("Resource expired. Fetching: " + resource);
|
|
this.do_get(resource, callback, 0, headers, max_attempts);
|
|
} else
|
|
this.getting[resource] = false;
|
|
}
|
|
|
|
ffz.prototype.do_get = function(resource, callback, attempts, headers, max_attempts) {
|
|
if ( !this.alive ) {
|
|
this.getting[resource] = false;
|
|
return;
|
|
}
|
|
|
|
var http = new XMLHttpRequest();
|
|
http.open("GET", resource);
|
|
|
|
if ( headers ) {
|
|
for (var hdr in headers) {
|
|
if ( headers.hasOwnProperty(hdr) )
|
|
http.setRequestHeader(hdr, headers[hdr]);
|
|
}
|
|
}
|
|
|
|
var f = this;
|
|
function try_again() {
|
|
var attempt = (attempts || 0) + 1, delay = 1000;
|
|
if ( !max_attempts || attempt <= max_attempts ) {
|
|
setTimeout(f.do_get.bind(f, resource, callback, attempt, headers, max_attempts), delay);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
http.addEventListener("error", function(e) {
|
|
if ( try_again() )
|
|
return;
|
|
|
|
f.getting[resource] = false;
|
|
try {
|
|
callback(undefined);
|
|
} catch(err) { f.log("Error in callback: " + err); }
|
|
}, false);
|
|
|
|
http.addEventListener("load", function(e) {
|
|
var result;
|
|
if ( http.status === 200 ) {
|
|
// Success!
|
|
result = http.responseText;
|
|
|
|
// Let's see if it was modified?
|
|
if ( window.localStorage ) {
|
|
var last = localStorage.getItem("ffz_last_" + resource),
|
|
nl = http.getResponseHeader("Last-Modified");
|
|
|
|
if ( last && last == nl ) {
|
|
// No change! Let's go.
|
|
f.log("Resource not modified: " + resource);
|
|
localStorage.setItem("ffz_age_" + resource, new Date().getTime());
|
|
f.getting[resource] = false;
|
|
return;
|
|
} else
|
|
// Save it!
|
|
localStorage.setItem("ffz_last_" + resource, nl);
|
|
}
|
|
|
|
} else if ( http.status === 304 ) {
|
|
// Not Modified!
|
|
f.log("Resource not modified: " + resource);
|
|
if ( window.localStorage )
|
|
localStorage.setItem("ffz_age_" + resource, new Date().getTime());
|
|
f.getting[resource] = false;
|
|
return;
|
|
|
|
} else if ( http.status === 404 ) {
|
|
// Not Found!
|
|
result = null;
|
|
|
|
} else {
|
|
// Try Again
|
|
if ( try_again() )
|
|
return;
|
|
result = undefined;
|
|
}
|
|
|
|
// Store it in localStorage if we can.
|
|
if ( window.localStorage && result !== undefined ) {
|
|
localStorage.setItem("ffz_" + resource, JSON.stringify(result));
|
|
localStorage.setItem("ffz_age_" + resource, new Date().getTime());
|
|
}
|
|
|
|
// And send it along.
|
|
f.getting[resource] = false;
|
|
try {
|
|
callback(result);
|
|
} catch(err) { f.log("Error in callback: " + err); }
|
|
}, false);
|
|
|
|
http.send();
|
|
}
|
|
|
|
|
|
// Finally, initialize FFZ.
|
|
window.ffz = new ffz();
|
|
})(this.unsafeWindow || window, window.chrome ? true : false); |