var FFZ = window.FrankerFaceZ,
utils = require("../utils"),
constants = require("../constants"),
styles = require("../compiled_styles"),
helpers,
TO_REG = /^\/t(?:imeout)? +([^ ]+)(?: +(\d+)(?: +(.+))?)?$/,
BAN_REG = /^\/b(?:an)? +([^ ]+)(?: +(.+))?$/,
keycodes = {
ESC: 27,
R: 82,
P: 80,
B: 66,
T: 84,
U: 85,
C: 67,
H: 72,
S: 83,
Y: 89,
N: 78
},
MESSAGE = '',
CHECK = '';
try {
helpers = window.require && window.require("web-client/helpers/chat/chat-line-helpers");
} catch(err) { }
// ----------------
// Settings
// ----------------
FFZ.settings_info.disable_bttv_mod_cards = {
type: "boolean",
value: false,
require_bttv: 7,
category: "Chat Moderation",
name: "Disable BTTV Mod Cards",
help: "This disables mod cards from BetterTTV, forcing FFZ mod cards to show instead.",
on_update: function(val) {
var CL = utils.ember_resolve('component:chat/chat-line'),
views = CL ? utils.ember_views() : [];
for(var vid in views) {
var view = views[vid];
if ( view instanceof CL && view.buildFromHTML ) {
view.$('.from').replaceWith(view.buildFromHTML());
if ( view.get('msgObject.to') )
view.$('.to').replaceWith(view.buildFromHTML(true));
}
}
}
};
FFZ.basic_settings.enhanced_moderation_cards = {
type: "boolean",
no_bttv: true,
category: "Chat",
name: "Enhanced Moderation Cards",
help: "Improve moderation cards with hotkeys, additional buttons, chat history, and other information to make moderating easier.",
get: function() {
return this.settings.mod_card_hotkeys &&
this.settings.mod_card_info &&
this.settings.mod_card_history;
},
set: function(val) {
this.settings.set('mod_card_hotkeys', val);
this.settings.set('mod_card_info', val);
this.settings.set('mod_card_history', val);
}
};
FFZ.basic_settings.chat_hover_pause = {
type: "boolean",
no_bttv: 6,
category: "Chat",
name: "Pause Chat Scrolling on Mouse Hover",
help: "Automatically prevent the chat from scrolling when moving the mouse over it to prevent moderation mistakes and link misclicks.",
get: 'chat_hover_pause',
set: 'chat_hover_pause'
};
FFZ.settings_info.highlight_messages_with_mod_card = {
type: "boolean",
value: false,
no_bttv: true,
category: "Chat Moderation",
name: "Highlight Messages with Mod Card Open",
help: "Highlight a user's messages in chat when their moderation card is open.",
on_update: function(val) {
if ( ! this._mod_card )
return;
if ( val )
utils.update_css(this._chat_style, 'mod-card-highlight', styles['chat-user-bg'].replace(/{user_id}/g, this._mod_card.get('cardInfo.user.id')));
else
utils.update_css(this._chat_style, 'mod-card-highlight');
}
};
FFZ.settings_info.logviewer_test = {
type: "boolean",
value: true,
no_bttv: true,
category: "Chat Moderation",
name: "Logviewer Integration",
help: "Display information from CBenni's Logviewer directly on moderation cards."
}
FFZ.settings_info.chat_mod_icon_visibility = {
type: "select",
options: {
0: "Disabled",
1: "Enabled",
2: "When Ctrl is Held",
3: "When " + constants.META_NAME + " is Held",
4: "When Alt is Held",
5: "When Shift is Held"
},
value: function() {
return this.settings.get_twitch("showModIcons") ? 1 : 0;
},
process_value: utils.process_int(0),
no_bttv: 6,
category: "Chat Moderation",
name: "Display In-Line Mod Icons",
help: "Choose when you should see in-line moderation icons in chat.",
on_update: function(val) {
var settings = utils.ember_settings();
if ( settings )
settings.set('showModIcons', val === 1);
}
}
FFZ.settings_info.chat_hover_pause = {
type: "select",
options: {
0: "Disabled",
1: "On Hover",
2: "When Ctrl is Held",
3: "When " + constants.META_NAME + " is Held",
4: "When Alt is Held",
5: "When Shift is Held",
6: "Ctrl or Hover",
7: constants.META_NAME + " or Hover",
8: "Alt or Hover",
9: "Shift or Hover"
},
value: 0,
process_value: utils.process_int(0, 0, 1),
no_bttv: 6,
category: "Chat Moderation",
name: "Pause Chat Scrolling",
help: "Automatically prevent the chat from scrolling when moving the mouse over it or holding Ctrl to prevent moderation mistakes and link misclicks.",
on_update: function(val) {
if ( ! this._roomv )
return;
this._roomv.ffzDisableFreeze();
// Remove the old warning to make sure the label updates.
var el = this._roomv.get('element'),
warning = el && el.querySelector('.chat-interface .more-messages-indicator.ffz-freeze-indicator');
if ( warning )
warning.parentElement.removeChild(warning);
if ( val )
this._roomv.ffzEnableFreeze();
}
};
FFZ.settings_info.short_commands = {
type: "boolean",
value: true,
no_bttv: 6,
category: "Chat Moderation",
name: "Short Moderation Commands",
help: "Use /t, /b, and /u in chat in place of /timeout, /ban, /unban for quicker moderation, and use /p for 1 second timeouts."
};
FFZ.settings_info.mod_card_hotkeys = {
type: "boolean",
value: false,
no_bttv: true,
category: "Chat Moderation",
name: "Moderation Card Hotkeys",
help: "With a moderation card selected, press B to ban the user, T to time them out for 10 minutes, P to time them out for 1 second, or U to unban them. ESC closes the card."
};
FFZ.settings_info.mod_card_info = {
type: "boolean",
value: true,
no_bttv: true,
category: "Chat Moderation",
name: "Moderation Card Additional Information",
help: "Display a channel's follower count, view count, and account age on moderation cards."
};
FFZ.settings_info.timeout_notices = {
type: "select",
options: {
0: "Disabled",
1: "If I'm a Moderator",
2: "Always"
},
value: 1,
process_value: utils.process_int(1),
no_bttv: 6,
category: "Chat Moderation",
name: "Display Timeout / Ban Notices",
help: "Display notices in chat when a user is timed out or banned. (You always see your own bans.)"
};
FFZ.settings_info.mod_card_history = {
type: "select",
options: {
0: "Disabled",
1: "On Rooms without Logviewer",
2: "Always"
},
value: 0,
process_value: utils.process_int(0, 0, 1),
no_bttv: true,
category: "Chat Moderation",
name: "Moderation Card History",
help: "Display a few of the user's previously sent messages on moderation cards.",
on_update: function(val) {
if ( val === 2 || ! this.rooms )
return;
// Delete all history~!
for(var room_id in this.rooms) {
var room = this.rooms[room_id];
if ( room && (val === 0 || room.has_logs) )
room.user_history = undefined;
}
}
};
FFZ.settings_info.mod_button_context = {
type: "select",
options: {
0: "Disabled",
1: "Show Ban Reasons Only",
2: "Show Chat Rules Only",
3: "Ban Reasons + Chat Rules"
},
value: 3,
process_value: utils.process_int(3),
no_bttv: 6,
category: "Chat Moderation",
name: "Mod Icon Context Menus",
help: "Choose the available options when right-clicking an in-line moderation icon."
};
FFZ.settings_info.mod_card_reasons = {
type: "button",
value: [
"One-Man Spam",
"Posting Bad Links",
"Ban Evasion",
"Threats / Personal Info",
"Hate / Harassment",
"Ignoring Broadcaster / Moderators"
],
category: "Chat Moderation",
no_bttv: 6,
name: "Ban / Timeout Reasons",
help: "Change the available options in the chat ban reasons list shown in moderation cards and when right-clicking an in-line ban or timeout button.",
method: function() {
var f = this,
old_val = this.settings.mod_card_reasons.join("\n"),
input = utils.createElement('textarea');
input.style.marginBottom = "20px";
utils.prompt(
"Moderation Card Ban Reasons",
"Please enter a list of ban reasons to select from. One item per line.",
old_val,
function(new_val) {
if ( new_val === null || new_val === undefined )
return;
var vals = new_val.trim().split(/\s*\n\s*/g),
i = vals.length;
while(i--)
if ( vals[i].length === 0 )
vals.splice(i,1);
f.settings.set('mod_card_reasons', vals);
},
600, input
);
}
};
FFZ.settings_info.mod_buttons = {
type: "button",
// Special Values
// false = Ban/Unban
// integer = Timeout (that amount of time)
value: [['', false, false], ['',600, false]], //, ['', 1, false]],
no_bttv: 6,
category: "Chat Moderation",
name: "Custom In-Line Moderation Icons",
help: "Change out the different in-line moderation icons to use any command quickly.",
method: function() {
var f = this,
old_val = "",
input = utils.createElement('textarea');
input.style.marginBottom = '20px';
input.placeholder = '/ban\n600';
for(var i=0; i < this.settings.mod_buttons.length; i++) {
var pair = this.settings.mod_buttons[i],
prefix = pair[0], cmd = pair[1], had_prefix = pair[2], non_mod = pair[4];
if ( cmd === false )
cmd = "/ban";
else if ( cmd === 600 )
cmd = "/timeout";
else if ( typeof cmd !== "string" )
cmd = '' + cmd;
prefix = had_prefix ? 'name:' + prefix + '=' : '';
old_val += (old_val.length ? '\n' : '') + (non_mod ? 'nonmod:' : '') + prefix + cmd;
}
utils.prompt(
"Custom In-Line Moderation Icons",
"Please enter a list of commands to be displayed as moderation buttons within chat lines. " +
"One item per line. As a shortcut for specific duration timeouts, you can enter the number of seconds by itself. " +
" To send multiple commands, separate them with <LINE>
. " +
"Variables, such as the target user's name, can be inserted into your commands. If no variables are detected " +
"in a line, {user}
will be added to the end of the first command.
name:
followed by the " +
"name of the button. End the name with an equals sign. Only the first character will be displayed.name:B=/ban {user}
nonmod:
nonmod:/w some_bot !info {user}
{user} | target user's name | " + "{user_name} | target user's name |
{user_display_name} | target user's display name | " + "{user_id} | target user's numeric ID |
{room} | chat room's name | " + "{room_name} | chat room's name |
{room_display_name} | chat room's display name | " + "{room_id} | chat room's numeric ID |
{id} | message's UUID |
<LINE>
. " +
"Variables, such as the target user's name, can be inserted into your commands. If no variables are detected " +
"in a line, {user}
will be added to the end of the first command.name:
followed by the name of the button. " +
"End the name with an equals sign.name:Boop=/timeout {user} 15 Boop!
{user} | target user's name | " + "{user_name} | target user's name |
{user_display_name} | target user's display name | " + "{user_id} | target user's numeric ID |
{room} | chat room's name | " + "{room_name} | chat room's name |
{room_display_name} | chat room's display name | " + "{room_id} | chat room's numeric ID |
Default: 300, 600, 3600, 43200, 86400, 604800",
old_val,
function(new_val) {
if ( new_val === null || new_val === undefined )
return;
if ( new_val === "reset" )
new_val = FFZ.settings_info.mod_card_durations.value.join(", ");
// Split them up.
new_val = new_val.trim().split(/[ ,]+/);
var vals = [];
for(var i=0; i < new_val.length; i++) {
var val = parseInt(new_val[i]);
if ( val === 0 )
val = 1;
if ( ! Number.isNaN(val) && val > 0 )
vals.push(val);
}
f.settings.set("mod_card_durations", vals);
}, 600);
}
};
// ----------------
// Initialization
// ----------------
FFZ.prototype.setup_mod_card = function() {
try {
helpers = window.require && window.require("web-client/helpers/chat/chat-line-helpers");
} catch(err) { }
this.log("Listening to the Settings controller to catch mod icon state changes.");
var f = this,
Settings = utils.ember_settings();
if ( Settings )
Settings.addObserver('showModIcons', function() {
if ( Settings.get('showModIcons') )
f.settings.set('chat_mod_icon_visibility', 1);
});
this.log("Modifying Mousetrap stopCallback so we can catch ESC.");
var orig_stop = Mousetrap.stopCallback;
Mousetrap.stopCallback = function(e, element, combo) {
if ( element.classList.contains('no-mousetrap') )
return true;
return orig_stop(e, element, combo);
}
Mousetrap.bind("up up down down left right left right b a", function() {
var el = document.querySelector(".app-main") || document.querySelector(".ember-chat-container");
el && el.classList.toggle('ffz-flip');
});
this.log("Hooking the Ember Moderation Card view.");
this.update_views('component:chat/moderation-card', this.modify_moderation_card);
}
FFZ.prototype.modify_moderation_card = function(component) {
var f = this;
utils.ember_reopen_view(component, {
ffzForceRedraw: function() {
this.rerender();
var el = this.get('element');
this.ffzChangePage(null);
var chat = utils.ember_lookup('controller:chat'),
room_id = chat && chat.get('currentRoom.id'),
user_id = this.get('cardInfo.user.id'),
user = f.get_user(),
is_me = user && user.login === user_id;
if (( this._lv_sock_room && this._lv_sock_room !== room_id ) || (this._lv_sock_user && this._lv_sock_user !== user_id) ) {
f.lv_ws_unsub('logs-' + this._lv_sock_room + '-' + this._lv_sock_user);
this._lv_sock_room = null;
this._lv_sock_user = null;
}
if ( f.settings.mod_card_history )
this.ffzRenderHistory();
Ember.run.next(this.ffzFixDefaultActions.bind(this));
// Highlight this user's chat messages.
if ( f.settings.highlight_messages_with_mod_card )
utils.update_css(f._chat_style, 'mod-card-highlight', styles['chat-user-bg'].replace(/{user_id}/g, this.get('cardInfo.user.id')));
}.observes("cardInfo.isModeratorOrHigher", "cardInfo.user.id"),
ffzRebuildInfo: function() {
var el = this.get('element'),
info = el && el.querySelector('.info');
if ( ! info )
return;
var out = '' + constants.EYE + ' ' + utils.number_commas(this.get('cardInfo.user.views') || 0) + '',
since = utils.parse_date(this.get('cardInfo.user.created_at') || ''),
followers = this.get('cardInfo.user.ffz_followers');
if ( typeof followers === "number" ) {
out += '' + constants.HEART + ' ' + utils.number_commas(followers || 0) + '';
} else if ( followers === undefined ) {
var t = this;
this.set('cardInfo.user.ffz_followers', false);
utils.api.get("channels/" + this.get('cardInfo.user.id') + '/follows', {limit:1}).done(function(data) {
t.set('cardInfo.user.ffz_followers', data._total);
t.ffzRebuildInfo();
}).fail(function(data) {
t.set('cardInfo.user.ffz_followers', undefined);
});
}
if ( since ) {
var now = Date.now() - (f._ws_server_offset || 0),
age = Math.floor((now - since.getTime()) / 1000);
if ( age > 0 ) {
out += '' + constants.CLOCK + ' ' + utils.human_time(age, 10) + '';
}
}
info.innerHTML = out;
}.observes("cardInfo.user.views"),
lvGetLogs: function() {
var t = this,
logs = this._lv_logs,
chat = utils.ember_lookup('controller:chat'),
room_id = chat && chat.get('currentRoom.id'),
user_id = this.get('cardInfo.user.id');
return new Promise(function(succeed, fail) {
// Don't expire data if we're connected to the websocket.
if ( logs && (f._lv_ws_open || logs.expires > Date.now()) && logs.room === room_id && logs.user === user_id )
return succeed(logs.data);
if ( t._lv_log_requests )
return t._lv_log_requests.push(succeed);
t._lv_log_requests = [succeed];
f.lv_get_logs(room_id, user_id).then(function(data) {
var new_room_id = chat && chat.get('currentRoom.id'),
new_user_id = t.get('cardInfo.user.id');
if ( user_id !== new_user_id || room_id !== new_room_id )
return;
t._lv_logs = {
expires: Date.now() + 30000,
room: room_id,
user: user_id,
data: data
};
t._lv_sock_room = room_id;
t._lv_sock_user = user_id;
f.lv_ws_sub('logs-' + room_id + '-' + user_id);
var requests = t._lv_log_requests;
t._lv_log_requests = null;
for(var i=0; i < requests.length; i++)
requests[i](data);
});
});
},
lvOnMessage: function(cmd, data) {
//f.log("[LV] Socket Message: " + cmd, data)
if ( cmd === "comment-add" ) {
if ( data.topic !== this.get('cardInfo.user.id') )
return;
FFZ.mod_card_pages.notes.add_note.call(f, this, this.get('element'), data);
} else if ( cmd === "comment-update" ) {
var el = this.get('element'),
line = el && el.querySelector('.user-notes .chat-line[data-lv-id="' + data.id + '"]');
if ( ! line )
return;
var new_line = FFZ.mod_card_pages.notes.build_note.call(f, this, data);
line.outerHTML = new_line.outerHTML;
} else if ( cmd === "comment-delete" ) {
var el = this.get('element'),
line = el && el.querySelector('.user-notes .chat-line[data-lv-id="' + data.id + '"]');
if ( ! line )
return;
// If we're the only message on this date, remove the timestamp line.
var before_line = line.previousElementSibling,
after_line = line.nextElementSibling;
if ( before_line && before_line.classList.contains('timestamp-line') &&
(! after_line || after_line.classList.contains('timestamp-line')) )
before_line.parentElement.removeChild(before_line);
// Remove the line itself.
line.parentElement.removeChild(line);
} else if ( cmd === "log-update" ) {
if ( ! this._lv_logs || ! this._lv_logs.data || data.nick !== this._lv_logs.data.user.nick )
return;
// Parse the message. Store the data.
var message = f.lv_parse_message(data),
msgs = this._lv_logs.data.before,
ind = -1,
i = msgs.length;
// Find the existing entry.
while(--i) {
var msg = msgs[i];
if ( msg.lv_id === message.lv_id ) {
ind = i;
break;
}
}
// Nothing to update, so don't.
if ( ind === -1 )
return;
msgs[ind] = message;
var el = this.get('element'),
container = el && el.querySelector('.ffz-tab-container'),
line = container && container.querySelector('.lv-history .chat-line[data-lv-id="' + message.lv_id + '"]');
if ( ! line )
return;
var new_line = f._build_mod_card_history(message, this, false,
FFZ.mod_card_pages.history.render_adjacent.bind(f, this, container, message));
line.parentElement.insertBefore(new_line, line);
line.parentElement.removeChild(line);
} else if ( cmd === "log-add" ) {
if ( ! this._lv_logs || ! this._lv_logs.data || data.nick !== this._lv_logs.data.user.nick )
return;
// Parse the message. Store the data.
var message = f.lv_parse_message(data);
this._lv_logs.data.before.push(message);
if ( message.is_ban )
this._lv_logs.data.user.timeouts++;
else if ( ! message.is_admin && (! message.is_notice || message.message.indexOf('Message: ') !== -1) )
this._lv_logs.data.user.messages++;
// If we're viewing the chat history, update it.
var el = this.get('element'),
container = el && el.querySelector('.ffz-tab-container'),
history = container && container.querySelector('.ffz-tab-container[data-page="history"] .chat-history.lv-history');
if ( history ) {
var was_at_bottom = history.scrollTop >= (history.scrollHeight - history.clientHeight),
last_line = history.querySelector('.chat-line:last-of-type'),
ll_date = last_line && last_line.getAttribute('data-date'),
date = message.date && message.date.toLocaleDateString();
if ( last_line.classList.contains('no-messages') ) {
last_line.parentElement.removeChild(last_line);
last_line = null;
ll_date = null;
}
if ( last_line && ll_date !== date ) {
var date_line = utils.createElement('li', 'chat-line timestamp-line', date);
date_line.setAttribute('data-date', date);
history.appendChild(date_line);
}
history.appendChild(f._build_mod_card_history(message, this, false,
FFZ.mod_card_pages.history.render_adjacent.bind(f, this, container, message)));
if ( was_at_bottom )
setTimeout(function() { history.scrollTop = history.scrollHeight; })
}
}
},
lvUpdateLevels: function(levels) {
var channel = levels.channel,
user = levels.me,
level = user && user.level || 0;
this.lv_view = level >= channel.viewlogs;
this.lv_view_mod = level >= channel.viewmodlogs;
this.lv_view_notes = level >= channel.viewcomments;
this.lv_write_notes = level >= channel.writecomments;
this.lv_delete_notes = level >= channel.deletecomments;
var el = this.get('element');
if ( el ) {
el.classList.toggle('lv-notes', this.lv_view_notes);
el.classList.toggle('lv-logs', this.lv_view);
el.classList.toggle('lv-tabs', this.lv_view || this.lv_view_notes);
}
},
ffz_destroy: function() {
if ( f._mod_card === this )
f._mod_card = undefined;
if ( this._lv_sock_room && this._lv_sock_user ) {
f.lv_ws_unsub('logs-' + this._lv_sock_room + '-' + this._lv_sock_user);
this._lv_sock_room = null;
this._lv_sock_user = null;
}
if ( this._lv_callback ) {
f.lv_ws_remove_callback(this._lv_callback);
this._lv_callback = null;
}
utils.update_css(f._chat_style, 'mod-card-highlight');
},
ffzFixDefaultActions: function() {
var el = this.get('element'),
t = this,
line,
is_mod = t.get('cardInfo.isModeratorOrHigher'),
ban_reasons,
chat = utils.ember_lookup('controller:chat'),
user = f.get_user(),
room = chat && chat.get('currentRoom'),
room_id = room && room.get('id'),
ffz_room = f.rooms && f.rooms[room_id] || {},
is_broadcaster = user && room_id === user.login,
user_id = this.get('cardInfo.user.id'),
is_me = user && user.login === user_id,
alias = f.aliases[user_id],
handle_key,
ban_reason = function() {
return ban_reasons && ban_reasons.value ? ' ' + ban_reasons.value : "";
},
alias_btn = utils.createElement('button', 'alias button float-left button--icon-only html-tooltip');
alias_btn.innerHTML = '
');
return "Custom Command" + (lines.length > 1 ? 's' : '') +
"
" + title;
}
});
btn.addEventListener('click', add_btn_click.bind(this, cmd));
return btn;
};
for(var i=0; i < f.settings.mod_card_buttons.length; i++) {
var label, cmd, pair = f.settings.mod_card_buttons[i];
if ( ! Array.isArray(pair) ) {
cmd = pair;
label = cmd.split(' ', 1)[0];
} else {
label = pair[0];
cmd = pair[1];
}
utils.CMD_VAR_REGEX.lastIndex = 0;
if ( ! utils.CMD_VAR_REGEX.test(cmd) ) {
var lines = cmd.split(/\s*