1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-25 03:58:30 +00:00
FrankerFaceZ/src/ember/room.js

2251 lines
No EOL
66 KiB
JavaScript

var FFZ = window.FrankerFaceZ,
HOSTED_SUB = / subscribed to /,
constants = require('../constants'),
utils = require('../utils'),
helpers,
NOTICE_MAPPING = {
'slow': 'slow_on',
'slowoff': 'slow_off',
'r9kbeta': 'r9k_on',
'r9kbetaoff': 'r9k_off',
'subscribers': 'subs_on',
'subscribersoff': 'subs_off',
'emoteonly': 'emote_only_on',
'emoteonlyoff': 'emote_only_off',
'host': 'host_on',
'unhost': 'host_off',
'clear': 'clear_chat'
},
STATUS_BADGES = [
["r9k", "r9k", "This room is in R9K-mode."],
["emote", "emoteOnly", "This room is in Twitch emoticons only mode. Emoticons added by extensions are not available in this mode."],
["sub", "subsOnly", "This room is in subscribers-only mode."],
["slow", "slow", function(room) { return "This room is in slow mode. You may send messages every <nobr>" + utils.number_commas(room && room.get('slow') || 120) + " seconds</nobr>." }],
["ban", "ffz_banned", "You have been banned from talking in this room."],
["delay", function(room) {
return room && (this.settings.chat_delay === -1 ?
room.get('roomProperties.chat_delay_duration')
: room.get('ffz_chat_delay'))
}, function(room) {
var is_mod = this.settings.chat_delay === -1;
return "Artificial chat delay is enabled" + (is_mod ? " for this channel" : "") + ". Messages are displayed after " + (room ? (is_mod ? room.get('roomProperties.chat_delay_duration') : room.get('ffz_chat_delay')/1000) : 0) + " seconds" + (is_mod ? " for <nobr>non-moderators</nobr>." : ".");
}, null, function(room) {
return room && this.settings.chat_delay === -1 && room.get('isModeratorOrHigher') || false;
}],
["batch", function() { return this.settings.chat_batching !== 0 }, function() { return "You have enabled chat message batching. Messages are displayed in <nobr>" + (this.settings.chat_batching/1000) + " second</nobr> increments." }]
],
// StrimBagZ Support
is_android = navigator.userAgent.indexOf('Android') !== -1,
moderator_css = function(room) {
if ( ! room.moderator_badge )
return "";
return '.from-display-preview[data-room="' + room.id + '"] .badges .moderator:not(.ffz-badge-replacement):not(.colored),' +
'.chat-line[data-room="' + room.id + '"] .badges .moderator:not(.ffz-badge-replacement):not(.colored) {' +
'background-repeat: no-repeat;' +
'background-size: initial !important;' +
'background-position: center;' +
'background-image:url("' + room.moderator_badge + '") !important; }' +
'.from-display-preview[data-room="' + room.id + '"] .badges .moderator:not(.ffz-badge-replacement).colored,' +
'.chat-line[data-room="' + room.id + '"] .badges .moderator:not(.ffz-badge-replacement).colored {' +
'-webkit-mask-repeat: no-repeat;' +
'-webkit-mask-size: initial !important;' +
'-webkit-mask-position: center;' +
'-webkit-mask-image: url("' + room.moderator_badge + '"); }';
};
try {
helpers = window.require && window.require("ember-twitch-chat/helpers/chat-line-helpers");
} catch(err) { }
// --------------------
// Initialization
// --------------------
FFZ.prototype.setup_room = function() {
this.log("Creating room style element.");
var f = this,
s = this._room_style = document.createElement("style");
s.id = "ffz-room-css";
document.head.appendChild(s);
this.log("Hooking the Ember Chat PubSub service.");
var PubSub = utils.ember_lookup('service:chat-pubsub');
if ( PubSub )
this._modify_chat_pubsub(PubSub);
else
this.error("Cannot locate the Chat PubSub service.");
this.log("Hooking the Ember Room controller.");
// Responsive ban button.
var RC = utils.ember_lookup('controller:room');
if ( RC ) {
var orig_ban = RC._actions.banUser,
orig_to = RC._actions.timeoutUser,
orig_show = RC._actions.showModOverlay;
RC._actions.banUser = function(e) {
orig_ban.call(this, e);
this.get("model").clearMessages(e.user, null, true);
}
RC._actions.timeoutUser = function(e) {
orig_to.call(this, e);
this.get("model").clearMessages(e.user, null, true);
}
RC._actions.showModOverlay = function(e) {
var Channel = utils.ember_resolve('model:deprecated-channel'),
chan = Channel && Channel.find && Channel.find({id: e.sender});
if ( ! chan ) {
f.log("Error opening mod card. model:deprecated-channel does not exist or does not have find!");
return orig_show.call(this, e);
}
// Don't try loading the channel if it's already loaded. Don't make mod cards
// refresh the channel page when you click the broadcaster, basically.
if ( ! chan.get('isLoaded') )
chan.load();
this.set("showModerationCard", true);
// We pass in renderBottom and renderRight, which we use to reposition the window
// after we know how big it actually is. This doesn't work a lot of the time.
this.set("moderationCardInfo", {
user: chan,
renderTop: e.real_top || e.top,
renderLeft: e.left,
renderBottom: e.bottom,
renderRight: e.right,
isIgnored: this.get("tmiSession").isIgnored(e.sender),
isChannelOwner: this.get("login.userData.login") === e.sender,
profileHref: Twitch.uri.profile(e.sender),
isModeratorOrHigher: this.get("model.isModeratorOrHigher")
});
}
}
this.log("Hooking the Ember Room model.");
var Room = utils.ember_resolve('model:room');
this._modify_room(Room);
// Modify all current instances of Room, as the changes to the base
// class won't be inherited automatically.
var instances = Room.instances;
for(var key in instances) {
if ( ! instances.hasOwnProperty(key) )
continue;
var inst = instances[key];
this.add_room(inst.id, inst);
this._modify_room(inst);
inst.ffzUpdateBadges();
inst.ffzPatchTMI();
}
this.log("Hooking the Ember Room view.");
this.update_views('view:room', this.modify_room_view);
}
// --------------------
// Ban Message Formatting
// --------------------
FFZ.prototype.format_ban_notice = function(username, is_me, duration, count, reasons, moderators, notices) {
var name = this.format_display_name(FFZ.get_capitalization(username), username, true),
duration_tip = [];
for(var mod_id in notices) {
var notice = notices[mod_id];
if ( ! Array.isArray(notice) )
notice = [notice];
var nd = (notice[0] === -Infinity || notice[0] === -1) ? 'unban' : (notice[0] !== null && isFinite(notice[0])) ? utils.duration_string(notice[0], true) : 'ban';
duration_tip.push(utils.sanitize(mod_id) + ' - ' + nd + (notice[1] ? ': ' + utils.sanitize(notice[1]) : ''));
}
return (is_me ? 'You have' : '<span data-user="' + utils.quote_san(username) + '" class="ban-target html-tooltip" title="' + utils.quote_attr(name[1] || '') + '">' + name[0] + '</span> has') +
' been ' + (duration_tip.length ? '<span class="ban-tip html-tooltip" title="' + utils.quote_attr(duration_tip.join('<br>')) + '">' : '') + (duration === -Infinity ? 'unbanned' :
(duration === 1 ? 'purged' : isFinite(duration) ? 'timed out for ' + utils.duration_string(duration) : 'banned')) +
(count > 1 ? ' (' + utils.number_commas(count) + ' times)' : '') +
(moderators && moderators.length ? ' by ' + utils.sanitize(moderators.join(', ')) : '') + (duration_tip.length ? '</span>' : '') +
(reasons && reasons.length ? ' with reason' + utils.pluralize(reasons.length) + ': ' + utils.sanitize(reasons.join(', ')) : '.');
}
// --------------------
// PubSub is fucking awful
// --------------------
FFZ.prototype._modify_chat_pubsub = function(pubsub) {
var f = this;
pubsub.reopen({
setupService: function(room_id, t) {
var n = this;
this.get("session").withCurrentUser(function(user) {
if ( n.isDestroyed )
return;
var ps = n._pubsub(),
token = user.chat_oauth_token,
new_topics = [
"chat_message_updated." + room_id,
"chat_moderator_actions." + user.id + "." + room_id];
for(var i=0; i < new_topics.length; i++)
ps.Listen({
topic: new_topics[i],
auth: token,
success: function() {},
failure: function(t) { f.log("[PubSub] Failed to listen to topic: " + new_topics[i], t); },
message: Ember.run.bind(n, n._onPubsubMessage, new_topics[i])
});
if ( n.chatTopics )
n.chatTopics = n.chatTopics.concat(new_topics);
else
n.chatTopics = new_topics;
ps.on("connected", Ember.run.bind(n, n._onPubsubConnect));
ps.on("disconnected", Ember.run.bind(n, n._onPubsubDisconnect));
t();
});
},
tearDownService: function(room_id) {
if ( ! this.chatTopics )
return;
var ps = this._pubsub(),
old_topics;
if ( ! room_id )
room_id = this.get("ffz_teardown_target");
if ( room_id ) {
// Make sure it's a string.
room_id = '.' + room_id;
old_topics = this.chatTopics.filter(function(x) { return x.substr(-room_id.length) === room_id });
} else
old_topics = this.chatTopics;
for(var i=0; i < old_topics.length; i++) {
var topic = old_topics[i];
// Try stupid stuff to remove duplicate events.
if ( topic.substr(0, 23) === 'chat_moderator_actions.' )
ps.Unlisten({
topic: topic.split('.', 2).join('.'),
success: function() {},
failure: function() {}
});
ps.Unlisten({
topic: topic,
success: function() {},
failure: function(topic, t) { f.log("[PubSub] Failed to unlisten to topic: " +topic, t); }.bind(this, topic)
});
this.chatTopics.removeObject(old_topics[i]);
}
if ( ! this.chatTopics.length )
this.chatTopics = null;
},
_onPubsubMessage: function(topic, e) {
if ( this.isDestroyed )
return;
var msg = JSON.parse(e),
msg_data = msg.data,
msg_type = msg.type || msg_data.type;
if ( msg_data )
msg_data.topic = topic;
this.trigger(msg_type, msg_data);
}
});
if ( ! pubsub.chatTopics )
return;
// Now that we've modified that, we need to re-listen to everything.
pubsub.get("session").withCurrentUser(function(user) {
if ( pubsub.isDestroyed )
return;
var ps = pubsub._pubsub(),
token = user.chat_oauth_token,
internal_topics = ps._client & ps._client._listens && ps._client._listens._events || {};
for(var i=0; i < pubsub.chatTopics.length; i++) {
var topic = pubsub.chatTopics[i];
ps.Unlisten({
topic: topic,
success: function() {},
failure: function(topic, t) { f.log("[PubSub] Failed to unlisten to topic: " + topic, t); }.bind(this, topic)
});
// Find the event and manually remove our listeners. We still want the topic so
// we don't clean it up too much, but we need to get rid of the existing bound
// functions to avoid anything screwing up.
var it = internal_topics[topic];
if ( it && it.length )
it.splice(0, it.length);
// Now, register our own event handler.
ps.Listen({
topic: topic,
auth: token,
success: function() {},
failure: function(topic, t) { f.log("[PubSub] Failed to listen to topic: " + topic, t); }.bind(this, topic),
message: Ember.run.bind(pubsub, pubsub._onPubsubMessage, topic)
});
}
});
}
// --------------------
// View Customization
// --------------------
FFZ.prototype.modify_room_view = function(view) {
var f = this;
utils.ember_reopen_view(view, {
ffz_init: function() {
f._roomv = this;
this.ffz_frozen = false;
this.ffz_ctrl = false;
// Fix scrolling.
this._ffz_mouse_down = this.ffzMouseDown.bind(this);
if ( is_android )
// We don't unbind scroll because that messes with the scrollbar. ;_;
this._$chatMessagesScroller.bind('scroll', this._ffz_mouse_down);
this._$chatMessagesScroller.unbind('mousedown');
this._$chatMessagesScroller.bind('mousedown', this._ffz_mouse_down);
if ( f.settings.chat_hover_pause )
this.ffzEnableFreeze();
if ( f.settings.room_status )
this.ffzUpdateStatus();
var controller = this.get('controller');
if ( controller ) {
controller.reopen({
calcRecipientEligibility: function(e) {
// Because this doesn't work properly with multiple channel rooms
// by default, do it ourselves.
if ( controller.get('model.isGroupRoom') ) {
controller.set('isRecipientBitsIneligible', true);
controller.set('isBitsHelperShown', false);
controller.set('minimumBits', 0);
controller.set('isBitsTooltipActive', false);
return;
}
var id = controller.get('model.roomProperties._id'),
update = function(data) {
if ( controller.isDestroyed || controller.get('model.roomProperties._id') !== id )
return;
controller.set('model._ffz_bits_eligibility', data);
controller.set('isRecipientBitsIneligible', ! data.eligible);
controller.set('isBitsHelperShown', data.eligible);
controller.set('minimumBits', data.minBits);
if ( ! data.eligible )
controller.set('isBitsTooltipActive', false);
};
if ( id === undefined )
return;
var data = controller.get('model._ffz_bits_eligibility');
if ( data === undefined )
controller.get('bits').loadRecipientEligibility(id).then(update);
else
update(data);
},
submitButtonText: function() {
if ( this.get("model.isWhisperMessage") && this.get("model.isWhispersEnabled") )
return i18n("Whisper");
var wait = this.get("model.slowWait"),
msg = this.get("model.messageToSend") || "";
if ( (msg.charAt(0) === "/" && msg.substr(0, 4) !== "/me ") || !wait || !f.settings.room_status )
return i18n("Chat");
return utils.time_to_string(wait, false, false, true);
}.property("model.isWhisperMessage", "model.isWhispersEnabled", "model.slowWait")
});
Ember.propertyDidChange(controller, 'submitButtonText');
}
},
ffz_destroy: function() {
if ( f._roomv === this )
f._roomv = undefined;
if ( this._ffz_chat_display )
this._ffz_chat_display = undefined;
this.ffzDisableFreeze();
},
ffzOnKey: function(event) {
this.ffz_ctrl = event.ctrlKey;
this.ffz_alt = event.altKey;
this.ffz_shift = event.shiftKey;
this.ffz_meta = event.metaKey;
var cmi = f.settings.chat_mod_icon_visibility;
if ( ! this._ffz_outside && cmi > 1 )
this.get('element').classList.toggle('show-mod-icons',
cmi === 2 ? this.ffz_ctrl :
cmi === 3 ? this.ffz_meta :
cmi === 4 ? this.ffz_alt :
this.ffz_shift);
if ( this._ffz_outside || f.settings.chat_hover_pause < 2 )
return;
// Okay, so at this point we should change the state of the freeze?
var should_freeze = this.ffzShouldBeFrozen(),
freeze_change = this.ffz_frozen !== should_freeze;
if ( freeze_change )
if ( should_freeze )
this.ffzFreeze();
else
this.ffzUnfreeze();
},
ffzUpdateStatus: function() {
var room = this.get('controller.model'),
el = this.get('element'),
cont = el && el.querySelector('.chat-buttons-container');
if ( ! cont )
return;
var btn = cont.querySelector('button');
if ( f.has_bttv || ! f.settings.room_status ) {
jQuery(".ffz.room-state", cont).remove();
if ( btn )
btn.classList.remove('ffz-waiting');
return;
} else if ( btn ) {
btn.classList.toggle('ffz-waiting', (room && room.get('slowWait') || 0));
btn.classList.toggle('ffz-banned', (room && room.get('ffz_banned') || false));
}
var badge, id, info, vis_count = 0, label;
for(var i=0; i < STATUS_BADGES.length; i++) {
info = STATUS_BADGES[i];
id = 'ffz-stat-' + info[0];
badge = cont.querySelector('#' + id);
visible = typeof info[1] === "function" ? info[1].call(f, room) : room && room.get(info[1]);
if ( typeof visible === "string" )
visible = visible === "1";
label = typeof info[3] === "function" ? info[3].call(f, room) : undefined;
if ( ! badge ) {
badge = utils.createElement('span', 'ffz room-state stat float-right', (label || info[0]).charAt(0).toUpperCase() + '<span>' + (label || info[0]).substr(1).toUpperCase() + '</span>');
badge.id = id;
jQuery(badge).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'se')});
cont.appendChild(badge);
}
if ( label )
badge.innerHTML = (label || info[0]).charAt(0).toUpperCase() + '<span>' + (label || info[0]).substr(1).toUpperCase() + '</span>';
badge.title = typeof info[2] === "function" ? info[2].call(f, room) : info[2];
badge.classList.toggle('hidden', ! visible);
badge.classList.toggle('faded', info[4] !== undefined ? typeof info[4] === "function" ? info[4].call(f, room) : info[4] : false);
if ( visible )
vis_count++;
}
jQuery(".ffz.room-state", cont).toggleClass("truncated", vis_count > 3);
}.observes('controller.model'),
ffzEnableFreeze: function() {
var el = this.get('element'),
messages = el.querySelector('.chat-messages');
if ( ! messages )
return;
this._ffz_messages = messages;
if ( ! this._ffz_interval )
this._ffz_interval = setInterval(this.ffzPulse.bind(this), 200);
if ( ! this._ffz_mouse_move ) {
this._ffz_mouse_move = this.ffzMouseMove.bind(this);
messages.addEventListener('mousemove', this._ffz_mouse_move);
messages.addEventListener('touchmove', this._ffz_mouse_move);
}
if ( ! this._ffz_mouse_out ) {
this._ffz_mouse_out = this.ffzMouseOut.bind(this);
messages.addEventListener('mouseout', this._ffz_mouse_out);
}
// Monitor the Ctrl key.
if ( ! this._ffz_keyw ) {
this._ffz_keyw = this.ffzOnKey.bind(this);
document.body.addEventListener('keydown', this._ffz_keyw);
document.body.addEventListener('keyup', this._ffz_keyw);
}
},
ffzDisableFreeze: function() {
if ( this._ffz_interval ) {
clearInterval(this._ffz_interval);
this._ffz_interval = undefined;
}
this.ffzUnfreeze();
var messages = this._ffz_messages;
if ( ! messages )
return;
this._ffz_messages = undefined;
if ( this._ffz_mouse_move ) {
messages.removeEventListener('mousemove', this._ffz_mouse_move);
messages.removeEventListener('touchmove', this._ffz_mouse_move);
this._ffz_mouse_move = undefined;
}
if ( this._ffz_mouse_out ) {
messages.removeEventListener('mouseout', this._ffz_mouse_out);
this._ffz_mouse_out = undefined;
}
if ( this._ffz_keyw ) {
document.body.removeEventListener('keydown', this._ffz_keyw);
document.body.removeEventListener('keyup', this._ffz_keyw);
this._ffz_keyw = undefined;
}
},
ffzPulse: function() {
if ( this.ffz_frozen && ! this.ffzShouldBeFrozen() )
this.ffzUnfreeze();
},
ffzUnfreeze: function(from_stuck) {
this.ffz_frozen = false;
this._ffz_last_move = 0;
this.ffzUnwarnPaused();
if ( ! from_stuck && this.get('stuckToBottom') )
this._scrollToBottom();
},
ffzFreeze: function() {
this.ffz_frozen = true;
if ( this.get('stuckToBottom') ) {
this.set('controller.model.messageBufferSize', f.settings.scrollback_length + 150);
this.ffzWarnPaused();
}
},
ffzMouseDown: function(event) {
var t = this._$chatMessagesScroller;
if ( t && t[0] && ((!this.ffz_frozen && "mousedown" === event.type) || "mousewheel" === event.type || (is_android && "scroll" === event.type) ) ) {
var r = t[0].scrollHeight - t[0].scrollTop - t[0].offsetHeight;
this._setStuckToBottom(10 >= r);
}
},
ffzMouseOut: function(event) {
this._ffz_outside = true;
var e = this;
setTimeout(function() {
if ( e._ffz_outside ) {
if ( f.settings.chat_mod_icon_visibility > 1 )
e.get('element').classList.toggle('show-mod-icons', false);
e.ffzUnfreeze();
}
}, 25);
},
ffzShouldBeFrozen: function(since) {
if ( since === undefined )
since = Date.now() - this._ffz_last_move;
var hp = f.settings.chat_hover_pause;
return (this.ffz_ctrl && (hp === 2 || hp === 6)) || (this.ffz_meta && (hp === 3 || hp === 7)) || (this.ffz_alt && (hp === 4 || hp === 8)) || (this.ffz_shift && (hp === 5 || hp === 9)) || (since < 750 && (hp === 1 || hp > 5));
},
ffzMouseMove: function(event) {
// Store the last move time.
this._ffz_last_move = Date.now();
this._ffz_outside = false;
// If nothing of interest has happened, stop.
if ( event.altKey === this.ffz_alt && event.shiftKey === this.ffz_shift && event.ctrlKey === this.ffz_ctrl && event.metaKey === this.ffz_meta && event.screenX === this._ffz_last_screenx && event.screenY === this._ffz_last_screeny )
return;
// Grab a bit of state.
this.ffz_ctrl = event.ctrlKey;
this.ffz_alt = event.altKey;
this.ffz_shift = event.shiftKey;
this.ffz_meta = event.metaKey;
this._ffz_last_screenx = event.screenX;
this._ffz_last_screeny = event.screenY;
var cmi = f.settings.chat_mod_icon_visibility;
if ( ! this._ffz_outside && cmi > 1 )
this.get('element').classList.toggle('show-mod-icons',
cmi === 2 ? this.ffz_ctrl :
cmi === 3 ? this.ffz_meta :
cmi === 4 ? this.ffz_alt :
this.ffz_shift);
// Should the state have changed?
var should_freeze = this.ffzShouldBeFrozen(),
freeze_change = this.ffz_frozen !== should_freeze;
if ( freeze_change )
if ( should_freeze )
this.ffzFreeze();
else
this.ffzUnfreeze();
},
_scrollToBottom: _.throttle(function() {
var e = this,
s = this._$chatMessagesScroller;
//this.runTask(function() {
Ember.run.next(function(){
// Trying random performance tweaks for fun and profit!
(window.requestAnimationFrame||setTimeout)(function(){
if ( e.ffz_frozen || ! s || ! s.length )
return;
s[0].scrollTop = s[0].scrollHeight;
e._setStuckToBottom(true);
})
})
}, 200),
_setStuckToBottom: function(val) {
this.set("stuckToBottom", val);
var model = this.get("controller.model");
if ( model )
model.messageBufferSize = f.settings.scrollback_length + (val ? 0 : 150);
if ( ! val )
this.ffzUnfreeze(true);
},
// Warnings~!
ffzWarnPaused: function() {
var el = this.get('element'),
warning = el && el.querySelector('.chat-interface .more-messages-indicator.ffz-freeze-indicator');
if ( ! el )
return;
if ( ! warning ) {
warning = document.createElement('div');
warning.className = 'more-messages-indicator ffz-freeze-indicator';
var hp = f.settings.chat_hover_pause,
label = hp === 2 ? 'Ctrl Key' :
hp === 3 ? (constants.META_NAME + ' Key') :
hp === 4 ? 'Alt Key' :
hp === 5 ? 'Shift Key' :
hp === 6 ? 'Ctrl or Mouse' :
hp === 7 ? (constants.META_NAME + ' or Mouse') :
hp === 8 ? 'Alt or Mouse' :
hp === 9 ? 'Shift or Mouse' :
'Mouse Movement';
warning.innerHTML = '(Chat Paused Due to ' + label + ')';
var cont = el.querySelector('.chat-interface');
if ( ! cont )
return;
cont.insertBefore(warning, cont.childNodes[0])
}
warning.classList.remove('hidden');
},
ffzUnwarnPaused: function() {
var el = this.get('element'),
warning = el && el.querySelector('.chat-interface .more-messages-indicator.ffz-freeze-indicator');
if ( warning )
warning.classList.add('hidden');
}
});
}
// --------------------
// Command System
// --------------------
FFZ.chat_commands = {};
FFZ.ffz_commands = {};
FFZ.prototype.room_message = function(room, text) {
if ( this.has_bttv ) {
var lines = text.split("\n");
for(var i=0; i < lines.length; i++)
BetterTTV.chat.handlers.onPrivmsg(room.id, {style: 'admin', date: new Date(), from: 'jtv', message: lines[i]});
} else
room.room.addMessage({ffz_line_returns: true, style: 'ffz admin', date: new Date(), from: 'FFZ', message: 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.call(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.call(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;
if ( ! text ) {
// Try to pop-up the menu.
var link = document.querySelector('a.ffz-ui-toggle');
if ( link )
return link.click();
text = "help";
}
var args = text.split(" "),
cmd = args.shift().toLowerCase();
this.log("Received Command: " + cmd, args, true);
var command = FFZ.ffz_commands[cmd], output;
if ( command ) {
try {
output = command.call(this, room, args);
} catch(err) {
this.log("Error Running Command - " + cmd + ": " + err, room);
output = "There was an error running the command.";
}
} else
output = 'There is no "' + cmd + '" command.';
if ( output )
this.room_message(room, output);
}
FFZ.ffz_commands.help = function(room, args) {
if ( args && args.length ) {
var command = FFZ.ffz_commands[args[0].toLowerCase()];
if ( ! command )
return 'There is no "' + args[0] + '" command.';
else if ( ! command.help )
return 'No help is available for the command "' + args[0] + '".';
else
return command.help;
}
var cmds = [];
for(var c in FFZ.ffz_commands)
FFZ.ffz_commands.hasOwnProperty(c) && cmds.push(c);
return "The available commands are: " + cmds.join(", ");
}
FFZ.ffz_commands.help.help = "Usage: /ffz help [command]\nList available commands, or show help for a specific command.";
// --------------------
// Room Management
// --------------------
FFZ.prototype.update_room_important = function(id, controller) {
var Chat = controller || utils.ember_lookup('controller:chat'),
current_room = Chat && Chat.get('currentChannelRoom'),
room = this.rooms[id];
if ( ! room )
return;
room.important = (room.room && current_room === room.room) || (current_room && current_room.ffz_host_target === id) || (room.room && room.room.get('isGroupRoom')) || (this.settings.pinned_rooms.indexOf(id) !== -1);
};
FFZ.prototype._update_room_badge_css = function(room_id) {
var room = this.rooms[room_id],
badges = room && room.badges || {},
output = [];
for(var badge_id in badges) {
var versions = badges[badge_id] && badges[badge_id].versions || {};
for(var version in versions)
output.push(utils.room_badge_css(room_id, badge_id, version, versions[version]));
}
utils.update_css(this._badge_style, 'twitch-room-' + room_id, output.join(''));
}
FFZ.prototype.add_room = function(room_id, room) {
if ( this.rooms[room_id] )
return this.log("Tried to add existing room: " + room_id);
this.log("Adding Room: " + room_id);
// Create a basic data table for this room.
var data = this.rooms[room_id] = {id: room_id, room: room, sets: [], ext_sets: [], css: null, needs_history: false};
if ( this.follow_sets && this.follow_sets[room_id] ) {
data.extra_sets = this.follow_sets[room_id];
delete this.follow_sets[room_id];
for(var i=0; i < data.extra_sets.length; i++) {
var sid = data.extra_sets[i],
set = this.emote_sets && this.emote_sets[sid];
if ( set ) {
if ( set.users.indexOf(room_id) === -1 )
set.users.push(room_id);
continue;
}
this.load_set(sid, function(success, data) {
if ( success )
data.users.push(room_id);
});
}
}
// Store the badges for this room now if we have them.
var bs = utils.ember_lookup('service:badges');
if ( bs && bs.badgeCollection && bs.badgeCollection.channel && bs.badgeCollection.channel.broadcasterName === room_id ) {
data.badges = bs.badgeCollection.channel;
this._update_room_badge_css(room_id);
}
// Look up if the room has moderation logs.
var f = this;
this.ws_send("has_logs", room_id, function(success, response) {
if ( ! success )
return;
data.has_logs = response.has_logs;
data.log_source = response.log_source;
}, true);
// Is the room important?
this.update_room_important(room_id);
if ( data.important )
// Let the server know where we are.
this.ws_sub("room." + room_id);
// Do we want history?
/*if ( ! this.has_bttv && this.settings.chat_history && room && (room.get('messages.length') || 0) < 10 ) {
if ( ! this.ws_send("chat_history", [id,25], this._load_history.bind(this, id)) )
data.needs_history = true;
}*/
// Why don't we set the scrollback length, too?
room.set('messageBufferSize', this.settings.scrollback_length + ((this._roomv && !this._roomv.get('stuckToBottom') && this._roomv.get('controller.model.id') === room_id) ? 150 : 0));
// Load the room's data from the API.
this.load_room(room_id);
// Announce this room to any extension callback functions.
this.api_trigger('room-add', room_id);
}
FFZ.prototype.remove_room = function(room_id) {
var room = this.rooms[room_id];
if ( ! room )
return;
this.log("Removing Room: " + room_id);
// Remove the CSS
if ( room.css || room.moderator_badge )
utils.update_css(this._room_style, room_id, null);
// Let the server know we're gone and delete our data for this room.
this.ws_unsub("room." + room_id);
delete this.rooms[room_id];
// Clean up sets we aren't using any longer.
var set = this.emote_sets[room.set];
if ( set ) {
set.users.removeObject(room_id);
if ( ! this.global_sets.contains(room.set) && ! set.users.length )
this.unload_set(room.set);
}
this.api_trigger('room-remove', room_id);
}
// --------------------
// Receiving Set Info
// --------------------
FFZ.prototype.load_room = function(room_id, callback, tries) {
var f = this;
jQuery.getJSON(constants.API_SERVER + "v1/room/" + room_id)
.done(function(data) {
if ( data.sets ) {
for(var key in data.sets)
data.sets.hasOwnProperty(key) && f._load_set_json(key, undefined, data.sets[key]);
}
f._load_room_json(room_id, callback, data);
}).fail(function(data) {
if ( data.status == 404 )
return typeof callback == "function" && callback(false);
tries = (tries || 0) + 1;
if ( tries < 10 )
return f.load_room(room_id, callback, tries);
return typeof callback == "function" && callback(false);
});
}
FFZ.prototype._load_room_json = function(room_id, callback, data) {
if ( ! data || ! data.room )
return typeof callback == "function" && callback(false);
data = data.room;
// Apply the data we've received to the room data model.
var model = this.rooms[room_id] = this.rooms[room_id] || {};
for(var key in data)
if ( key !== 'room' && data.hasOwnProperty(key) )
model[key] = data[key];
// Preserve the pointer to the Room instance.
/*if ( this.rooms[room_id] )
data.room = this.rooms[room_id].room;
// Preserve everything else.
for(var key in this.rooms[room_id]) {
if ( key !== 'room' && this.rooms[room_id].hasOwnProperty(key) && ! data.hasOwnProperty(key) )
data[key] = this.rooms[room_id][key];
}
data.needs_history = this.rooms[room_id] && this.rooms[room_id].needs_history || false;
this.rooms[room_id] = data;*/
if ( model.css || model.moderator_badge )
utils.update_css(this._room_style, room_id, moderator_css(model) + (model.css || ""));
if ( ! this.emote_sets.hasOwnProperty(model.set) )
this.load_set(model.set, function(success, set) {
if ( set.users.indexOf(room_id) === -1 )
set.users.push(room_id);
});
else if ( this.emote_sets[model.set].users.indexOf(room_id) === -1 )
this.emote_sets[model.set].users.push(room_id);
this.update_ui_link();
if ( model.set )
this.rerender_feed_cards(model.set);
if ( callback )
callback(true, model);
}
// --------------------
// Ember Modifications
// --------------------
FFZ.prototype._modify_room = function(room) {
var f = this;
room.reopen({
slowWaiting: false,
slow: 0,
ffz_banned: false,
mru_list: [],
ffzUpdateBadges: function() {
if ( this.get('isGroupRoom') )
return;
var room_name = this.get('id'),
room_id = this.get('roomProperties._id'),
ffz_room = f.rooms && f.rooms[room_name];
if ( ! ffz_room || ! room_id || ffz_room.badges )
return;
fetch("https://badges.twitch.tv/v1/badges/channels/" + room_id + "/display?language=" + (Twitch.receivedLanguage || "en"), {
headers: {
'Client-ID': constants.CLIENT_ID
}
}).then(utils.json).then(function(data) {
ffz_room.badges = data && data.badge_sets;
f._update_room_badge_css(room_name);
});
}.observes('roomProperties._id'),
updateWait: function(value, was_banned, update) {
var wait = this.get('slowWait') || 0;
this.set('slowWait', value);
if ( wait < 1 && value > 0 ) {
if ( this._ffz_wait_timer )
clearTimeout(this._ffz_wait_timer);
this._ffz_wait_timer = setTimeout(this.ffzUpdateWait.bind(this), 1000);
! update && f._roomv && f._roomv.ffzUpdateStatus();
} else if ( (wait > 0 && value < 1) || was_banned ) {
this.set('ffz_banned', false);
! update && f._roomv && f._roomv.ffzUpdateStatus();
}
},
ffzUpdateWait: function() {
this._ffz_wait_timer = undefined;
var wait = this.get('slowWait') || 0;
if ( wait < 1 )
return;
this.set('slowWait', --wait);
if ( wait > 0 )
this._ffz_wait_timer = setTimeout(this.ffzUpdateWait.bind(this), 1000);
else {
this.set('ffz_banned', false);
f._roomv && f._roomv.ffzUpdateStatus();
}
},
ffzScheduleDestroy: function() {
if ( this._ffz_destroy_timer )
return;
var t = this;
this._ffz_destroy_timer = setTimeout(function() {
t._ffz_destroy_timer = null;
t.ffzCheckDestroy();
}, 5000);
},
ffzCheckDestroy: function() {
var Chat = utils.ember_lookup('controller:chat'),
user = f.get_user(),
room_id = this.get('id');
// Don't destroy the room if it's still relevant.
if ( (Chat && Chat.get('currentChannelRoom') === this) || (user && user.login === room_id) || (f._chatv && f._chatv._ffz_host === room_id) || (f.settings.pinned_rooms && f.settings.pinned_rooms.indexOf(room_id) !== -1) )
return;
this.destroy();
},
ffzUpdateStatus: function() {
if ( f._roomv )
f._roomv.ffzUpdateStatus();
}.observes('r9k', 'subsOnly', 'emoteOnly', 'slow', 'ffz_banned'),
// User Level
ffzUserLevel: function() {
if ( this.get('isStaff') )
return 5;
else if ( this.get('isAdmin') )
return 4;
else if ( this.get('isBroadcaster') )
return 3;
else if ( this.get('isGlobalModerator') )
return 2;
else if ( this.get('isModerator') )
return 1;
return 0;
}.property('id', 'chatLabels.[]'),
// Track which rooms the user is currently in.
init: function() {
this._super();
try {
f.add_room(this.id, this);
this.set("ffz_chatters", {});
this.set("ffz_ids", this.get('ffz_ids') || {});
this.set("ffz_last_notices", this.get('ffz_last_notices') || {});
} catch(err) {
f.error("add_room: " + err);
}
},
willDestroy: function() {
this.get("pubsub").set("ffz_teardown_target", this.get('roomProperties._id'));
this._super();
this.get("pubsub").set("ffz_teardown_target", null);
try {
f.remove_room(this.id);
} catch(err) {
f.error("remove_room: " + err);
}
},
addChannelModerationMessage: function(event) {
// Throw out messages that are for other rooms.
var room_id = '.' + this.get("roomProperties._id");
if ( event.topic && event.topic.substr(-room_id.length) !== room_id || event.created_by === this.get("session.userData.login") )
return;
if ( ! f.settings.get_twitch('showModerationActions') )
return;
var target_notice = NOTICE_MAPPING[event.moderation_action];
if ( target_notice ) {
var last_notice = this.ffz_last_notices && this.ffz_last_notices[target_notice];
if ( last_notice && ! last_notice.has_owner ) {
last_notice.message += ' (By: ' + event.created_by + ')';
last_notice.has_owner = true;
last_notice.cachedTokens = undefined;
if ( last_notice._line )
Ember.propertyDidChange(last_notice._line, 'ffzTokenizedMessage');
} else {
var waiting = this.ffz_waiting_notices = this.ffz_waiting_notices || {};
waiting[target_notice] = event.created_by;
}
} else
this._super(event);
},
addLoginModerationMessage: function(event) {
// Throw out messages that are for other rooms or that don't have topics.
var room_id = '.' + this.get("roomProperties._id");
if ( ! event.topic || event.topic.substr(-room_id.length) !== room_id || event.created_by === this.get("session.userData.login") )
return;
//f.log("Login Moderation for " + this.get('id') + ' [' + room_id + ']', event);
// In case we get unexpected input, do the other thing.
if ( f.has_bttv || ["ban", "unban", "timeout"].indexOf(event.moderation_action) === -1 )
return this._super(event);
var msg_id,
reason = event.args[2],
duration = event.moderation_action === 'unban' ? -Infinity : event.args[1];
if ( typeof duration === "string" )
duration = parseInt(duration);
if ( isNaN(duration) )
duration = Infinity;
if ( reason ) {
var match = constants.UUID_TEST.exec(reason);
if ( match ) {
msg_id = match[1];
reason = reason.substr(0, reason.length - match[0].length);
if ( ! reason.length )
reason = null;
}
}
this.addBanNotice(event.args[0].toLowerCase(), duration, reason, event.created_by, msg_id, true);
},
addBanNotice: function(username, duration, reason, moderator, msg_id, report_only) {
var current_user = f.get_user(),
is_me = current_user && current_user.login === username,
show_notice = is_me || this.ffzShouldDisplayNotice(),
show_reason = is_me || this.get('isModeratorOrHigher'),
show_moderator = f.settings.get_twitch('showModerationActions'),
now = new Date,
room_id = this.get('id'),
ffz_room = f.rooms[room_id],
ban_history, last_ban;
// Find an existing ban to modify.
if ( ffz_room ) {
var ban_history = ffz_room.ban_history = ffz_room.ban_history || {};
last_ban = ban_history[username];
// Only overwrite bans in the last 15 seconds.
if ( ! last_ban || Math.abs(now - last_ban.date) > 15000 )
last_ban = null;
}
// If we have an existing ban, modify that.
if ( last_ban ) {
if ( reason && last_ban.reasons.indexOf(reason) === -1 )
last_ban.reasons.push(reason);
if ( moderator && last_ban.moderators.indexOf(moderator) === -1 )
last_ban.moderators.push(moderator);
if ( moderator )
last_ban.notices[moderator] = [duration, reason];
if ( ! report_only )
last_ban.count++;
// Don't update the displayed duration if the new end time is within five
// seconds to avoid changing messages when bots do multiple timeouts.
var end_time = now.getTime() + (duration * 1000);
if ( Math.abs(end_time - last_ban.end_time) > 5000 ) {
last_ban.duration = duration;
last_ban.end_time = end_time;
} else
duration = last_ban.duration;
last_ban.message = f.format_ban_notice(username, is_me, duration, last_ban.count, show_reason && last_ban.reasons, show_moderator && last_ban.moderators, show_moderator && last_ban.notices);
last_ban.cachedTokens = [{type: "raw", html: last_ban.message}];
if ( last_ban._line )
Ember.propertyDidChange(last_ban._line, 'ffzTokenizedMessage');
} else {
var notices = {};
if ( moderator )
notices[moderator] = [duration, reason];
var count = report_only ? 0 : 1,
msg = f.format_ban_notice(username, is_me, duration, count, show_reason && reason && [reason], show_moderator && moderator && [moderator], show_moderator && notices),
message = {
style: 'admin',
date: now,
room: room_id,
ffz_ban_target: username,
reasons: reason ? [reason] : [],
moderators: moderator ? [moderator] : [],
notices: notices,
duration: duration,
end_time: now.getTime() + (duration * 1000),
count: count,
message: msg,
cachedTokens: [{type: "raw", html: msg}]
};
if ( ban_history )
ban_history[username] = message;
if ( show_notice )
this.addMessage(message);
this.addUserHistory(message);
}
},
addUserHistory: function(message) {
var room_id = this.get('id'),
ffz_room = f.rooms[room_id];
if ( ! ffz_room || ! f.settings.mod_card_history )
return;
var username = message.ffz_ban_target || message.from,
historical = message.tags && message.tags.historical,
chat_history = ffz_room.user_history = ffz_room.user_history || {},
user_history = chat_history[username] = chat_history[username] || [];
if ( historical ) {
if ( user_history.length >= 20 )
return;
user_history.unshift(message);
} else {
user_history.push(message);
while ( user_history.length > 20 )
user_history.shift();
}
if ( f._mod_card && f._mod_card.ffz_room_id === room_id && f._mod_card.get('cardInfo.user.id') === username) {
var el = f._mod_card.get('element'),
history = el && el.querySelector('.chat-history.live-history');
if ( history ) {
var was_at_top = history.scrollTop >= (history.scrollHeight - history.clientHeight),
line = f._build_mod_card_history(message, f._mod_card);
if ( historical )
history.insertBefore(line, history.firstElementChild);
else
history.appendChild(line);
if ( was_at_top )
setTimeout(function() { history.scrollTop = history.scrollHeight });
if ( history.childElementCount > 20 )
history.removeChild(history.firstElementChild);
}
}
},
clearMessages: function(user, tags, disable_log) {
var t = this;
if ( user ) {
var duration = Infinity,
reason = undefined,
moderator = undefined,
msg_id = undefined,
current_user = f.get_user(),
is_me = current_user && current_user.login === user;
// Read the ban duration and reason from the message tags.
if ( tags && tags['ban-duration'] ) {
duration = tags['ban-duration'];
if ( typeof duration === 'string' )
duration = parseInt(duration);
if ( isNaN(duration) )
duration = Infinity;
}
if ( tags && tags['ban-reason'] && (is_me || t.get('isModeratorOrHigher')) )
reason = tags['ban-reason'];
if ( tags && tags['ban-moderator'] && (is_me || t.get('isModeratorOrHigher')) )
moderator = tags['ban-moderator'];
// Is there a UUID on the end of the ban reason?
if ( reason ) {
var match = constants.UUID_TEST.exec(reason);
if ( match ) {
msg_id = match[1];
reason = reason.substr(0, reason.length - match[0].length);
if ( ! reason.length )
reason = undefined;
}
}
// If we were banned, set the state and update the UI.
if ( is_me ) {
t.set('ffz_banned', true);
if ( duration )
if ( isFinite(duration) )
t.updateWait(duration);
else {
t.set('slowWait', 0);
f._roomv && f._roomv.ffzUpdateStatus();
}
}
// Mark the user as recently banned.
if ( ! t.ffzRecentlyBanned )
t.ffzRecentlyBanned = [];
t.ffzRecentlyBanned.push(user);
while ( t.ffzRecentlyBanned.length > 100 )
t.ffzRecentlyBanned.shift();
// Are we deleting a specific message?
if ( msg_id && this.ffz_ids ) {
var msg = this.ffz_ids[msg_id];
if ( msg && msg.from === user ) {
msg.ffz_deleted = true;
if ( ! f.settings.prevent_clear )
msg.deleted = true;
if ( f.settings.remove_deleted )
if ( msg.pending )
msg.removed = true;
else {
var msgs = t.get('messages'),
total = msgs.get('length'),
i = total;
while(i--) {
var msg = msgs.get(i);
if ( msg.tags && msg.tags.id === msg_id ) {
msgs.removeAt(i);
delete this.ffz_ids[msg_id];
var notice_type = msg.tags && msg.tags['msg-id'];
if ( notice_type && this.ffz_last_notices && this.ffz_last_notices[notice_type] === msg )
delete this.ffz_last_notices[notice_type];
break;
}
}
}
if ( msg._line ) {
Ember.propertyDidChange(msg._line, 'msgObject.ffz_deleted');
Ember.propertyDidChange(msg._line, 'msgObject.deleted');
}
} else if ( msg.from !== user )
f.log("Banned Message ID #" + msg_id + " not owned by: " + user);
else
f.log("Banned Message ID #" + msg_id + " not found in chat.");
} else {
// Delete all messages from this user / chat.
// Delete Visible Messages
var msgs = t.get('messages'),
total = msgs.get('length'),
i = total,
removed = 0;
while(i--) {
var msg = msgs.get(i);
if ( msg.from === user ) {
if ( f.settings.remove_deleted ) {
// Remove this message from the ID tracker.
var msg_id = msg.tags && msg.tags.id,
notice_type = msg.tags && msg.tags['msg-id'];
if ( msg_id && this.ffz_ids && this.ffz_ids[msg_id] )
delete this.ffz_ids[msg_id];
if ( notice_type && this.ffz_last_notices && this.ffz_last_notices[notice_type] === msg )
delete this.ffz_last_notices[notice_type];
msgs.removeAt(i);
removed++;
continue;
}
t.set('messages.' + i + '.ffz_deleted', true);
if ( ! f.settings.prevent_clear )
t.set('messages.' + i + '.deleted', true);
}
}
// Delete Panding Messages
if ( t.ffzPending ) {
msgs = t.ffzPending;
i = msgs.length;
while(i--) {
var msg = msgs.get(i);
if ( msg.from !== user ) continue;
msg.ffz_deleted = true;
msg.deleted = !f.settings.prevent_clear;
msg.removed = f.settings.remove_deleted;
}
}
}
if ( ! disable_log )
this.addBanNotice(user, duration, reason, null, msg_id);
} else {
if ( f.settings.prevent_clear )
t.addMessage({
style: 'admin',
message: "A moderator's attempt to clear chat was ignored.",
tags: {
'msg-id': 'clear_chat'
}
});
else {
var msgs = t.get("messages");
t.set("messages", []);
t.addMessage({
style: 'admin',
message: i18n("Chat was cleared by a moderator"),
tags: {
'msg-id': 'clear_chat'
}
});
}
}
},
// Artificial chat delay
ffz_chat_delay: function() {
var val = f.settings.chat_delay;
if ( val !== -1 )
return val;
val = this.get('roomProperties.chat_delay_duration');
return ( Number.isNaN(val) || ! Number.isFinite(val) || this.get('isModeratorOrHigher') ) ? 0 : val;
}.property('roomProperties.chat_delay_duration', 'isModeratorOrHigher'),
ffz_update_display: function() {
if ( f._roomv )
f._roomv.ffzUpdateStatus();
}.observes('roomProperties.chat_delay_duration'),
pushMessage: function(msg) {
if ( f.settings.chat_batching !== 0 || this.get('ffz_chat_delay') !== 0 || (this.ffzPending && this.ffzPending.length) ) {
if ( ! this.ffzPending )
this.ffzPending = [];
var now = msg.time = Date.now();
msg.pending = true;
this.ffzPending.push(msg);
this.ffzSchedulePendingFlush(now);
} else {
this.ffzPushMessages([msg]);
}
},
ffzPushMessages: function(messages) {
var new_messages = [],
new_unread = 0;
for(var i=0; i < messages.length; i++) {
var msg = messages[i];
if ( this.shouldShowMessage(msg) && this.ffzShouldShowMessage(msg) ) {
new_messages.push(msg);
if ( ! (msg.tags && msg.tags.historical) && msg.style !== "admin" && msg.style !== "whisper" ) {
if ( msg.ffz_has_mention )
this.ffz_last_mention = Date.now();
new_unread++;
}
}
}
if ( ! new_messages.length )
return;
var room_messages = this.get("messages"),
raw_remove = room_messages.length + new_messages.length > this.messageBufferSize ?
Math.max(0, room_messages.length - this.messageBufferSize) + new_messages.length : 0,
to_remove = raw_remove - raw_remove % 2,
removed = room_messages.slice(0, to_remove),
trimmed = room_messages.slice(to_remove, room_messages.length);
// Garbage collect removed messages.
for(var i=0; i < removed.length; i++) {
var msg = removed[i],
msg_id = msg.tags && msg.tags.id,
notice_type = msg.tags && msg.tags['msg-id'];
if ( msg_id && this.ffz_ids && this.ffz_ids[msg_id] )
delete this.ffz_ids[msg_id];
if ( notice_type && this.ffz_last_notices && this.ffz_last_notices[notice_type] === msg )
delete this.ffz_last_notices[notice_type];
}
var earliest_message;
for(var i=0; i < trimmed.length; i++)
if ( trimmed[i].date ) {
earliest_message = trimmed[i].date;
break;
}
for(var i = 0; i < new_messages.length; i++) {
var msg = new_messages[i];
if ( msg.tags && msg.tags.historical ) {
// Add a warning about really old messages.
if ( earliest_message ) {
var age = earliest_message - msg.date;
if ( age > 300000 )
trimmed.unshift({
color: '#755000',
date: msg.date,
from: 'frankerfacez_admin',
style: 'admin',
message: '(Last message is ' + utils.human_time(age/1000) + ' old.)',
room: msg.room,
from_server: true
});
}
trimmed.unshift(msg);
earliest_message = null;
} else
trimmed.push(msg);
}
this.set("messages", trimmed);
if ( new_unread ) {
this.incrementProperty("unreadCount", new_unread);
this.ffz_last_activity = Date.now();
}
},
ffzSchedulePendingFlush: function(now) {
// Instead of just blindly looping every x seconds, we want to calculate the time until
// the next message should be displayed, and then set the timeout for that. We'll
// end up looping a bit more frequently, but it'll make chat feel more responsive.
// If we have a pending flush, don't reschedule. It wouldn't change.
if ( this._ffz_pending_flush )
return;
if ( this.ffzPending && this.ffzPending.length ) {
// We need either the amount of chat delay past the first message, if chat_delay is on, or the
// amount of time from the last batch.
now = now || Date.now();
var t = this,
chat_delay = this.get('ffz_chat_delay'),
delay = Math.max(
(chat_delay !== 0 ? 50 + Math.max(0, (chat_delay + (this.ffzPending[0].time||0)) - now) : 0),
(f.settings.chat_batching !== 0 ? Math.max(0, f.settings.chat_batching - (now - (this._ffz_last_batch||0))) : 0));
this._ffz_pending_flush = setTimeout(this.ffzPendingFlush.bind(this), delay);
}
},
ffzPendingFlush: function() {
this._ffz_pending_flush = null;
var now = this._ffz_last_batch = Date.now(),
chat_delay = this.get('ffz_chat_delay'),
to_display = [];
for (var i = 0, l = this.ffzPending.length; i < l; i++) {
var msg = this.ffzPending[i];
if ( msg.removed ) {
// Don't keep this message ID around.
var msg_id = msg && msg.tags && msg.tags.id,
notice_type = msg && msg.tags && msg.tags['msg-id'];
if ( msg_id && this.ffz_ids && this.ffz_ids[msg_id] )
delete this.ffz_ids[msg_id];
if ( notice_type && this.ffz_last_notices && this.ffz_last_notices[notice_type] === msg )
delete this.ffz_last_notices[notice_type];
continue;
}
if ( chat_delay !== 0 && (chat_delay + msg.time > now) )
break;
msg.pending = false;
to_display.push(msg);
}
this.ffzPushMessages(to_display);
this.ffzPending = this.ffzPending.slice(i);
this.ffzSchedulePendingFlush(now);
},
ffzShouldShowMessage: function (msg) {
if ( ! f.settings.hosted_sub_notices && msg.style === 'notification' && HOSTED_SUB.test(msg.message) )
return false;
if (f.settings.remove_bot_ban_notices && this.ffzRecentlyBanned) {
var banned = '(' + this.ffzRecentlyBanned.join('|') + ')';
var bots = {
'nightbot': '^' + banned,
'moobot': '\\(' + banned + '\\)',
'xanbot': '^' + banned,
};
if (msg.from in bots && (new RegExp(bots[msg.from])).test(msg.message)) {
return false;
}
}
return true;
},
ffzShouldDisplayNotice: function() {
return f.settings.timeout_notices === 2 || (f.settings.timeout_notices === 1 && this.get('isModeratorOrHigher'));
},
addNotification: function(msg) {
if ( msg ) {
// We don't want to display these notices because we're injecting our own messages.
if ( (msg.msgId === 'timeout_success' || msg.msgId === 'ban_success') && this.ffzShouldDisplayNotice() )
return;
// f.log("Notification", msg);
if ( ! msg.tags )
msg.tags = {};
if ( ! msg.tags['msg-id'] )
msg.tags['msg-id'] = msg.msgId;
if ( ! msg.style )
msg.style = 'admin';
if ( this.ffz_waiting_notices && this.ffz_waiting_notices[msg.msgId]) {
msg.has_owner = true;
msg.message += ' (By: ' + this.ffz_waiting_notices[msg.msgId] + ')';
delete this.ffz_waiting_notices[msg.msgId];
}
return this.addMessage(msg);
}
},
onMessage: function(msg) {
// We do our own batching. With blackjack, and hookers. You know what? Forget the batching.
this.addMessage(msg);
},
ffzProcessMessage: function(msg) {
if ( msg ) {
var notice_type = msg.tags && msg.tags['msg-id'],
is_resub = notice_type === 'resub',
room_id = this.get('id'),
msg_id = msg.tags && msg.tags.id;
// Ignore resubs in other rooms.
if ( is_resub && ! f.settings.hosted_sub_notices && (msg.tags['room-id'] != this.get('roomProperties._id') || HOSTED_SUB.test(msg.tags['system-msg'])) )
return;
// Split this into two messages if requested.
if ( is_resub && f.settings.old_sub_notices ) {
var message = msg.tags['system-msg'],
months = /for (\d+) months/.exec(message);
this.addMessage({
style: "notification",
from: "twitchnotify",
date: msg.date || new Date,
room: room_id,
message: message,
ffz_sub_months: months && parseInt(months[1])
});
// If there's no message just quit now.
if ( ! msg.message )
return;
// And delete the system message so it won't render weirdly.
msg.tags['system-msg'] = '';
}
// Fix dates for historical messages.
if ( ! msg.date && msg.tags && msg.tags['tmi-sent-ts'] ) {
var sent = parseInt(msg.tags['tmi-sent-ts']);
if ( sent && ! isNaN(sent) && isFinite(sent) )
msg.date = new Date(sent);
}
var is_whisper = msg.style === 'whisper';
// Ignore whispers if conversations are enabled.
if ( is_whisper ) {
var conv_enabled = utils.ember_lookup('controller:application').get('isConversationsEnabled');
if ( conv_enabled || (!conv_enabled && f.settings.hide_whispers_in_embedded_chat) )
return;
}
if ( ! is_whisper )
msg.room = room_id;
// Look up color and labels.
if ( this.tmiRoom && msg.from ) {
if ( ! msg.color )
msg.color = msg.tags && msg.tags.color ? msg.tags.color : this.tmiSession.getColor(msg.from.toLowerCase());
if ( ! msg.labels )
msg.labels = this.tmiRoom.getLabels(msg.from);
}
// Tag the broadcaster.
if ( room_id === msg.from )
msg.tags.mod = true;
// Tokenization
f.tokenize_chat_line(msg, false, this.get('roomProperties.hide_chat_links'));
// Check for a new subscription line that would need a chat badge.
if ( msg.from === 'twitchnotify' && msg.message.indexOf('subscribed to') === -1 && msg.message.indexOf('subscribed') !== -1 ) {
if ( ! msg.tags )
msg.tags = {};
if ( ! msg.tags.badges )
msg.tags.badges = {};
var ffz_room = f.rooms && f.rooms[room_id],
badges = ffz_room && ffz_room.badges || {},
sub_version = badges && badges.subscriber && badges.subscriber.versions;
if ( ! isNaN(msg.ffz_sub_months) ) {
var months = msg.ffz_sub_months;
msg.tags.badges.subscriber = ( months >= 24 && sub_version[24] ) ? 24 :
(months >= 12 && sub_version[12] ) ? 12 :
(months >= 6 && sub_version[6] ) ? 6 :
(months >= 3 && sub_version[3] ) ? 3 : 1;
} else
msg.tags.badges.subscriber = 1;
msg.tags.subscriber = true;
if ( msg.labels && msg.labels.indexOf("subscriber") === -1 )
msg.labels.push("subscriber");
}
// Color processing.
if ( msg.color )
f._handle_color(msg.color);
return msg;
}
},
addMessage: function(msg) {
msg = this.ffzProcessMessage(msg);
if ( ! msg )
return;
var msg_id = msg.tags && msg.tags.id,
notice_type = msg.tags && msg.tags['msg-id'],
is_whisper = msg.style === 'whisper';
// If this message is already in the room, discard the duplicate.
if ( msg_id && this.ffz_ids && this.ffz_ids[msg_id] )
return;
// If it's historical, make sure it's for this room.
if ( msg.tags && msg.tags.historical && msg.tags['room-id'] != this.get('roomProperties._id') )
return;
// Keep the history.
if ( ! is_whisper && msg.from && msg.from !== 'jtv' && msg.from !== 'twitchnotify' )
this.addUserHistory({
from: msg.from,
tags: {
id: msg.tags && msg.tags.id,
'display-name': msg.tags && msg.tags['display-name'],
bits: msg.tags && msg.tags.bits
},
message: msg.message,
cachedTokens: msg.cachedTokens,
style: msg.style,
date: msg.date
});
// Clear the last ban for that user.
var f_room = f.rooms && f.rooms[msg.room],
ban_history = f_room && f_room.ban_history;
if ( ban_history && msg.from ) {
// Is the last ban within 200ms? Chances are Twitch screwed up message order.
if ( ban_history[msg.from] && (new Date - ban_history[msg.from].date) <= 200 ) {
msg.ffz_deleted = true;
msg.deleted = !f.settings.prevent_clear;
} else
ban_history[msg.from] = false;
}
// Check for message from us.
if ( ! is_whisper && ! msg.ffz_deleted ) {
var user = f.get_user();
if ( user && user.login === msg.from ) {
var was_banned = this.get('ffz_banned');
this.set('ffz_banned', false);
// Update the wait time.
if ( this.get('isSubscriber') || this.get('isModeratorOrHigher') || ! this.get('slowMode') )
this.updateWait(0, was_banned)
else if ( this.get('slowMode') )
this.updateWait(this.get('slow'));
}
}
// Message Filtering
f.api_trigger('room-message', msg);
// Also update chatters.
if ( ! is_whisper && this.chatters && ! this.chatters[msg.from] && msg.from !== 'twitchnotify' && msg.from !== 'jtv' )
this.ffzUpdateChatters(msg.from);
// We're past the last return, so store the message
// now that we know we're keeping it.
if ( msg_id ) {
var ids = this.ffz_ids = this.ffz_ids || {};
ids[msg_id] = msg;
}
// If this is a notice, store that this is the last of its type.
if ( notice_type ) {
var ids = this.ffz_last_notices = this.ffz_last_notices || {};
ids[notice_type] = msg;
}
// Report this message to the dashboard.
if ( window !== window.parent && parent.postMessage && msg.from && msg.from !== "jtv" && msg.from !== "twitchnotify" )
parent.postMessage({from_ffz: true, command: 'chat_message', data: {from: msg.from, room: msg.room}}, "*"); //location.protocol + "//www.twitch.tv/");
// Flagging for review.
if ( msg.tags && msg.tags.risk === "high" )
msg.flaggedForReview = true;
// Add the message. We don't do super anymore because it does stupid stuff.'
this.pushMessage(msg);
msg.from && msg.style !== "admin" && msg.style !== "notification" && msg.tags && this.addChatter(msg);
this.trackLatency(msg);
//return this._super(msg);
},
setHostMode: function(e) {
this.set('ffz_host_target', e && e.hostTarget || null);
var user = f.get_user();
if ( user && f._cindex && this.get('id') === user.login )
f._cindex.ffzUpdateHostButton();
// If hosting is disabled, or this isn't the current channel room,
// ignore the host mode.
var Chat = utils.ember_lookup('controller:chat');
if ( ! Chat || Chat.get('currentChannelRoom') !== this )
return;
var target = f.settings.hosted_channels ? (e.hostTarget ? e.hostTarget.toLowerCase() : null) : null,
channel = this.get("channel");
if ( channel ) {
var delay = 0;
if ( target && ! e.recentlyJoined ) {
var percentile = Math.max((e.numViewers || 0) / .5, 4000);
delay = 3000 + Math.floor((percentile || 0) * Math.random());
}
if ( this.get("experiments.shouldSeeRedesign") ) {
var c = this.get("store").peekRecord("channel", channel.get("name"));
if ( c ) {
if ( target )
this.pendingFetchHostModeTarget = Ember.run.debounce(this, "fetchHostModeTarget", {
currentChannel: c,
targetName: target
}, delay);
else
c.set("hostModeTarget", null);
}
} else channel.setHostMode({
target: target, delay: delay
});
}
},
send: function(text, ignore_history, used_aliases) {
try {
this.ffz_last_input = Date.now();
var first_char = text.charAt(0),
is_cmd = first_char === '/' || first_char === '.';
// Strip trailing whitespace from commands.
if ( is_cmd )
text = text.replace(/\s+$/, '');
if ( text && ! ignore_history ) {
// Command History
var mru = this.get('mru_list'),
ind = mru.indexOf(text);
if ( ind !== -1 )
mru.splice(ind, 1)
else if ( mru.length > 20 )
mru.pop();
mru.unshift(text);
}
if ( is_cmd ) {
var cmd = text.substr(1).split(' ', 1)[0].toLowerCase(),
was_handled = false;
if ( cmd === "ffz" ) {
f.run_ffz_command(text.substr(5), this.get('id'));
was_handled = true;
} else if ( f._command_aliases[cmd] ) {
used_aliases = used_aliases || [];
if ( used_aliases.indexOf(cmd) !== -1 ) {
f.room_message(f.rooms[this.get('id')], "Error: Your command aliases are recursing. [Path: " + used_aliases.join(", ") + "]");
was_handled = true;
} else {
var alias = f._command_aliases[cmd][0],
args = text.substr(1 + cmd.length).trimLeft().split(/\s+/g),
output = utils.replace_cmd_variables(alias, null, this, null, args);
used_aliases.push(cmd);
this.set("messageToSend", "");
var lines = output.split(/\s*<LINE>\s*/g);
for(var i=0; i < lines.length; i++)
this.send(lines[i], true, used_aliases);
return;
}
} else if ( f.run_command(text, this.get('id')) )
was_handled = true;
if ( was_handled ) {
this.set("messageToSend", "");
return;
}
}
} catch(err) {
f.error("send: " + err);
}
return this._super(text);
},
ffzUpdateUnread: function() {
var Chat = utils.ember_lookup('controller:chat');
if ( Chat && Chat.get('currentRoom') === this )
this.resetUnreadCount();
else if ( f._chatv )
f._chatv.ffzUpdateUnread(this.get('id'));
}.observes('unreadCount'),
ffzInitChatterCount: function() {
if ( ! this.tmiRoom )
return;
if ( this._ffz_chatter_timer ) {
clearTimeout(this._ffz_chatter_timer);
this._ffz_chatter_timer = undefined;
}
var room = this;
this.tmiRoom.list().done(function(data) {
var chatters = {};
data = data.data.chatters;
if ( data && data.admins )
for(var i=0; i < data.admins.length; i++)
chatters[data.admins[i]] = true;
if ( data && data.global_mods )
for(var i=0; i < data.global_mods.length; i++)
chatters[data.global_mods[i]] = true;
if ( data && data.moderators )
for(var i=0; i < data.moderators.length; i++)
chatters[data.moderators[i]] = true;
if ( data && data.staff )
for(var i=0; i < data.staff.length; i++)
chatters[data.staff[i]] = true;
if ( data && data.viewers )
for(var i=0; i < data.viewers.length; i++)
chatters[data.viewers[i]] = true;
room.set("ffz_chatters", chatters);
room.ffzUpdateChatters();
}).always(function() {
room._ffz_chatter_timer = setTimeout(room.ffzInitChatterCount.bind(room), 300000);
});
},
ffzUpdateChatters: function(add, remove) {
var chatters = this.get("ffz_chatters") || {};
if ( add )
chatters[add] = true;
if ( remove && chatters[remove] )
delete chatters[remove];
if ( ! f.settings.chatter_count )
return;
if ( f._cindex )
f._cindex.ffzUpdateMetadata('chatters');
if ( window !== window.parent && parent.postMessage )
parent.postMessage({from_ffz: true, command: 'chatter_count', data: {room: this.get('id'), chatters: Object.keys(this.get('ffz_chatters') || {}).length}}, "*"); //location.protocol + "//www.twitch.tv/");
},
ffzPatchTMI: function() {
var tmi = this.get('tmiRoom'),
room = this;
if ( this.get('ffz_is_patched') || ! tmi )
return;
if ( f.settings.chatter_count )
this.ffzInitChatterCount();
// Let's get chatter information!
// TODO: Remove this cause it's terrible.
var connection = tmi._roomConn._connection;
if ( ! connection.ffz_cap_patched ) {
connection.ffz_cap_patched = true;
connection._send("CAP REQ :twitch.tv/membership");
connection.on("opened", function() {
this._send("CAP REQ :twitch.tv/membership");
}, connection);
}
// NOTICE for catching slow-mode updates
tmi.on('notice', function(msg) {
if ( msg.msgId === 'msg_slowmode' ) {
var match = /in (\d+) seconds/.exec(msg.message);
if ( match ) {
room.updateWait(parseInt(match[1]));
}
}
if ( msg.msgId === 'msg_timedout' ) {
var match = /for (\d+) more seconds/.exec(msg.message);
if ( match ) {
room.set('ffz_banned', true);
room.updateWait(parseInt(match[1]));
}
}
if ( msg.msgId === 'msg_banned' ) {
room.set('ffz_banned', true);
f._roomv && f._roomv.ffzUpdateStatus();
}
if ( msg.msgId === 'hosts_remaining' ) {
var match = /(\d+) host command/.exec(msg.message);
if ( match ) {
room.set('ffz_hosts_left', parseInt(match[1] || 0));
f._cindex && f._cindex.ffzUpdateHostButton();
}
}
});
// Check this shit.
tmi._roomConn._connection.off("message", tmi._roomConn._onIrcMessage, tmi._roomConn);
tmi._roomConn._onIrcMessage = function(ircMsg) {
if ( ircMsg.target != this.ircChannel )
return;
switch ( ircMsg.command ) {
case "JOIN":
if ( this._session && this._session.nickname === ircMsg.sender ) {
this._onIrcJoin(ircMsg);
} else
f.settings.chatter_count && room.ffzUpdateChatters(ircMsg.sender);
break;
case "PART":
if ( this._session && this._session.nickname === ircMsg.sender ) {
this._resetActiveState();
this._connection._exitedRoomConn();
this._trigger("exited");
} else
f.settings.chatter_count && room.ffzUpdateChatters(null, ircMsg.sender);
break;
default:
break;
}
}
tmi._roomConn._connection.on("message", tmi._roomConn._onIrcMessage, tmi._roomConn);
this.set('ffz_is_patched', true);
}.observes('tmiRoom'),
// Room State Stuff
onSlowOff: function() {
if ( ! this.get('slowMode') )
this.updateWait(0);
}.observes('slowMode')
});
}