diff --git a/script.js b/script.js index a83570de..04c94e3b 100644 --- a/script.js +++ b/script.js @@ -190,15 +190,29 @@ FFZ.prototype._legacy_parse_donors = function(data) { this.log("Added donor badge to " + utils.number_commas(count) + " users."); } -},{"./constants":3,"./utils":25}],2:[function(require,module,exports){ +},{"./constants":3,"./utils":24}],2:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; +// ----------------- +// Log Export +// ----------------- + +FFZ.ffz_commands.log = function(room, args) { + this._pastebin(this._log_data.join("\n"), function(url) { + if ( ! url ) + return this.room_message(room, "There was an error uploading the FrankerFaceZ log."); + + this.room_message(room, "Your FrankerFaceZ log has been pasted to: " + url); + }); +}; + + // ----------------- // Mass Moderation // ----------------- -FFZ.chat_commands.massunmod = function(room, args) { +FFZ.ffz_commands.massunmod = function(room, args) { args = args.join(" ").trim(); if ( ! args.length ) @@ -222,10 +236,10 @@ FFZ.chat_commands.massunmod = function(room, args) { return "Sent unmod command for " + count + " users."; } -FFZ.chat_commands.massunmod.help = "Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list."; +FFZ.ffz_commands.massunmod.help = "Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list."; -FFZ.chat_commands.massmod = function(room, args) { +FFZ.ffz_commands.massmod = function(room, args) { args = args.join(" ").trim(); if ( ! args.length ) @@ -240,7 +254,6 @@ FFZ.chat_commands.massmod = function(room, args) { if ( args.length > 50 ) return "Each user you mod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses."; - var count = args.length; while(args.length) { var name = args.shift(); @@ -250,7 +263,7 @@ FFZ.chat_commands.massmod = function(room, args) { return "Sent mod command for " + count + " users."; } -FFZ.chat_commands.massmod.help = "Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."; +FFZ.ffz_commands.massmod.help = "Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."; },{}],3:[function(require,module,exports){ var SVGPATH = '', DEBUG = localStorage.ffzDebugMode == "true" && document.body.classList.contains('ffz-dev'); @@ -274,7 +287,7 @@ var FFZ = window.FrankerFaceZ; // Developer Mode Command // ----------------------- -FFZ.chat_commands.developer_mode = function(room, args) { +FFZ.ffz_commands.developer_mode = function(room, args) { var enabled, args = args && args.length ? args[0].toLowerCase() : null; if ( args == "y" || args == "yes" || args == "true" || args == "on" ) enabled = true; @@ -288,7 +301,7 @@ FFZ.chat_commands.developer_mode = function(room, args) { return "Developer Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; } -FFZ.chat_commands.developer_mode.help = "Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."; +FFZ.ffz_commands.developer_mode.help = "Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."; },{}],5:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -318,7 +331,11 @@ FFZ.prototype.setup_chatview = function() { continue; this.log("Adding UI link manually to Chat view.", view); - view.$('.textarea-contain').append(this.build_ui_link(view)); + try { + view.$('.textarea-contain').append(this.build_ui_link(view)); + } catch(err) { + this.error("setup: build_ui_link: " + err); + } } } @@ -333,21 +350,34 @@ FFZ.prototype._modify_cview = function(view) { view.reopen({ didInsertElement: function() { this._super(); - this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); + try { + this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); + } catch(err) { + f.error("didInsertElement: build_ui_link: " + err); + } }, willClearRender: function() { this._super(); - this.$(".ffz-ui-toggle").remove(); + try { + this.$(".ffz-ui-toggle").remove(); + } catch(err) { + f.error("willClearRender: remove ui link: " + err); + } }, ffzUpdateLink: Ember.observer('controller.currentRoom', function() { - f.update_ui_link(); + try { + f.update_ui_link(); + } catch(err) { + f.error("ffzUpdateLink: update_ui_link: " + err); + } }) }); } },{}],6:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, + utils = require("../utils"), reg_escape = function(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); @@ -362,6 +392,7 @@ FFZ.settings_info.capitalize = { type: "boolean", value: true, + category: "Chat", visible: function() { return ! this.has_bttv }, name: "Username Capitalization", @@ -373,6 +404,7 @@ FFZ.settings_info.keywords = { type: "button", value: [], + category: "Chat", visible: function() { return ! this.has_bttv }, name: "Highlight Keywords", @@ -382,7 +414,7 @@ FFZ.settings_info.keywords = { var old_val = this.settings.keywords.join(", "), new_val = prompt("Highlight Keywords\n\nPlease enter a comma-separated list of words that you would like to be highlighted in chat.", old_val); - if ( ! new_val ) + if ( new_val === null || new_val === undefined ) return; // Split them up. @@ -396,17 +428,40 @@ FFZ.settings_info.keywords = { }; +FFZ.settings_info.fix_color = { + type: "boolean", + value: false, + + category: "Chat", + visible: function() { return ! this.has_bttv }, + + name: "Adjust Username Colors", + help: "Ensure that username colors contrast with the background enough to be readable.", + + on_update: function(val) { + if ( this.has_bttv ) + return; + + document.body.classList.toggle("ffz-chat-colors", val); + } + }; + + FFZ.settings_info.chat_rows = { type: "boolean", value: false, + category: "Chat", visible: function() { return ! this.has_bttv }, name: "Chat Line Backgrounds", help: "Display alternating background colors for lines in chat.", on_update: function(val) { - document.querySelector(".app-main").classList.toggle("ffz-chat-background", val); + if ( this.has_bttv ) + return; + + document.body.classList.toggle("ffz-chat-background", val); } }; @@ -416,10 +471,19 @@ FFZ.settings_info.chat_rows = { // --------------------- FFZ.prototype.setup_line = function() { - // Alternating Background - document.querySelector('.app-main').classList.toggle('ffz-chat-background', this.settings.chat_rows); + // Chat Enhancements + document.body.classList.toggle("ffz-chat-colors", !this.has_bttv && this.settings.fix_color); + document.body.classList.toggle('ffz-chat-background', !this.has_bttv && this.settings.chat_rows); + + this._colors = {}; this._last_row = {}; + var s = this._fix_color_style = document.createElement('style'); + s.id = "ffz-style-username-colors"; + s.type = 'text/css'; + document.head.appendChild(s); + + this.log("Hooking the Ember Line controller."); var Line = App.__container__.resolve('controller:line'), @@ -428,11 +492,20 @@ FFZ.prototype.setup_line = function() { Line.reopen({ tokenizedMessage: function() { // Add our own step to the tokenization procedure. - var tokens = f._emoticonize(this, this._super()), - user = f.get_user(); + var tokens = this._super(); - if ( ! user || this.get("model.from") != user.login ) - tokens = f._mentionize(this, tokens); + try { + tokens = f._emoticonize(this, tokens); + var user = f.get_user(); + + if ( ! user || this.get("model.from") != user.login ) + tokens = f._mentionize(this, tokens); + + } catch(err) { + try { + f.error("LineController tokenizedMessage: " + err); + } catch(err) { } + } return tokens; @@ -446,60 +519,84 @@ FFZ.prototype.setup_line = function() { Line.reopen({ didInsertElement: function() { this._super(); + try { + var el = this.get('element'), + user = this.get('context.model.from'), + room = this.get('context.parentController.content.id'), + color = this.get('context.model.color'), - var el = this.get('element'), - user = this.get('context.model.from'), - room = this.get('context.parentController.content.id'), - row_type = this.get('context.model.ffzAlternate'); + row_type = this.get('context.model.ffz_alternate'); - if ( row_type === undefined ) { - row_type = f._last_row[room] = f._last_row.hasOwnProperty(room) ? !f._last_row[room] : false; - this.set("context.model.ffzAlternate", row_type); - } - el.classList.toggle('ffz-alternate', row_type); - el.setAttribute('data-room', room); - el.setAttribute('data-sender', user); + // Color Processing + if ( color ) + f._handle_color(color); - f.render_badge(this); - if ( f.settings.capitalize ) - f.capitalize(this, user); - - // Check for any mentions. - var mentioned = el.querySelector('span.mentioned'); - if ( mentioned ) { - el.classList.add("ffz-mentioned"); - - if ( ! document.hasFocus() && ! this.get('context.model.ffzNotified') && f.settings.highlight_notifications ) { - var cap_room = FFZ.get_capitalization(room), - cap_user = FFZ.get_capitalization(user), - room_name = cap_room, - msg = this.get("context.model.message"); - - if ( this.get("context.parentController.content.isGroupRoom") ) - room_name = this.get("context.parentController.content.tmiRoom.displayName"); - - if ( this.get("context.model.style") == "action" ) - msg = "* " + cap_user + " " + msg; - else - msg = cap_user + ": " + msg; - - f.show_notification( - msg, - "Twitch Chat Mention in " + room_name, - cap_room, - 60000, - window.focus.bind(window) - ); + // Row Alternation + if ( row_type === undefined ) { + row_type = f._last_row[room] = f._last_row.hasOwnProperty(room) ? !f._last_row[room] : false; + this.set("context.model.ffz_alternate", row_type); } - } - // Mark that we've checked this message for mentions. - this.set('context.model.ffzNotified', true); + el.classList.toggle('ffz-alternate', row_type); + + + // Basic Data + el.setAttribute('data-room', room); + el.setAttribute('data-sender', user); + + + // Badge + f.render_badge(this); + + + // Capitalization + if ( f.settings.capitalize ) + f.capitalize(this, user); + + + // Mention Highlighting + var mentioned = el.querySelector('span.mentioned'); + if ( mentioned ) { + el.classList.add("ffz-mentioned"); + + if ( ! document.hasFocus() && ! this.get('context.model.ffz_notified') && f.settings.highlight_notifications ) { + var cap_room = FFZ.get_capitalization(room), + cap_user = FFZ.get_capitalization(user), + room_name = cap_room, + msg = this.get("context.model.message"); + + if ( this.get("context.parentController.content.isGroupRoom") ) + room_name = this.get("context.parentController.content.tmiRoom.displayName"); + + if ( this.get("context.model.style") == "action" ) + msg = "* " + cap_user + " " + msg; + else + msg = cap_user + ": " + msg; + + f.show_notification( + msg, + "Twitch Chat Mention in " + room_name, + cap_room, + 60000, + window.focus.bind(window) + ); + } + } + + // Mark that we've checked this message for mentions. + this.set('context.model.ffz_notified', true); + + } catch(err) { + try { + f.error("LineView didInsertElement: " + err); + } catch(err) { } + } } }); + // Store the capitalization of our own name. var user = this.get_user(); if ( user && user.name ) @@ -507,6 +604,69 @@ FFZ.prototype.setup_line = function() { } +// --------------------- +// Fix Name Colors +// --------------------- + +FFZ.prototype._handle_color = function(color) { + if ( ! color || this._colors[color] ) + return; + + this._colors[color] = true; + + // Parse the color. + var raw = parseInt(color.substr(1), 16), + rgb = [ + (raw >> 16), + (raw >> 8 & 0x00FF), + (raw & 0x0000FF) + ], + + lum = utils.get_luminance(rgb), + + output = "", + rule = 'span[style="color:' + color + '"]', + matched = false; + + if ( lum > 0.3 ) { + // Color Too Bright. We need a lum of 0.3 or less. + matched = true; + + var s = 255, + nc = rgb; + while(s--) { + nc = utils.darken(nc); + if ( utils.get_luminance(nc) <= 0.3 ) + break; + } + + output += '.ffz-chat-colors .ember-chat-container:not(.dark) .chat-line ' + rule + ', .ffz-chat-colors .chat-container:not(.dark) .chat-line ' + rule + ' { color: ' + utils.rgb_to_css(nc) + ' !important; }\n'; + } else + output += '.ffz-chat-colors .ember-chat-container:not(.dark) .chat-line ' + rule + ', .ffz-chat-colors .chat-container:not(.dark) .chat-line ' + rule + ' { color: ' + color + ' !important; }\n'; + + if ( lum < 0.1 ) { + // Color Too Dark. We need a lum of 0.1 or more. + matched = true; + + var s = 255, + nc = rgb; + while(s--) { + nc = utils.brighten(nc); + if ( utils.get_luminance(nc) >= 0.1 ) + break; + } + + output += '.ffz-chat-colors .theatre .chat-container .chat-line ' + rule + ', .ffz-chat-colors .chat-container.dark .chat-line ' + rule + ', .ffz-chat-colors .ember-chat-container.dark .chat-line ' + rule + ' { color: ' + utils.rgb_to_css(nc) + ' !important; }\n'; + } else + output += '.ffz-chat-colors .theatre .chat-container .chat-line ' + rule + ', .ffz-chat-colors .chat-container.dark .chat-line ' + rule + ', .ffz-chat-colors .ember-chat-container.dark .chat-line ' + rule + ' { color: ' + color + ' !important; }\n'; + + + if ( matched ) + this._fix_color_style.innerHTML += output; +} + + + // --------------------- // Capitalization // --------------------- @@ -554,30 +714,13 @@ FFZ.prototype.capitalize = function(view, user) { } -FFZ.chat_commands.capitalization = 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 ) - return "Chat Name Capitalization is currently " + (this.settings.capitalize ? "enabled." : "disabled."); - - this.settings.set("capitalize", enabled); - return "Chat Name Capitalization is now " + (enabled ? "enabled." : "disabled."); -} - -FFZ.chat_commands.capitalization.help = "Usage: /ffz capitalization \nEnable or disable Chat Name Capitalization. This setting does not work with BetterTTV."; - - // --------------------- // Extra Mentions // --------------------- FFZ._regex_cache = {}; -FFZ._get_rex = function(word) { +FFZ._get_regex = function(word) { return FFZ._regex_cache[word] = FFZ._regex_cache[word] || RegExp("\\b" + reg_escape(word) + "\\b", "ig"); } @@ -615,25 +758,6 @@ FFZ.prototype._mentionize = function(controller, tokens) { } -FFZ.chat_commands.mentionize = function(room, args) { - if ( args && args.length ) { - var mention_words = args.join(" ").trim().split(/\W*,\W*/); - if ( mention_words.length == 1 && mention_words[0] == "disable" ) - mention_words = []; - - this.settings.set("keywords", mention_words); - } - - var mention_words = this.settings.keywords; - if ( mention_words.length ) - return "The following words will be highlighted: " + mention_words.join(", "); - else - return "There are no words set that will be highlighted."; -} - -FFZ.chat_commands.mentionize.help = "Usage: /ffz mentionize \nSet a list of words that will also be highlighted in chat."; - - // --------------------- // Emoticon Replacement // --------------------- @@ -693,7 +817,255 @@ FFZ.prototype._emoticonize = function(controller, tokens) { return tokens; } -},{}],7:[function(require,module,exports){ +},{"../utils":24}],7:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + utils = require("../utils"), + + keycodes = { + ESC: 27, + P: 80, + B: 66, + T: 84 + }, + + btns = [ + ['5m', 300], + ['10m', 600], + ['1hr', 3600], + ['12hr', 43200], + ['24hr', 86400]], + + MESSAGE = ''; + + +// ---------------- +// Settings +// ---------------- + +FFZ.settings_info.enhanced_moderation = { + type: "boolean", + value: false, + + visible: function() { return ! this.has_bttv }, + category: "Chat", + + name: "Enhanced Moderation", + help: "Use /p, /t, /u and /b in chat to moderator, or use hotkeys with moderation cards." + }; + + +// ---------------- +// Initialization +// ---------------- + +FFZ.prototype.setup_mod_card = function() { + this.log("Hooking the Ember Moderation Card view."); + var Card = App.__container__.resolve('view:moderation-card'), + f = this; + + Card.reopen({ + didInsertElement: function() { + this._super(); + try { + if ( ! f.settings.enhanced_moderation ) + return; + + var el = this.get('element'), + controller = this.get('context'); + + // Only do the big stuff if we're mod. + if ( controller.get('parentController.model.isModeratorOrHigher') ) { + el.classList.add('ffz-moderation-card'); + el.setAttribute('tabindex', 1); + + // Key Handling + el.addEventListener('keyup', function(e) { + var key = e.keyCode || e.which, + user_id = controller.get('model.user.id'), + room = controller.get('parentController.model'); + + if ( key == keycodes.P ) + room.send("/timeout " + user_id + " 1"); + + else if ( key == keycodes.B ) + room.send("/ban " + user_id); + + else if ( key == keycodes.T ) + room.send("/timeout " + user_id + " 600"); + + else if ( key != keycodes.ESC ) + return; + + controller.send('hideModOverlay'); + }); + + + // Extra Moderation + var line = document.createElement('div'); + line.className = 'interface clearfix'; + + var btn_click = function(timeout) { + var user_id = controller.get('model.user.id'), + room = controller.get('parentController.model'); + + if ( timeout === -1 ) + room.send("/unban " + user_id); + else + room.send("/timeout " + user_id + " " + timeout); + }, + + btn_make = function(text, timeout) { + var btn = document.createElement('button'); + btn.className = 'button'; + btn.innerHTML = text; + btn.title = "Timeout User for " + utils.number_commas(timeout) + " Second" + (timeout != 1 ? "s" : ""); + + if ( timeout === 600 ) + btn.title = "(T)" + btn.title.substr(1); + else if ( timeout === 1 ) + btn.title = "(P)urge - " + btn.title; + + jQuery(btn).tipsy(); + + btn.addEventListener('click', btn_click.bind(this, timeout)); + return btn; + }; + + line.appendChild(btn_make('Purge', 1)); + + var s = document.createElement('span'); + s.className = 'right'; + line.appendChild(s); + + for(var i=0; i < btns.length; i++) + s.appendChild(btn_make(btns[i][0], btns[i][1])); + + el.appendChild(line); + + + // Unban Button + + var unban_btn = document.createElement('button'); + unban_btn.className = 'unban button glyph-only light'; + unban_btn.innerHTML = "✓"; + unban_btn.title = "(U)nban User"; + + jQuery(unban_btn).tipsy(); + unban_btn.addEventListener("click", btn_click.bind(this, -1)); + + var ban_btn = el.querySelector('button.ban'); + ban_btn.setAttribute('title', '(B)an User'); + + jQuery(ban_btn).after(unban_btn); + + + // Fix Other Buttons + this.$("button.timeout").remove(); + } + + + // More Fixing Other Buttons + var op_btn = el.querySelector('button.mod'); + if ( op_btn ) { + var model = controller.get('parentController.model'), + can_op = model.get('isBroadcaster') || model.get('isStaff') || model.get('isAdmin'); + + if ( ! can_op ) + op_btn.parentElement.removeChild(op_btn); + } + + + var msg_btn = el.querySelector(".interface > button"); + if ( msg_btn && msg_btn.className == "button" ) { + msg_btn.innerHTML = MESSAGE; + msg_btn.classList.add('glyph-only'); + msg_btn.classList.add('message'); + + msg_btn.title = "Message User"; + jQuery(msg_btn).tipsy(); + } + + + // Focus the Element + this.$().draggable({ + start: function() { + el.focus(); + }}); + + el.focus(); + + } catch(err) { + try { + f.error("ModerationCardView didInsertElement: " + err); + } catch(err) { } + } + }}); +} + + +// ---------------- +// Chat Commands +// ---------------- + +FFZ.chat_commands.purge = FFZ.chat_commands.p = function(room, args) { + if ( ! args || ! args.length ) + return "Purge Usage: /p username [more usernames separated by spaces]"; + + if ( args.length > 10 ) + return "Please only purge up to 10 users at once."; + + for(var i=0; i < args.length; i++) { + var name = args[i]; + if ( name ) + room.room.send("/timeout " + name + " 1"); + } +} + +FFZ.chat_commands.p.enabled = function() { return this.settings.enhanced_moderation; } + + +FFZ.chat_commands.t = function(room, args) { + if ( ! args || ! args.length ) + return "Timeout Usage: /t username [duration]"; + room.room.send("/timeout " + args.join(" ")); +} + +FFZ.chat_commands.t.enabled = function() { return this.settings.enhanced_moderation; } + + +FFZ.chat_commands.b = function(room, args) { + if ( ! args || ! args.length ) + return "Ban Usage: /b username [more usernames separated by spaces]"; + + if ( args.length > 10 ) + return "Please only ban up to 10 users at once."; + + for(var i=0; i < args.length; i++) { + var name = args[i]; + if ( name ) + room.room.send("/ban " + name); + } +} + +FFZ.chat_commands.b.enabled = function() { return this.settings.enhanced_moderation; } + + +FFZ.chat_commands.u = function(room, args) { + if ( ! args || ! args.length ) + return "Unban Usage: /b username [more usernames separated by spaces]"; + + if ( args.length > 10 ) + return "Please only unban up to 10 users at once."; + + for(var i=0; i < args.length; i++) { + var name = args[i]; + if ( name ) + room.room.send("/unban " + name); + } +} + +FFZ.chat_commands.u.enabled = function() { return this.settings.enhanced_moderation; } +},{"../utils":24}],8:[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*['"]([^'"]+)['"][^}]+(?:}|$)/, @@ -746,6 +1118,7 @@ FFZ.prototype.setup_room = function() { // -------------------- FFZ.chat_commands = {}; +FFZ.ffz_commands = {}; FFZ.prototype.room_message = function(room, text) { @@ -762,6 +1135,54 @@ FFZ.prototype.room_message = function(room, text) { FFZ.prototype.run_command = function(text, room_id) { + var room = this.rooms[room_id]; + if ( ! room || ! room.room ) + return false; + + if ( ! text ) + return; + + var args = text.split(" "), + cmd = args.shift().substr(1).toLowerCase(), + + command = FFZ.chat_commands[cmd], + output; + + if ( ! command ) + return false; + + if ( command.hasOwnProperty('enabled') ) { + var val = command.enabled; + if ( typeof val == "function" ) { + try { + val = command.enabled.bind(this)(room, args); + } catch(err) { + this.error('command "' + cmd + '" enabled: ' + err); + val = false; + } + } + + if ( ! val ) + return false; + } + + this.log("Received Command: " + cmd, args, true); + + try { + output = command.bind(this)(room, args); + } catch(err) { + this.error('command "' + cmd + '" runner: ' + err); + output = "There was an error running the command."; + } + + if ( output ) + this.room_message(room, output); + + return true; +} + + +FFZ.prototype.run_ffz_command = function(text, room_id) { var room = this.rooms[room_id]; if ( ! room || !room.room ) return; @@ -780,7 +1201,7 @@ FFZ.prototype.run_command = function(text, room_id) { this.log("Received Command: " + cmd, args, true); - var command = FFZ.chat_commands[cmd], output; + var command = FFZ.ffz_commands[cmd], output; if ( command ) { try { output = command.bind(this)(room, args); @@ -796,9 +1217,9 @@ FFZ.prototype.run_command = function(text, room_id) { } -FFZ.chat_commands.help = function(room, args) { +FFZ.ffz_commands.help = function(room, args) { if ( args && args.length ) { - var command = FFZ.chat_commands[args[0].toLowerCase()]; + var command = FFZ.ffz_commands[args[0].toLowerCase()]; if ( ! command ) return 'There is no "' + args[0] + '" command.'; @@ -810,13 +1231,13 @@ FFZ.chat_commands.help = function(room, args) { } var cmds = []; - for(var c in FFZ.chat_commands) - FFZ.chat_commands.hasOwnProperty(c) && cmds.push(c); + for(var c in FFZ.ffz_commands) + FFZ.ffz_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."; +FFZ.ffz_commands.help.help = "Usage: /ffz help [command]\nList available commands, or show help for a specific command."; // -------------------- @@ -910,12 +1331,20 @@ FFZ.prototype._modify_room = function(room) { // Track which rooms the user is currently in. init: function() { this._super(); - f.add_room(this.id, this); + try { + f.add_room(this.id, this); + } catch(err) { + f.error("add_room: " + err); + } }, willDestroy: function() { this._super(); - f.remove_room(this.id); + try { + f.remove_room(this.id); + } catch(err) { + f.error("remove_room: " + err); + } }, getSuggestions: function() { @@ -924,19 +1353,35 @@ FFZ.prototype._modify_room = function(room) { // filteredSuggestions property of the chat-input component would // be even better, but I was already hooking the room model. var suggestions = this._super(); - if ( this.settings.capitalize ) - suggestions = _.map(suggestions, FFZ.get_capitalization); + + try { + if ( f.settings.capitalize ) + suggestions = _.map(suggestions, FFZ.get_capitalization); + } catch(err) { + f.error("get_suggestions: " + err); + } return suggestions; }, 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); + try { + var cmd = text.split(' ', 1)[0].toLowerCase(); + if ( cmd === "/ffz" ) { + this.set("messageToSend", ""); + f.run_ffz_command(text.substr(5), this.get('id')); + return; + + } else if ( cmd.charAt(0) === "/" && f.run_command(text, this.get('id')) ) { + this.set("messageToSend", ""); + return; + } + + } catch(err) { + f.error("send: " + err); + } + + return this._super(text); } }); } @@ -988,25 +1433,7 @@ FFZ.prototype._legacy_load_room_css = function(room_id, callback, data) { output.css = data || null; return this._load_room_json(room_id, callback, output); } -},{"../constants":3,"../utils":25}],8:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ; - - -// -------------------- -// Initialization -// -------------------- - -FFZ.prototype.setup_router = function() { - this.log("Hooking the Ember router."); - - var f = this; - App.__container__.lookup('router:main').reopen({ - ffzTransition: function() { - f.track_page(); - }.on('didTransition') - }); -} -},{}],9:[function(require,module,exports){ +},{"../constants":3,"../utils":24}],9:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -1027,72 +1454,77 @@ FFZ.prototype._modify_viewers = function(controller) { controller.reopen({ lines: function() { - var viewers = this._super(), - categories = [], - data = {}, - last_category = null; + var viewers = this._super(); + try { + var categories = [], + data = {}, + last_category = null; - // Get the broadcaster name. - var Channel = App.__container__.lookup('controller:channel'), - room_id = this.get('parentController.model.id'), - broadcaster = Channel && Channel.get('id'); + // Get the broadcaster name. + var Channel = App.__container__.lookup('controller:channel'), + room_id = this.get('parentController.model.id'), + broadcaster = Channel && Channel.get('id'); - // We can get capitalization for the broadcaster from the channel. - if ( broadcaster ) { - var display_name = Channel.get('display_name'); - if ( display_name ) - FFZ.capitalization[broadcaster] = [display_name, Date.now()]; - } + // We can get capitalization for the broadcaster from the channel. + if ( broadcaster ) { + var display_name = Channel.get('display_name'); + if ( display_name ) + FFZ.capitalization[broadcaster] = [display_name, Date.now()]; + } - // If the current room isn't the channel's chat, then we shouldn't - // display them as the broadcaster. - if ( room_id != broadcaster ) - broadcaster = null; + // If the current room isn't the channel's chat, then we shouldn't + // display them as the broadcaster. + if ( room_id != broadcaster ) + broadcaster = null; - // Now, break the viewer array down into something we can use. - for(var i=0; i < viewers.length; i++) { - var entry = viewers[i]; - if ( entry.category ) { - last_category = entry.category; - categories.push(last_category); - data[last_category] = []; + // Now, break the viewer array down into something we can use. + for(var i=0; i < viewers.length; i++) { + var entry = viewers[i]; + if ( entry.category ) { + last_category = entry.category; + categories.push(last_category); + data[last_category] = []; - } else { - var viewer = entry.chatter.toLowerCase(); - if ( ! viewer ) + } else { + var viewer = entry.chatter.toLowerCase(); + if ( ! viewer ) + continue; + + // If the viewer is the broadcaster, give them their own + // group. Don't put them with normal mods! + if ( viewer == broadcaster ) { + categories.unshift("Broadcaster"); + data["Broadcaster"] = [viewer]; + + } else if ( data.hasOwnProperty(last_category) ) + data[last_category].push(viewer); + } + } + + // Now, rebuild the viewer list. However, we're going to actually + // sort it this time. + viewers = []; + for(var i=0; i < categories.length; i++) { + var category = categories[i], + chatters = data[category]; + + if ( ! chatters || ! chatters.length ) continue; - // If the viewer is the broadcaster, give them their own - // group. Don't put them with normal mods! - if ( viewer == broadcaster ) { - categories.unshift("Broadcaster"); - data["Broadcaster"] = [viewer]; + viewers.push({category: category}); + viewers.push({chatter: ""}); - } else if ( data.hasOwnProperty(last_category) ) - data[last_category].push(viewer); + // Push the chatters, capitalizing them as we go. + chatters.sort(); + while(chatters.length) { + var viewer = chatters.shift(); + viewer = FFZ.get_capitalization(viewer); + viewers.push({chatter: viewer}); + } } - } - // Now, rebuild the viewer list. However, we're going to actually - // sort it this time. - viewers = []; - for(var i=0; i < categories.length; i++) { - var category = categories[i], - chatters = data[category]; - - if ( ! chatters || ! chatters.length ) - continue; - - viewers.push({category: category}); - viewers.push({chatter: ""}); - - // Push the chatters, capitalizing them as we go. - chatters.sort(); - while(chatters.length) { - var viewer = chatters.shift(); - viewer = FFZ.get_capitalization(viewer); - viewers.push({chatter: viewer}); - } + } catch(err) { + f.error("ViewersController lines: " + err); } return viewers; @@ -1297,7 +1729,7 @@ FFZ.prototype._legacy_load_css = function(set_id, callback, data) { this._load_set_json(set_id, callback, output); } -},{"./constants":3,"./utils":25}],11:[function(require,module,exports){ +},{"./constants":3,"./utils":24}],11:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, SENDER_REGEX = /(\sdata-sender="[^"]*"(?=>))/; @@ -1323,22 +1755,27 @@ FFZ.prototype.setup_bttv = function(delay) { this.log("BetterTTV was detected after " + delay + "ms. Hooking."); this.has_bttv = true; - this.track('setCustomVariable', '3', 'BetterTTV', BetterTTV.info.versionString()); + // this.track('setCustomVariable', '3', 'BetterTTV', BetterTTV.info.versionString()); // Disable Dark if it's enabled. - document.querySelector(".app-main").classList.remove("ffz-dark"); + document.body.classList.remove("ffz-dark"); if ( this._dark_style ) { this._dark_style.parentElement.removeChild(this._dark_style); delete this._dark_style; } + // Disable other features too. + document.body.classList.remove("ffz-chat-colors"); + document.body.classList.remove("ffz-chat-background"); + + // Send Message Behavior var original_send = BetterTTV.chat.helpers.sendMessage, f = this; BetterTTV.chat.helpers.sendMessage = function(message) { var cmd = message.split(' ', 1)[0].toLowerCase(); if ( cmd === "/ffz" ) - f.run_command(message.substr(5), BetterTTV.chat.store.currentRoom); + f.run_ffz_command(message.substr(5), BetterTTV.chat.store.currentRoom); else return original_send(message); } @@ -1521,6 +1958,9 @@ require('./shims'); var FFZ = window.FrankerFaceZ = function() { FFZ.instance = this; + // Logging + this._log_data = []; + // Get things started. this.initialize(); } @@ -1531,7 +1971,7 @@ FFZ.get = function() { return FFZ.instance; } // Version var VER = FFZ.version_info = { - major: 3, minor: 0, revision: 0, + major: 3, minor: 1, revision: 0, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -1542,6 +1982,8 @@ var VER = FFZ.version_info = { FFZ.prototype.log = function(msg, data, to_json) { msg = "FFZ: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); + this._log_data.push(msg); + if ( data !== undefined && console.groupCollapsed && console.dir ) { console.groupCollapsed(msg); if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) @@ -1555,6 +1997,43 @@ FFZ.prototype.log = function(msg, data, to_json) { } +FFZ.prototype.error = function(msg, data, to_json) { + msg = "FFZ Error: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); + this._log_data.push(msg); + + if ( data !== undefined && console.groupCollapsed && console.dir ) { + console.groupCollapsed(msg); + if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) + console.log(data); + else + console.dir(data); + + console.groupEnd(msg); + } else + console.assert(false, msg); +} + + +FFZ.prototype.paste_logs = function() { + this._pastebin(this._log_data.join("\n"), function(url) { + if ( ! url ) + return console.log("FFZ Error: Unable to upload log to pastebin."); + + console.log("FFZ: Your FrankerFaceZ log has been pasted to: " + url); + }); +} + + +FFZ.prototype._pastebin = function(data, callback) { + jQuery.ajax({url: "http://putco.de/", type: "PUT", data: data, context: this}) + .success(function(e) { + callback.bind(this)(e.trim() + ".log"); + }).fail(function(e) { + callback.bind(this)(null); + }); +} + + // ------------------- // User Data // ------------------- @@ -1581,14 +2060,15 @@ require('./socket'); require('./emoticons'); require('./badges'); -require('./ember/router'); +// Analytics: require('./ember/router'); require('./ember/room'); require('./ember/line'); require('./ember/chatview'); require('./ember/viewers'); +require('./ember/moderation-card'); //require('./ember/teams'); -require('./tracking'); +// Analytics: require('./tracking'); require('./debug'); @@ -1653,13 +2133,14 @@ FFZ.prototype.setup_ember = function(delay) { this.setup_emoticons(); this.setup_badges(); - this.setup_piwik(); + //this.setup_piwik(); - this.setup_router(); + //this.setup_router(); this.setup_room(); this.setup_line(); this.setup_chatview(); this.setup_viewers(); + this.setup_mod_card(); //this.setup_teams(); @@ -1678,7 +2159,7 @@ FFZ.prototype.setup_ember = function(delay) { this.log("Initialization complete in " + duration + "ms"); } -},{"./badges":1,"./commands":2,"./debug":4,"./ember/chatview":5,"./ember/line":6,"./ember/room":7,"./ember/router":8,"./ember/viewers":9,"./emoticons":10,"./ext/betterttv":11,"./ext/emote_menu":12,"./featurefriday":14,"./settings":15,"./shims":16,"./socket":17,"./tracking":18,"./ui/menu":19,"./ui/menu_button":20,"./ui/notifications":21,"./ui/races":22,"./ui/styles":23,"./ui/viewer_count":24}],14:[function(require,module,exports){ +},{"./badges":1,"./commands":2,"./debug":4,"./ember/chatview":5,"./ember/line":6,"./ember/moderation-card":7,"./ember/room":8,"./ember/viewers":9,"./emoticons":10,"./ext/betterttv":11,"./ext/emote_menu":12,"./featurefriday":14,"./settings":15,"./shims":16,"./socket":17,"./ui/menu":18,"./ui/menu_button":19,"./ui/notifications":20,"./ui/races":21,"./ui/styles":22,"./ui/viewer_count":23}],14:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('./constants'); @@ -1756,7 +2237,7 @@ FFZ.prototype._feature_friday_ui = function(room_id, parent, view) { btn.innerHTML = "" + message + ""; // Track the number of users to click this button. - btn.addEventListener('click', function() { f.track('trackLink', this.href, 'link'); }); + // btn.addEventListener('click', function() { f.track('trackLink', this.href, 'link'); }); btnc.appendChild(btn); parent.appendChild(btnc); @@ -2113,138 +2594,6 @@ FFZ.prototype.ws_send = function(func, data, callback, can_wait) { return request; } },{}],18:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ, - constants = require('./constants'), - PIWIK = ("https:" == document.location.protocol ? 'https:' : 'http:') + '//sir.stendec.me/ffz_piwik/'; - - -// -------------------- -// Initialization -// -------------------- - -FFZ.prototype.setup_piwik = function() { - if ( window._paq != undefined ) { - this.log("Piwik is already present. Disabling analytics."); - this._tracking = false; - return; - } - - if ( localStorage['ffzTracking'] == "false" ) { - this.log("The user has opted out of tracking. Disabling analytics."); - this._tracking = false; - return; - } - - this.log("Initializing Piwik."); - this._tracking = true; - var _paq = window._paq = []; - - _paq.push(['setSiteId', 1]); - _paq.push(['setTrackerUrl', PIWIK + 'piwik.php']); - - if ( this.has_bttv ) - _paq.push(['setCustomVariable', '3', 'BetterTTV', BetterTTV.info.versionString()]); - - var user = this.get_user(), f = this; - if ( user ) { - _paq.push(['setCustomVariable', '1', 'Partnered', user.is_partner ? "Yes" : "No"]) - _paq.push(['setCustomVariable', '2', 'User Type', user.is_staff ? "Staff" : (user.is_admin ? "Admin" : "User")]); - _paq.push(['setUserId', user.login]); - - Twitch.api.get("channels/" + user.login) - .done(function(data) { - if ( data.logo ) - f.track('setCustomVariable', '4', 'Avatar', data.logo); - }).always(function() { f.track_page(); }); - - } else - this.track_page(); - - // If someone turned analytics back ON, track that. - if ( localStorage['ffzTracking'] == "true" ) { - this.track('trackEvent', 'Analytics', 'Enable'); - localStorage.removeItem('ffzTracking'); - } - - var script = document.createElement('script'); - script.type = 'text/javascript'; - script.defer = true; - script.async = true; - script.src = PIWIK + 'piwik.js'; - document.head.appendChild(script); -} - - -// -------------------- -// Command -// -------------------- - -FFZ.chat_commands.analytics = 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 ) - return "Analytics are currently " + (localStorage.ffzTracking != "false" ? "enabled." : "disabled."); - - // Track that someone turned off analytics. - if ( this._tracking && ! enabled && localStorage.ffzTracking != "false" ) - this.track('trackEvent', 'Analytics', 'Disable'); - - localStorage.ffzTracking = enabled; - - return "Analytics are now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; -} - -FFZ.chat_commands.analytics.help = "Usage: /ffz analytics \nEnable or disable FrankerFaceZ analytics. We collect some data about your browser and how you use FrankerFaceZ to help us improve the script. Turn off analytics if you'd rather we not."; - - - -// -------------------- -// Tracking Helpers -// -------------------- - -FFZ.prototype.track = function() { - if ( ! this._tracking ) - return; - - window._paq && _paq.push(Array.prototype.slice.call(arguments)); -} - - -FFZ.prototype.track_page = function() { - if ( ! this._tracking ) - return; - - if ( this._old_url ) - this.track('setReferrerUrl', this._old_url); - - this._old_url = document.location.toString(); - this.track('setCustomUrl', this._old_url); - - this.track('deleteCustomVariable', '1', 'page'); - this.track('deleteCustomVariable', '3', 'page'); - - var routes = App.__container__.resolve('router:main').router.currentHandlerInfos; - if ( ! routes || routes.length == 0 ) - return; - - var last = routes[routes.length - 1]; - if ( last.name == "channel.index" && last.context ) { - var following = last.context.get("isFollowing.isFollowing"); - if ( following !== undefined && following !== null ) - this.track('setCustomVariable', '1', 'Following', (following ? "Yes" : "No"), 'page'); - - var game = last.context.get("game"); - if ( game ) - this.track("setCustomVariable", "3", "Game", game, "page"); - - this.track("trackPageView", document.title); - } -} -},{"./constants":3}],19:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'); @@ -2342,7 +2691,7 @@ FFZ.prototype.build_ui_popup = function(view) { // Add the menu to the DOM. this._popup = container; - sub_container.style.maxHeight = Math.max(300, view.$().height() - 212) + "px"; + sub_container.style.maxHeight = Math.max(100, view.$().height() - 162) + "px"; view.$('.chat-interface').append(container); } @@ -2371,31 +2720,12 @@ FFZ.prototype._ui_change_page = function(view, menu, container, page) { FFZ.menu_pages.settings = { render: function(view, container) { - var menu = document.createElement('div'); - menu.className = 'chat-menu-content'; - - var settings = []; - for(var key in FFZ.settings_info) - settings.push([key, FFZ.settings_info[key]]); - - settings.sort(function(a,b) { - var ai = a[1], - bi = b[1], - - an = ai.name.toLowerCase(), - bn = bi.name.toLowerCase(); - - if ( an < bn ) return -1; - else if ( an > bn ) return 1; - return 0; - }); - - - for(var i=0; i < settings.length; i++) { - var key = settings[i][0], - info = settings[i][1], - el = document.createElement('p'), - val = this.settings.get(key); + var settings = {}, + categories = []; + for(var key in FFZ.settings_info) { + var info = FFZ.settings_info[key], + cat = info.category || "Miscellaneous", + cs = settings[cat]; if ( info.visible !== undefined && info.visible !== null ) { var visible = info.visible; @@ -2406,45 +2736,94 @@ FFZ.menu_pages.settings = { continue; } - el.className = 'clearfix'; - - if ( info.type == "boolean" ) { - var swit = document.createElement('a'), - label = document.createElement('span'); - - swit.className = 'switch'; - swit.classList.toggle('active', val); - swit.innerHTML = ""; - - label.className = 'switch-label'; - label.innerHTML = info.name; - - el.appendChild(swit); - el.appendChild(label); - - swit.addEventListener("click", this._ui_toggle_setting.bind(this, swit, key)); - - } else { - el.classList.add("option"); - var link = document.createElement('a'); - link.innerHTML = info.name; - link.href = "#"; - el.appendChild(link); - - link.addEventListener("click", info.method.bind(this)); + if ( ! cs ) { + categories.push(cat); + cs = settings[cat] = []; } - if ( info.help ) { - var help = document.createElement('span'); - help.className = 'help'; - help.innerHTML = info.help; - el.appendChild(help); - } - - menu.appendChild(el); + cs.push([key, info]); } - container.appendChild(menu); + categories.sort(function(a,b) { + var a = a.toLowerCase(), + b = b.toLowerCase(); + + if ( a < b ) return -1; + else if ( a > b ) return 1; + return 0; + }); + + for(var ci=0; ci < categories.length; ci++) { + var category = categories[ci], + cset = settings[category], + + menu = document.createElement('div'), + heading = document.createElement('div'); + + heading.className = 'heading'; + menu.className = 'chat-menu-content'; + heading.innerHTML = category; + menu.appendChild(heading); + + cset.sort(function(a,b) { + var ai = a[1], + bi = b[1], + + an = ai.name.toLowerCase(), + bn = bi.name.toLowerCase(); + + if ( an < bn ) return -1; + else if ( an > bn ) return 1; + return 0; + }); + + + for(var i=0; i < cset.length; i++) { + var key = cset[i][0], + info = cset[i][1], + el = document.createElement('p'), + val = this.settings.get(key); + + el.className = 'clearfix'; + + if ( info.type == "boolean" ) { + var swit = document.createElement('a'), + label = document.createElement('span'); + + swit.className = 'switch'; + swit.classList.toggle('active', val); + swit.innerHTML = ""; + + label.className = 'switch-label'; + label.innerHTML = info.name; + + el.appendChild(swit); + el.appendChild(label); + + swit.addEventListener("click", this._ui_toggle_setting.bind(this, swit, key)); + + } else { + el.classList.add("option"); + var link = document.createElement('a'); + link.innerHTML = info.name; + link.href = "#"; + el.appendChild(link); + + link.addEventListener("click", info.method.bind(this)); + } + + if ( info.help ) { + var help = document.createElement('span'); + help.className = 'help'; + help.innerHTML = info.help; + el.appendChild(help); + } + + menu.appendChild(el); + } + + container.appendChild(menu); + } }, name: "Settings", @@ -2483,8 +2862,7 @@ FFZ.menu_pages.channel = { var room_id = view.get('controller.currentRoom.id'), room = this.rooms[room_id]; - this.log("Menu for Room: " + room_id, room); - this.track('trackEvent', 'Menu', 'Open', room_id); + //this.track('trackEvent', 'Menu', 'Open', room_id); // Add the header and ad button. /*var btn = document.createElement('a'); @@ -2582,7 +2960,7 @@ FFZ.prototype._add_emote = function(view, emote) { room.set('messageToSend', current_text + (emote.name || emote)); } -},{"../constants":3}],20:[function(require,module,exports){ +},{"../constants":3}],19:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'); @@ -2633,7 +3011,7 @@ FFZ.prototype.update_ui_link = function(link) { link.classList.toggle('dark', dark); link.classList.toggle('blue', blue); } -},{"../constants":3}],21:[function(require,module,exports){ +},{"../constants":3}],20:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -2655,6 +3033,7 @@ FFZ.settings_info.highlight_notifications = { type: "boolean", value: false, + category: "Chat", visible: function() { return ! this.has_bttv }, name: "Highlight Notifications", @@ -2690,7 +3069,7 @@ FFZ.settings_info.highlight_notifications = { // --------------------- FFZ.ws_commands.message = function(message) { - this.show_mesage(message); + this.show_message(message); } @@ -2781,7 +3160,7 @@ FFZ.prototype.show_message = function(message) { closeWith: ["button"] }).show(); } -},{}],22:[function(require,module,exports){ +},{}],21:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require('../utils'); @@ -2803,6 +3182,8 @@ FFZ.prototype.setup_races = function() { FFZ.settings_info.srl_races = { type: "boolean", value: true, + + category: "Channel Metadata", name: "SRL Race Information", help: 'Display information about SpeedRunsLive races under channels.', on_update: function(val) { @@ -3084,7 +3465,7 @@ FFZ.prototype._update_race = function(not_timer) { } } } -},{"../utils":25}],23:[function(require,module,exports){ +},{"../utils":24}],22:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'); @@ -3109,7 +3490,7 @@ FFZ.prototype.setup_css = function() { } }; } -},{"../constants":3}],24:[function(require,module,exports){ +},{"../constants":3}],23:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'), utils = require('../utils'); @@ -3146,7 +3527,7 @@ FFZ.ws_commands.viewers = function(data) { jQuery(view_count).tipsy(); } } -},{"../constants":3,"../utils":25}],25:[function(require,module,exports){ +},{"../constants":3,"../utils":24}],24:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('./constants'); @@ -3160,8 +3541,42 @@ var sanitize_cache = {}, else if ( num == 3 ) return '3rd'; else if ( num == null ) return '---'; return num + "th"; + }, + + brighten = function(rgb, amount) { + amount = (amount === 0) ? 0 : (amount || 1); + amount = Math.round(255 * -(amount / 100)); + + var r = Math.max(0, Math.min(255, rgb[0] - amount)), + g = Math.max(0, Math.min(255, rgb[1] - amount)), + b = Math.max(0, Math.min(255, rgb[2] - amount)); + + return [r,g,b]; + }, + + rgb_to_css = function(rgb) { + return "rgb(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ")"; + }, + + darken = function(rgb, amount) { + amount = (amount === 0) ? 0 : (amount || 1); + return brighten(rgb, -amount); + }, + + get_luminance = function(rgb) { + rgb = [rgb[0]/255, rgb[1]/255, rgb[2]/255]; + for (var i =0; ie?this._legacy_add_donors(e):void 0):void 0})},n.prototype._legacy_parse_donors=function(e){var t=0;if(null!=e)for(var n=e.trim().split(/\W+/),i=0;i50)return"Each user you unmod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var i=t.length;t.length;){var s=t.shift();e.room.tmiRoom.sendMessage("/unmod "+s)}return"Sent unmod command for "+i+" users."},t.chat_commands.massunmod.help="Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list.",t.chat_commands.massmod=function(e,t){if(t=t.join(" ").trim(),!t.length)return"You must provide a list of users to mod.";t=t.split(/\W*,\W*/);var n=this.get_user();if(!n||!n.login==e.id)return"You must be the broadcaster to use massmod.";if(t.length>50)return"Each user you mod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var i=t.length;t.length;){var s=t.shift();e.room.tmiRoom.sendMessage("/mod "+s)}return"Sent mod command for "+i+" users."},t.chat_commands.massmod.help="Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."},{}],3:[function(e,t){var n='',i="true"==localStorage.ffzDebugMode&&document.body.classList.contains("ffz-dev");t.exports={DEBUG:i,SERVER:i?"//localhost:8000/":"//cdn.frankerfacez.com/",SVGPATH:n,ZREKNARF:''+n+"",CHAT_BUTTON:''+n+"",GEAR:'',HEART:''}},{}],4:[function(){var t=e.FrankerFaceZ;t.chat_commands.developer_mode=function(e,t){var n,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?n=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(n=!1),void 0===n?"Developer Mode is currently "+("true"==localStorage.ffzDebugMode?"enabled.":"disabled."):(localStorage.ffzDebugMode=n,"Developer Mode is now "+(n?"enabled":"disabled")+". Please refresh your browser.")},t.chat_commands.developer_mode.help="Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."},{}],5:[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 n=Ember.View.views[t];n instanceof e&&(this.log("Adding UI link manually to Chat view.",n),n.$(".textarea-contain").append(this.build_ui_link(n)))}},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()})})}},{}],6:[function(){var t=e.FrankerFaceZ,n=function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")};t.settings_info.capitalize={type:"boolean",value:!0,visible:function(){return!this.has_bttv},name:"Username Capitalization",help:"Display names in chat with proper capitalization."},t.settings_info.keywords={type:"button",value:[],visible:function(){return!this.has_bttv},name:"Highlight Keywords",help:"Set additional keywords that will be highlighted in chat.",method:function(){var e=this.settings.keywords.join(", "),t=prompt("Highlight Keywords\n\nPlease enter a comma-separated list of words that you would like to be highlighted in chat.",e);t&&(t=t.trim().split(/\W*,\W*/),1!=t.length||""!=t[0]&&"disable"!=t[0]||(t=[]),this.settings.set("keywords",t))}},t.settings_info.chat_rows={type:"boolean",value:!1,visible:function(){return!this.has_bttv},name:"Chat Line Backgrounds",help:"Display alternating background colors for lines in chat.",on_update:function(e){document.querySelector(".app-main").classList.toggle("ffz-chat-background",e)}},t.prototype.setup_line=function(){document.querySelector(".app-main").classList.toggle("ffz-chat-background",this.settings.chat_rows),this._last_row={},this.log("Hooking the Ember Line controller.");var n=App.__container__.resolve("controller:line"),i=this;n.reopen({tokenizedMessage:function(){var e=i._emoticonize(this,this._super()),t=i.get_user();return t&&this.get("model.from")==t.login||(e=i._mentionize(this,e)),e}.property("model.message","isModeratorOrHigher")}),this.log("Hooking the Ember Line view.");var n=App.__container__.resolve("view:line");n.reopen({didInsertElement:function(){this._super();var n=this.get("element"),s=this.get("context.model.from"),o=this.get("context.parentController.content.id"),a=this.get("context.model.ffzAlternate");void 0===a&&(a=i._last_row[o]=i._last_row.hasOwnProperty(o)?!i._last_row[o]:!1,this.set("context.model.ffzAlternate",a)),n.classList.toggle("ffz-alternate",a),n.setAttribute("data-room",o),n.setAttribute("data-sender",s),i.render_badge(this),i.settings.capitalize&&i.capitalize(this,s);var r=n.querySelector("span.mentioned");if(r&&(n.classList.add("ffz-mentioned"),!document.hasFocus()&&!this.get("context.model.ffzNotified")&&i.settings.highlight_notifications)){var l=t.get_capitalization(o),c=t.get_capitalization(s),h=l,u=this.get("context.model.message");this.get("context.parentController.content.isGroupRoom")&&(h=this.get("context.parentController.content.tmiRoom.displayName")),u="action"==this.get("context.model.style")?"* "+c+" "+u:c+": "+u,i.show_notification(u,"Twitch Chat Mention in "+h,l,6e4,e.focus.bind(e))}this.set("context.model.ffzNotified",!0)}});var s=this.get_user();s&&s.name&&(t.capitalization[s.login]=[s.name,Date.now()])},t.capitalization={},t._cap_fetching=0,t.get_capitalization=function(n,i){if(e.BetterTTV)return BetterTTV.chat.helpers.lookupDisplayName(n);if(!n)return n;if(n=n.toLowerCase(),"jtv"==n||"twitchnotify"==n)return n;var s=t.capitalization[n];return s&&Date.now()-s[1]<36e5?s[0]:(t._cap_fetching<5&&(t._cap_fetching++,Twitch.api.get("users/"+n).always(function(e){var s=e.display_name||n;t.capitalization[n]=[s,Date.now()],t._cap_fetching--,"function"==typeof i&&i(s)})),s?s[0]:n)},t.prototype.capitalize=function(e,n){var i=t.get_capitalization(n,this.capitalize.bind(this,e));i&&e.$(".from").text(i)},t.chat_commands.capitalization=function(e,t){var n,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?n=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(n=!1),void 0===n?"Chat Name Capitalization is currently "+(this.settings.capitalize?"enabled.":"disabled."):(this.settings.set("capitalize",n),"Chat Name Capitalization is now "+(n?"enabled.":"disabled."))},t.chat_commands.capitalization.help="Usage: /ffz capitalization \nEnable or disable Chat Name Capitalization. This setting does not work with BetterTTV.",t._regex_cache={},t._get_rex=function(e){return t._regex_cache[e]=t._regex_cache[e]||RegExp("\\b"+n(e)+"\\b","ig")},t._mentions_to_regex=function(e){return t._regex_cache[e]=t._regex_cache[e]||RegExp("\\b(?:"+_.chain(e).map(n).value().join("|")+")\\b","ig")},t.prototype._mentionize=function(e,n){var i=this.settings.keywords;if(!i||!i.length)return n;"string"==typeof n&&(n=[n]);var s=t._mentions_to_regex(i);return _.chain(n).map(function(e){return _.isString(e)?e.match(s)?_.zip(_.map(e.split(s),_.identity),_.map(e.match(s),function(e){return{mentionedUser:e,own:!1}})):[e]:e}).flatten().compact().value()},t.chat_commands.mentionize=function(e,t){if(t&&t.length){var n=t.join(" ").trim().split(/\W*,\W*/);1==n.length&&"disable"==n[0]&&(n=[]),this.settings.set("keywords",n)}var n=this.settings.keywords;return n.length?"The following words will be highlighted: "+n.join(", "):"There are no words set that will be highlighted."},t.chat_commands.mentionize.help="Usage: /ffz mentionize \nSet a list of words that will also be highlighted in chat.",t.prototype._emoticonize=function(e,t){var n=e.get("parentController.model.id"),i=e.get("model.from"),s=this,o=this.getEmotes(i,n),a=[];return _.each(o,function(e){var n=s.emote_sets[e];n&&_.each(n.emotes,function(e){_.any(t,function(t){return _.isString(t)&&t.match(e.regex)})&&a.push(e)})}),a.length?("string"==typeof t&&(t=[t]),_.each(a,function(e){var n={isEmoticon:!0,cls:e.klass,emoticonSrc:e.url,altText:e.hidden?"???":e.name};t=_.compact(_.flatten(_.map(t,function(t){if(_.isObject(t))return t;var i=t.split(e.regex),s=[];return i.forEach(function(e,t){s.push(e),t!==i.length-1&&s.push(n)}),s})))}),t):t}},{}],7:[function(t){var n=e.FrankerFaceZ,i=/\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/gm,s=/[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/,o=/^_([^_]+)_\d+$/,a=t("../constants"),r=t("../utils"),l=function(e){return e.moderator_badge?'.chat-line[data-room="'+e.id+'"] .badges .moderator { background-image:url("'+e.moderator_badge+'") !important; }':""};n.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 n=t.instances;for(var i in n)if(n.hasOwnProperty(i)){var s=n[i];this.add_room(s.id,s),this._modify_room(s)}},n.chat_commands={},n.prototype.room_message=function(e,t){var n=t.split("\n");if(this.has_bttv)for(var i=0;in?this._legacy_add_room(e,t,n):void 0)})},n.prototype._legacy_load_room_css=function(e,t,n){var a=e,r=a.match(o);r&&r[1]&&(a=r[1]);var l={id:e,menu_sets:[a],sets:[a],moderator_badge:null,css:null};return n&&(n=n.replace(i,"").trim()),n&&(n=n.replace(s,function(e,t){return l.moderator_badge||"modicon.png"!==t.substr(-11)?e:(l.moderator_badge=t,"")})),l.css=n||null,this._load_room_json(e,t,l)}},{"../constants":3,"../utils":25}],8:[function(){var t=e.FrankerFaceZ;t.prototype.setup_router=function(){this.log("Hooking the Ember router.");var e=this;App.__container__.lookup("router:main").reopen({ffzTransition:function(){e.track_page()}.on("didTransition")})}},{}],9:[function(){var t=e.FrankerFaceZ;t.prototype.setup_viewers=function(){this.log("Hooking the Ember Viewers controller.");var e=App.__container__.resolve("controller:viewers");this._modify_viewers(e)},t.prototype._modify_viewers=function(e){e.reopen({lines:function(){var e=this._super(),n=[],i={},s=null,o=App.__container__.lookup("controller:channel"),a=this.get("parentController.model.id"),r=o&&o.get("id");if(r){var l=o.get("display_name");l&&(t.capitalization[r]=[l,Date.now()])}a!=r&&(r=null);for(var c=0;cn?this._legacy_load_set(e,t,n):"function"==typeof t&&t(!1))})},n.prototype._legacy_load_css=function(e,t,n){var o={},a={id:e,emotes:o,extra_css:null},r=this;n=n.replace(i,function(e,t,n,i,s,a,c,h){s=parseInt(s),a=parseInt(a),c=l(c,s);var u="."===i.substr(i.lastIndexOf("/")+1,1),d=++r._last_emote_id,p={id:d,hidden:u,name:n,height:s,width:a,url:i,margins:c,extra_css:h};return o[d]=p,""}).trim(),n&&n.replace(s,function(e,t){a.icon||"modicon.png"!==t.substr(-11)||(a.icon=t)}),this._load_set_json(e,t,a)}},{"./constants":3,"./utils":25}],11:[function(){var t=e.FrankerFaceZ,n=/(\sdata-sender="[^"]*"(?=>))/;t.prototype.find_bttv=function(t,n){return this.has_bttv=!1,e.BTTVLOADED?this.setup_bttv(n||0):void(n>=6e4?this.log("BetterTTV was not detected after 60 seconds."):setTimeout(this.find_bttv.bind(this,t,(n||0)+t),t))},t.prototype.setup_bttv=function(e){this.log("BetterTTV was detected after "+e+"ms. Hooking."),this.has_bttv=!0,this.track("setCustomVariable","3","BetterTTV",BetterTTV.info.versionString()),document.querySelector(".app-main").classList.remove("ffz-dark"),this._dark_style&&(this._dark_style.parentElement.removeChild(this._dark_style),delete this._dark_style);var t=BetterTTV.chat.helpers.sendMessage,i=this;BetterTTV.chat.helpers.sendMessage=function(e){var n=e.split(" ",1)[0].toLowerCase();return"/ffz"!==n?t(e):void i.run_command(e.substr(5),BetterTTV.chat.store.currentRoom)};var s,o=BetterTTV.chat.handlers.privmsg;BetterTTV.chat.handlers.privmsg=function(e,t){s=e;var n=o(e,t);return s=null,n};var a=BetterTTV.chat.templates.privmsg;BetterTTV.chat.templates.privmsg=function(e,t,o,r,l){i.bttv_badges(l);var c=a(e,t,o,r,l);return c.replace(n,'$1 data-room="'+s+'"')};var r,l=BetterTTV.chat.templates.message;BetterTTV.chat.templates.message=function(e,t,n,i){r=e;var s=l(e,t,n,i);return r=null,s};var c=BetterTTV.chat.templates.emoticonize;BetterTTV.chat.templates.emoticonize=function(e,t){var n=c(e,t),o=i.getEmotes(r,s),t=[];return _.each(o,function(e){var s=i.emote_sets[e];s&&_.each(s.emotes,function(e){_.any(n,function(t){return _.isString(t)&&t.match(e.regex)})&&t.push(e)})}),t.length?(_.each(t,function(e){var t=[''+e.name+''],i=n;if(n=[],!i||!i.length)return n;for(var s=0;s=6e4?this.log("Emote Menu for Twitch was not detected after 60 seconds."):setTimeout(this.find_emote_menu.bind(this,t,(n||0)+t),t))},t.prototype.setup_emote_menu=function(e){this.log("Emote Menu for Twitch was detected after "+e+"ms. Registering emote enumerator."),emoteMenu.registerEmoteGetter("FrankerFaceZ",this._emote_menu_enumerator.bind(this))},t.prototype._emote_menu_enumerator=function(){for(var e=this.get_user(),n=e?e.login:null,i=App.__container__.lookup("controller:chat"),s=i?i.get("currentRoom.id"):null,o=this.getEmotes(n,s),a=[],r=0;r=6e4?this.log('Twitch application not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.initialize.bind(this,t,(n||0)+t),t)))},n.prototype.setup_ember=function(t){var i=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch application after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.load_settings(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_piwik(),this.setup_router(),this.setup_room(),this.setup_line(),this.setup_chatview(),this.setup_viewers(),this.setup_notifications(),this.setup_css(),this.setup_menu(),this.setup_races(),this.find_bttv(10),this.find_emote_menu(10),this.check_ff();var s=e.performance&&performance.now?performance.now():Date.now(),o=s-i;this.log("Initialization complete in "+o+"ms")}},{"./badges":1,"./commands":2,"./debug":4,"./ember/chatview":5,"./ember/line":6,"./ember/room":7,"./ember/router":8,"./ember/viewers":9,"./emoticons":10,"./ext/betterttv":11,"./ext/emote_menu":12,"./featurefriday":14,"./settings":15,"./shims":16,"./socket":17,"./tracking":18,"./ui/menu":19,"./ui/menu_button":20,"./ui/notifications":21,"./ui/races":22,"./ui/styles":23,"./ui/viewer_count":24}],14:[function(t){var n=e.FrankerFaceZ,i=t("./constants");n.prototype.feature_friday=null,n.prototype.check_ff=function(e){e||this.log("Checking for Feature Friday data..."),jQuery.ajax(i.SERVER+"script/event.json",{cache:!1,dataType:"json",context:this}).done(function(e){return this._load_ff(e)}).fail(function(t){return 404==t.status?this._load_ff(null):(e=e||0,e++,10>e?setTimeout(this.check_ff.bind(this,e),250):this._load_ff(null))})},n.ws_commands.reload_ff=function(){this.check_ff()},n.prototype._feature_friday_ui=function(e,t,n){if(this.feature_friday&&this.feature_friday.channel!=e){this._emotes_for_sets(t,n,[this.feature_friday.set],"Feature Friday");var i=App.__container__.lookup("controller:channel");if(!i||i.get("id")!=this.feature_friday.channel){var s=this.feature_friday,o=this,a=document.createElement("div"),r=document.createElement("a");a.className="chat-menu-content",a.style.textAlign="center";var l=s.display_name+(s.live?" is live now!":"");r.className="button primary",r.classList.toggle("live",s.live),r.classList.toggle("blue",this.has_bttv&&BetterTTV.settings.get("showBlueButtons")),r.href="http://www.twitch.tv/"+s.channel,r.title=l,r.target="_new",r.innerHTML=""+l+"",r.addEventListener("click",function(){o.track("trackLink",this.href,"link")}),a.appendChild(r),t.appendChild(a)}}},n.prototype._load_ff=function(e){if(this.feature_friday){this.global_sets.removeObject(this.feature_friday.set);var t=this.emote_sets[this.feature_friday.set];t&&(t.global=!1),this.feature_friday=null,this.update_ui_link()}e&&e.set&&e.channel&&(this.feature_friday={set:e.set,channel:e.channel,live:!1,display_name:n.get_capitalization(e.channel,this._update_ff_name.bind(this))},this.global_sets.push(e.set),this.load_set(e.set,this._update_ff_set.bind(this)),this._update_ff_live())},n.prototype._update_ff_live=function(){if(this.feature_friday){var e=this;Twitch.api.get("streams/"+this.feature_friday.channel).done(function(t){e.feature_friday.live=null!=t.stream,e.update_ui_link()}).always(function(){e.feature_friday.timer=setTimeout(e._update_ff_live.bind(e),12e4)})}},n.prototype._update_ff_set=function(e,t){t&&(t.global=!0)},n.prototype._update_ff_name=function(e){this.feature_friday&&(this.feature_friday.display_name=e)}},{"./constants":3}],15:[function(){var t=e.FrankerFaceZ,n=function(e){return"ffz_setting_"+e};t.settings_info={},t.prototype.load_settings=function(){this.log("Loading settings."),this.settings={};for(var i in t.settings_info){var s=n(i),o=t.settings_info[i],a=o.hasOwnProperty("value")?o.value:void 0;if(localStorage.hasOwnProperty(s))try{a=JSON.parse(localStorage.getItem(s))}catch(r){this.log('Error loading value for "'+i+'": '+r)}this.settings[i]=a}this.settings.get=this._setting_get.bind(this),this.settings.set=this._setting_set.bind(this),this.settings.del=this._setting_del.bind(this),e.addEventListener("storage",this._setting_update.bind(this)) -},t.prototype._setting_update=function(n){if(n||(n=e.event),this.log("Storage Event",n),n.key&&"ffz_setting_"===n.key.substr(0,12)){var i=n.key,s=i.substr(12),o=void 0,a=t.settings_info[s];this.log("Updated Setting: "+s);try{o=JSON.parse(n.newValue)}catch(r){this.log('Error loading new value for "'+s+'": '+r),o=a.value||void 0}if(this.settings[s]=o,a.on_update)try{a.on_update.bind(this)(o,!1)}catch(r){this.log('Error running updater for setting "'+s+'": '+r)}}},t.prototype._setting_get=function(e){return this.settings[e]},t.prototype._setting_set=function(e,i){var s=n(e),o=t.settings_info[e],a=JSON.stringify(i);if(this.settings[e]=i,localStorage.setItem(s,a),this.log('Changed Setting "'+e+'" to: '+a),o.on_update)try{o.on_update.bind(this)(i,!0)}catch(r){this.log('Error running updater for setting "'+e+'": '+r)}},t.prototype._setting_del=function(e){var i=n(e),s=t.settings_info[e],o=void 0;if(localStorage.hasOwnProperty(i)&&localStorage.removeItem(i),delete this.settings[e],s&&(o=this.settings[e]=s.hasOwnProperty("value")?s.value:void 0),s.on_update)try{s.on_update.bind(this)(o,!0)}catch(a){this.log('Error running updater for setting "'+e+'": '+a)}}},{}],16:[function(){Array.prototype.equals=function(e){if(!e)return!1;if(this.length!=e.length)return!1;for(var t=0,n=this.length;n>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}},{}],17:[function(){var t=e.FrankerFaceZ;t.prototype._ws_open=!1,t.prototype._ws_delay=0,t.ws_commands={},t.ws_on_close=[],t.prototype.ws_create=function(){var e,n=this;this._ws_last_req=0,this._ws_callbacks={},this._ws_pending=this._ws_pending||[];try{e=this._ws_sock=new WebSocket("ws://ffz.stendec.me/")}catch(i){return this._ws_exists=!1,this.log("Error Creating WebSocket: "+i)}this._ws_exists=!0,e.onopen=function(){n._ws_open=!0,n._ws_delay=0,n.log("Socket connected.");var e=n.get_user();e&&n.ws_send("setuser",e.login);for(var t in n.rooms)n.ws_send("sub",t);var i=n._ws_pending;n._ws_pending=[];for(var s=0;sFrankerFaceZ",o.appendChild(r),r.addEventListener("click",this._add_emote.bind(this,e,"To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com"));var l=document.createElement("div");l.className="ffz-ui-menu-page",s.appendChild(l);for(var c in n.menu_pages){var h=n.menu_pages[c];if(h&&(!h.hasOwnProperty("visible")||h.visible&&("function"!=typeof h.visible||h.visible.bind(this)()))){var r=document.createElement("li"),u=document.createElement("a");r.className="item",r.id="ffz-menu-page-"+c,u.title=h.name,u.innerHTML=h.icon,u.addEventListener("click",this._ui_change_page.bind(this,e,o,l,c)),r.appendChild(u),o.appendChild(r)}}this._ui_change_page(e,o,l,this._last_page||"channel"),this._popup=i,l.style.maxHeight=Math.max(300,e.$().height()-212)+"px",e.$(".chat-interface").append(i)},n.prototype._ui_change_page=function(e,t,i,s){this._last_page=s,i.innerHTML="";for(var o=t.querySelectorAll("li.active"),a=0;as?-1:s>o?1:0});for(var a=0;a",d.className="switch-label",d.innerHTML=r.name,l.appendChild(u),l.appendChild(d),u.addEventListener("click",this._ui_toggle_setting.bind(this,u,o))}else{l.classList.add("option");var p=document.createElement("a");p.innerHTML=r.name,p.href="#",l.appendChild(p),p.addEventListener("click",r.method.bind(this))}if(r.help){var _=document.createElement("span");_.className="help",_.innerHTML=r.help,l.appendChild(_)}i.appendChild(l)}t.appendChild(i)},name:"Settings",icon:i.GEAR},n.prototype._ui_toggle_setting=function(e,t){var n=!this.settings.get(t);this.settings.set(t,n),e.classList.toggle("active",n)},n.menu_pages.channel={render:function(e,t){var n=e.get("controller.currentRoom.id"),i=this.rooms[n];this.log("Menu for Room: "+n,i),this.track("trackEvent","Menu","Open",n);this._emotes_for_sets(t,e,i&&i.menu_sets||[]);this._feature_friday_ui(n,t,e)},name:"Channel",icon:i.ZREKNARF},n.prototype._emotes_for_sets=function(e,t,n,i,s){if(null!=i){var o=document.createElement("div");o.className="list-header",o.appendChild(document.createTextNode(i)),s&&o.appendChild(s),e.appendChild(o)}var a=document.createElement("div"),r=0;a.className="emoticon-grid";for(var l=0;l0){s=!0;break}}e.classList.toggle("no-emotes",!s),e.classList.toggle("live",r),e.classList.toggle("dark",o),e.classList.toggle("blue",a)}}},{"../constants":3}],21:[function(){var t=e.FrankerFaceZ;t.prototype.setup_notifications=function(){this.log("Adding event handler for window focus."),e.addEventListener("focus",this.clear_notifications.bind(this))},t.settings_info.highlight_notifications={type:"boolean",value:!1,visible:function(){return!this.has_bttv},name:"Highlight Notifications",help:"Display notifications when a highlighted word appears in chat in an unfocused tab.",on_update:function(e,t){if(e&&t){if("denied"===Notification.permission)return this.log("Notifications have been denied by the user."),void this.settings.set("highlight_notifications",!1);if("granted"!==Notification.permission){var n=this;Notification.requestPermission(function(e){"denied"===e&&(n.log("Notifications have been denied by the user."),n.settings.set("highlight_notifications",!1))})}}}},t.ws_commands.message=function(e){this.show_mesage(e)},t._notifications={},t._last_notification=0,t.prototype.clear_notifications=function(){for(var e in t._notifications){var n=t._notifications[e];if(n)try{n.close()}catch(i){}}t._notifications={},t._last_notification=0},t.prototype.show_notification=function(e,n,i,s,o,a){var r=Notification.permission;if("denied "===r)return!1;if("granted"===r){n=n||"FrankerFaceZ",s=s||1e4;var l={lang:"en-US",dir:"ltr",body:e,tag:i||"FrankerFaceZ",icon:"http://cdn.frankerfacez.com/icon32.png"},c=this,h=new Notification(n,l),u=t._last_notification++;return t._notifications[u]=h,h.addEventListener("click",function(){delete t._notifications[u],o&&o.bind(c)()}),h.addEventListener("close",function(){delete t._notifications[u],a&&a.bind(c)()}),void("number"==typeof s&&h.addEventListener("show",function(){setTimeout(function(){delete t._notifications[u],h.close()},s)}))}var c=this;Notification.requestPermission(function(){c.show_notification(e,n,i)})},t.prototype.show_message=function(t){e.noty({text:t,theme:"ffzTheme",layout:"bottomCenter",closeWith:["button"]}).show()}},{}],22:[function(t){var n=e.FrankerFaceZ,i=t("../utils");n.prototype.setup_races=function(){this.log("Initializing race support."),this.srl_races={}},n.settings_info.srl_races={type:"boolean",value:!0,name:"SRL Race Information",help:'Display information about SpeedRunsLive races under channels.',on_update:function(){this.rebuild_race_ui()}},n.ws_on_close.push(function(){var e=App.__container__.lookup("controller:channel"),t=e.get("id"),n=!1;for(var i in this.srl_races)delete this.srl_races[i],i==t&&(n=!0);n&&this.rebuild_race_ui()}),n.ws_commands.srl_race=function(e){for(var t=App.__container__.lookup("controller:channel"),n=t.get("id"),i=!1,s=0;s=300?"right":"left")+" share dropmenu",this._popup_kill=this._race_kill.bind(this),this._popup=e;var l="http://kadgar.net/live",c=!1;for(var h in a.entrants){var u=a.entrants[h].state;a.entrants.hasOwnProperty(h)&&a.entrants[h].channel&&("racing"==u||"entered"==u)&&(l+="/"+a.entrants[h].channel,c=!0)}var d=document.querySelector(".app-main.theatre")?document.body.clientHeight-300:t.parentElement.offsetTop-175,p=App.__container__.lookup("controller:channel"),_=p?p.get("display_name"):n.get_capitalization(o),f=encodeURIComponent("I'm watching "+_+" race "+a.goal+" in "+a.game+" on SpeedRunsLive!");r='
',r+='
',r+="
#Entrant Time
",r+='
',r+='',r+='

SRL',c&&(r+='   Multitwitch'),r+="

",e.innerHTML=r,t.appendChild(e),this._update_race(!0)}}},n.prototype._update_race=function(e){this._race_timer&&e&&(clearTimeout(this._race_timer),delete this._race_timer);var t=document.querySelector("#ffz-ui-race");if(t){var n=t.getAttribute("data-channel"),s=this.srl_races[n];if(!s)return t.parentElement.removeChild(t),this._popup_kill&&this._popup_kill(),void(this._popup&&(delete this._popup,delete this._popup_kill));var o=s.twitch_entrants[n],a=s.entrants[o],r=t.querySelector("#ffz-race-popup"),l=Date.now()/1e3,c=Math.floor(l-s.time);if(t.querySelector(".logo").innerHTML=i.placement(a),r){var h=r.querySelector("tbody"),u=r.querySelector(".heading span"),d=r.querySelector(".heading div");h.innerHTML="";var p=[],_=!0;for(var f in s.entrants)s.entrants.hasOwnProperty(f)&&("racing"==s.entrants[f].state&&(_=!1),p.push(s.entrants[f]));p.sort(function(e,t){var n=e.place||9999,i=t.place||9999,s=e.time||c,o=t.time||c;return("forfeit"==e.state||"dq"==e.state)&&(n=1e4),("forfeit"==t.state||"dq"==t.state)&&(i=1e4),i>n?-1:n>i?1:e.namet.name?1:o>s?-1:s>o?1:void 0});for(var m=0;m'+f.display_name+"",v=f.channel?'':"",y=f.hitbox?'':"",b=c?i.time_to_string(f.time||c):"",w=i.place_string(f.place),k=f.comment?i.sanitize(f.comment):"";h.innerHTML+="'+w+""+g+""+v+y+''+("forfeit"==f.state?"Forfeit":b)+""}if(this._race_game!=s.game||this._race_goal!=s.goal){this._race_game=s.game,this._race_goal=s.goal;var T=i.sanitize(s.game),z=i.sanitize(s.goal);d.innerHTML='

'+T+"

Goal: "+z}c?_?u.innerHTML="Done":(u.innerHTML=i.time_to_string(c),this._race_timer=setTimeout(this._update_race.bind(this),1e3)):u.innerHTML="Entry Open"}}}},{"../utils":25}],23:[function(t){var n=e.FrankerFaceZ,i=t("../constants");n.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",i.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":3}],24:[function(t){var n=e.FrankerFaceZ,i=t("../constants"),s=t("../utils");n.ws_commands.viewers=function(e){var t=e[0],n=e[1],o=App.__container__.lookup("controller:channel"),a=o&&o.get&&o.get("id");if(a===t){var r=document.querySelector(".channel-stats .ffz.stat"),l=i.ZREKNARF+" "+s.number_commas(n);if(r)r.innerHTML=l;else{var c=document.querySelector(".channel-stats");if(!c)return;r=document.createElement("span"),r.className="ffz stat",r.title="Viewers with FrankerFaceZ",r.innerHTML=l,c.appendChild(r),jQuery(r).tipsy()}}}},{"../constants":3,"../utils":25}],25:[function(t,n){var i=(e.FrankerFaceZ,t("./constants"),{}),s=document.createElement("span"),o=function(e){return 1==e?"1st":2==e?"2nd":3==e?"3rd":null==e?"---":e+"th"};n.exports={update_css:function(e,t,n){var i=e.innerHTML,s="/*BEGIN "+t+"*/",o="/*END "+t+"*/",a=i.indexOf(s),r=i.indexOf(o),l=-1!==a&&-1!==r&&r>a;(l||n)&&(l&&(i=i.substr(0,a)+i.substr(r+o.length)),n&&(i+=s+n+o),e.innerHTML=i)},number_commas:function(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")},place_string:o,placement:function(e){return"forfeit"==e.state?"Forfeit":"dq"==e.state?"DQed":e.place?o(e.place):""},sanitize:function(e){var t=i[e];return t||(s.textContent=e,t=i[e]=s.innerHTML,s.innerHTML=""),t},time_to_string:function(e){var t=e%60,n=Math.floor(e/60),i=Math.floor(n/60);return n%=60,(10>i?"0":"")+i+":"+(10>n?"0":"")+n+":"+(10>t?"0":"")+t}}},{"./constants":3}]},{},[13]),e.ffz=new FrankerFaceZ}(window); \ No newline at end of file +!function(e){!function t(e,n,i){function o(r,a){if(!n[r]){if(!e[r]){var c="function"==typeof require&&require;if(!a&&c)return c(r,!0);if(s)return s(r,!0);throw new Error("Cannot find module '"+r+"'")}var l=n[r]={exports:{}};e[r][0].call(l.exports,function(t){var n=e[r][1][t];return o(n?n:t)},l,l.exports,t,e,n,i)}return n[r].exports}for(var s="function"==typeof require&&require,r=0;re?this._legacy_add_donors(e):void 0):void 0})},n.prototype._legacy_parse_donors=function(e){var t=0;if(null!=e)for(var n=e.trim().split(/\W+/),i=0;i50)return"Each user you unmod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var i=t.length;t.length;){var o=t.shift();e.room.tmiRoom.sendMessage("/unmod "+o)}return"Sent unmod command for "+i+" users."},t.ffz_commands.massunmod.help="Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list.",t.ffz_commands.massmod=function(e,t){if(t=t.join(" ").trim(),!t.length)return"You must provide a list of users to mod.";t=t.split(/\W*,\W*/);var n=this.get_user();if(!n||!n.login==e.id)return"You must be the broadcaster to use massmod.";if(t.length>50)return"Each user you mod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var i=t.length;t.length;){var o=t.shift();e.room.tmiRoom.sendMessage("/mod "+o)}return"Sent mod command for "+i+" users."},t.ffz_commands.massmod.help="Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."},{}],3:[function(e,t){var n='',i="true"==localStorage.ffzDebugMode&&document.body.classList.contains("ffz-dev");t.exports={DEBUG:i,SERVER:i?"//localhost:8000/":"//cdn.frankerfacez.com/",SVGPATH:n,ZREKNARF:''+n+"",CHAT_BUTTON:''+n+"",GEAR:'',HEART:''}},{}],4:[function(){var t=e.FrankerFaceZ;t.ffz_commands.developer_mode=function(e,t){var n,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?n=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(n=!1),void 0===n?"Developer Mode is currently "+("true"==localStorage.ffzDebugMode?"enabled.":"disabled."):(localStorage.ffzDebugMode=n,"Developer Mode is now "+(n?"enabled":"disabled")+". Please refresh your browser.")},t.ffz_commands.developer_mode.help="Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."},{}],5:[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 n=Ember.View.views[t];if(n instanceof e){this.log("Adding UI link manually to Chat view.",n);try{n.$(".textarea-contain").append(this.build_ui_link(n))}catch(i){this.error("setup: build_ui_link: "+i)}}}},t.prototype._modify_cview=function(e){var t=this;e.reopen({didInsertElement:function(){this._super();try{this.$()&&this.$(".textarea-contain").append(t.build_ui_link(this))}catch(e){t.error("didInsertElement: build_ui_link: "+e)}},willClearRender:function(){this._super();try{this.$(".ffz-ui-toggle").remove()}catch(e){t.error("willClearRender: remove ui link: "+e)}},ffzUpdateLink:Ember.observer("controller.currentRoom",function(){try{t.update_ui_link()}catch(e){t.error("ffzUpdateLink: update_ui_link: "+e)}})})}},{}],6:[function(t){var n=e.FrankerFaceZ,i=t("../utils"),o=function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")};n.settings_info.capitalize={type:"boolean",value:!0,category:"Chat",visible:function(){return!this.has_bttv},name:"Username Capitalization",help:"Display names in chat with proper capitalization."},n.settings_info.keywords={type:"button",value:[],category:"Chat",visible:function(){return!this.has_bttv},name:"Highlight Keywords",help:"Set additional keywords that will be highlighted in chat.",method:function(){var e=this.settings.keywords.join(", "),t=prompt("Highlight Keywords\n\nPlease enter a comma-separated list of words that you would like to be highlighted in chat.",e);null!==t&&void 0!==t&&(t=t.trim().split(/\W*,\W*/),1!=t.length||""!=t[0]&&"disable"!=t[0]||(t=[]),this.settings.set("keywords",t))}},n.settings_info.fix_color={type:"boolean",value:!1,category:"Chat",visible:function(){return!this.has_bttv},name:"Adjust Username Colors",help:"Ensure that username colors contrast with the background enough to be readable.",on_update:function(e){this.has_bttv||document.body.classList.toggle("ffz-chat-colors",e)}},n.settings_info.chat_rows={type:"boolean",value:!1,category:"Chat",visible:function(){return!this.has_bttv},name:"Chat Line Backgrounds",help:"Display alternating background colors for lines in chat.",on_update:function(e){this.has_bttv||document.body.classList.toggle("ffz-chat-background",e)}},n.prototype.setup_line=function(){document.body.classList.toggle("ffz-chat-colors",!this.has_bttv&&this.settings.fix_color),document.body.classList.toggle("ffz-chat-background",!this.has_bttv&&this.settings.chat_rows),this._colors={},this._last_row={};var t=this._fix_color_style=document.createElement("style");t.id="ffz-style-username-colors",t.type="text/css",document.head.appendChild(t),this.log("Hooking the Ember Line controller.");var i=App.__container__.resolve("controller:line"),o=this;i.reopen({tokenizedMessage:function(){var e=this._super();try{e=o._emoticonize(this,e);var t=o.get_user();t&&this.get("model.from")==t.login||(e=o._mentionize(this,e))}catch(n){try{o.error("LineController tokenizedMessage: "+n)}catch(n){}}return e}.property("model.message","isModeratorOrHigher")}),this.log("Hooking the Ember Line view.");var i=App.__container__.resolve("view:line");i.reopen({didInsertElement:function(){this._super();try{var t=this.get("element"),i=this.get("context.model.from"),s=this.get("context.parentController.content.id"),r=this.get("context.model.color"),a=this.get("context.model.ffz_alternate");r&&o._handle_color(r),void 0===a&&(a=o._last_row[s]=o._last_row.hasOwnProperty(s)?!o._last_row[s]:!1,this.set("context.model.ffz_alternate",a)),t.classList.toggle("ffz-alternate",a),t.setAttribute("data-room",s),t.setAttribute("data-sender",i),o.render_badge(this),o.settings.capitalize&&o.capitalize(this,i);var c=t.querySelector("span.mentioned");if(c&&(t.classList.add("ffz-mentioned"),!document.hasFocus()&&!this.get("context.model.ffz_notified")&&o.settings.highlight_notifications)){var l=n.get_capitalization(s),d=n.get_capitalization(i),u=l,h=this.get("context.model.message");this.get("context.parentController.content.isGroupRoom")&&(u=this.get("context.parentController.content.tmiRoom.displayName")),h="action"==this.get("context.model.style")?"* "+d+" "+h:d+": "+h,o.show_notification(h,"Twitch Chat Mention in "+u,l,6e4,e.focus.bind(e))}this.set("context.model.ffz_notified",!0)}catch(p){try{o.error("LineView didInsertElement: "+p)}catch(p){}}}});var s=this.get_user();s&&s.name&&(n.capitalization[s.login]=[s.name,Date.now()])},n.prototype._handle_color=function(e){if(e&&!this._colors[e]){this._colors[e]=!0;var t=parseInt(e.substr(1),16),n=[t>>16,t>>8&255,255&t],o=i.get_luminance(n),s="",r='span[style="color:'+e+'"]',a=!1;if(o>.3){a=!0;for(var c=255,l=n;c--&&(l=i.darken(l),!(i.get_luminance(l)<=.3)););s+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+r+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+r+" { color: "+i.rgb_to_css(l)+" !important; }\n"}else s+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+r+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+r+" { color: "+e+" !important; }\n";if(.1>o){a=!0;for(var c=255,l=n;c--&&(l=i.brighten(l),!(i.get_luminance(l)>=.1)););s+=".ffz-chat-colors .theatre .chat-container .chat-line "+r+", .ffz-chat-colors .chat-container.dark .chat-line "+r+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+r+" { color: "+i.rgb_to_css(l)+" !important; }\n"}else s+=".ffz-chat-colors .theatre .chat-container .chat-line "+r+", .ffz-chat-colors .chat-container.dark .chat-line "+r+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+r+" { color: "+e+" !important; }\n";a&&(this._fix_color_style.innerHTML+=s)}},n.capitalization={},n._cap_fetching=0,n.get_capitalization=function(t,i){if(e.BetterTTV)return BetterTTV.chat.helpers.lookupDisplayName(t);if(!t)return t;if(t=t.toLowerCase(),"jtv"==t||"twitchnotify"==t)return t;var o=n.capitalization[t];return o&&Date.now()-o[1]<36e5?o[0]:(n._cap_fetching<5&&(n._cap_fetching++,Twitch.api.get("users/"+t).always(function(e){var o=e.display_name||t;n.capitalization[t]=[o,Date.now()],n._cap_fetching--,"function"==typeof i&&i(o)})),o?o[0]:t)},n.prototype.capitalize=function(e,t){var i=n.get_capitalization(t,this.capitalize.bind(this,e));i&&e.$(".from").text(i)},n._regex_cache={},n._get_regex=function(e){return n._regex_cache[e]=n._regex_cache[e]||RegExp("\\b"+o(e)+"\\b","ig")},n._mentions_to_regex=function(e){return n._regex_cache[e]=n._regex_cache[e]||RegExp("\\b(?:"+_.chain(e).map(o).value().join("|")+")\\b","ig")},n.prototype._mentionize=function(e,t){var i=this.settings.keywords;if(!i||!i.length)return t;"string"==typeof t&&(t=[t]);var o=n._mentions_to_regex(i);return _.chain(t).map(function(e){return _.isString(e)?e.match(o)?_.zip(_.map(e.split(o),_.identity),_.map(e.match(o),function(e){return{mentionedUser:e,own:!1}})):[e]:e}).flatten().compact().value()},n.prototype._emoticonize=function(e,t){var n=e.get("parentController.model.id"),i=e.get("model.from"),o=this,s=this.getEmotes(i,n),r=[];return _.each(s,function(e){var n=o.emote_sets[e];n&&_.each(n.emotes,function(e){_.any(t,function(t){return _.isString(t)&&t.match(e.regex)})&&r.push(e)})}),r.length?("string"==typeof t&&(t=[t]),_.each(r,function(e){var n={isEmoticon:!0,cls:e.klass,emoticonSrc:e.url,altText:e.hidden?"???":e.name};t=_.compact(_.flatten(_.map(t,function(t){if(_.isObject(t))return t;var i=t.split(e.regex),o=[];return i.forEach(function(e,t){o.push(e),t!==i.length-1&&o.push(n)}),o})))}),t):t}},{"../utils":24}],7:[function(t){var n=e.FrankerFaceZ,i=t("../utils"),o={ESC:27,P:80,B:66,T:84},s=[["5m",300],["10m",600],["1hr",3600],["12hr",43200],["24hr",86400]],r='';n.settings_info.enhanced_moderation={type:"boolean",value:!1,visible:function(){return!this.has_bttv},category:"Chat",name:"Enhanced Moderation",help:"Use /p, /t, /u and /b in chat to moderator, or use hotkeys with moderation cards."},n.prototype.setup_mod_card=function(){this.log("Hooking the Ember Moderation Card view.");var e=App.__container__.resolve("view:moderation-card"),t=this;e.reopen({didInsertElement:function(){this._super();try{if(!t.settings.enhanced_moderation)return;var e=this.get("element"),n=this.get("context");if(n.get("parentController.model.isModeratorOrHigher")){e.classList.add("ffz-moderation-card"),e.setAttribute("tabindex",1),e.addEventListener("keyup",function(e){var t=e.keyCode||e.which,i=n.get("model.user.id"),s=n.get("parentController.model");if(t==o.P)s.send("/timeout "+i+" 1");else if(t==o.B)s.send("/ban "+i);else if(t==o.T)s.send("/timeout "+i+" 600");else if(t!=o.ESC)return;n.send("hideModOverlay")});var a=document.createElement("div");a.className="interface clearfix";var c=function(e){var t=n.get("model.user.id"),i=n.get("parentController.model");i.send(-1===e?"/unban "+t:"/timeout "+t+" "+e)},l=function(e,t){var n=document.createElement("button");return n.className="button",n.innerHTML=e,n.title="Timeout User for "+i.number_commas(t)+" Second"+(1!=t?"s":""),600===t?n.title="(T)"+n.title.substr(1):1===t&&(n.title="(P)urge - "+n.title),jQuery(n).tipsy(),n.addEventListener("click",c.bind(this,t)),n};a.appendChild(l("Purge",1));var d=document.createElement("span");d.className="right",a.appendChild(d);for(var u=0;u button");g&&"button"==g.className&&(g.innerHTML=r,g.classList.add("glyph-only"),g.classList.add("message"),g.title="Message User",jQuery(g).tipsy()),this.$().draggable({start:function(){e.focus()}}),e.focus()}catch(v){try{t.error("ModerationCardView didInsertElement: "+v)}catch(v){}}}})},n.chat_commands.purge=n.chat_commands.p=function(e,t){if(!t||!t.length)return"Purge Usage: /p username [more usernames separated by spaces]";if(t.length>10)return"Please only purge up to 10 users at once.";for(var n=0;n10)return"Please only ban up to 10 users at once.";for(var n=0;n10)return"Please only unban up to 10 users at once.";for(var n=0;nn?this._legacy_add_room(e,t,n):void 0)})},n.prototype._legacy_load_room_css=function(e,t,n){var r=e,a=r.match(s);a&&a[1]&&(r=a[1]);var c={id:e,menu_sets:[r],sets:[r],moderator_badge:null,css:null};return n&&(n=n.replace(i,"").trim()),n&&(n=n.replace(o,function(e,t){return c.moderator_badge||"modicon.png"!==t.substr(-11)?e:(c.moderator_badge=t,"")})),c.css=n||null,this._load_room_json(e,t,c)}},{"../constants":3,"../utils":24}],9:[function(){var t=e.FrankerFaceZ;t.prototype.setup_viewers=function(){this.log("Hooking the Ember Viewers controller.");var e=App.__container__.resolve("controller:viewers");this._modify_viewers(e)},t.prototype._modify_viewers=function(e){var n=this;e.reopen({lines:function(){var e=this._super();try{var i=[],o={},s=null,r=App.__container__.lookup("controller:channel"),a=this.get("parentController.model.id"),c=r&&r.get("id");if(c){var l=r.get("display_name");l&&(t.capitalization[c]=[l,Date.now()])}a!=c&&(c=null);for(var d=0;dn?this._legacy_load_set(e,t,n):"function"==typeof t&&t(!1))})},n.prototype._legacy_load_css=function(e,t,n){var s={},r={id:e,emotes:s,extra_css:null},a=this;n=n.replace(i,function(e,t,n,i,o,r,l,d){o=parseInt(o),r=parseInt(r),l=c(l,o);var u="."===i.substr(i.lastIndexOf("/")+1,1),h=++a._last_emote_id,p={id:h,hidden:u,name:n,height:o,width:r,url:i,margins:l,extra_css:d};return s[h]=p,""}).trim(),n&&n.replace(o,function(e,t){r.icon||"modicon.png"!==t.substr(-11)||(r.icon=t)}),this._load_set_json(e,t,r)}},{"./constants":3,"./utils":24}],11:[function(){var t=e.FrankerFaceZ,n=/(\sdata-sender="[^"]*"(?=>))/;t.prototype.find_bttv=function(t,n){return this.has_bttv=!1,e.BTTVLOADED?this.setup_bttv(n||0):void(n>=6e4?this.log("BetterTTV was not detected after 60 seconds."):setTimeout(this.find_bttv.bind(this,t,(n||0)+t),t))},t.prototype.setup_bttv=function(e){this.log("BetterTTV was detected after "+e+"ms. Hooking."),this.has_bttv=!0,document.body.classList.remove("ffz-dark"),this._dark_style&&(this._dark_style.parentElement.removeChild(this._dark_style),delete this._dark_style),document.body.classList.remove("ffz-chat-colors"),document.body.classList.remove("ffz-chat-background");var t=BetterTTV.chat.helpers.sendMessage,i=this;BetterTTV.chat.helpers.sendMessage=function(e){var n=e.split(" ",1)[0].toLowerCase();return"/ffz"!==n?t(e):void i.run_ffz_command(e.substr(5),BetterTTV.chat.store.currentRoom)};var o,s=BetterTTV.chat.handlers.privmsg;BetterTTV.chat.handlers.privmsg=function(e,t){o=e;var n=s(e,t);return o=null,n};var r=BetterTTV.chat.templates.privmsg;BetterTTV.chat.templates.privmsg=function(e,t,s,a,c){i.bttv_badges(c);var l=r(e,t,s,a,c);return l.replace(n,'$1 data-room="'+o+'"')};var a,c=BetterTTV.chat.templates.message;BetterTTV.chat.templates.message=function(e,t,n,i){a=e;var o=c(e,t,n,i);return a=null,o};var l=BetterTTV.chat.templates.emoticonize;BetterTTV.chat.templates.emoticonize=function(e,t){var n=l(e,t),s=i.getEmotes(a,o),t=[];return _.each(s,function(e){var o=i.emote_sets[e];o&&_.each(o.emotes,function(e){_.any(n,function(t){return _.isString(t)&&t.match(e.regex)})&&t.push(e)})}),t.length?(_.each(t,function(e){var t=[''+e.name+''],i=n;if(n=[],!i||!i.length)return n;for(var o=0;o=6e4?this.log("Emote Menu for Twitch was not detected after 60 seconds."):setTimeout(this.find_emote_menu.bind(this,t,(n||0)+t),t)) +},t.prototype.setup_emote_menu=function(e){this.log("Emote Menu for Twitch was detected after "+e+"ms. Registering emote enumerator."),emoteMenu.registerEmoteGetter("FrankerFaceZ",this._emote_menu_enumerator.bind(this))},t.prototype._emote_menu_enumerator=function(){for(var e=this.get_user(),n=e?e.login:null,i=App.__container__.lookup("controller:chat"),o=i?i.get("currentRoom.id"):null,s=this.getEmotes(n,o),r=[],a=0;a=6e4?this.log('Twitch application not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.initialize.bind(this,t,(n||0)+t),t)))},n.prototype.setup_ember=function(t){var i=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch application after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.load_settings(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_room(),this.setup_line(),this.setup_chatview(),this.setup_viewers(),this.setup_mod_card(),this.setup_notifications(),this.setup_css(),this.setup_menu(),this.setup_races(),this.find_bttv(10),this.find_emote_menu(10),this.check_ff();var o=e.performance&&performance.now?performance.now():Date.now(),s=o-i;this.log("Initialization complete in "+s+"ms")}},{"./badges":1,"./commands":2,"./debug":4,"./ember/chatview":5,"./ember/line":6,"./ember/moderation-card":7,"./ember/room":8,"./ember/viewers":9,"./emoticons":10,"./ext/betterttv":11,"./ext/emote_menu":12,"./featurefriday":14,"./settings":15,"./shims":16,"./socket":17,"./ui/menu":18,"./ui/menu_button":19,"./ui/notifications":20,"./ui/races":21,"./ui/styles":22,"./ui/viewer_count":23}],14:[function(t){var n=e.FrankerFaceZ,i=t("./constants");n.prototype.feature_friday=null,n.prototype.check_ff=function(e){e||this.log("Checking for Feature Friday data..."),jQuery.ajax(i.SERVER+"script/event.json",{cache:!1,dataType:"json",context:this}).done(function(e){return this._load_ff(e)}).fail(function(t){return 404==t.status?this._load_ff(null):(e=e||0,e++,10>e?setTimeout(this.check_ff.bind(this,e),250):this._load_ff(null))})},n.ws_commands.reload_ff=function(){this.check_ff()},n.prototype._feature_friday_ui=function(e,t,n){if(this.feature_friday&&this.feature_friday.channel!=e){this._emotes_for_sets(t,n,[this.feature_friday.set],"Feature Friday");var i=App.__container__.lookup("controller:channel");if(!i||i.get("id")!=this.feature_friday.channel){var o=this.feature_friday,s=document.createElement("div"),r=document.createElement("a");s.className="chat-menu-content",s.style.textAlign="center";var a=o.display_name+(o.live?" is live now!":"");r.className="button primary",r.classList.toggle("live",o.live),r.classList.toggle("blue",this.has_bttv&&BetterTTV.settings.get("showBlueButtons")),r.href="http://www.twitch.tv/"+o.channel,r.title=a,r.target="_new",r.innerHTML=""+a+"",s.appendChild(r),t.appendChild(s)}}},n.prototype._load_ff=function(e){if(this.feature_friday){this.global_sets.removeObject(this.feature_friday.set);var t=this.emote_sets[this.feature_friday.set];t&&(t.global=!1),this.feature_friday=null,this.update_ui_link()}e&&e.set&&e.channel&&(this.feature_friday={set:e.set,channel:e.channel,live:!1,display_name:n.get_capitalization(e.channel,this._update_ff_name.bind(this))},this.global_sets.push(e.set),this.load_set(e.set,this._update_ff_set.bind(this)),this._update_ff_live())},n.prototype._update_ff_live=function(){if(this.feature_friday){var e=this;Twitch.api.get("streams/"+this.feature_friday.channel).done(function(t){e.feature_friday.live=null!=t.stream,e.update_ui_link()}).always(function(){e.feature_friday.timer=setTimeout(e._update_ff_live.bind(e),12e4)})}},n.prototype._update_ff_set=function(e,t){t&&(t.global=!0)},n.prototype._update_ff_name=function(e){this.feature_friday&&(this.feature_friday.display_name=e)}},{"./constants":3}],15:[function(){var t=e.FrankerFaceZ,n=function(e){return"ffz_setting_"+e};t.settings_info={},t.prototype.load_settings=function(){this.log("Loading settings."),this.settings={};for(var i in t.settings_info){var o=n(i),s=t.settings_info[i],r=s.hasOwnProperty("value")?s.value:void 0;if(localStorage.hasOwnProperty(o))try{r=JSON.parse(localStorage.getItem(o))}catch(a){this.log('Error loading value for "'+i+'": '+a)}this.settings[i]=r}this.settings.get=this._setting_get.bind(this),this.settings.set=this._setting_set.bind(this),this.settings.del=this._setting_del.bind(this),e.addEventListener("storage",this._setting_update.bind(this))},t.prototype._setting_update=function(n){if(n||(n=e.event),this.log("Storage Event",n),n.key&&"ffz_setting_"===n.key.substr(0,12)){var i=n.key,o=i.substr(12),s=void 0,r=t.settings_info[o];this.log("Updated Setting: "+o);try{s=JSON.parse(n.newValue)}catch(a){this.log('Error loading new value for "'+o+'": '+a),s=r.value||void 0}if(this.settings[o]=s,r.on_update)try{r.on_update.bind(this)(s,!1)}catch(a){this.log('Error running updater for setting "'+o+'": '+a)}}},t.prototype._setting_get=function(e){return this.settings[e]},t.prototype._setting_set=function(e,i){var o=n(e),s=t.settings_info[e],r=JSON.stringify(i);if(this.settings[e]=i,localStorage.setItem(o,r),this.log('Changed Setting "'+e+'" to: '+r),s.on_update)try{s.on_update.bind(this)(i,!0)}catch(a){this.log('Error running updater for setting "'+e+'": '+a)}},t.prototype._setting_del=function(e){var i=n(e),o=t.settings_info[e],s=void 0;if(localStorage.hasOwnProperty(i)&&localStorage.removeItem(i),delete this.settings[e],o&&(s=this.settings[e]=o.hasOwnProperty("value")?o.value:void 0),o.on_update)try{o.on_update.bind(this)(s,!0)}catch(r){this.log('Error running updater for setting "'+e+'": '+r)}}},{}],16:[function(){Array.prototype.equals=function(e){if(!e)return!1;if(this.length!=e.length)return!1;for(var t=0,n=this.length;n>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}},{}],17:[function(){var t=e.FrankerFaceZ;t.prototype._ws_open=!1,t.prototype._ws_delay=0,t.ws_commands={},t.ws_on_close=[],t.prototype.ws_create=function(){var e,n=this;this._ws_last_req=0,this._ws_callbacks={},this._ws_pending=this._ws_pending||[];try{e=this._ws_sock=new WebSocket("ws://ffz.stendec.me/")}catch(i){return this._ws_exists=!1,this.log("Error Creating WebSocket: "+i)}this._ws_exists=!0,e.onopen=function(){n._ws_open=!0,n._ws_delay=0,n.log("Socket connected.");var e=n.get_user();e&&n.ws_send("setuser",e.login);for(var t in n.rooms)n.ws_send("sub",t);var i=n._ws_pending;n._ws_pending=[];for(var o=0;oFrankerFaceZ",s.appendChild(a),a.addEventListener("click",this._add_emote.bind(this,e,"To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com"));var c=document.createElement("div");c.className="ffz-ui-menu-page",o.appendChild(c);for(var l in n.menu_pages){var d=n.menu_pages[l];if(d&&(!d.hasOwnProperty("visible")||d.visible&&("function"!=typeof d.visible||d.visible.bind(this)()))){var a=document.createElement("li"),u=document.createElement("a");a.className="item",a.id="ffz-menu-page-"+l,u.title=d.name,u.innerHTML=d.icon,u.addEventListener("click",this._ui_change_page.bind(this,e,s,c,l)),a.appendChild(u),s.appendChild(a)}}this._ui_change_page(e,s,c,this._last_page||"channel"),this._popup=i,c.style.maxHeight=Math.max(100,e.$().height()-162)+"px",e.$(".chat-interface").append(i)},n.prototype._ui_change_page=function(e,t,i,o){this._last_page=o,i.innerHTML="";for(var s=t.querySelectorAll("li.active"),r=0;re?-1:e>t?1:0});for(var d=0;do?-1:o>s?1:0});for(var f=0;f",b.className="switch-label",b.innerHTML=r.name,m.appendChild(v),m.appendChild(b),v.addEventListener("click",this._ui_toggle_setting.bind(this,v,s))}else{m.classList.add("option");var y=document.createElement("a");y.innerHTML=r.name,y.href="#",m.appendChild(y),y.addEventListener("click",r.method.bind(this))}if(r.help){var w=document.createElement("span");w.className="help",w.innerHTML=r.help,m.appendChild(w)}p.appendChild(m)}t.appendChild(p)}},name:"Settings",icon:i.GEAR},n.prototype._ui_toggle_setting=function(e,t){var n=!this.settings.get(t);this.settings.set(t,n),e.classList.toggle("active",n)},n.menu_pages.channel={render:function(e,t){{var n=e.get("controller.currentRoom.id"),i=this.rooms[n];this._emotes_for_sets(t,e,i&&i.menu_sets||[])}this._feature_friday_ui(n,t,e)},name:"Channel",icon:i.ZREKNARF},n.prototype._emotes_for_sets=function(e,t,n,i,o){if(null!=i){var s=document.createElement("div");s.className="list-header",s.appendChild(document.createTextNode(i)),o&&s.appendChild(o),e.appendChild(s)}var r=document.createElement("div"),a=0;r.className="emoticon-grid";for(var c=0;c0){o=!0;break}}e.classList.toggle("no-emotes",!o),e.classList.toggle("live",a),e.classList.toggle("dark",s),e.classList.toggle("blue",r)}}},{"../constants":3}],20:[function(){var t=e.FrankerFaceZ;t.prototype.setup_notifications=function(){this.log("Adding event handler for window focus."),e.addEventListener("focus",this.clear_notifications.bind(this))},t.settings_info.highlight_notifications={type:"boolean",value:!1,category:"Chat",visible:function(){return!this.has_bttv},name:"Highlight Notifications",help:"Display notifications when a highlighted word appears in chat in an unfocused tab.",on_update:function(e,t){if(e&&t){if("denied"===Notification.permission)return this.log("Notifications have been denied by the user."),void this.settings.set("highlight_notifications",!1);if("granted"!==Notification.permission){var n=this;Notification.requestPermission(function(e){"denied"===e&&(n.log("Notifications have been denied by the user."),n.settings.set("highlight_notifications",!1))})}}}},t.ws_commands.message=function(e){this.show_message(e)},t._notifications={},t._last_notification=0,t.prototype.clear_notifications=function(){for(var e in t._notifications){var n=t._notifications[e];if(n)try{n.close()}catch(i){}}t._notifications={},t._last_notification=0},t.prototype.show_notification=function(e,n,i,o,s,r){var a=Notification.permission;if("denied "===a)return!1;if("granted"===a){n=n||"FrankerFaceZ",o=o||1e4;var c={lang:"en-US",dir:"ltr",body:e,tag:i||"FrankerFaceZ",icon:"http://cdn.frankerfacez.com/icon32.png"},l=this,d=new Notification(n,c),u=t._last_notification++;return t._notifications[u]=d,d.addEventListener("click",function(){delete t._notifications[u],s&&s.bind(l)()}),d.addEventListener("close",function(){delete t._notifications[u],r&&r.bind(l)()}),void("number"==typeof o&&d.addEventListener("show",function(){setTimeout(function(){delete t._notifications[u],d.close()},o)}))}var l=this;Notification.requestPermission(function(){l.show_notification(e,n,i)})},t.prototype.show_message=function(t){e.noty({text:t,theme:"ffzTheme",layout:"bottomCenter",closeWith:["button"]}).show()}},{}],21:[function(t){var n=e.FrankerFaceZ,i=t("../utils");n.prototype.setup_races=function(){this.log("Initializing race support."),this.srl_races={}},n.settings_info.srl_races={type:"boolean",value:!0,category:"Channel Metadata",name:"SRL Race Information",help:'Display information about SpeedRunsLive races under channels.',on_update:function(){this.rebuild_race_ui()}},n.ws_on_close.push(function(){var e=App.__container__.lookup("controller:channel"),t=e.get("id"),n=!1;for(var i in this.srl_races)delete this.srl_races[i],i==t&&(n=!0);n&&this.rebuild_race_ui()}),n.ws_commands.srl_race=function(e){for(var t=App.__container__.lookup("controller:channel"),n=t.get("id"),i=!1,o=0;o=300?"right":"left")+" share dropmenu",this._popup_kill=this._race_kill.bind(this),this._popup=e;var c="http://kadgar.net/live",l=!1;for(var d in r.entrants){var u=r.entrants[d].state;r.entrants.hasOwnProperty(d)&&r.entrants[d].channel&&("racing"==u||"entered"==u)&&(c+="/"+r.entrants[d].channel,l=!0)}var h=document.querySelector(".app-main.theatre")?document.body.clientHeight-300:t.parentElement.offsetTop-175,p=App.__container__.lookup("controller:channel"),_=p?p.get("display_name"):n.get_capitalization(s),f=encodeURIComponent("I'm watching "+_+" race "+r.goal+" in "+r.game+" on SpeedRunsLive!");a='
',a+='
',a+="
#Entrant Time
",a+='
',a+='',a+='

SRL',l&&(a+='   Multitwitch'),a+="

",e.innerHTML=a,t.appendChild(e),this._update_race(!0)}}},n.prototype._update_race=function(e){this._race_timer&&e&&(clearTimeout(this._race_timer),delete this._race_timer);var t=document.querySelector("#ffz-ui-race");if(t){var n=t.getAttribute("data-channel"),o=this.srl_races[n];if(!o)return t.parentElement.removeChild(t),this._popup_kill&&this._popup_kill(),void(this._popup&&(delete this._popup,delete this._popup_kill));var s=o.twitch_entrants[n],r=o.entrants[s],a=t.querySelector("#ffz-race-popup"),c=Date.now()/1e3,l=Math.floor(c-o.time);if(t.querySelector(".logo").innerHTML=i.placement(r),a){var d=a.querySelector("tbody"),u=a.querySelector(".heading span"),h=a.querySelector(".heading div");d.innerHTML="";var p=[],_=!0;for(var f in o.entrants)o.entrants.hasOwnProperty(f)&&("racing"==o.entrants[f].state&&(_=!1),p.push(o.entrants[f]));p.sort(function(e,t){var n=e.place||9999,i=t.place||9999,o=e.time||l,s=t.time||l;return("forfeit"==e.state||"dq"==e.state)&&(n=1e4),("forfeit"==t.state||"dq"==t.state)&&(i=1e4),i>n?-1:n>i?1:e.namet.name?1:s>o?-1:o>s?1:void 0});for(var m=0;m'+f.display_name+"",v=f.channel?'':"",b=f.hitbox?'':"",y=l?i.time_to_string(f.time||l):"",w=i.place_string(f.place),k=f.comment?i.sanitize(f.comment):"";d.innerHTML+="'+w+""+g+""+v+b+''+("forfeit"==f.state?"Forfeit":y)+""}if(this._race_game!=o.game||this._race_goal!=o.goal){this._race_game=o.game,this._race_goal=o.goal;var x=i.sanitize(o.game),z=i.sanitize(o.goal);h.innerHTML='

'+x+"

Goal: "+z}l?_?u.innerHTML="Done":(u.innerHTML=i.time_to_string(l),this._race_timer=setTimeout(this._update_race.bind(this),1e3)):u.innerHTML="Entry Open"}}}},{"../utils":24}],22:[function(t){var n=e.FrankerFaceZ,i=t("../constants");n.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",i.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":3}],23:[function(t){var n=e.FrankerFaceZ,i=t("../constants"),o=t("../utils");n.ws_commands.viewers=function(e){var t=e[0],n=e[1],s=App.__container__.lookup("controller:channel"),r=s&&s.get&&s.get("id");if(r===t){var a=document.querySelector(".channel-stats .ffz.stat"),c=i.ZREKNARF+" "+o.number_commas(n);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":3,"../utils":24}],24:[function(t,n){var i=(e.FrankerFaceZ,t("./constants"),{}),o=document.createElement("span"),s=function(e){return 1==e?"1st":2==e?"2nd":3==e?"3rd":null==e?"---":e+"th"},r=function(e,t){t=0===t?0:t||1,t=Math.round(255*-(t/100));var n=Math.max(0,Math.min(255,e[0]-t)),i=Math.max(0,Math.min(255,e[1]-t)),o=Math.max(0,Math.min(255,e[2]-t));return[n,i,o]},a=function(e){return"rgb("+e[0]+", "+e[1]+", "+e[2]+")"},c=function(e,t){return t=0===t?0:t||1,r(e,-t)},l=function(e){e=[e[0]/255,e[1]/255,e[2]/255];for(var t=0;tr;(c||n)&&(c&&(i=i.substr(0,r)+i.substr(a+s.length)),n&&(i+=o+n+s),e.innerHTML=i)},get_luminance:l,brighten:r,darken:c,rgb_to_css:a,number_commas:function(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")},place_string:s,placement:function(e){return"forfeit"==e.state?"Forfeit":"dq"==e.state?"DQed":e.place?s(e.place):""},sanitize:function(e){var t=i[e];return t||(o.textContent=e,t=i[e]=o.innerHTML,o.innerHTML=""),t},time_to_string:function(e){var t=e%60,n=Math.floor(e/60),i=Math.floor(n/60);return n%=60,(10>i?"0":"")+i+":"+(10>n?"0":"")+n+":"+(10>t?"0":"")+t}}},{"./constants":3}]},{},[13]),e.ffz=new FrankerFaceZ}(window); \ No newline at end of file diff --git a/src/commands.js b/src/commands.js index a2400508..60d3ed9a 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,11 +1,25 @@ var FFZ = window.FrankerFaceZ; +// ----------------- +// Log Export +// ----------------- + +FFZ.ffz_commands.log = function(room, args) { + this._pastebin(this._log_data.join("\n"), function(url) { + if ( ! url ) + return this.room_message(room, "There was an error uploading the FrankerFaceZ log."); + + this.room_message(room, "Your FrankerFaceZ log has been pasted to: " + url); + }); +}; + + // ----------------- // Mass Moderation // ----------------- -FFZ.chat_commands.massunmod = function(room, args) { +FFZ.ffz_commands.massunmod = function(room, args) { args = args.join(" ").trim(); if ( ! args.length ) @@ -29,10 +43,10 @@ FFZ.chat_commands.massunmod = function(room, args) { return "Sent unmod command for " + count + " users."; } -FFZ.chat_commands.massunmod.help = "Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list."; +FFZ.ffz_commands.massunmod.help = "Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list."; -FFZ.chat_commands.massmod = function(room, args) { +FFZ.ffz_commands.massmod = function(room, args) { args = args.join(" ").trim(); if ( ! args.length ) @@ -47,7 +61,6 @@ FFZ.chat_commands.massmod = function(room, args) { if ( args.length > 50 ) return "Each user you mod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses."; - var count = args.length; while(args.length) { var name = args.shift(); @@ -57,4 +70,4 @@ FFZ.chat_commands.massmod = function(room, args) { return "Sent mod command for " + count + " users."; } -FFZ.chat_commands.massmod.help = "Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."; \ No newline at end of file +FFZ.ffz_commands.massmod.help = "Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."; \ No newline at end of file diff --git a/src/debug.js b/src/debug.js index c91e0108..b7aadf46 100644 --- a/src/debug.js +++ b/src/debug.js @@ -5,7 +5,7 @@ var FFZ = window.FrankerFaceZ; // Developer Mode Command // ----------------------- -FFZ.chat_commands.developer_mode = function(room, args) { +FFZ.ffz_commands.developer_mode = function(room, args) { var enabled, args = args && args.length ? args[0].toLowerCase() : null; if ( args == "y" || args == "yes" || args == "true" || args == "on" ) enabled = true; @@ -19,4 +19,4 @@ FFZ.chat_commands.developer_mode = function(room, args) { return "Developer Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; } -FFZ.chat_commands.developer_mode.help = "Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."; +FFZ.ffz_commands.developer_mode.help = "Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."; diff --git a/src/ember/chatview.js b/src/ember/chatview.js index cd3a787b..2068d173 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -25,7 +25,11 @@ FFZ.prototype.setup_chatview = function() { continue; this.log("Adding UI link manually to Chat view.", view); - view.$('.textarea-contain').append(this.build_ui_link(view)); + try { + view.$('.textarea-contain').append(this.build_ui_link(view)); + } catch(err) { + this.error("setup: build_ui_link: " + err); + } } } @@ -40,16 +44,28 @@ FFZ.prototype._modify_cview = function(view) { view.reopen({ didInsertElement: function() { this._super(); - this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); + try { + this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); + } catch(err) { + f.error("didInsertElement: build_ui_link: " + err); + } }, willClearRender: function() { this._super(); - this.$(".ffz-ui-toggle").remove(); + try { + this.$(".ffz-ui-toggle").remove(); + } catch(err) { + f.error("willClearRender: remove ui link: " + err); + } }, ffzUpdateLink: Ember.observer('controller.currentRoom', function() { - f.update_ui_link(); + try { + f.update_ui_link(); + } catch(err) { + f.error("ffzUpdateLink: update_ui_link: " + err); + } }) }); } \ No newline at end of file diff --git a/src/ember/line.js b/src/ember/line.js index 7a576f1a..bac5479f 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -1,4 +1,5 @@ var FFZ = window.FrankerFaceZ, + utils = require("../utils"), reg_escape = function(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); @@ -13,6 +14,7 @@ FFZ.settings_info.capitalize = { type: "boolean", value: true, + category: "Chat", visible: function() { return ! this.has_bttv }, name: "Username Capitalization", @@ -24,6 +26,7 @@ FFZ.settings_info.keywords = { type: "button", value: [], + category: "Chat", visible: function() { return ! this.has_bttv }, name: "Highlight Keywords", @@ -33,7 +36,7 @@ FFZ.settings_info.keywords = { var old_val = this.settings.keywords.join(", "), new_val = prompt("Highlight Keywords\n\nPlease enter a comma-separated list of words that you would like to be highlighted in chat.", old_val); - if ( ! new_val ) + if ( new_val === null || new_val === undefined ) return; // Split them up. @@ -47,17 +50,40 @@ FFZ.settings_info.keywords = { }; +FFZ.settings_info.fix_color = { + type: "boolean", + value: false, + + category: "Chat", + visible: function() { return ! this.has_bttv }, + + name: "Adjust Username Colors", + help: "Ensure that username colors contrast with the background enough to be readable.", + + on_update: function(val) { + if ( this.has_bttv ) + return; + + document.body.classList.toggle("ffz-chat-colors", val); + } + }; + + FFZ.settings_info.chat_rows = { type: "boolean", value: false, + category: "Chat", visible: function() { return ! this.has_bttv }, name: "Chat Line Backgrounds", help: "Display alternating background colors for lines in chat.", on_update: function(val) { - document.querySelector(".app-main").classList.toggle("ffz-chat-background", val); + if ( this.has_bttv ) + return; + + document.body.classList.toggle("ffz-chat-background", val); } }; @@ -67,10 +93,19 @@ FFZ.settings_info.chat_rows = { // --------------------- FFZ.prototype.setup_line = function() { - // Alternating Background - document.querySelector('.app-main').classList.toggle('ffz-chat-background', this.settings.chat_rows); + // Chat Enhancements + document.body.classList.toggle("ffz-chat-colors", !this.has_bttv && this.settings.fix_color); + document.body.classList.toggle('ffz-chat-background', !this.has_bttv && this.settings.chat_rows); + + this._colors = {}; this._last_row = {}; + var s = this._fix_color_style = document.createElement('style'); + s.id = "ffz-style-username-colors"; + s.type = 'text/css'; + document.head.appendChild(s); + + this.log("Hooking the Ember Line controller."); var Line = App.__container__.resolve('controller:line'), @@ -79,11 +114,20 @@ FFZ.prototype.setup_line = function() { Line.reopen({ tokenizedMessage: function() { // Add our own step to the tokenization procedure. - var tokens = f._emoticonize(this, this._super()), - user = f.get_user(); + var tokens = this._super(); - if ( ! user || this.get("model.from") != user.login ) - tokens = f._mentionize(this, tokens); + try { + tokens = f._emoticonize(this, tokens); + var user = f.get_user(); + + if ( ! user || this.get("model.from") != user.login ) + tokens = f._mentionize(this, tokens); + + } catch(err) { + try { + f.error("LineController tokenizedMessage: " + err); + } catch(err) { } + } return tokens; @@ -97,60 +141,84 @@ FFZ.prototype.setup_line = function() { Line.reopen({ didInsertElement: function() { this._super(); + try { + var el = this.get('element'), + user = this.get('context.model.from'), + room = this.get('context.parentController.content.id'), + color = this.get('context.model.color'), - var el = this.get('element'), - user = this.get('context.model.from'), - room = this.get('context.parentController.content.id'), - row_type = this.get('context.model.ffzAlternate'); + row_type = this.get('context.model.ffz_alternate'); - if ( row_type === undefined ) { - row_type = f._last_row[room] = f._last_row.hasOwnProperty(room) ? !f._last_row[room] : false; - this.set("context.model.ffzAlternate", row_type); - } - el.classList.toggle('ffz-alternate', row_type); - el.setAttribute('data-room', room); - el.setAttribute('data-sender', user); + // Color Processing + if ( color ) + f._handle_color(color); - f.render_badge(this); - if ( f.settings.capitalize ) - f.capitalize(this, user); - - // Check for any mentions. - var mentioned = el.querySelector('span.mentioned'); - if ( mentioned ) { - el.classList.add("ffz-mentioned"); - - if ( ! document.hasFocus() && ! this.get('context.model.ffzNotified') && f.settings.highlight_notifications ) { - var cap_room = FFZ.get_capitalization(room), - cap_user = FFZ.get_capitalization(user), - room_name = cap_room, - msg = this.get("context.model.message"); - - if ( this.get("context.parentController.content.isGroupRoom") ) - room_name = this.get("context.parentController.content.tmiRoom.displayName"); - - if ( this.get("context.model.style") == "action" ) - msg = "* " + cap_user + " " + msg; - else - msg = cap_user + ": " + msg; - - f.show_notification( - msg, - "Twitch Chat Mention in " + room_name, - cap_room, - 60000, - window.focus.bind(window) - ); + // Row Alternation + if ( row_type === undefined ) { + row_type = f._last_row[room] = f._last_row.hasOwnProperty(room) ? !f._last_row[room] : false; + this.set("context.model.ffz_alternate", row_type); } - } - // Mark that we've checked this message for mentions. - this.set('context.model.ffzNotified', true); + el.classList.toggle('ffz-alternate', row_type); + + + // Basic Data + el.setAttribute('data-room', room); + el.setAttribute('data-sender', user); + + + // Badge + f.render_badge(this); + + + // Capitalization + if ( f.settings.capitalize ) + f.capitalize(this, user); + + + // Mention Highlighting + var mentioned = el.querySelector('span.mentioned'); + if ( mentioned ) { + el.classList.add("ffz-mentioned"); + + if ( ! document.hasFocus() && ! this.get('context.model.ffz_notified') && f.settings.highlight_notifications ) { + var cap_room = FFZ.get_capitalization(room), + cap_user = FFZ.get_capitalization(user), + room_name = cap_room, + msg = this.get("context.model.message"); + + if ( this.get("context.parentController.content.isGroupRoom") ) + room_name = this.get("context.parentController.content.tmiRoom.displayName"); + + if ( this.get("context.model.style") == "action" ) + msg = "* " + cap_user + " " + msg; + else + msg = cap_user + ": " + msg; + + f.show_notification( + msg, + "Twitch Chat Mention in " + room_name, + cap_room, + 60000, + window.focus.bind(window) + ); + } + } + + // Mark that we've checked this message for mentions. + this.set('context.model.ffz_notified', true); + + } catch(err) { + try { + f.error("LineView didInsertElement: " + err); + } catch(err) { } + } } }); + // Store the capitalization of our own name. var user = this.get_user(); if ( user && user.name ) @@ -158,6 +226,69 @@ FFZ.prototype.setup_line = function() { } +// --------------------- +// Fix Name Colors +// --------------------- + +FFZ.prototype._handle_color = function(color) { + if ( ! color || this._colors[color] ) + return; + + this._colors[color] = true; + + // Parse the color. + var raw = parseInt(color.substr(1), 16), + rgb = [ + (raw >> 16), + (raw >> 8 & 0x00FF), + (raw & 0x0000FF) + ], + + lum = utils.get_luminance(rgb), + + output = "", + rule = 'span[style="color:' + color + '"]', + matched = false; + + if ( lum > 0.3 ) { + // Color Too Bright. We need a lum of 0.3 or less. + matched = true; + + var s = 255, + nc = rgb; + while(s--) { + nc = utils.darken(nc); + if ( utils.get_luminance(nc) <= 0.3 ) + break; + } + + output += '.ffz-chat-colors .ember-chat-container:not(.dark) .chat-line ' + rule + ', .ffz-chat-colors .chat-container:not(.dark) .chat-line ' + rule + ' { color: ' + utils.rgb_to_css(nc) + ' !important; }\n'; + } else + output += '.ffz-chat-colors .ember-chat-container:not(.dark) .chat-line ' + rule + ', .ffz-chat-colors .chat-container:not(.dark) .chat-line ' + rule + ' { color: ' + color + ' !important; }\n'; + + if ( lum < 0.1 ) { + // Color Too Dark. We need a lum of 0.1 or more. + matched = true; + + var s = 255, + nc = rgb; + while(s--) { + nc = utils.brighten(nc); + if ( utils.get_luminance(nc) >= 0.1 ) + break; + } + + output += '.ffz-chat-colors .theatre .chat-container .chat-line ' + rule + ', .ffz-chat-colors .chat-container.dark .chat-line ' + rule + ', .ffz-chat-colors .ember-chat-container.dark .chat-line ' + rule + ' { color: ' + utils.rgb_to_css(nc) + ' !important; }\n'; + } else + output += '.ffz-chat-colors .theatre .chat-container .chat-line ' + rule + ', .ffz-chat-colors .chat-container.dark .chat-line ' + rule + ', .ffz-chat-colors .ember-chat-container.dark .chat-line ' + rule + ' { color: ' + color + ' !important; }\n'; + + + if ( matched ) + this._fix_color_style.innerHTML += output; +} + + + // --------------------- // Capitalization // --------------------- @@ -205,30 +336,13 @@ FFZ.prototype.capitalize = function(view, user) { } -FFZ.chat_commands.capitalization = 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 ) - return "Chat Name Capitalization is currently " + (this.settings.capitalize ? "enabled." : "disabled."); - - this.settings.set("capitalize", enabled); - return "Chat Name Capitalization is now " + (enabled ? "enabled." : "disabled."); -} - -FFZ.chat_commands.capitalization.help = "Usage: /ffz capitalization \nEnable or disable Chat Name Capitalization. This setting does not work with BetterTTV."; - - // --------------------- // Extra Mentions // --------------------- FFZ._regex_cache = {}; -FFZ._get_rex = function(word) { +FFZ._get_regex = function(word) { return FFZ._regex_cache[word] = FFZ._regex_cache[word] || RegExp("\\b" + reg_escape(word) + "\\b", "ig"); } @@ -266,25 +380,6 @@ FFZ.prototype._mentionize = function(controller, tokens) { } -FFZ.chat_commands.mentionize = function(room, args) { - if ( args && args.length ) { - var mention_words = args.join(" ").trim().split(/\W*,\W*/); - if ( mention_words.length == 1 && mention_words[0] == "disable" ) - mention_words = []; - - this.settings.set("keywords", mention_words); - } - - var mention_words = this.settings.keywords; - if ( mention_words.length ) - return "The following words will be highlighted: " + mention_words.join(", "); - else - return "There are no words set that will be highlighted."; -} - -FFZ.chat_commands.mentionize.help = "Usage: /ffz mentionize \nSet a list of words that will also be highlighted in chat."; - - // --------------------- // Emoticon Replacement // --------------------- diff --git a/src/ember/moderation-card.js b/src/ember/moderation-card.js new file mode 100644 index 00000000..51118d72 --- /dev/null +++ b/src/ember/moderation-card.js @@ -0,0 +1,247 @@ +var FFZ = window.FrankerFaceZ, + utils = require("../utils"), + + keycodes = { + ESC: 27, + P: 80, + B: 66, + T: 84 + }, + + btns = [ + ['5m', 300], + ['10m', 600], + ['1hr', 3600], + ['12hr', 43200], + ['24hr', 86400]], + + MESSAGE = ''; + + +// ---------------- +// Settings +// ---------------- + +FFZ.settings_info.enhanced_moderation = { + type: "boolean", + value: false, + + visible: function() { return ! this.has_bttv }, + category: "Chat", + + name: "Enhanced Moderation", + help: "Use /p, /t, /u and /b in chat to moderator, or use hotkeys with moderation cards." + }; + + +// ---------------- +// Initialization +// ---------------- + +FFZ.prototype.setup_mod_card = function() { + this.log("Hooking the Ember Moderation Card view."); + var Card = App.__container__.resolve('view:moderation-card'), + f = this; + + Card.reopen({ + didInsertElement: function() { + this._super(); + try { + if ( ! f.settings.enhanced_moderation ) + return; + + var el = this.get('element'), + controller = this.get('context'); + + // Only do the big stuff if we're mod. + if ( controller.get('parentController.model.isModeratorOrHigher') ) { + el.classList.add('ffz-moderation-card'); + el.setAttribute('tabindex', 1); + + // Key Handling + el.addEventListener('keyup', function(e) { + var key = e.keyCode || e.which, + user_id = controller.get('model.user.id'), + room = controller.get('parentController.model'); + + if ( key == keycodes.P ) + room.send("/timeout " + user_id + " 1"); + + else if ( key == keycodes.B ) + room.send("/ban " + user_id); + + else if ( key == keycodes.T ) + room.send("/timeout " + user_id + " 600"); + + else if ( key != keycodes.ESC ) + return; + + controller.send('hideModOverlay'); + }); + + + // Extra Moderation + var line = document.createElement('div'); + line.className = 'interface clearfix'; + + var btn_click = function(timeout) { + var user_id = controller.get('model.user.id'), + room = controller.get('parentController.model'); + + if ( timeout === -1 ) + room.send("/unban " + user_id); + else + room.send("/timeout " + user_id + " " + timeout); + }, + + btn_make = function(text, timeout) { + var btn = document.createElement('button'); + btn.className = 'button'; + btn.innerHTML = text; + btn.title = "Timeout User for " + utils.number_commas(timeout) + " Second" + (timeout != 1 ? "s" : ""); + + if ( timeout === 600 ) + btn.title = "(T)" + btn.title.substr(1); + else if ( timeout === 1 ) + btn.title = "(P)urge - " + btn.title; + + jQuery(btn).tipsy(); + + btn.addEventListener('click', btn_click.bind(this, timeout)); + return btn; + }; + + line.appendChild(btn_make('Purge', 1)); + + var s = document.createElement('span'); + s.className = 'right'; + line.appendChild(s); + + for(var i=0; i < btns.length; i++) + s.appendChild(btn_make(btns[i][0], btns[i][1])); + + el.appendChild(line); + + + // Unban Button + + var unban_btn = document.createElement('button'); + unban_btn.className = 'unban button glyph-only light'; + unban_btn.innerHTML = "✓"; + unban_btn.title = "(U)nban User"; + + jQuery(unban_btn).tipsy(); + unban_btn.addEventListener("click", btn_click.bind(this, -1)); + + var ban_btn = el.querySelector('button.ban'); + ban_btn.setAttribute('title', '(B)an User'); + + jQuery(ban_btn).after(unban_btn); + + + // Fix Other Buttons + this.$("button.timeout").remove(); + } + + + // More Fixing Other Buttons + var op_btn = el.querySelector('button.mod'); + if ( op_btn ) { + var model = controller.get('parentController.model'), + can_op = model.get('isBroadcaster') || model.get('isStaff') || model.get('isAdmin'); + + if ( ! can_op ) + op_btn.parentElement.removeChild(op_btn); + } + + + var msg_btn = el.querySelector(".interface > button"); + if ( msg_btn && msg_btn.className == "button" ) { + msg_btn.innerHTML = MESSAGE; + msg_btn.classList.add('glyph-only'); + msg_btn.classList.add('message'); + + msg_btn.title = "Message User"; + jQuery(msg_btn).tipsy(); + } + + + // Focus the Element + this.$().draggable({ + start: function() { + el.focus(); + }}); + + el.focus(); + + } catch(err) { + try { + f.error("ModerationCardView didInsertElement: " + err); + } catch(err) { } + } + }}); +} + + +// ---------------- +// Chat Commands +// ---------------- + +FFZ.chat_commands.purge = FFZ.chat_commands.p = function(room, args) { + if ( ! args || ! args.length ) + return "Purge Usage: /p username [more usernames separated by spaces]"; + + if ( args.length > 10 ) + return "Please only purge up to 10 users at once."; + + for(var i=0; i < args.length; i++) { + var name = args[i]; + if ( name ) + room.room.send("/timeout " + name + " 1"); + } +} + +FFZ.chat_commands.p.enabled = function() { return this.settings.enhanced_moderation; } + + +FFZ.chat_commands.t = function(room, args) { + if ( ! args || ! args.length ) + return "Timeout Usage: /t username [duration]"; + room.room.send("/timeout " + args.join(" ")); +} + +FFZ.chat_commands.t.enabled = function() { return this.settings.enhanced_moderation; } + + +FFZ.chat_commands.b = function(room, args) { + if ( ! args || ! args.length ) + return "Ban Usage: /b username [more usernames separated by spaces]"; + + if ( args.length > 10 ) + return "Please only ban up to 10 users at once."; + + for(var i=0; i < args.length; i++) { + var name = args[i]; + if ( name ) + room.room.send("/ban " + name); + } +} + +FFZ.chat_commands.b.enabled = function() { return this.settings.enhanced_moderation; } + + +FFZ.chat_commands.u = function(room, args) { + if ( ! args || ! args.length ) + return "Unban Usage: /b username [more usernames separated by spaces]"; + + if ( args.length > 10 ) + return "Please only unban up to 10 users at once."; + + for(var i=0; i < args.length; i++) { + var name = args[i]; + if ( name ) + room.room.send("/unban " + name); + } +} + +FFZ.chat_commands.u.enabled = function() { return this.settings.enhanced_moderation; } \ No newline at end of file diff --git a/src/ember/room.js b/src/ember/room.js index 227e8326..d4732367 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -50,6 +50,7 @@ FFZ.prototype.setup_room = function() { // -------------------- FFZ.chat_commands = {}; +FFZ.ffz_commands = {}; FFZ.prototype.room_message = function(room, text) { @@ -66,6 +67,54 @@ FFZ.prototype.room_message = function(room, text) { FFZ.prototype.run_command = function(text, room_id) { + var room = this.rooms[room_id]; + if ( ! room || ! room.room ) + return false; + + if ( ! text ) + return; + + var args = text.split(" "), + cmd = args.shift().substr(1).toLowerCase(), + + command = FFZ.chat_commands[cmd], + output; + + if ( ! command ) + return false; + + if ( command.hasOwnProperty('enabled') ) { + var val = command.enabled; + if ( typeof val == "function" ) { + try { + val = command.enabled.bind(this)(room, args); + } catch(err) { + this.error('command "' + cmd + '" enabled: ' + err); + val = false; + } + } + + if ( ! val ) + return false; + } + + this.log("Received Command: " + cmd, args, true); + + try { + output = command.bind(this)(room, args); + } catch(err) { + this.error('command "' + cmd + '" runner: ' + err); + output = "There was an error running the command."; + } + + if ( output ) + this.room_message(room, output); + + return true; +} + + +FFZ.prototype.run_ffz_command = function(text, room_id) { var room = this.rooms[room_id]; if ( ! room || !room.room ) return; @@ -84,7 +133,7 @@ FFZ.prototype.run_command = function(text, room_id) { this.log("Received Command: " + cmd, args, true); - var command = FFZ.chat_commands[cmd], output; + var command = FFZ.ffz_commands[cmd], output; if ( command ) { try { output = command.bind(this)(room, args); @@ -100,9 +149,9 @@ FFZ.prototype.run_command = function(text, room_id) { } -FFZ.chat_commands.help = function(room, args) { +FFZ.ffz_commands.help = function(room, args) { if ( args && args.length ) { - var command = FFZ.chat_commands[args[0].toLowerCase()]; + var command = FFZ.ffz_commands[args[0].toLowerCase()]; if ( ! command ) return 'There is no "' + args[0] + '" command.'; @@ -114,13 +163,13 @@ FFZ.chat_commands.help = function(room, args) { } var cmds = []; - for(var c in FFZ.chat_commands) - FFZ.chat_commands.hasOwnProperty(c) && cmds.push(c); + for(var c in FFZ.ffz_commands) + FFZ.ffz_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."; +FFZ.ffz_commands.help.help = "Usage: /ffz help [command]\nList available commands, or show help for a specific command."; // -------------------- @@ -214,12 +263,20 @@ FFZ.prototype._modify_room = function(room) { // Track which rooms the user is currently in. init: function() { this._super(); - f.add_room(this.id, this); + try { + f.add_room(this.id, this); + } catch(err) { + f.error("add_room: " + err); + } }, willDestroy: function() { this._super(); - f.remove_room(this.id); + try { + f.remove_room(this.id); + } catch(err) { + f.error("remove_room: " + err); + } }, getSuggestions: function() { @@ -228,19 +285,35 @@ FFZ.prototype._modify_room = function(room) { // filteredSuggestions property of the chat-input component would // be even better, but I was already hooking the room model. var suggestions = this._super(); - if ( this.settings.capitalize ) - suggestions = _.map(suggestions, FFZ.get_capitalization); + + try { + if ( f.settings.capitalize ) + suggestions = _.map(suggestions, FFZ.get_capitalization); + } catch(err) { + f.error("get_suggestions: " + err); + } return suggestions; }, 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); + try { + var cmd = text.split(' ', 1)[0].toLowerCase(); + if ( cmd === "/ffz" ) { + this.set("messageToSend", ""); + f.run_ffz_command(text.substr(5), this.get('id')); + return; + + } else if ( cmd.charAt(0) === "/" && f.run_command(text, this.get('id')) ) { + this.set("messageToSend", ""); + return; + } + + } catch(err) { + f.error("send: " + err); + } + + return this._super(text); } }); } diff --git a/src/ember/router.js b/src/ember/router.js index 03cb808f..201179c6 100644 --- a/src/ember/router.js +++ b/src/ember/router.js @@ -11,7 +11,11 @@ FFZ.prototype.setup_router = function() { var f = this; App.__container__.lookup('router:main').reopen({ ffzTransition: function() { - f.track_page(); + try { + f.track_page(); + } catch(err) { + f.error("ffzTransition: " + err); + } }.on('didTransition') }); } \ No newline at end of file diff --git a/src/ember/viewers.js b/src/ember/viewers.js index 0f5c2ee3..fe17fefa 100644 --- a/src/ember/viewers.js +++ b/src/ember/viewers.js @@ -18,72 +18,77 @@ FFZ.prototype._modify_viewers = function(controller) { controller.reopen({ lines: function() { - var viewers = this._super(), - categories = [], - data = {}, - last_category = null; + var viewers = this._super(); + try { + var categories = [], + data = {}, + last_category = null; - // Get the broadcaster name. - var Channel = App.__container__.lookup('controller:channel'), - room_id = this.get('parentController.model.id'), - broadcaster = Channel && Channel.get('id'); + // Get the broadcaster name. + var Channel = App.__container__.lookup('controller:channel'), + room_id = this.get('parentController.model.id'), + broadcaster = Channel && Channel.get('id'); - // We can get capitalization for the broadcaster from the channel. - if ( broadcaster ) { - var display_name = Channel.get('display_name'); - if ( display_name ) - FFZ.capitalization[broadcaster] = [display_name, Date.now()]; - } + // We can get capitalization for the broadcaster from the channel. + if ( broadcaster ) { + var display_name = Channel.get('display_name'); + if ( display_name ) + FFZ.capitalization[broadcaster] = [display_name, Date.now()]; + } - // If the current room isn't the channel's chat, then we shouldn't - // display them as the broadcaster. - if ( room_id != broadcaster ) - broadcaster = null; + // If the current room isn't the channel's chat, then we shouldn't + // display them as the broadcaster. + if ( room_id != broadcaster ) + broadcaster = null; - // Now, break the viewer array down into something we can use. - for(var i=0; i < viewers.length; i++) { - var entry = viewers[i]; - if ( entry.category ) { - last_category = entry.category; - categories.push(last_category); - data[last_category] = []; + // Now, break the viewer array down into something we can use. + for(var i=0; i < viewers.length; i++) { + var entry = viewers[i]; + if ( entry.category ) { + last_category = entry.category; + categories.push(last_category); + data[last_category] = []; - } else { - var viewer = entry.chatter.toLowerCase(); - if ( ! viewer ) + } else { + var viewer = entry.chatter.toLowerCase(); + if ( ! viewer ) + continue; + + // If the viewer is the broadcaster, give them their own + // group. Don't put them with normal mods! + if ( viewer == broadcaster ) { + categories.unshift("Broadcaster"); + data["Broadcaster"] = [viewer]; + + } else if ( data.hasOwnProperty(last_category) ) + data[last_category].push(viewer); + } + } + + // Now, rebuild the viewer list. However, we're going to actually + // sort it this time. + viewers = []; + for(var i=0; i < categories.length; i++) { + var category = categories[i], + chatters = data[category]; + + if ( ! chatters || ! chatters.length ) continue; - // If the viewer is the broadcaster, give them their own - // group. Don't put them with normal mods! - if ( viewer == broadcaster ) { - categories.unshift("Broadcaster"); - data["Broadcaster"] = [viewer]; + viewers.push({category: category}); + viewers.push({chatter: ""}); - } else if ( data.hasOwnProperty(last_category) ) - data[last_category].push(viewer); + // Push the chatters, capitalizing them as we go. + chatters.sort(); + while(chatters.length) { + var viewer = chatters.shift(); + viewer = FFZ.get_capitalization(viewer); + viewers.push({chatter: viewer}); + } } - } - // Now, rebuild the viewer list. However, we're going to actually - // sort it this time. - viewers = []; - for(var i=0; i < categories.length; i++) { - var category = categories[i], - chatters = data[category]; - - if ( ! chatters || ! chatters.length ) - continue; - - viewers.push({category: category}); - viewers.push({chatter: ""}); - - // Push the chatters, capitalizing them as we go. - chatters.sort(); - while(chatters.length) { - var viewer = chatters.shift(); - viewer = FFZ.get_capitalization(viewer); - viewers.push({chatter: viewer}); - } + } catch(err) { + f.error("ViewersController lines: " + err); } return viewers; diff --git a/src/ext/betterttv.js b/src/ext/betterttv.js index 6d8efa3e..2369f6f0 100644 --- a/src/ext/betterttv.js +++ b/src/ext/betterttv.js @@ -23,22 +23,27 @@ FFZ.prototype.setup_bttv = function(delay) { this.log("BetterTTV was detected after " + delay + "ms. Hooking."); this.has_bttv = true; - this.track('setCustomVariable', '3', 'BetterTTV', BetterTTV.info.versionString()); + // this.track('setCustomVariable', '3', 'BetterTTV', BetterTTV.info.versionString()); // Disable Dark if it's enabled. - document.querySelector(".app-main").classList.remove("ffz-dark"); + document.body.classList.remove("ffz-dark"); if ( this._dark_style ) { this._dark_style.parentElement.removeChild(this._dark_style); delete this._dark_style; } + // Disable other features too. + document.body.classList.remove("ffz-chat-colors"); + document.body.classList.remove("ffz-chat-background"); + + // Send Message Behavior var original_send = BetterTTV.chat.helpers.sendMessage, f = this; BetterTTV.chat.helpers.sendMessage = function(message) { var cmd = message.split(' ', 1)[0].toLowerCase(); if ( cmd === "/ffz" ) - f.run_command(message.substr(5), BetterTTV.chat.store.currentRoom); + f.run_ffz_command(message.substr(5), BetterTTV.chat.store.currentRoom); else return original_send(message); } diff --git a/src/featurefriday.js b/src/featurefriday.js index 3ab417e2..045502a2 100644 --- a/src/featurefriday.js +++ b/src/featurefriday.js @@ -75,7 +75,7 @@ FFZ.prototype._feature_friday_ui = function(room_id, parent, view) { btn.innerHTML = "" + message + ""; // Track the number of users to click this button. - btn.addEventListener('click', function() { f.track('trackLink', this.href, 'link'); }); + // btn.addEventListener('click', function() { f.track('trackLink', this.href, 'link'); }); btnc.appendChild(btn); parent.appendChild(btnc); diff --git a/src/main.js b/src/main.js index 2a722ada..15ef7fb2 100644 --- a/src/main.js +++ b/src/main.js @@ -9,6 +9,9 @@ require('./shims'); var FFZ = window.FrankerFaceZ = function() { FFZ.instance = this; + // Logging + this._log_data = []; + // Get things started. this.initialize(); } @@ -19,7 +22,7 @@ FFZ.get = function() { return FFZ.instance; } // Version var VER = FFZ.version_info = { - major: 3, minor: 0, revision: 0, + major: 3, minor: 1, revision: 0, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -30,6 +33,8 @@ var VER = FFZ.version_info = { FFZ.prototype.log = function(msg, data, to_json) { msg = "FFZ: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); + this._log_data.push(msg); + if ( data !== undefined && console.groupCollapsed && console.dir ) { console.groupCollapsed(msg); if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) @@ -43,6 +48,43 @@ FFZ.prototype.log = function(msg, data, to_json) { } +FFZ.prototype.error = function(msg, data, to_json) { + msg = "FFZ Error: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); + this._log_data.push(msg); + + if ( data !== undefined && console.groupCollapsed && console.dir ) { + console.groupCollapsed(msg); + if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) + console.log(data); + else + console.dir(data); + + console.groupEnd(msg); + } else + console.assert(false, msg); +} + + +FFZ.prototype.paste_logs = function() { + this._pastebin(this._log_data.join("\n"), function(url) { + if ( ! url ) + return console.log("FFZ Error: Unable to upload log to pastebin."); + + console.log("FFZ: Your FrankerFaceZ log has been pasted to: " + url); + }); +} + + +FFZ.prototype._pastebin = function(data, callback) { + jQuery.ajax({url: "http://putco.de/", type: "PUT", data: data, context: this}) + .success(function(e) { + callback.bind(this)(e.trim() + ".log"); + }).fail(function(e) { + callback.bind(this)(null); + }); +} + + // ------------------- // User Data // ------------------- @@ -69,14 +111,15 @@ require('./socket'); require('./emoticons'); require('./badges'); -require('./ember/router'); +// Analytics: require('./ember/router'); require('./ember/room'); require('./ember/line'); require('./ember/chatview'); require('./ember/viewers'); +require('./ember/moderation-card'); //require('./ember/teams'); -require('./tracking'); +// Analytics: require('./tracking'); require('./debug'); @@ -141,13 +184,14 @@ FFZ.prototype.setup_ember = function(delay) { this.setup_emoticons(); this.setup_badges(); - this.setup_piwik(); + //this.setup_piwik(); - this.setup_router(); + //this.setup_router(); this.setup_room(); this.setup_line(); this.setup_chatview(); this.setup_viewers(); + this.setup_mod_card(); //this.setup_teams(); diff --git a/src/ui/dark.js b/src/ui/dark.js index 3083b0af..e93387ac 100644 --- a/src/ui/dark.js +++ b/src/ui/dark.js @@ -19,7 +19,7 @@ FFZ.settings_info.dark_twitch = { if ( this.has_bttv ) return; - document.querySelector(".app-main").classList.toggle("ffz-dark", val); + document.body.classList.toggle("ffz-dark", val); if ( val ) this._load_dark_css(); } @@ -34,7 +34,7 @@ FFZ.prototype.setup_dark = function() { if ( this.has_bttv ) return; - document.querySelector(".app-main").classList.toggle("ffz-dark", this.settings.dark_twitch); + document.body.classList.toggle("ffz-dark", this.settings.dark_twitch); if ( this.settings.dark_twitch ) this._load_dark_css(); } diff --git a/src/ui/menu.js b/src/ui/menu.js index 64cf276c..32d56173 100644 --- a/src/ui/menu.js +++ b/src/ui/menu.js @@ -95,7 +95,7 @@ FFZ.prototype.build_ui_popup = function(view) { // Add the menu to the DOM. this._popup = container; - sub_container.style.maxHeight = Math.max(300, view.$().height() - 212) + "px"; + sub_container.style.maxHeight = Math.max(100, view.$().height() - 162) + "px"; view.$('.chat-interface').append(container); } @@ -124,31 +124,12 @@ FFZ.prototype._ui_change_page = function(view, menu, container, page) { FFZ.menu_pages.settings = { render: function(view, container) { - var menu = document.createElement('div'); - menu.className = 'chat-menu-content'; - - var settings = []; - for(var key in FFZ.settings_info) - settings.push([key, FFZ.settings_info[key]]); - - settings.sort(function(a,b) { - var ai = a[1], - bi = b[1], - - an = ai.name.toLowerCase(), - bn = bi.name.toLowerCase(); - - if ( an < bn ) return -1; - else if ( an > bn ) return 1; - return 0; - }); - - - for(var i=0; i < settings.length; i++) { - var key = settings[i][0], - info = settings[i][1], - el = document.createElement('p'), - val = this.settings.get(key); + var settings = {}, + categories = []; + for(var key in FFZ.settings_info) { + var info = FFZ.settings_info[key], + cat = info.category || "Miscellaneous", + cs = settings[cat]; if ( info.visible !== undefined && info.visible !== null ) { var visible = info.visible; @@ -159,45 +140,94 @@ FFZ.menu_pages.settings = { continue; } - el.className = 'clearfix'; - - if ( info.type == "boolean" ) { - var swit = document.createElement('a'), - label = document.createElement('span'); - - swit.className = 'switch'; - swit.classList.toggle('active', val); - swit.innerHTML = ""; - - label.className = 'switch-label'; - label.innerHTML = info.name; - - el.appendChild(swit); - el.appendChild(label); - - swit.addEventListener("click", this._ui_toggle_setting.bind(this, swit, key)); - - } else { - el.classList.add("option"); - var link = document.createElement('a'); - link.innerHTML = info.name; - link.href = "#"; - el.appendChild(link); - - link.addEventListener("click", info.method.bind(this)); + if ( ! cs ) { + categories.push(cat); + cs = settings[cat] = []; } - if ( info.help ) { - var help = document.createElement('span'); - help.className = 'help'; - help.innerHTML = info.help; - el.appendChild(help); - } - - menu.appendChild(el); + cs.push([key, info]); } - container.appendChild(menu); + categories.sort(function(a,b) { + var a = a.toLowerCase(), + b = b.toLowerCase(); + + if ( a < b ) return -1; + else if ( a > b ) return 1; + return 0; + }); + + for(var ci=0; ci < categories.length; ci++) { + var category = categories[ci], + cset = settings[category], + + menu = document.createElement('div'), + heading = document.createElement('div'); + + heading.className = 'heading'; + menu.className = 'chat-menu-content'; + heading.innerHTML = category; + menu.appendChild(heading); + + cset.sort(function(a,b) { + var ai = a[1], + bi = b[1], + + an = ai.name.toLowerCase(), + bn = bi.name.toLowerCase(); + + if ( an < bn ) return -1; + else if ( an > bn ) return 1; + return 0; + }); + + + for(var i=0; i < cset.length; i++) { + var key = cset[i][0], + info = cset[i][1], + el = document.createElement('p'), + val = this.settings.get(key); + + el.className = 'clearfix'; + + if ( info.type == "boolean" ) { + var swit = document.createElement('a'), + label = document.createElement('span'); + + swit.className = 'switch'; + swit.classList.toggle('active', val); + swit.innerHTML = ""; + + label.className = 'switch-label'; + label.innerHTML = info.name; + + el.appendChild(swit); + el.appendChild(label); + + swit.addEventListener("click", this._ui_toggle_setting.bind(this, swit, key)); + + } else { + el.classList.add("option"); + var link = document.createElement('a'); + link.innerHTML = info.name; + link.href = "#"; + el.appendChild(link); + + link.addEventListener("click", info.method.bind(this)); + } + + if ( info.help ) { + var help = document.createElement('span'); + help.className = 'help'; + help.innerHTML = info.help; + el.appendChild(help); + } + + menu.appendChild(el); + } + + container.appendChild(menu); + } }, name: "Settings", @@ -236,8 +266,7 @@ FFZ.menu_pages.channel = { var room_id = view.get('controller.currentRoom.id'), room = this.rooms[room_id]; - this.log("Menu for Room: " + room_id, room); - this.track('trackEvent', 'Menu', 'Open', room_id); + //this.track('trackEvent', 'Menu', 'Open', room_id); // Add the header and ad button. /*var btn = document.createElement('a'); diff --git a/src/ui/notifications.js b/src/ui/notifications.js index 4e8e9c42..0ef99fbf 100644 --- a/src/ui/notifications.js +++ b/src/ui/notifications.js @@ -19,6 +19,7 @@ FFZ.settings_info.highlight_notifications = { type: "boolean", value: false, + category: "Chat", visible: function() { return ! this.has_bttv }, name: "Highlight Notifications", @@ -54,7 +55,7 @@ FFZ.settings_info.highlight_notifications = { // --------------------- FFZ.ws_commands.message = function(message) { - this.show_mesage(message); + this.show_message(message); } diff --git a/src/ui/races.js b/src/ui/races.js index d44a06ea..0f9abe0a 100644 --- a/src/ui/races.js +++ b/src/ui/races.js @@ -19,6 +19,8 @@ FFZ.prototype.setup_races = function() { FFZ.settings_info.srl_races = { type: "boolean", value: true, + + category: "Channel Metadata", name: "SRL Race Information", help: 'Display information about SpeedRunsLive races under channels.', on_update: function(val) { diff --git a/src/utils.js b/src/utils.js index 990154b2..f999923b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -11,8 +11,42 @@ var sanitize_cache = {}, else if ( num == 3 ) return '3rd'; else if ( num == null ) return '---'; return num + "th"; + }, + + brighten = function(rgb, amount) { + amount = (amount === 0) ? 0 : (amount || 1); + amount = Math.round(255 * -(amount / 100)); + + var r = Math.max(0, Math.min(255, rgb[0] - amount)), + g = Math.max(0, Math.min(255, rgb[1] - amount)), + b = Math.max(0, Math.min(255, rgb[2] - amount)); + + return [r,g,b]; + }, + + rgb_to_css = function(rgb) { + return "rgb(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ")"; + }, + + darken = function(rgb, amount) { + amount = (amount === 0) ? 0 : (amount || 1); + return brighten(rgb, -amount); + }, + + get_luminance = function(rgb) { + rgb = [rgb[0]/255, rgb[1]/255, rgb[2]/255]; + for (var i =0; i