From f1377bc989910b446c2888d9087d48e3099baed2 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Mon, 12 Jan 2015 17:58:07 -0500 Subject: [PATCH] Initial commit for modular FrankerFaceZ rewrite. --- .gitignore | 11 +- Chrome Extension/icon128.png | Bin 0 -> 17420 bytes Chrome Extension/icon16.png | Bin 0 -> 733 bytes Chrome Extension/icon48.png | Bin 0 -> 4145 bytes Chrome Extension/manifest.json | 24 + Chrome Extension/script.js | 27 + gulpfile.js | 40 + package.json | 28 + script.js | 2275 ++++++++++++++++++-------------- script.min.js | 31 +- src/badges.js | 135 ++ src/constants.js | 11 + src/debug.js | 22 + src/ember/chatview.js | 55 + src/ember/line.js | 100 ++ src/ember/room.js | 285 ++++ src/emoticons.js | 149 +++ src/main.js | 128 ++ src/shims.js | 24 + src/socket.js | 93 ++ src/ui/menu.js | 134 ++ src/ui/menu_button.js | 35 + src/ui/notifications.js | 15 + src/ui/styles.js | 24 + src/ui/viewer_count.js | 36 + src/utils.js | 30 + test/server.js | 39 + 27 files changed, 2717 insertions(+), 1034 deletions(-) create mode 100644 Chrome Extension/icon128.png create mode 100644 Chrome Extension/icon16.png create mode 100644 Chrome Extension/icon48.png create mode 100644 Chrome Extension/manifest.json create mode 100644 Chrome Extension/script.js create mode 100644 gulpfile.js create mode 100644 package.json create mode 100644 src/badges.js create mode 100644 src/constants.js create mode 100644 src/debug.js create mode 100644 src/ember/chatview.js create mode 100644 src/ember/line.js create mode 100644 src/ember/room.js create mode 100644 src/emoticons.js create mode 100644 src/main.js create mode 100644 src/shims.js create mode 100644 src/socket.js create mode 100644 src/ui/menu.js create mode 100644 src/ui/menu_button.js create mode 100644 src/ui/notifications.js create mode 100644 src/ui/styles.js create mode 100644 src/ui/viewer_count.js create mode 100644 src/utils.js create mode 100644 test/server.js diff --git a/.gitignore b/.gitignore index e8eb6c44..a91459d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,3 @@ -# Backup files -*.sw[po] -*~ - -# System cruft -Thumbs.db -.DS_Store -/.idea/ +node_modules +npm_debug.log +build \ No newline at end of file diff --git a/Chrome Extension/icon128.png b/Chrome Extension/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..76a66b1e2f8614522a9ff86ceb3c2f5956a05ee4 GIT binary patch literal 17420 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�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 literal 0 HcmV?d00001 diff --git a/Chrome Extension/icon16.png b/Chrome Extension/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..5aca50a0b701c72a8208388e83672592a856d2d1 GIT binary patch literal 733 zcmV<30wVp1P)VmHG|%L|H;BB*7;Y+JLrc5~Y~XJ@?$!GxfP{>Pu6kN@EV{xL+XvbD99R7!;x z{ob{jB$YC#HyB#Vst$dKwIyRgcjskUrl)5wblf#(+a0dL&HB`Vsi~=izXEhwEa@UC zT{$xO33i7A;}hdZ&oqiTE?T-F;6&Blic_^EHvnL?=ShU1Bxt*K6=g;FsHrXoPf0K{ zKG*^Pm(QPTD9h3It>0~|D^;_uA5L%qgnbds%JP?B@!@*knOhGLu-hP%%MeG?Xm4%+ z6zYS1>)dVo(xQ=|)#lFLp+*uc*Um6u7!0X!lRTr#(AWBA25{+QJ-i$b2~EQ}ISB9k z5Z?B88zf@Frb^Z$=not-6=i0SKKDwoA~6Y`V8q5Sg52+{vdu$pg=h99*nksJhQs3r zk04U&5El(qFaHcsEX%^Z>VisVXcCGDo(}N>kfDG)Q55VQbBbu=jzW~|OoLXf0_V2l z{+k7ac^-N4_|ovI0|Hel2$BNpT{V#km9~>$e4xcbIYkQS%3LM(r??;z3jj)nP|$;; z`!p!ur3VuVgNQKTm^CQmauCr-n7s4sbu%NNdLj&q5JzIoxrj)>4o(mc5CAVuKyDg^ zOoW literal 0 HcmV?d00001 diff --git a/Chrome Extension/icon48.png b/Chrome Extension/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..5b3c765457e60999a3827a697aa4c961a5910a1d GIT binary patch literal 4145 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 v3~W= 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; +},{"./constants":2,"./utils":16}],2:[function(require,module,exports){ +var SVGPATH = '', + DEBUG = localStorage.ffzDebugMode == "true"; + +module.exports = { + DEBUG: DEBUG, + SERVER: DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/", + + SVGPATH: SVGPATH, + ZREKNARF: '' + SVGPATH + '', + CHAT_BUTTON: '' + SVGPATH + '' } - - -// ----------------- -// 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()); +},{}],3:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ; + + +// -------------------- +// Debug Command +// -------------------- + +FFZ.chat_commands.debug = function(room, args) { + var enabled, args = args && args.length ? args[0].toLowerCase() : null; + if ( args == "y" || args == "yes" || args == "true" || args == "on" ) + enabled = true; + else if ( args == "n" || args == "no" || args == "false" || args == "off" ) + enabled = false; + + if ( enabled === undefined ) + enabled = !(localStorage.ffzDebugMode == "true"); + + localStorage.ffzDebugMode = enabled; + return "Debug Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; +} + +FFZ.chat_commands.debug.help = "Usage: /ffz debug [on|off]\nEnable or disable Debug Mode. When Debug Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."; +},{}],4:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ; + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_chatview = function() { + this.log("Hooking the Ember Chat view."); + + var Chat = App.__container__.resolve('view:chat'); + this._modify_cview(Chat); + + // For some reason, this doesn't work unless we create an instance of the + // chat view and then destroy it immediately. + Chat.create().destroy(); + + // Modify all existing Chat views. + for(var key in Ember.View.views) { + if ( ! Ember.View.views.hasOwnProperty(key) ) + continue; + + var view = Ember.View.views[key]; + if ( !(view instanceof Chat) ) + continue; + + this.log("Adding UI link manually to Chat view.", view); + view.$('.textarea-contain').append(this.build_ui_link(view)); + } +} + + +// -------------------- +// Modify Chat View +// -------------------- + +FFZ.prototype._modify_cview = function(view) { + var f = this; + + view.reopen({ + didInsertElement: function() { + this._super(); + this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); + }, + + willClearRender: function() { + this._super(); + this.$(".ffz-ui-toggle").remove(); + }, + + ffzUpdateLink: Ember.observer('controller.currentRoom', function() { + f.update_ui_link(); + }) + }); } - - -// ----------------- -// Commands -// ----------------- - -ffz.prototype._msg = function(room, out) { - if ( this.has_bttv ) - return BetterTTV.chat.helpers.serverMessage(out.replace(/\n/g, "
")); - - out = out.split("\n"); - for(var i=0; i < out.length; i++) - room.addMessage({style: 'ffz admin', from: 'FFZ', message: out[i]}); +},{}],5:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ; + + +// --------------------- +// Initialization +// --------------------- + +FFZ.prototype.setup_line = function() { + this.log("Hooking the Ember Line controller."); + + var Line = App.__container__.resolve('controller:line'), + f = this; + + Line.reopen({ + tokenizedMessage: function() { + // Add our own step to the tokenization procedure. + return f._emoticonize(this, this._super()); + + }.property("model.message", "isModeratorOrHigher", "controllers.emoticons.emoticons.[]") + // TODO: Copy the new properties from the new Twitch! + }); + + + this.log("Hooking the Ember Line view."); + var Line = App.__container__.resolve('view:line'); + + Line.reopen({ + didInsertElement: function() { + this._super(); + + var el = this.get('element'); + el.setAttribute('data-room', this.get('context.parentController.content.id')); + el.setAttribute('data-sender', this.get('context.model.from')); + + f.render_badge(this); + } + }); +} + + +// --------------------- +// Emoticon Replacement +// --------------------- + +FFZ.prototype._emoticonize = function(controller, tokens) { + var room_id = controller.get("parentController.model.id"), + user_id = controller.get("model.from"), + user = this.users[user_id], + room = this.rooms[room_id], + f = this; + + // Get our sets. + var sets = _.union(user && user.sets || [], room && room.sets || [], f.global_sets), + emotes = []; + + // Build a list of emotes that match. + _.each(sets, function(set_id) { + var set = f.emote_sets[set_id]; + if ( ! set ) + return; + + _.each(set.emotes, function(emote) { + _.any(tokens, function(token) { + return _.isString(token) && token.match(emote.regex); + }) && emotes.push(emote); + }); + }); + + // Don't bother proceeding if we have no emotes. + if ( ! emotes.length ) + return tokens; + + // Now that we have all the matching tokens, do crazy stuff. + if ( typeof tokens == "string" ) + tokens = [tokens]; + + // This is weird stuff I basically copied from the old Twitch code. + // Here, for each emote, we split apart every text token and we + // put it back together with the matching bits of text replaced + // with an object telling Twitch's line template how to render the + // emoticon. + _.each(emotes, function(emote) { + var eo = {isEmoticon:true, cls: emote.klass}; + + tokens = _.compact(_.flatten(_.map(tokens, function(token) { + if ( _.isObject(token) ) + return token; + + var tbits = token.split(emote.regex), bits = []; + tbits.forEach(function(val, ind) { + bits.push(val); + if ( ind !== tbits.length - 1 ) + bits.push(eo); + }); + return bits; + }))); + }); + + return tokens; } - -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); +},{}],6:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/mg, + MOD_CSS = /[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/, + GROUP_CHAT = /^_([^_]+)_\d+$/, + constants = require('../constants'), + utils = require('../utils'), + + + moderator_css = function(room) { + if ( ! room.moderator_badge ) + return ""; + + return '.chat-line[data-room="' + room.id + '"] .badges .moderator { background-image:url("' + room.moderator_badge + '") !important; }'; + } + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_room = function() { + this.rooms = {}; + + this.log("Creating room style element."); + var s = this._room_style = document.createElement("style"); + s.id = "ffz-room-css"; + document.head.appendChild(s); + + this.log("Hooking the Ember Room model."); + + var Room = App.__container__.resolve('model:room'); + this._modify_room(Room); + + // Modify all current instances of Room, as the changes to the base + // class won't be inherited automatically. + var instances = Room.instances; + for(var key in instances) { + if ( ! instances.hasOwnProperty(key) ) + continue; + + var inst = instances[key]; + this.add_room(inst.id, inst); + this._modify_room(inst); + } +} + + +// -------------------- +// Command System +// -------------------- + +FFZ.chat_commands = {}; + + +FFZ.prototype.room_message = function(room, text) { + var lines = text.split("\n"); + for(var i=0; i < lines.length; i++) + room.room.addMessage({style: 'ffz admin', from: 'FFZ', message: lines[i]}); +} + + +FFZ.prototype.run_command = function(text, room_id) { + var room = this.rooms[room_id]; + if ( ! room || !room.room ) + return; + + if ( ! text ) + text = "help"; + + var args = text.split(" "), + cmd = args.shift().toLowerCase(); + + this.log("Received Command: " + cmd, args, true); + + var command = FFZ.chat_commands[cmd], output; + if ( command ) { + try { + output = command.bind(this)(room, args); + } catch(err) { + this.log("Error Running Command - " + cmd + ": " + err, room); + output = "There was an error running the command."; + } + } else + output = 'There is no "' + cmd + '" command.'; + + if ( output ) + this.room_message(room, output); +} + + +FFZ.chat_commands.help = function(room, args) { + if ( args && args.length ) { + var command = FFZ.chat_commands[args[0].toLowerCase()]; + if ( ! command ) + return 'There is no "' + args[0] + '" command.'; + + else if ( ! command.help ) + return 'No help is available for the command "' + args[0] + '".'; + + else + return command.help; + } + + var cmds = []; + for(var c in FFZ.chat_commands) + FFZ.chat_commands.hasOwnProperty(c) && cmds.push(c); + + return "The available commands are: " + cmds.join(", "); +} + +FFZ.chat_commands.help.help = "Usage: /ffz help [command]\nList available commands, or show help for a specific command."; + + +// -------------------- +// Room Management +// -------------------- + +FFZ.prototype.add_room = function(id, room) { + if ( this.rooms[id] ) + return this.log("Tried to add existing room: " + id); + + this.log("Adding Room: " + id); + + // Create a basic data table for this room. + this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null}; + + // Let the server know where we are. + this.ws_send("sub", id); + + // For now, we use the legacy function to grab the .css file. + this._legacy_add_room(id); +} + + +FFZ.prototype.remove_room = function(id) { + var room = this.rooms[id]; + if ( ! room ) + return; + + this.log("Removing Room: " + id); + + // Remove the CSS + if ( room.css || room.moderator_badge ) + utils.update_css(this._room_style, id, null); + + // Let the server know we're gone and delete our data for this room. + this.ws_send("unsub", id); + delete this.rooms[id]; + + // Clean up sets we aren't using any longer. + for(var i=0; i < room.sets.length; i++) { + var set_id = room.sets[i], set = this.emote_sets[set_id]; + if ( ! set ) + continue; + + set.users.removeObject(id); + if ( !set.global && !set.users.length ) + this.unload_set(set_id); + } +} + + +// -------------------- +// Receiving Set Info +// -------------------- + +FFZ.prototype.load_room = function(room_id, callback) { + return this._legacy_load_room(room_id, callback); +} + + +FFZ.prototype._load_room_json = function(room_id, callback, data) { + // Preserve the pointer to the Room instance. + if ( this.rooms[room_id] ) + data.room = this.rooms[room_id].room; + + this.rooms[room_id] = data; + + if ( data.css || data.moderator_badge ) + utils.update_css(this._room_style, room_id, moderator_css(data) + (data.css||"")); + + for(var i=0; i < data.sets.length; i++) { + var set_id = data.sets[i]; + if ( ! this.emote_sets.hasOwnProperty(set_id) ) + this.load_set(set_id); + } + + this.update_ui_link(); + + if ( callback ) + callback(true, data); +} + + +/*FFZ.ws_commands.sets_for_room = function(data) { + var room = this.rooms[data.room]; + if ( ! room ) + return; + + for(var i=0; i < data.sets.length; i++) { + var set = data.sets[i]; + if ( room.sets.contains(set) ) + continue; + + room.sets.push(set); + this.load_set(set); + } +}*/ + + +// -------------------- +// Ember Modifications +// -------------------- + +FFZ.prototype._modify_room = function(room) { + var f = this; + room.reopen({ + init: function() { + this._super(); + f.add_room(this.id, this); + }, + + willDestroy: function() { + this._super(); + f.remove_room(this.id); + }, + + send: function(text) { + var cmd = text.split(' ', 1)[0].toLowerCase(); + if ( cmd === "/ffz" ) { + this.set("messageToSend", ""); + f.run_command(text.substr(5), this.get('id')); + } else + return this._super(text); + } + }); +} + + +// -------------------- +// Legacy Data Support +// -------------------- + +FFZ.prototype._legacy_add_room = function(room_id, callback, tries) { + jQuery.ajax(constants.SERVER + "channel/" + room_id + ".css", {cache: false, context:this}) + .done(function(data) { + this._legacy_load_room_css(room_id, callback, data); + + }).fail(function(data) { + if ( data.status == 404 ) + return this._legacy_load_room_css(room_id, callback, null); + + tries = tries || 0; + tries++; + if ( tries < 10 ) + return this._legacy_add_room(room_id, callback, tries); + }); +} + + +FFZ.prototype._legacy_load_room_css = function(room_id, callback, data) { + var set_id = room_id, + match = set_id.match(GROUP_CHAT); + + if ( match && match[1] ) + set_id = match[1]; + + var output = {id: room_id, menu_sets: [set_id], sets: [set_id], moderator_badge: null, css: null}; + + if ( data ) + data = data.replace(CSS, "").trim(); + + if ( data ) { + data = data.replace(MOD_CSS, function(match, url) { + if ( output.moderator_badge || url.substr(-11) !== 'modicon.png' ) + return match; + + output.moderator_badge = url; + return ""; + }); + } + + output.css = data || null; + return this._load_room_json(room_id, callback, output); } - -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(", "); +},{"../constants":2,"../utils":16}],7:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/mg, + constants = require('./constants'), + utils = require('./utils'); + + +var loaded_global = function(set_id, success, data) { + if ( ! success ) + return; + + data.global = true; + this.global_sets.push(set_id); +} + +var check_margins = function(margins, height) { + var mlist = margins.split(/ +/); + if ( mlist.length != 2 ) + return margins; + + mlist[0] = parseFloat(mlist[0]); + mlist[1] = parseFloat(mlist[1]); + + if ( mlist[0] == (height - 18) / -2 && mlist[1] == 0 ) + return null; + + return margins; +} + + +FFZ.prototype.setup_emoticons = function() { + this.log("Preparing emoticon system."); + + this.emote_sets = {}; + this.global_sets = []; + this._last_emote_id = 0; + + this.log("Creating emoticon style element."); + var s = this._emote_style = document.createElement('style'); + s.id = "ffz-emoticon-css"; + document.head.appendChild(s); + + this.log("Loading global emote set."); + this.load_set("global", loaded_global.bind(this, "global")); +} + + + +FFZ.ws_commands.reload_set = function(set_id) { + this.load_set(set_id); +} + + +FFZ.prototype.load_set = function(set_id, callback) { + return this._legacy_load_set(set_id, callback); +} + + +FFZ.prototype.unload_set = function(set_id) { + var set = this.emote_sets[set_id]; + if ( ! set ) + return; + + this.log("Unloading emoticons for set: " + set_id); + + utils.update_css(this._emote_style, set_id, null); + delete this.emote_sets[set_id]; + + for(var i=0; i < set.users.length; i++) { + var room = this.rooms[set.users[i]]; + if ( room ) + room.sets.removeObject(set_id); + } +} + + +var build_css = function(emote) { + var margin = emote.margins; + if ( ! margin ) + margin = ((emote.height - 18) / -2) + "px 0"; + return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.url + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (emote.extra_css ? "; " + emote.extra_css : "") + "}\n"; +} + +FFZ.prototype._load_set_json = function(set_id, callback, data) { + // Store our set. + this.emote_sets[set_id] = data; + data.users = []; + data.global = false; + + // Iterate through all the emoticons, building CSS and regex objects as appropriate. + var output_css = ""; + + for(var key in data.emotes) { + if ( ! data.emotes.hasOwnProperty(key) ) + continue; + + var emote = data.emotes[key]; + emote.klass = "ffz-emote-" + emote.id; + + if ( emote.name[emote.name.length-1] === "!" ) + emote.regex = new RegExp("\\b" + emote.name + "(?=\\W|$)", "g"); + else + emote.regex = new RegExp("\\b" + emote.name + "\\b", "g"); + + output_css += build_css(emote); + } + + utils.update_css(this._emote_style, set_id, output_css + (data.extra_css || "")); + this.log("Updated emoticons for set: " + set_id, data); + + if ( callback ) + callback(true, data); +} + + +FFZ.prototype._legacy_load_set = function(set_id, callback, tries) { + jQuery.ajax(constants.SERVER + "channel/" + set_id + ".css", {cache: false, context:this}) + .done(function(data) { + this._legacy_load_css(set_id, callback, data); + + }).fail(function(data) { + if ( data.status == 404 ) + return callback && callback(false); + + tries = tries || 0; + tries++; + if ( tries < 10 ) + return this._legacy_load_set(set_id, callback, tries); + + return callback && callback(false); + }); +} + + +FFZ.prototype._legacy_load_css = function(set_id, callback, data) { + var emotes = {}, output = {id: set_id, emotes: emotes, extra_css: null}, f = this; + + data.replace(CSS, function(match, klass, name, path, height, width, margins, extra) { + height = parseInt(height); width = parseInt(width); + margins = check_margins(margins, height); + var hidden = path.substr(path.lastIndexOf("/") + 1, 1) === ".", + id = ++f._last_emote_id, + emote = {id: id, hidden: hidden, name: name, height: height, width: width, url: path, margins: margins, extra_css: extra}; + + emotes[id] = emote; + return ""; + }); + + this._load_set_json(set_id, callback, output); } -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"); +},{"./constants":2,"./utils":16}],8:[function(require,module,exports){ +// Modify Array and others. +require('./shims'); + + +// ---------------- +// The Constructor +// ---------------- + +var FFZ = window.FrankerFaceZ = function() { + FFZ.instance = this; + + // Get things started. + this.initialize(); +} + + +FFZ.get = function() { return FFZ.instance; } + + +// Version +var VER = FFZ.version_info = { + major: 3, minor: 0, revision: 0, + toString: function() { + return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); + } +} + + +// Logging + +FFZ.prototype.log = function(msg, data, to_json) { + msg = "FFZ: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); + if ( data !== undefined && console.groupCollapsed && console.dir ) { + console.groupCollapsed(msg); + console.dir(data); + console.groupEnd(msg); + } else + console.log(msg); +} + + +// ------------------- +// User Data +// ------------------- + +FFZ.prototype.get_user = function() { + if ( window.PP && PP.login ) { + return PP; + } else if ( window.App ) { + var nc = App.__container__.lookup("controller:navigation"); + return nc ? nc.get("userData") : undefined; + } +} + + +// ------------------- +// Import Everything! +// ------------------- + +require('./socket'); +require('./emoticons'); +require('./badges'); + +require('./ember/room'); +require('./ember/line'); +require('./ember/chatview'); + +require('./debug'); + +require('./ui/styles'); +require('./ui/notifications'); +require('./ui/viewer_count'); + +require('./ui/menu_button'); +require('./ui/menu'); + +// --------------- +// Initialization +// --------------- + +FFZ.prototype.initialize = function(increment, delay) { + // Make sure that FrankerFaceZ doesn't start setting itself up until the + // Twitch ember application is ready. + + // TODO: Special Dashboard check. + + var loaded = window.App != undefined && + App.__container__ != undefined && + App.__container__.resolve('model:room') != undefined; + + if ( !loaded ) { + increment = increment || 10; + if ( delay >= 60000 ) + this.log("Twitch application not detected in \"" + location.toString() + "\". Aborting."); + else + setTimeout(this.initialize.bind(this, increment, (delay||0) + increment), + increment); + return; + } + + this.setup(delay); +} + + +FFZ.prototype.setup = function(delay) { + this.log("Found Twitch application after " + (delay||0) + " ms in \"" + location + "\". Initializing FrankerFaceZ version " + FFZ.version_info); + + this.users = {}; + + try { + this.ws_create(); + this.setup_emoticons(); + this.setup_badges(); + + this.setup_room(); + this.setup_line(); + this.setup_chatview(); + + this.setup_css(); + this.setup_menu(); + + } catch(err) { + this.log("An error occurred while starting FrankerFaceZ: " + err); + return; + } + + this.log("Initialization complete."); } -ffz.commands['log'].help = "Usage: /ffz log\nOpen a window with FFZ's debugging output."; +},{"./badges":1,"./debug":3,"./ember/chatview":4,"./ember/line":5,"./ember/room":6,"./emoticons":7,"./shims":9,"./socket":10,"./ui/menu":11,"./ui/menu_button":12,"./ui/notifications":13,"./ui/styles":14,"./ui/viewer_count":15}],9:[function(require,module,exports){ +Array.prototype.equals = function (array) { + // if the other array is a falsy value, return + if (!array) + return false; + + // compare lengths - can save a lot of time + if (this.length != array.length) + return false; + + for (var i = 0, l=this.length; i < l; i++) { + // Check if we have nested arrays + if (this[i] instanceof Array && array[i] instanceof Array) { + // recurse into the nested arrays + if (!this[i].equals(array[i])) + return false; + } + else if (this[i] != array[i]) { + // Warning - two different object instances will never be equal: {x:20} != {x:20} + return false; + } + } + return true; +} + -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 += ""; - - 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 += ""; - 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 += ""; - else { - t = t[0] + "\u200B" + t.substr(1); - output += " " + t + " = " + emote.text + "\n"; - } - } - } - if ( html ) output += ""; - } - - if ( html ) output += "
" + name + "
" + t + "" + emote.image.html + "
"; - - // Make sure we actually have output. - if ( output.indexOf(html ? '' : '\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; +},{}],10:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ; + +FFZ.prototype._ws_open = false; +FFZ.prototype._ws_delay = 0; + +FFZ.ws_commands = {}; + + +// ---------------- +// Socket Creation +// ---------------- + +FFZ.prototype.ws_create = function() { + var f = this; + + this._ws_last_req = 0; + this._ws_callbacks = {}; + + var ws = this._ws_sock = new WebSocket("ws://ffz.stendec.me/"); + + ws.onopen = function(e) { + f._ws_open = true; + f._ws_delay = 0; + f.log("Socket connected."); + + var user = f.get_user(); + if ( user ) + f.ws_send("setuser", user.login); + + // Send the current rooms. + for(var room_id in f.rooms) + f.ws_send("sub", room_id); + } + + ws.onclose = function(e) { + f.log("Socket closed."); + f._ws_open = false; + + // We never ever want to not have a socket. + if ( f._ws_delay < 30000 ) + f._ws_delay += 5000; + + setTimeout(f.ws_create.bind(f), f._ws_delay); + } + + ws.onmessage = function(e) { + // Messages are formatted as REQUEST_ID SUCCESS/FUNCTION_NAME[ JSON_DATA] + var cmd, data, ind = e.data.indexOf(" "), + msg = e.data.substr(ind + 1), + request = parseInt(e.data.slice(0, ind)); + + ind = msg.indexOf(" "); + if ( ind === -1 ) + ind = msg.length; + + cmd = msg.slice(0, ind); + msg = msg.substr(ind + 1); + if ( msg ) + data = JSON.parse(msg); + + if ( request === -1 ) { + // It's a command from the server. + var command = FFZ.ws_commands[cmd]; + if ( command ) + command.bind(f)(data); + else + f.log("Invalid command: " + cmd, data); + + } else { + var success = cmd === 'True', + callback = f._ws_callbacks[request]; + f.log("Socket Reply to " + request + " - " + (success ? "SUCCESS" : "FAIL"), data); + if ( callback ) { + delete f._ws_callbacks[request]; + callback(success, data); + } + } + } +} + + +FFZ.prototype.ws_send = function(func, data, callback) { + if ( ! this._ws_open ) return false; + + var request = ++this._ws_last_req; + data = data !== undefined ? " " + JSON.stringify(data) : ""; + + if ( callback ) + this._ws_callbacks[request] = callback; + + this._ws_sock.send(request + " " + func + data); + return request; } -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."; +},{}],11:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ; + + +// -------------------- +// Initializer +// -------------------- + +FFZ.prototype.setup_menu = function() { + this.log("Installing mouse-up event to auto-close menus."); + var f = this; + + jQuery(document).mouseup(function(e) { + var popup = f._popup, parent; + if ( ! popup ) return; + popup = jQuery(popup); + parent = popup.parent(); + + if ( ! parent.is(e.target) && parent.has(e.target).length === 0 ) { + popup.remove(); + delete f._popup; + } + }); +} + + +// -------------------- +// Create Menu +// -------------------- + +FFZ.prototype.build_ui_popup = function(view) { + var popup = this._popup; + if ( popup ) { + popup.parentElement.removeChild(popup); + delete this._popup; + return; + } + + // Start building the DOM. + var container = document.createElement('div'), + inner = document.createElement('div'); + + container.className = 'emoticon-selector chat-menu ffz-ui-popup'; + inner.className = 'emoticon-selector-box dropmenu'; + container.appendChild(inner); + + // TODO: Modularize for multiple menu pages! + + // Get the current room. + var room_id = view.get('controller.currentRoom.id'), + room = this.rooms[room_id]; + + this.log("Menu for Room: " + room_id, room); + + // Add the header and ad button. + var btn = document.createElement('a'); + btn.className = 'button glyph-only ffz-button'; + btn.title = 'Advertise for FrankerFaceZ in chat!'; + btn.href = '#'; + btn.innerHTML = ''; + + var hdr = document.createElement('div'); + hdr.className = 'list-header first'; + hdr.appendChild(btn); + hdr.appendChild(document.createTextNode('FrankerFaceZ')); + inner.appendChild(hdr); + + var c = this._emotes_for_sets(inner, view, room && room.menu_sets || []); + + if ( c === 0 ) + btn.addEventListener('click', this._add_emote.bind(this, view, "To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com")); + else + btn.addEventListener('click', this._add_emote.bind(this, view, "To view this channel's emoticons, get FrankerFaceZ from http://www.frankerfacez.com")); + + + // Add the menu to the DOM. + this._popup = container; + inner.style.maxHeight = Math.max(300, view.$().height() - 171) + "px"; + view.$('.chat-interface').append(container); +} + + +// -------------------- +// Emotes for Sets +// -------------------- + +FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) { + if ( header != null ) { + var el_header = document.createElement('div'); + el_header.className = 'list-header'; + el_header.appendChild(document.createTextNode(header)); + + if ( btn ) + el_header.appendChild(btn); + + parent.appendChild(el_header); + } + + var grid = document.createElement('div'), c = 0; + grid.className = 'emoticon-grid'; + + for(var i=0; i < sets.length; i++) { + var set = this.emote_sets[sets[i]]; + for(var eid in set.emotes) { + var emote = set.emotes[eid]; + if ( !set.emotes.hasOwnProperty(eid) || emote.hidden ) + continue; + + c++; + var s = document.createElement('span'); + s.className = 'emoticon ' + emote.klass + ' tooltip'; + s.title = emote.name; + s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); + grid.appendChild(s); + } + } + + if ( !c ) { + grid.innerHTML = "This channel has no emoticons."; + grid.className = "chat-menu-content ffz-no-emotes center"; + } + + parent.appendChild(grid); +} + + +FFZ.prototype._add_emote = function(view, emote) { + var room = view.get('controller.currentRoom'), + current_text = room.get('messageToSend') || ''; + + if ( current_text && current_text.substr(-1) !== " " ) + current_text += ' '; + + room.set('messageToSend', current_text + (emote.name || emote)); } -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); +},{}],12:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + constants = require('../constants'); + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.build_ui_link = function(view) { + // TODO: Detect dark mode from BTTV. + + var link = document.createElement('a'); + link.className = 'ffz-ui-toggle'; + link.innerHTML = constants.CHAT_BUTTON; + + link.addEventListener('click', this.build_ui_popup.bind(this, view)); + + this.update_ui_link(link); + return link; +} + + +FFZ.prototype.update_ui_link = function(link) { + var controller = App.__container__.lookup('controller:chat'); + link = link || document.querySelector('a.ffz-ui-toggle'); + if ( !link || !controller ) return; + + var room_id = controller.get('currentRoom.id'), + room = this.rooms[room_id], + has_emotes = room && room.sets.length > 0; + + if ( has_emotes ) + link.classList.remove('no-emotes'); + else + link.classList.add('no-emotes'); } -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])); +},{"../constants":2}],13:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ; + +FFZ.prototype.show_notification = function(message) { + window.noty({ + text: message, + theme: "ffzTheme", + layout: "bottomCenter", + closeWith: ["button"] + }).show(); +} + + +FFZ.ws_commands.message = function(message) { + this.show_notification(message); } - - -// ----------------- -// 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); +},{}],14:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + constants = require('../constants'); + +FFZ.prototype.setup_css = function() { + this.log("Injecting main FrankerFaceZ CSS."); + + var s = this._main_style = document.createElement('link'); + + s.id = "ffz-ui-css"; + s.setAttribute('rel', 'stylesheet'); + s.setAttribute('href', constants.SERVER + "script/style.css"); + document.head.appendChild(s); + + jQuery.noty.themes.ffzTheme = { + name: "ffzTheme", + style: function() { + this.$bar.removeClass().addClass("noty_bar").addClass("ffz-noty").addClass(this.options.type); + }, + callback: { + onShow: function() {}, + onClose: function() {} + } + }; } - -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 badge = _.defaults({}, donor_badge); - if ( BetterTTV.settings.get('alphaTags') ) - badge['type'] = badge['type'] + ' alpha'; - - 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, badge); - inserted = true; - break; - } - if ( ! inserted ) - data.badges.push(badge); - } - - return privmsg(highlight, action, server, isMod, data); - } +},{"../constants":2}],15:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + constants = require('../constants'), + utils = require('../utils'); + +// ------------ +// Set Viewers +// ------------ + +FFZ.ws_commands.viewers = function(data) { + var channel = data[0], count = data[1]; + + var controller = App.__container__.lookup('controller:channel'), + id = controller && controller.get && controller.get('id'); + + if ( id !== channel ) + return; + + var view_count = document.querySelector('.channel-stats .ffz.stat'), + content = constants.ZREKNARF + ' ' + utils.number_commas(count); + + if ( view_count ) + view_count.innerHTML = content; + else { + var parent = document.querySelector('.channel-stats'); + if ( ! parent ) + return; + + view_count = document.createElement('span'); + view_count.className = 'ffz stat'; + view_count.title = 'Viewers with FrankerFaceZ'; + view_count.innerHTML = content; + + parent.appendChild(view_count); + jQuery(view_count).tipsy(); + } } - - -// ----------------- -// 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); +},{"../constants":2,"../utils":16}],16:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + constants = require('./constants'); + +module.exports = { + update_css: function(element, id, css) { + var all = element.innerHTML, + start = "/*BEGIN " + id + "*/", + end = "/*END " + id + "*/", + s_ind = all.indexOf(start), + e_ind = all.indexOf(end), + found = s_ind !== -1 && e_ind !== -1 && e_ind > s_ind; + + if ( !found && !css ) + return; + + if ( found ) + all = all.substr(0, s_ind) + all.substr(e_ind + end.length); + + if ( css ) + all += start + css + end; + + element.innerHTML = all; + }, + + number_commas: function(x) { + var parts = x.toString().split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return parts.join("."); + } } - -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 + "channel/" + 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: '', - 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 + "scripts/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); \ No newline at end of file +},{"./constants":2}]},{},[8]);window.ffz = new FrankerFaceZ()}(window)); \ No newline at end of file diff --git a/script.min.js b/script.min.js index 8d2663aa..ac472366 100644 --- a/script.min.js +++ b/script.min.js @@ -1,30 +1 @@ -(function wrapper(m,s){if(s){var p=document.createElement("script");p.textContent="("+wrapper+")(window, false)";document.body.appendChild(p);document.body.removeChild(p)}else{var t=/\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)[^}]*\}/mg,r=-1!==location.search.indexOf("frankerfacez"),c=function(){this.alive=!0;this.donors={};this.getting={};this.emoticons=[];this.emotesets={};this.channels={};this.collections={};this.globals= -{};this.global_sets=[];this.styles={};this.pending_styles=[];this._log=[];this._log2=[];this.init(10)};c.prototype.last_set=0;c.prototype.last_emote=0;c.prototype.manger=null;c.prototype.has_bttv=!1;c.commands={};c.prototype.log=function(b){this._log.push(b);b="FFZ"+(this.alive?": ":" (Dead): ")+b;console.log(b);if(r){var d,a;for(a in this.channels)if(this.channels[a]&&this.channels[a].room){d=this.channels[a];break}d?d.room.addTmiMessage(b):this._log2.push(b)}};c.prototype.init=function(b,d){this.alive&& -(void 0==m.Ember||void 0==m.App||void 0==App.EmoticonsController||void 0==App.Room?6E4<=d?this.log('Twitch API not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.init.bind(this,b,(d||0)+b),b):this.setup())};c.prototype.setup=function(){this.alive&&(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"),document.body||(this.listen_dom=this.listen_dom.bind(this), -document.addEventListener("DOMContentLoaded",this.listen_dom,!1)),this.find_bttv(10),this.log("Initialization complete."))};c.prototype.destroy=function(){this.alive&&(alive=!1,m.ffz===this&&(m.ffz=void 0),delete this._log,delete this._log2)};c.prototype.listen_dom=function(){for(document.removeEventListener("DOMContentLoaded",this.listen_dom,!1);this.pending_styles.length;)document.body.appendChild(this.pending_styles.pop())};c.prototype._msg=function(b,d){if(this.has_bttv)return BetterTTV.chat.helpers.serverMessage(d.replace(/\n/g, -"
"));d=d.split("\n");for(var a=0;a'+f+""):a+(f+"\n"),e=this.collections[f],l;for(l in e)if(e.hasOwnProperty(l)){var k=e[l],n=k.text;c?a+=''+n+""+k.image.html+"":(n=n[0]+"\u200b"+ -n.substr(1),a+=" "+n+" = "+k.text+"\n")}c&&(a+="")}}c&&(a+="");return-1===a.indexOf(c?"":"\u200b")?"There are no available FFZ channel emoticons. If this is in error, please try the /ffz reload command.":"The following emotes are available:\n"+a};c.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.";c.commands.global=function(b,d){return c.commands.list.bind(this)(b,["global"])}; -c.commands.global.help="Usage: /ffz global\nShorthand for /ffz list global. List ALL FFZ emoticons, including FFZ global emoticons.";c.commands.reload=function(b,d){for(var a in this.channels)this.channels.hasOwnProperty(a)&&this.channels[a]&&this.load_emotes(a,!0);this.load_emotes("global");this.load_donors();return"Attempting to reload FFZ data from the server."};c.commands.reload.help="Usage: /ffz reload\nAttempt to reload FFZ emoticons and donors.";c.commands.inject=function(b,d){if(!d||1!==d.length)return"/ffz inject requires exactly 1 argument."; -var a=d[0].split("/").pop().split("?").shift().split("#").shift();this._msg(b,'Attempting to load test emoticons from imgur album "'+a+'"...');var c="https://api.imgur.com/3/album/"+a;m.localStorage&&localStorage.removeItem("ffz_"+c);this.get(c,this.do_imgur.bind(this,b,a),1,{Accept:"application/json",Authorization:"Client-ID e48d122e3437051"},5)};c.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"; -c.prototype.do_imgur=function(b,d,a){if(void 0===a)return this._msg(b,"An error occurred communicating with Imgur.");if(!a)return this._msg(b,"The named album does not exist or is private.");a=JSON.parse(a).data;a=a.images;for(var g="",h=0;h',id:--k.last_emote};a="!"===e[e.length-1]?new RegExp("\\b"+e+"(?=\\W|$)","g"):new RegExp("\\b"+e+"\\b","g");e={image:g,images:[g],text:e,channel:d,hidden:!1,regex:a,ffzset:b};n.push(e);k.emoticons.push(e); -f.push({isEmoticon:!0,cls:c,regex:a});l++});this.log("Loaded "+l+" emotes from collection: "+b);return l}};c.prototype.unload_emotes=function(b){if(this.alive){var d=this.channels[b],a,c,h;if(!1!==d){d?(a=d.set_id,c=d.style,h="FFZ Channel Emotes: "+b,delete d.set,delete d.set_id,delete d.style):(a=this.globals[b],c=this.styles[b],h="FFZ Global Emotes"+("global"!=b?": "+b:""),delete this.globals[b],delete this.styles[b],d=this.global_sets.indexOf(a),-1!==d&&this.global_sets.splice(d,1));this.collections[h]&& -delete this.collections[h];if(c)try{c.parentNode.removeChild(c)}catch(f){}delete this.emotesets[a];this.manager&&delete this.manager.emoticonSets[a];this.emoticons=this.emoticons.filter(function(a){return a.ffzgroup!==b})}}};c.prototype.check_donor=function(b){return this.donors[b]||!1};c.prototype.load_donors=function(b){this.get("//cdn.frankerfacez.com/scripts/donors.txt",this.process_donors.bind(this),b?1:108E5)};c.prototype.process_donors=function(b){if(this.alive){this.donors={};var d=0;if(null!= -b){b=b.trim().split(/\W+/);for(var a=0;aa?(this.log("Resource expired. Fetching: "+b),this.do_get(b,d,0,c,h)):this.getting[b]=!1}};c.prototype.do_get=function(b,d,a,c,h){function f(){var e=(a||0)+1;if(!h||e<=h)return setTimeout(k.do_get.bind(k,b,d,e,c,h),1E3),!0}if(this.alive){var e=new XMLHttpRequest;e.open("GET",b);if(c)for(var l in c)c.hasOwnProperty(l)&&e.setRequestHeader(l,c[l]);var k=this;e.addEventListener("error",function(a){if(!f()){k.getting[b]=!1;try{d(void 0)}catch(c){k.log("Error in callback: "+ -c)}}},!1);e.addEventListener("load",function(a){if(200===e.status){if(a=e.responseText,m.localStorage){var c=localStorage.getItem("ffz_last_"+b),g=e.getResponseHeader("Last-Modified");if(c&&c==g){k.log("Resource not modified: "+b);localStorage.setItem("ffz_age_"+b,(new Date).getTime());k.getting[b]=!1;return}localStorage.setItem("ffz_last_"+b,g)}}else{if(304===e.status){k.log("Resource not modified: "+b);m.localStorage&&localStorage.setItem("ffz_age_"+b,(new Date).getTime());k.getting[b]=!1;return}if(404=== -e.status)a=null;else{if(f())return;a=void 0}}m.localStorage&&void 0!==a&&(localStorage.setItem("ffz_"+b,JSON.stringify(a)),localStorage.setItem("ffz_age_"+b,(new Date).getTime()));k.getting[b]=!1;try{d(a)}catch(h){k.log("Error in callback: "+h)}},!1);e.send()}else this.getting[b]=!1};m.ffz=new c}})(this.unsafeWindow||window,window.chrome?!0:!1); \ No newline at end of file +!function(e){!function t(e,o,s){function n(i,a){if(!o[i]){if(!e[i]){var c="function"==typeof require&&require;if(!a&&c)return c(i,!0);if(r)return r(i,!0);throw new Error("Cannot find module '"+i+"'")}var l=o[i]={exports:{}};e[i][0].call(l.exports,function(t){var o=e[i][1][t];return n(o?o:t)},l,l.exports,t,e,o,s)}return o[i].exports}for(var r="function"==typeof require&&require,i=0;ie?this._legacy_add_donors(e):void 0):void 0})},o.prototype._legacy_parse_donors=function(e){var t=0;if(null!=e)for(var o=e.trim().split(/\W+/),s=0;s'+o+"",CHAT_BUTTON:''+o+""}},{}],3:[function(){var t=e.FrankerFaceZ;t.chat_commands.debug=function(e,t){var o,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?o=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(o=!1),void 0===o&&(o=!("true"==localStorage.ffzDebugMode)),localStorage.ffzDebugMode=o,"Debug Mode is now "+(o?"enabled":"disabled")+". Please refresh your browser."},t.chat_commands.debug.help="Usage: /ffz debug [on|off]\nEnable or disable Debug Mode. When Debug Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."},{}],4:[function(){var t=e.FrankerFaceZ;t.prototype.setup_chatview=function(){this.log("Hooking the Ember Chat view.");var e=App.__container__.resolve("view:chat");this._modify_cview(e),e.create().destroy();for(var t in Ember.View.views)if(Ember.View.views.hasOwnProperty(t)){var o=Ember.View.views[t];o instanceof e&&(this.log("Adding UI link manually to Chat view.",o),o.$(".textarea-contain").append(this.build_ui_link(o)))}},t.prototype._modify_cview=function(e){var t=this;e.reopen({didInsertElement:function(){this._super(),this.$()&&this.$(".textarea-contain").append(t.build_ui_link(this))},willClearRender:function(){this._super(),this.$(".ffz-ui-toggle").remove()},ffzUpdateLink:Ember.observer("controller.currentRoom",function(){t.update_ui_link()})})}},{}],5:[function(){var t=e.FrankerFaceZ;t.prototype.setup_line=function(){this.log("Hooking the Ember Line controller.");var e=App.__container__.resolve("controller:line"),t=this;e.reopen({tokenizedMessage:function(){return t._emoticonize(this,this._super())}.property("model.message","isModeratorOrHigher","controllers.emoticons.emoticons.[]")}),this.log("Hooking the Ember Line view.");var e=App.__container__.resolve("view:line");e.reopen({didInsertElement:function(){this._super();var e=this.get("element");e.setAttribute("data-room",this.get("context.parentController.content.id")),e.setAttribute("data-sender",this.get("context.model.from")),t.render_badge(this)}})},t.prototype._emoticonize=function(e,t){var o=e.get("parentController.model.id"),s=e.get("model.from"),n=this.users[s],r=this.rooms[o],i=this,a=_.union(n&&n.sets||[],r&&r.sets||[],i.global_sets),c=[];return _.each(a,function(e){var o=i.emote_sets[e];o&&_.each(o.emotes,function(e){_.any(t,function(t){return _.isString(t)&&t.match(e.regex)})&&c.push(e)})}),c.length?("string"==typeof t&&(t=[t]),_.each(c,function(e){var o={isEmoticon:!0,cls:e.klass};t=_.compact(_.flatten(_.map(t,function(t){if(_.isObject(t))return t;var s=t.split(e.regex),n=[];return s.forEach(function(e,t){n.push(e),t!==s.length-1&&n.push(o)}),n})))}),t):t}},{}],6:[function(t){var o=e.FrankerFaceZ,s=/\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/gm,n=/[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/,r=/^_([^_]+)_\d+$/,i=t("../constants"),a=t("../utils"),c=function(e){return e.moderator_badge?'.chat-line[data-room="'+e.id+'"] .badges .moderator { background-image:url("'+e.moderator_badge+'") !important; }':""};o.prototype.setup_room=function(){this.rooms={},this.log("Creating room style element.");var e=this._room_style=document.createElement("style");e.id="ffz-room-css",document.head.appendChild(e),this.log("Hooking the Ember Room model.");var t=App.__container__.resolve("model:room");this._modify_room(t);var o=t.instances;for(var s in o)if(o.hasOwnProperty(s)){var n=o[s];this.add_room(n.id,n),this._modify_room(n)}},o.chat_commands={},o.prototype.room_message=function(e,t){for(var o=t.split("\n"),s=0;so?this._legacy_add_room(e,t,o):void 0)})},o.prototype._legacy_load_room_css=function(e,t,o){var i=e,a=i.match(r);a&&a[1]&&(i=a[1]);var c={id:e,menu_sets:[i],sets:[i],moderator_badge:null,css:null};return o&&(o=o.replace(s,"").trim()),o&&(o=o.replace(n,function(e,t){return c.moderator_badge||"modicon.png"!==t.substr(-11)?e:(c.moderator_badge=t,"")})),c.css=o||null,this._load_room_json(e,t,c)}},{"../constants":2,"../utils":16}],7:[function(t){var o=e.FrankerFaceZ,s=/\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/gm,n=t("./constants"),r=t("./utils"),i=function(e,t,o){t&&(o.global=!0,this.global_sets.push(e))},a=function(e,t){var o=e.split(/ +/);return 2!=o.length?e:(o[0]=parseFloat(o[0]),o[1]=parseFloat(o[1]),o[0]==(t-18)/-2&&0==o[1]?null:e)};o.prototype.setup_emoticons=function(){this.log("Preparing emoticon system."),this.emote_sets={},this.global_sets=[],this._last_emote_id=0,this.log("Creating emoticon style element.");var e=this._emote_style=document.createElement("style");e.id="ffz-emoticon-css",document.head.appendChild(e),this.log("Loading global emote set."),this.load_set("global",i.bind(this,"global"))},o.ws_commands.reload_set=function(e){this.load_set(e)},o.prototype.load_set=function(e,t){return this._legacy_load_set(e,t)},o.prototype.unload_set=function(e){var t=this.emote_sets[e];if(t){this.log("Unloading emoticons for set: "+e),r.update_css(this._emote_style,e,null),delete this.emote_sets[e];for(var o=0;oo?this._legacy_load_set(e,t,o):t&&t(!1))})},o.prototype._legacy_load_css=function(e,t,o){var n={},r={id:e,emotes:n,extra_css:null},i=this;o.replace(s,function(e,t,o,s,r,c,l,d){r=parseInt(r),c=parseInt(c),l=a(l,r);var u="."===s.substr(s.lastIndexOf("/")+1,1),m=++i._last_emote_id,h={id:m,hidden:u,name:o,height:r,width:c,url:s,margins:l,extra_css:d};return n[m]=h,""}),this._load_set_json(e,t,r)}},{"./constants":2,"./utils":16}],8:[function(t){t("./shims");var o=e.FrankerFaceZ=function(){o.instance=this,this.initialize()};o.get=function(){return o.instance};var s=o.version_info={major:3,minor:0,revision:0,toString:function(){return[s.major,s.minor,s.revision].join(".")+(s.extra||"")}};o.prototype.log=function(e,t,o){e="FFZ: "+e+(o?" -- "+JSON.stringify(t):""),void 0!==t&&console.groupCollapsed&&console.dir?(console.groupCollapsed(e),console.dir(t),console.groupEnd(e)):console.log(e)},o.prototype.get_user=function(){if(e.PP&&PP.login)return PP;if(e.App){var t=App.__container__.lookup("controller:navigation");return t?t.get("userData"):void 0}},t("./socket"),t("./emoticons"),t("./badges"),t("./ember/room"),t("./ember/line"),t("./ember/chatview"),t("./debug"),t("./ui/styles"),t("./ui/notifications"),t("./ui/viewer_count"),t("./ui/menu_button"),t("./ui/menu"),o.prototype.initialize=function(t,o){var s=void 0!=e.App&&void 0!=App.__container__&&void 0!=App.__container__.resolve("model:room");return s?void this.setup(o):(t=t||10,void(o>=6e4?this.log('Twitch application not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.initialize.bind(this,t,(o||0)+t),t)))},o.prototype.setup=function(e){this.log("Found Twitch application after "+(e||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+o.version_info),this.users={};try{this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_room(),this.setup_line(),this.setup_chatview(),this.setup_css(),this.setup_menu()}catch(t){return void this.log("An error occurred while starting FrankerFaceZ: "+t)}this.log("Initialization complete.")}},{"./badges":1,"./debug":3,"./ember/chatview":4,"./ember/line":5,"./ember/room":6,"./emoticons":7,"./shims":9,"./socket":10,"./ui/menu":11,"./ui/menu_button":12,"./ui/notifications":13,"./ui/styles":14,"./ui/viewer_count":15}],9:[function(){Array.prototype.equals=function(e){if(!e)return!1;if(this.length!=e.length)return!1;for(var t=0,o=this.length;o>t;t++)if(this[t]instanceof Array&&e[t]instanceof Array){if(!this[t].equals(e[t]))return!1}else if(this[t]!=e[t])return!1;return!0}},{}],10:[function(){var t=e.FrankerFaceZ;t.prototype._ws_open=!1,t.prototype._ws_delay=0,t.ws_commands={},t.prototype.ws_create=function(){var e=this;this._ws_last_req=0,this._ws_callbacks={};var o=this._ws_sock=new WebSocket("ws://ffz.stendec.me/");o.onopen=function(){e._ws_open=!0,e._ws_delay=0,e.log("Socket connected.");var t=e.get_user();t&&e.ws_send("setuser",t.login);for(var o in e.rooms)e.ws_send("sub",o)},o.onclose=function(){e.log("Socket closed."),e._ws_open=!1,e._ws_delay<3e4&&(e._ws_delay+=5e3),setTimeout(e.ws_create.bind(e),e._ws_delay)},o.onmessage=function(o){var s,n,r=o.data.indexOf(" "),i=o.data.substr(r+1),a=parseInt(o.data.slice(0,r));if(r=i.indexOf(" "),-1===r&&(r=i.length),s=i.slice(0,r),i=i.substr(r+1),i&&(n=JSON.parse(i)),-1===a){var c=t.ws_commands[s];c?c.bind(e)(n):e.log("Invalid command: "+s,n)}else{var l="True"===s,d=e._ws_callbacks[a];e.log("Socket Reply to "+a+" - "+(l?"SUCCESS":"FAIL"),n),d&&(delete e._ws_callbacks[a],d(l,n))}}},t.prototype.ws_send=function(e,t,o){if(!this._ws_open)return!1;var s=++this._ws_last_req;return t=void 0!==t?" "+JSON.stringify(t):"",o&&(this._ws_callbacks[s]=o),this._ws_sock.send(s+" "+e+t),s}},{}],11:[function(){var t=e.FrankerFaceZ;t.prototype.setup_menu=function(){this.log("Installing mouse-up event to auto-close menus.");var e=this;jQuery(document).mouseup(function(t){var o,s=e._popup;s&&(s=jQuery(s),o=s.parent(),o.is(t.target)||0!==o.has(t.target).length||(s.remove(),delete e._popup))})},t.prototype.build_ui_popup=function(e){var t=this._popup;if(t)return t.parentElement.removeChild(t),void delete this._popup;var o=document.createElement("div"),s=document.createElement("div");o.className="emoticon-selector chat-menu ffz-ui-popup",s.className="emoticon-selector-box dropmenu",o.appendChild(s);var n=e.get("controller.currentRoom.id"),r=this.rooms[n];this.log("Menu for Room: "+n,r);var i=document.createElement("a");i.className="button glyph-only ffz-button",i.title="Advertise for FrankerFaceZ in chat!",i.href="#",i.innerHTML='';var a=document.createElement("div");a.className="list-header first",a.appendChild(i),a.appendChild(document.createTextNode("FrankerFaceZ")),s.appendChild(a);var c=this._emotes_for_sets(s,e,r&&r.menu_sets||[]);0===c?i.addEventListener("click",this._add_emote.bind(this,e,"To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com")):i.addEventListener("click",this._add_emote.bind(this,e,"To view this channel's emoticons, get FrankerFaceZ from http://www.frankerfacez.com")),this._popup=o,s.style.maxHeight=Math.max(300,e.$().height()-171)+"px",e.$(".chat-interface").append(o)},t.prototype._emotes_for_sets=function(e,t,o,s,n){if(null!=s){var r=document.createElement("div");r.className="list-header",r.appendChild(document.createTextNode(s)),n&&r.appendChild(n),e.appendChild(r)}var i=document.createElement("div"),a=0;i.className="emoticon-grid";for(var c=0;c0;n?e.classList.remove("no-emotes"):e.classList.add("no-emotes")}}},{"../constants":2}],13:[function(){var t=e.FrankerFaceZ;t.prototype.show_notification=function(t){e.noty({text:t,theme:"ffzTheme",layout:"bottomCenter",closeWith:["button"]}).show()},t.ws_commands.message=function(e){this.show_notification(e)}},{}],14:[function(t){var o=e.FrankerFaceZ,s=t("../constants");o.prototype.setup_css=function(){this.log("Injecting main FrankerFaceZ CSS.");var e=this._main_style=document.createElement("link");e.id="ffz-ui-css",e.setAttribute("rel","stylesheet"),e.setAttribute("href",s.SERVER+"script/style.css"),document.head.appendChild(e),jQuery.noty.themes.ffzTheme={name:"ffzTheme",style:function(){this.$bar.removeClass().addClass("noty_bar").addClass("ffz-noty").addClass(this.options.type)},callback:{onShow:function(){},onClose:function(){}}}}},{"../constants":2}],15:[function(t){var o=e.FrankerFaceZ,s=t("../constants"),n=t("../utils");o.ws_commands.viewers=function(e){var t=e[0],o=e[1],r=App.__container__.lookup("controller:channel"),i=r&&r.get&&r.get("id");if(i===t){var a=document.querySelector(".channel-stats .ffz.stat"),c=s.ZREKNARF+" "+n.number_commas(o);if(a)a.innerHTML=c;else{var l=document.querySelector(".channel-stats");if(!l)return;a=document.createElement("span"),a.className="ffz stat",a.title="Viewers with FrankerFaceZ",a.innerHTML=c,l.appendChild(a),jQuery(a).tipsy()}}}},{"../constants":2,"../utils":16}],16:[function(t,o){e.FrankerFaceZ,t("./constants");o.exports={update_css:function(e,t,o){var s=e.innerHTML,n="/*BEGIN "+t+"*/",r="/*END "+t+"*/",i=s.indexOf(n),a=s.indexOf(r),c=-1!==i&&-1!==a&&a>i;(c||o)&&(c&&(s=s.substr(0,i)+s.substr(a+r.length)),o&&(s+=n+o+r),e.innerHTML=s)},number_commas:function(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")}}},{"./constants":2}]},{},[8]),e.ffz=new FrankerFaceZ}(window); \ No newline at end of file diff --git a/src/badges.js b/src/badges.js new file mode 100644 index 00000000..114b69e5 --- /dev/null +++ b/src/badges.js @@ -0,0 +1,135 @@ +var FFZ = window.FrankerFaceZ, + constants = require('./constants'), + utils = require('./utils'); + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_badges = function() { + this.log("Preparing badge system."); + this.badges = {}; + + this.log("Creating badge style element."); + var s = this._badge_style = document.createElement('style'); + s.id = "ffz-badge-css"; + document.head.appendChild(s); + + this.log("Adding legacy donor badges."); + this._legacy_add_donors(); +} + + +// -------------------- +// Badge CSS +// -------------------- + +var badge_css = function(badge) { + return ".badges .ffz-badge-" + badge.id + " { background-color: " + badge.color + '; background-image: url("' + badge.image + '"); ' + (badge.extra_css || "") + '}'; +} + + +// -------------------- +// Render Badge +// -------------------- + +FFZ.prototype.render_badge = function(view) { + var user = view.get('context.model.from'), + room_id = view.get('context.parentController.content.id'), + badges = view.$('.badges'); + + var data = this.users[user]; + if ( ! data || ! data.badges ) + return; + + // Figure out where to place our badge(s). + var before = badges.find('.badge').filter(function(i) { + var t = this.title.toLowerCase(); + return t == "subscriber" || t == "turbo"; + }).first(); + + var badges_out = [], reverse = !(!before.length); + for ( var slot in data.badges ) { + if ( ! data.badges.hasOwnProperty(slot) ) + continue; + + var badge = data.badges[slot], + full_badge = this.badges[badge.id] || {}; + + var el = document.createElement('div'); + el.className = 'badge float-left tooltip ffz-badge-' + badge.id; + el.setAttribute('title', badge.title || full_badge.title); + + if ( badge.image ) + el.style.backgroundImage = 'url("' + badge.image + '")'; + + if ( badge.color ) + el.style.backgroundColor = badge.color; + + if ( badge.extra_css ) + el.style.cssText += badge.extra_css; + + badges_out.push([((reverse ? 1 : -1) * slot), el]); + } + + badges_out.sort(function(a,b){return a[0] - b[0]}); + + if ( reverse ) { + while(badges_out.length) + before.before(badges_out.shift()[1]); + } else { + while(badges_out.length) + badges.append(badges_out.shift()[1]); + } +} + + +// -------------------- +// Legacy Support +// -------------------- + +FFZ.prototype._legacy_add_donors = function(tries) { + this.badges[1] = {id: 1, title: "FFZ Donor", color: "#755000", image: "http://cdn.frankerfacez.com/channel/global/donoricon.png"}; + utils.update_css(this._badge_style, 1, badge_css(this.badges[1])); + + // Developer Badges + // TODO: Upload the badge to the proper CDN. + this.badges[0] = {id: 0, title: "FFZ Developer", color: "#FAAF19", image: "http://sir.stendec.me/devicon.png"}; + utils.update_css(this._badge_style, 0, badge_css(this.badges[0])); + this.users.sirstendec = {badges: {0: {id:0}}}; + + jQuery.ajax(constants.SERVER + "script/donors.txt", {cache: false, context: this}) + .done(function(data) { + this._legacy_parse_donors(data); + + }).fail(function(data) { + if ( data.status == 404 ) + return; + + tries = (tries || 0) + 1; + if ( tries < 10 ) + return this._legacy_add_donors(tries); + }); +} + + +FFZ.prototype._legacy_parse_donors = function(data) { + var count = 0; + if ( data != null ) { + var lines = data.trim().split(/\W+/); + for(var i=0; i < lines.length; i++) { + var user_id = lines[i], + user = this.users[user_id] = this.users[user_id] || {}, + badges = user.badges = user.badges || {}; + + if ( badges[0] ) + continue; + + badges[0] = {id:1}; + count += 1; + } + } + + this.log("Added donor badge to " + utils.number_commas(count) + " users."); +} \ No newline at end of file diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 00000000..94047010 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,11 @@ +var SVGPATH = '', + DEBUG = localStorage.ffzDebugMode == "true"; + +module.exports = { + DEBUG: DEBUG, + SERVER: DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/", + + SVGPATH: SVGPATH, + ZREKNARF: '' + SVGPATH + '', + CHAT_BUTTON: '' + SVGPATH + '' +} \ No newline at end of file diff --git a/src/debug.js b/src/debug.js new file mode 100644 index 00000000..1a87ef40 --- /dev/null +++ b/src/debug.js @@ -0,0 +1,22 @@ +var FFZ = window.FrankerFaceZ; + + +// -------------------- +// Debug Command +// -------------------- + +FFZ.chat_commands.debug = function(room, args) { + var enabled, args = args && args.length ? args[0].toLowerCase() : null; + if ( args == "y" || args == "yes" || args == "true" || args == "on" ) + enabled = true; + else if ( args == "n" || args == "no" || args == "false" || args == "off" ) + enabled = false; + + if ( enabled === undefined ) + enabled = !(localStorage.ffzDebugMode == "true"); + + localStorage.ffzDebugMode = enabled; + return "Debug Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; +} + +FFZ.chat_commands.debug.help = "Usage: /ffz debug [on|off]\nEnable or disable Debug Mode. When Debug Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."; \ No newline at end of file diff --git a/src/ember/chatview.js b/src/ember/chatview.js new file mode 100644 index 00000000..66a199c4 --- /dev/null +++ b/src/ember/chatview.js @@ -0,0 +1,55 @@ +var FFZ = window.FrankerFaceZ; + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_chatview = function() { + this.log("Hooking the Ember Chat view."); + + var Chat = App.__container__.resolve('view:chat'); + this._modify_cview(Chat); + + // For some reason, this doesn't work unless we create an instance of the + // chat view and then destroy it immediately. + Chat.create().destroy(); + + // Modify all existing Chat views. + for(var key in Ember.View.views) { + if ( ! Ember.View.views.hasOwnProperty(key) ) + continue; + + var view = Ember.View.views[key]; + if ( !(view instanceof Chat) ) + continue; + + this.log("Adding UI link manually to Chat view.", view); + view.$('.textarea-contain').append(this.build_ui_link(view)); + } +} + + +// -------------------- +// Modify Chat View +// -------------------- + +FFZ.prototype._modify_cview = function(view) { + var f = this; + + view.reopen({ + didInsertElement: function() { + this._super(); + this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); + }, + + willClearRender: function() { + this._super(); + this.$(".ffz-ui-toggle").remove(); + }, + + ffzUpdateLink: Ember.observer('controller.currentRoom', function() { + f.update_ui_link(); + }) + }); +} \ No newline at end of file diff --git a/src/ember/line.js b/src/ember/line.js new file mode 100644 index 00000000..47f33df8 --- /dev/null +++ b/src/ember/line.js @@ -0,0 +1,100 @@ +var FFZ = window.FrankerFaceZ; + + +// --------------------- +// Initialization +// --------------------- + +FFZ.prototype.setup_line = function() { + this.log("Hooking the Ember Line controller."); + + var Line = App.__container__.resolve('controller:line'), + f = this; + + Line.reopen({ + tokenizedMessage: function() { + // Add our own step to the tokenization procedure. + return f._emoticonize(this, this._super()); + + }.property("model.message", "isModeratorOrHigher", "controllers.emoticons.emoticons.[]") + // TODO: Copy the new properties from the new Twitch! + }); + + + this.log("Hooking the Ember Line view."); + var Line = App.__container__.resolve('view:line'); + + Line.reopen({ + didInsertElement: function() { + this._super(); + + var el = this.get('element'); + el.setAttribute('data-room', this.get('context.parentController.content.id')); + el.setAttribute('data-sender', this.get('context.model.from')); + + f.render_badge(this); + } + }); +} + + +// --------------------- +// Emoticon Replacement +// --------------------- + +FFZ.prototype._emoticonize = function(controller, tokens) { + var room_id = controller.get("parentController.model.id"), + user_id = controller.get("model.from"), + user = this.users[user_id], + room = this.rooms[room_id], + f = this; + + // Get our sets. + var sets = _.union(user && user.sets || [], room && room.sets || [], f.global_sets), + emotes = []; + + // Build a list of emotes that match. + _.each(sets, function(set_id) { + var set = f.emote_sets[set_id]; + if ( ! set ) + return; + + _.each(set.emotes, function(emote) { + _.any(tokens, function(token) { + return _.isString(token) && token.match(emote.regex); + }) && emotes.push(emote); + }); + }); + + // Don't bother proceeding if we have no emotes. + if ( ! emotes.length ) + return tokens; + + // Now that we have all the matching tokens, do crazy stuff. + if ( typeof tokens == "string" ) + tokens = [tokens]; + + // This is weird stuff I basically copied from the old Twitch code. + // Here, for each emote, we split apart every text token and we + // put it back together with the matching bits of text replaced + // with an object telling Twitch's line template how to render the + // emoticon. + _.each(emotes, function(emote) { + var eo = {isEmoticon:true, cls: emote.klass}; + + tokens = _.compact(_.flatten(_.map(tokens, function(token) { + if ( _.isObject(token) ) + return token; + + var tbits = token.split(emote.regex), bits = []; + tbits.forEach(function(val, ind) { + bits.push(val); + if ( ind !== tbits.length - 1 ) + bits.push(eo); + }); + return bits; + }))); + }); + + return tokens; +} \ No newline at end of file diff --git a/src/ember/room.js b/src/ember/room.js new file mode 100644 index 00000000..4a702d76 --- /dev/null +++ b/src/ember/room.js @@ -0,0 +1,285 @@ +var FFZ = window.FrankerFaceZ, + CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/mg, + MOD_CSS = /[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/, + GROUP_CHAT = /^_([^_]+)_\d+$/, + constants = require('../constants'), + utils = require('../utils'), + + + moderator_css = function(room) { + if ( ! room.moderator_badge ) + return ""; + + return '.chat-line[data-room="' + room.id + '"] .badges .moderator { background-image:url("' + room.moderator_badge + '") !important; }'; + } + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_room = function() { + this.rooms = {}; + + this.log("Creating room style element."); + var s = this._room_style = document.createElement("style"); + s.id = "ffz-room-css"; + document.head.appendChild(s); + + this.log("Hooking the Ember Room model."); + + var Room = App.__container__.resolve('model:room'); + this._modify_room(Room); + + // Modify all current instances of Room, as the changes to the base + // class won't be inherited automatically. + var instances = Room.instances; + for(var key in instances) { + if ( ! instances.hasOwnProperty(key) ) + continue; + + var inst = instances[key]; + this.add_room(inst.id, inst); + this._modify_room(inst); + } +} + + +// -------------------- +// Command System +// -------------------- + +FFZ.chat_commands = {}; + + +FFZ.prototype.room_message = function(room, text) { + var lines = text.split("\n"); + for(var i=0; i < lines.length; i++) + room.room.addMessage({style: 'ffz admin', from: 'FFZ', message: lines[i]}); +} + + +FFZ.prototype.run_command = function(text, room_id) { + var room = this.rooms[room_id]; + if ( ! room || !room.room ) + return; + + if ( ! text ) + text = "help"; + + var args = text.split(" "), + cmd = args.shift().toLowerCase(); + + this.log("Received Command: " + cmd, args, true); + + var command = FFZ.chat_commands[cmd], output; + if ( command ) { + try { + output = command.bind(this)(room, args); + } catch(err) { + this.log("Error Running Command - " + cmd + ": " + err, room); + output = "There was an error running the command."; + } + } else + output = 'There is no "' + cmd + '" command.'; + + if ( output ) + this.room_message(room, output); +} + + +FFZ.chat_commands.help = function(room, args) { + if ( args && args.length ) { + var command = FFZ.chat_commands[args[0].toLowerCase()]; + if ( ! command ) + return 'There is no "' + args[0] + '" command.'; + + else if ( ! command.help ) + return 'No help is available for the command "' + args[0] + '".'; + + else + return command.help; + } + + var cmds = []; + for(var c in FFZ.chat_commands) + FFZ.chat_commands.hasOwnProperty(c) && cmds.push(c); + + return "The available commands are: " + cmds.join(", "); +} + +FFZ.chat_commands.help.help = "Usage: /ffz help [command]\nList available commands, or show help for a specific command."; + + +// -------------------- +// Room Management +// -------------------- + +FFZ.prototype.add_room = function(id, room) { + if ( this.rooms[id] ) + return this.log("Tried to add existing room: " + id); + + this.log("Adding Room: " + id); + + // Create a basic data table for this room. + this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null}; + + // Let the server know where we are. + this.ws_send("sub", id); + + // For now, we use the legacy function to grab the .css file. + this._legacy_add_room(id); +} + + +FFZ.prototype.remove_room = function(id) { + var room = this.rooms[id]; + if ( ! room ) + return; + + this.log("Removing Room: " + id); + + // Remove the CSS + if ( room.css || room.moderator_badge ) + utils.update_css(this._room_style, id, null); + + // Let the server know we're gone and delete our data for this room. + this.ws_send("unsub", id); + delete this.rooms[id]; + + // Clean up sets we aren't using any longer. + for(var i=0; i < room.sets.length; i++) { + var set_id = room.sets[i], set = this.emote_sets[set_id]; + if ( ! set ) + continue; + + set.users.removeObject(id); + if ( !set.global && !set.users.length ) + this.unload_set(set_id); + } +} + + +// -------------------- +// Receiving Set Info +// -------------------- + +FFZ.prototype.load_room = function(room_id, callback) { + return this._legacy_load_room(room_id, callback); +} + + +FFZ.prototype._load_room_json = function(room_id, callback, data) { + // Preserve the pointer to the Room instance. + if ( this.rooms[room_id] ) + data.room = this.rooms[room_id].room; + + this.rooms[room_id] = data; + + if ( data.css || data.moderator_badge ) + utils.update_css(this._room_style, room_id, moderator_css(data) + (data.css||"")); + + for(var i=0; i < data.sets.length; i++) { + var set_id = data.sets[i]; + if ( ! this.emote_sets.hasOwnProperty(set_id) ) + this.load_set(set_id); + } + + this.update_ui_link(); + + if ( callback ) + callback(true, data); +} + + +/*FFZ.ws_commands.sets_for_room = function(data) { + var room = this.rooms[data.room]; + if ( ! room ) + return; + + for(var i=0; i < data.sets.length; i++) { + var set = data.sets[i]; + if ( room.sets.contains(set) ) + continue; + + room.sets.push(set); + this.load_set(set); + } +}*/ + + +// -------------------- +// Ember Modifications +// -------------------- + +FFZ.prototype._modify_room = function(room) { + var f = this; + room.reopen({ + init: function() { + this._super(); + f.add_room(this.id, this); + }, + + willDestroy: function() { + this._super(); + f.remove_room(this.id); + }, + + send: function(text) { + var cmd = text.split(' ', 1)[0].toLowerCase(); + if ( cmd === "/ffz" ) { + this.set("messageToSend", ""); + f.run_command(text.substr(5), this.get('id')); + } else + return this._super(text); + } + }); +} + + +// -------------------- +// Legacy Data Support +// -------------------- + +FFZ.prototype._legacy_add_room = function(room_id, callback, tries) { + jQuery.ajax(constants.SERVER + "channel/" + room_id + ".css", {cache: false, context:this}) + .done(function(data) { + this._legacy_load_room_css(room_id, callback, data); + + }).fail(function(data) { + if ( data.status == 404 ) + return this._legacy_load_room_css(room_id, callback, null); + + tries = tries || 0; + tries++; + if ( tries < 10 ) + return this._legacy_add_room(room_id, callback, tries); + }); +} + + +FFZ.prototype._legacy_load_room_css = function(room_id, callback, data) { + var set_id = room_id, + match = set_id.match(GROUP_CHAT); + + if ( match && match[1] ) + set_id = match[1]; + + var output = {id: room_id, menu_sets: [set_id], sets: [set_id], moderator_badge: null, css: null}; + + if ( data ) + data = data.replace(CSS, "").trim(); + + if ( data ) { + data = data.replace(MOD_CSS, function(match, url) { + if ( output.moderator_badge || url.substr(-11) !== 'modicon.png' ) + return match; + + output.moderator_badge = url; + return ""; + }); + } + + output.css = data || null; + return this._load_room_json(room_id, callback, output); +} \ No newline at end of file diff --git a/src/emoticons.js b/src/emoticons.js new file mode 100644 index 00000000..b38b7759 --- /dev/null +++ b/src/emoticons.js @@ -0,0 +1,149 @@ +var FFZ = window.FrankerFaceZ, + CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/mg, + constants = require('./constants'), + utils = require('./utils'); + + +var loaded_global = function(set_id, success, data) { + if ( ! success ) + return; + + data.global = true; + this.global_sets.push(set_id); +} + +var check_margins = function(margins, height) { + var mlist = margins.split(/ +/); + if ( mlist.length != 2 ) + return margins; + + mlist[0] = parseFloat(mlist[0]); + mlist[1] = parseFloat(mlist[1]); + + if ( mlist[0] == (height - 18) / -2 && mlist[1] == 0 ) + return null; + + return margins; +} + + +FFZ.prototype.setup_emoticons = function() { + this.log("Preparing emoticon system."); + + this.emote_sets = {}; + this.global_sets = []; + this._last_emote_id = 0; + + this.log("Creating emoticon style element."); + var s = this._emote_style = document.createElement('style'); + s.id = "ffz-emoticon-css"; + document.head.appendChild(s); + + this.log("Loading global emote set."); + this.load_set("global", loaded_global.bind(this, "global")); +} + + + +FFZ.ws_commands.reload_set = function(set_id) { + this.load_set(set_id); +} + + +FFZ.prototype.load_set = function(set_id, callback) { + return this._legacy_load_set(set_id, callback); +} + + +FFZ.prototype.unload_set = function(set_id) { + var set = this.emote_sets[set_id]; + if ( ! set ) + return; + + this.log("Unloading emoticons for set: " + set_id); + + utils.update_css(this._emote_style, set_id, null); + delete this.emote_sets[set_id]; + + for(var i=0; i < set.users.length; i++) { + var room = this.rooms[set.users[i]]; + if ( room ) + room.sets.removeObject(set_id); + } +} + + +var build_css = function(emote) { + var margin = emote.margins; + if ( ! margin ) + margin = ((emote.height - 18) / -2) + "px 0"; + return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.url + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (emote.extra_css ? "; " + emote.extra_css : "") + "}\n"; +} + +FFZ.prototype._load_set_json = function(set_id, callback, data) { + // Store our set. + this.emote_sets[set_id] = data; + data.users = []; + data.global = false; + + // Iterate through all the emoticons, building CSS and regex objects as appropriate. + var output_css = ""; + + for(var key in data.emotes) { + if ( ! data.emotes.hasOwnProperty(key) ) + continue; + + var emote = data.emotes[key]; + emote.klass = "ffz-emote-" + emote.id; + + if ( emote.name[emote.name.length-1] === "!" ) + emote.regex = new RegExp("\\b" + emote.name + "(?=\\W|$)", "g"); + else + emote.regex = new RegExp("\\b" + emote.name + "\\b", "g"); + + output_css += build_css(emote); + } + + utils.update_css(this._emote_style, set_id, output_css + (data.extra_css || "")); + this.log("Updated emoticons for set: " + set_id, data); + + if ( callback ) + callback(true, data); +} + + +FFZ.prototype._legacy_load_set = function(set_id, callback, tries) { + jQuery.ajax(constants.SERVER + "channel/" + set_id + ".css", {cache: false, context:this}) + .done(function(data) { + this._legacy_load_css(set_id, callback, data); + + }).fail(function(data) { + if ( data.status == 404 ) + return callback && callback(false); + + tries = tries || 0; + tries++; + if ( tries < 10 ) + return this._legacy_load_set(set_id, callback, tries); + + return callback && callback(false); + }); +} + + +FFZ.prototype._legacy_load_css = function(set_id, callback, data) { + var emotes = {}, output = {id: set_id, emotes: emotes, extra_css: null}, f = this; + + data.replace(CSS, function(match, klass, name, path, height, width, margins, extra) { + height = parseInt(height); width = parseInt(width); + margins = check_margins(margins, height); + var hidden = path.substr(path.lastIndexOf("/") + 1, 1) === ".", + id = ++f._last_emote_id, + emote = {id: id, hidden: hidden, name: name, height: height, width: width, url: path, margins: margins, extra_css: extra}; + + emotes[id] = emote; + return ""; + }); + + this._load_set_json(set_id, callback, output); +} \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..9028c3e7 --- /dev/null +++ b/src/main.js @@ -0,0 +1,128 @@ +// Modify Array and others. +require('./shims'); + + +// ---------------- +// The Constructor +// ---------------- + +var FFZ = window.FrankerFaceZ = function() { + FFZ.instance = this; + + // Get things started. + this.initialize(); +} + + +FFZ.get = function() { return FFZ.instance; } + + +// Version +var VER = FFZ.version_info = { + major: 3, minor: 0, revision: 0, + toString: function() { + return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); + } +} + + +// Logging + +FFZ.prototype.log = function(msg, data, to_json) { + msg = "FFZ: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); + if ( data !== undefined && console.groupCollapsed && console.dir ) { + console.groupCollapsed(msg); + console.dir(data); + console.groupEnd(msg); + } else + console.log(msg); +} + + +// ------------------- +// User Data +// ------------------- + +FFZ.prototype.get_user = function() { + if ( window.PP && PP.login ) { + return PP; + } else if ( window.App ) { + var nc = App.__container__.lookup("controller:navigation"); + return nc ? nc.get("userData") : undefined; + } +} + + +// ------------------- +// Import Everything! +// ------------------- + +require('./socket'); +require('./emoticons'); +require('./badges'); + +require('./ember/room'); +require('./ember/line'); +require('./ember/chatview'); + +require('./debug'); + +require('./ui/styles'); +require('./ui/notifications'); +require('./ui/viewer_count'); + +require('./ui/menu_button'); +require('./ui/menu'); + +// --------------- +// Initialization +// --------------- + +FFZ.prototype.initialize = function(increment, delay) { + // Make sure that FrankerFaceZ doesn't start setting itself up until the + // Twitch ember application is ready. + + // TODO: Special Dashboard check. + + var loaded = window.App != undefined && + App.__container__ != undefined && + App.__container__.resolve('model:room') != undefined; + + if ( !loaded ) { + increment = increment || 10; + if ( delay >= 60000 ) + this.log("Twitch application not detected in \"" + location.toString() + "\". Aborting."); + else + setTimeout(this.initialize.bind(this, increment, (delay||0) + increment), + increment); + return; + } + + this.setup(delay); +} + + +FFZ.prototype.setup = function(delay) { + this.log("Found Twitch application after " + (delay||0) + " ms in \"" + location + "\". Initializing FrankerFaceZ version " + FFZ.version_info); + + this.users = {}; + + try { + this.ws_create(); + this.setup_emoticons(); + this.setup_badges(); + + this.setup_room(); + this.setup_line(); + this.setup_chatview(); + + this.setup_css(); + this.setup_menu(); + + } catch(err) { + this.log("An error occurred while starting FrankerFaceZ: " + err); + return; + } + + this.log("Initialization complete."); +} \ No newline at end of file diff --git a/src/shims.js b/src/shims.js new file mode 100644 index 00000000..e062c67f --- /dev/null +++ b/src/shims.js @@ -0,0 +1,24 @@ +Array.prototype.equals = function (array) { + // if the other array is a falsy value, return + if (!array) + return false; + + // compare lengths - can save a lot of time + if (this.length != array.length) + return false; + + for (var i = 0, l=this.length; i < l; i++) { + // Check if we have nested arrays + if (this[i] instanceof Array && array[i] instanceof Array) { + // recurse into the nested arrays + if (!this[i].equals(array[i])) + return false; + } + else if (this[i] != array[i]) { + // Warning - two different object instances will never be equal: {x:20} != {x:20} + return false; + } + } + return true; +} + diff --git a/src/socket.js b/src/socket.js new file mode 100644 index 00000000..e72a2cb4 --- /dev/null +++ b/src/socket.js @@ -0,0 +1,93 @@ +var FFZ = window.FrankerFaceZ; + +FFZ.prototype._ws_open = false; +FFZ.prototype._ws_delay = 0; + +FFZ.ws_commands = {}; + + +// ---------------- +// Socket Creation +// ---------------- + +FFZ.prototype.ws_create = function() { + var f = this; + + this._ws_last_req = 0; + this._ws_callbacks = {}; + + var ws = this._ws_sock = new WebSocket("ws://ffz.stendec.me/"); + + ws.onopen = function(e) { + f._ws_open = true; + f._ws_delay = 0; + f.log("Socket connected."); + + var user = f.get_user(); + if ( user ) + f.ws_send("setuser", user.login); + + // Send the current rooms. + for(var room_id in f.rooms) + f.ws_send("sub", room_id); + } + + ws.onclose = function(e) { + f.log("Socket closed."); + f._ws_open = false; + + // We never ever want to not have a socket. + if ( f._ws_delay < 30000 ) + f._ws_delay += 5000; + + setTimeout(f.ws_create.bind(f), f._ws_delay); + } + + ws.onmessage = function(e) { + // Messages are formatted as REQUEST_ID SUCCESS/FUNCTION_NAME[ JSON_DATA] + var cmd, data, ind = e.data.indexOf(" "), + msg = e.data.substr(ind + 1), + request = parseInt(e.data.slice(0, ind)); + + ind = msg.indexOf(" "); + if ( ind === -1 ) + ind = msg.length; + + cmd = msg.slice(0, ind); + msg = msg.substr(ind + 1); + if ( msg ) + data = JSON.parse(msg); + + if ( request === -1 ) { + // It's a command from the server. + var command = FFZ.ws_commands[cmd]; + if ( command ) + command.bind(f)(data); + else + f.log("Invalid command: " + cmd, data); + + } else { + var success = cmd === 'True', + callback = f._ws_callbacks[request]; + f.log("Socket Reply to " + request + " - " + (success ? "SUCCESS" : "FAIL"), data); + if ( callback ) { + delete f._ws_callbacks[request]; + callback(success, data); + } + } + } +} + + +FFZ.prototype.ws_send = function(func, data, callback) { + if ( ! this._ws_open ) return false; + + var request = ++this._ws_last_req; + data = data !== undefined ? " " + JSON.stringify(data) : ""; + + if ( callback ) + this._ws_callbacks[request] = callback; + + this._ws_sock.send(request + " " + func + data); + return request; +} \ No newline at end of file diff --git a/src/ui/menu.js b/src/ui/menu.js new file mode 100644 index 00000000..f4e19818 --- /dev/null +++ b/src/ui/menu.js @@ -0,0 +1,134 @@ +var FFZ = window.FrankerFaceZ; + + +// -------------------- +// Initializer +// -------------------- + +FFZ.prototype.setup_menu = function() { + this.log("Installing mouse-up event to auto-close menus."); + var f = this; + + jQuery(document).mouseup(function(e) { + var popup = f._popup, parent; + if ( ! popup ) return; + popup = jQuery(popup); + parent = popup.parent(); + + if ( ! parent.is(e.target) && parent.has(e.target).length === 0 ) { + popup.remove(); + delete f._popup; + } + }); +} + + +// -------------------- +// Create Menu +// -------------------- + +FFZ.prototype.build_ui_popup = function(view) { + var popup = this._popup; + if ( popup ) { + popup.parentElement.removeChild(popup); + delete this._popup; + return; + } + + // Start building the DOM. + var container = document.createElement('div'), + inner = document.createElement('div'); + + container.className = 'emoticon-selector chat-menu ffz-ui-popup'; + inner.className = 'emoticon-selector-box dropmenu'; + container.appendChild(inner); + + // TODO: Modularize for multiple menu pages! + + // Get the current room. + var room_id = view.get('controller.currentRoom.id'), + room = this.rooms[room_id]; + + this.log("Menu for Room: " + room_id, room); + + // Add the header and ad button. + var btn = document.createElement('a'); + btn.className = 'button glyph-only ffz-button'; + btn.title = 'Advertise for FrankerFaceZ in chat!'; + btn.href = '#'; + btn.innerHTML = ''; + + var hdr = document.createElement('div'); + hdr.className = 'list-header first'; + hdr.appendChild(btn); + hdr.appendChild(document.createTextNode('FrankerFaceZ')); + inner.appendChild(hdr); + + var c = this._emotes_for_sets(inner, view, room && room.menu_sets || []); + + if ( c === 0 ) + btn.addEventListener('click', this._add_emote.bind(this, view, "To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com")); + else + btn.addEventListener('click', this._add_emote.bind(this, view, "To view this channel's emoticons, get FrankerFaceZ from http://www.frankerfacez.com")); + + + // Add the menu to the DOM. + this._popup = container; + inner.style.maxHeight = Math.max(300, view.$().height() - 171) + "px"; + view.$('.chat-interface').append(container); +} + + +// -------------------- +// Emotes for Sets +// -------------------- + +FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) { + if ( header != null ) { + var el_header = document.createElement('div'); + el_header.className = 'list-header'; + el_header.appendChild(document.createTextNode(header)); + + if ( btn ) + el_header.appendChild(btn); + + parent.appendChild(el_header); + } + + var grid = document.createElement('div'), c = 0; + grid.className = 'emoticon-grid'; + + for(var i=0; i < sets.length; i++) { + var set = this.emote_sets[sets[i]]; + for(var eid in set.emotes) { + var emote = set.emotes[eid]; + if ( !set.emotes.hasOwnProperty(eid) || emote.hidden ) + continue; + + c++; + var s = document.createElement('span'); + s.className = 'emoticon ' + emote.klass + ' tooltip'; + s.title = emote.name; + s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); + grid.appendChild(s); + } + } + + if ( !c ) { + grid.innerHTML = "This channel has no emoticons."; + grid.className = "chat-menu-content ffz-no-emotes center"; + } + + parent.appendChild(grid); +} + + +FFZ.prototype._add_emote = function(view, emote) { + var room = view.get('controller.currentRoom'), + current_text = room.get('messageToSend') || ''; + + if ( current_text && current_text.substr(-1) !== " " ) + current_text += ' '; + + room.set('messageToSend', current_text + (emote.name || emote)); +} \ No newline at end of file diff --git a/src/ui/menu_button.js b/src/ui/menu_button.js new file mode 100644 index 00000000..573eb94e --- /dev/null +++ b/src/ui/menu_button.js @@ -0,0 +1,35 @@ +var FFZ = window.FrankerFaceZ, + constants = require('../constants'); + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.build_ui_link = function(view) { + // TODO: Detect dark mode from BTTV. + + var link = document.createElement('a'); + link.className = 'ffz-ui-toggle'; + link.innerHTML = constants.CHAT_BUTTON; + + link.addEventListener('click', this.build_ui_popup.bind(this, view)); + + this.update_ui_link(link); + return link; +} + + +FFZ.prototype.update_ui_link = function(link) { + var controller = App.__container__.lookup('controller:chat'); + link = link || document.querySelector('a.ffz-ui-toggle'); + if ( !link || !controller ) return; + + var room_id = controller.get('currentRoom.id'), + room = this.rooms[room_id], + has_emotes = room && room.sets.length > 0; + + if ( has_emotes ) + link.classList.remove('no-emotes'); + else + link.classList.add('no-emotes'); +} \ No newline at end of file diff --git a/src/ui/notifications.js b/src/ui/notifications.js new file mode 100644 index 00000000..06d39059 --- /dev/null +++ b/src/ui/notifications.js @@ -0,0 +1,15 @@ +var FFZ = window.FrankerFaceZ; + +FFZ.prototype.show_notification = function(message) { + window.noty({ + text: message, + theme: "ffzTheme", + layout: "bottomCenter", + closeWith: ["button"] + }).show(); +} + + +FFZ.ws_commands.message = function(message) { + this.show_notification(message); +} \ No newline at end of file diff --git a/src/ui/styles.js b/src/ui/styles.js new file mode 100644 index 00000000..6be8a1b2 --- /dev/null +++ b/src/ui/styles.js @@ -0,0 +1,24 @@ +var FFZ = window.FrankerFaceZ, + constants = require('../constants'); + +FFZ.prototype.setup_css = function() { + this.log("Injecting main FrankerFaceZ CSS."); + + var s = this._main_style = document.createElement('link'); + + s.id = "ffz-ui-css"; + s.setAttribute('rel', 'stylesheet'); + s.setAttribute('href', constants.SERVER + "script/style.css"); + document.head.appendChild(s); + + jQuery.noty.themes.ffzTheme = { + name: "ffzTheme", + style: function() { + this.$bar.removeClass().addClass("noty_bar").addClass("ffz-noty").addClass(this.options.type); + }, + callback: { + onShow: function() {}, + onClose: function() {} + } + }; +} \ No newline at end of file diff --git a/src/ui/viewer_count.js b/src/ui/viewer_count.js new file mode 100644 index 00000000..5d129073 --- /dev/null +++ b/src/ui/viewer_count.js @@ -0,0 +1,36 @@ +var FFZ = window.FrankerFaceZ, + constants = require('../constants'), + utils = require('../utils'); + +// ------------ +// Set Viewers +// ------------ + +FFZ.ws_commands.viewers = function(data) { + var channel = data[0], count = data[1]; + + var controller = App.__container__.lookup('controller:channel'), + id = controller && controller.get && controller.get('id'); + + if ( id !== channel ) + return; + + var view_count = document.querySelector('.channel-stats .ffz.stat'), + content = constants.ZREKNARF + ' ' + utils.number_commas(count); + + if ( view_count ) + view_count.innerHTML = content; + else { + var parent = document.querySelector('.channel-stats'); + if ( ! parent ) + return; + + view_count = document.createElement('span'); + view_count.className = 'ffz stat'; + view_count.title = 'Viewers with FrankerFaceZ'; + view_count.innerHTML = content; + + parent.appendChild(view_count); + jQuery(view_count).tipsy(); + } +} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 00000000..c3a1ec9c --- /dev/null +++ b/src/utils.js @@ -0,0 +1,30 @@ +var FFZ = window.FrankerFaceZ, + constants = require('./constants'); + +module.exports = { + update_css: function(element, id, css) { + var all = element.innerHTML, + start = "/*BEGIN " + id + "*/", + end = "/*END " + id + "*/", + s_ind = all.indexOf(start), + e_ind = all.indexOf(end), + found = s_ind !== -1 && e_ind !== -1 && e_ind > s_ind; + + if ( !found && !css ) + return; + + if ( found ) + all = all.substr(0, s_ind) + all.substr(e_ind + end.length); + + if ( css ) + all += start + css + end; + + element.innerHTML = all; + }, + + number_commas: function(x) { + var parts = x.toString().split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return parts.join("."); + } +} \ No newline at end of file diff --git a/test/server.js b/test/server.js new file mode 100644 index 00000000..f20fdde6 --- /dev/null +++ b/test/server.js @@ -0,0 +1,39 @@ +var fs = require("fs"), + http = require("http"), + path = require("path"), + request = require("request"), + url = require("url"); + +http.createServer(function(req, res) { + var uri = url.parse(req.url).pathname, + lpath = path.join(uri).split(path.sep); + + if ( ! lpath[0] ) + lpath.shift(); + + if ( lpath[0] == "script" ) + lpath.shift(); + else + lpath.splice(0, 0, "cdn"); + + var file = path.join(process.cwd(), lpath.join(path.sep)); + + fs.exists(file, function(exists) { + if ( ! exists ) { + console.log("[CDN] GET " + uri); + return request.get("http://cdn.frankerfacez.com/" + uri).pipe(res); + } + + if ( fs.lstatSync(file).isDirectory() ) { + console.log("[403] GET " + uri); + res.writeHead(403); + res.write('403 Forbidden'); + return res.end(); + } + + console.log("[200] GET " + uri); + res.writeHead(200, {"Access-Control-Allow-Origin": "*"}); + fs.createReadStream(file).pipe(res); + }); + +}).listen(8000); \ No newline at end of file