1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00

3.5.313. Logviewer integration.

This commit is contained in:
SirStendec 2016-10-05 01:31:10 -04:00
parent d12277776e
commit 695a013f4a
12 changed files with 1222 additions and 94 deletions

View file

@ -1,3 +1,8 @@
<div class="list-header">3.5.313 <time datetime="2016-10-03">(2016-10-05)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Logviewer Integration (Beta)</li>
</ul>
<div class="list-header">3.5.312 <time datetime="2016-10-03">(2016-10-04)</time></div> <div class="list-header">3.5.312 <time datetime="2016-10-03">(2016-10-04)</time></div>
<ul class="chat-menu-content menu-side-padding"> <ul class="chat-menu-content menu-side-padding">
<li>Fixed: Typo causing chat lines to not render correctly if a message is deleted.</li> <li>Fixed: Typo causing chat lines to not render correctly if a message is deleted.</li>
@ -9,11 +14,6 @@
<li>Updated Fix: Message Doubling. Properly check a message's <code>hasSystemMsg</code> property as <code>systemMsg</code> will always contain a string now.</li> <li>Updated Fix: Message Doubling. Properly check a message's <code>hasSystemMsg</code> property as <code>systemMsg</code> will always contain a string now.</li>
</ul> </ul>
<div class="list-header">3.5.310 <time datetime="2016-10-03">(2016-10-04)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: Twitch broke message handling by deciding <em>every</em> message is a system message. I'll fix it properly later.</li>
</ul>
<div class="list-header">3.5.309 <time datetime="2016-10-03">(2016-10-03)</time></div> <div class="list-header">3.5.309 <time datetime="2016-10-03">(2016-10-03)</time></div>
<ul class="chat-menu-content menu-side-padding"> <ul class="chat-menu-content menu-side-padding">
<li>Fixed: Properly position the Theater Mode metadata bar when whispers are at the bottom of the screen.</li> <li>Fixed: Properly position the Theater Mode metadata bar when whispers are at the bottom of the screen.</li>

View file

@ -8,6 +8,7 @@
<div class="list-header">Awesome Resources</div> <div class="list-header">Awesome Resources</div>
<ul class="chat-menu-content menu-side-padding"> <ul class="chat-menu-content menu-side-padding">
<li><a href="https://cbenni.com/" target="_blank">CBenni's logviewer</a> - Chat moderation logs for Twitch</li>
<li><a href="https://letsencrypt.org/" target="_blank">Let's Encrypt</a> - Free and Automated SSL for Everyone</li> <li><a href="https://letsencrypt.org/" target="_blank">Let's Encrypt</a> - Free and Automated SSL for Everyone</li>
<li><a href="https://github.com/eligrey/FileSaver.js" target="_blank">FileSaver.js</a> - saveAs() implementation</li> <li><a href="https://github.com/eligrey/FileSaver.js" target="_blank">FileSaver.js</a> - saveAs() implementation</li>
<li><a href="http://objectpath.org/" target="_blank">ObjectPath</a> - Lightweight query language for JSON</li> <li><a href="http://objectpath.org/" target="_blank">ObjectPath</a> - Lightweight query language for JSON</li>

View file

@ -385,15 +385,9 @@ FFZ.prototype.get_twitch_badges = function(badge_tag) {
} }
// VoD Chat lines don't have the badges pre-parsed for some reason. // VoD Chat lines don't have the badges pre-parsed for some reason.
else if ( typeof badge_tag === 'string' ) { else if ( typeof badge_tag === 'string' )
var val = badge_tag.split(','); badge_tag = utils.parse_badge_tag(badge_tag);
badge_tag = {};
for(var i=0; i < val.length; i++) {
var parts = val[i].split('/');
if ( parts.length === 2 )
badge_tag[parts[0]] = parts[1];
}
}
for(var badge in badge_tag) { for(var badge in badge_tag) {
var version = badge_tag[badge]; var version = badge_tag[badge];

View file

@ -18,6 +18,8 @@ module.exports = FrankerFaceZ.constants = {
DEBUG: DEBUG, DEBUG: DEBUG,
SERVER: SERVER, SERVER: SERVER,
LV_SOCKET_SERVER: "wss://cbenni.com/socket.io/",
IS_OSX: IS_OSX, IS_OSX: IS_OSX,
IS_WIN: IS_WIN, IS_WIN: IS_WIN,
META_NAME: IS_OSX ? "⌘" : (IS_WIN ? "Win" : "Meta"), META_NAME: IS_OSX ? "⌘" : (IS_WIN ? "Win" : "Meta"),

View file

@ -887,9 +887,9 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) {
output += body; output += body;
var old_messages = this.get('msgObject.ffz_old_messages'); /*var old_messages = this.get('msgObject.ffz_old_messages');
if ( old_messages && old_messages.length ) if ( old_messages && old_messages.length )
output += '<div class="button primary float-right ffz-old-messages">Show ' + utils.number_commas(old_messages.length) + ' Old</div>'; output += '<div class="button primary float-right ffz-old-messages">Show ' + utils.number_commas(old_messages.length) + ' Old</div>';*/
return output + '</span>'; return output + '</span>';
}, },
@ -909,8 +909,12 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) {
el.innerHTML = output; el.innerHTML = output;
}, },
systemMsg: function() {
return this.get('msgObject.tags.system-msg')
}.property('msgObject.tags.system-msg'),
ffzShouldRenderMessageBody: function() { ffzShouldRenderMessageBody: function() {
return !this.get("hasSystemMsg") || this.get("hasMessageBody"); return ! this.get('hasSystemMsg') || this.get('hasMessageBody');
}.property('hasSystemMsg', 'hasMessageBody'), }.property('hasSystemMsg', 'hasMessageBody'),
hasSystemMsg: function() { hasSystemMsg: function() {
@ -1090,10 +1094,10 @@ FFZ.prototype._modify_chat_subline = function(component) {
var cl = e.target.classList, var cl = e.target.classList,
from = this.get("msgObject.from"); from = this.get("msgObject.from");
if ( cl.contains('ffz-old-messages') ) /*if ( cl.contains('ffz-old-messages') )
return f._show_deleted(this.get('msgObject.room')); return f._show_deleted(this.get('msgObject.room'));*/
else if ( cl.contains('deleted-word') ) { if ( cl.contains('deleted-word') ) {
jQuery(e.target).trigger('mouseout'); jQuery(e.target).trigger('mouseout');
e.target.outerHTML = e.target.getAttribute('data-text'); e.target.outerHTML = e.target.getAttribute('data-text');

View file

@ -13,7 +13,11 @@ var FFZ = window.FrankerFaceZ,
P: 80, P: 80,
B: 66, B: 66,
T: 84, T: 84,
U: 85 U: 85,
C: 67,
H: 72,
S: 83,
N: 78
}, },
MESSAGE = '<svg class="svg-messages" height="16px" version="1.1" viewBox="0 0 18 18" width="16px" x="0px" y="0px"><path clip-rule="evenodd" d="M1,15V3h16v12H1z M15.354,5.354l-0.707-0.707L9,10.293L3.354,4.646L2.646,5.354L6.293,9l-3.646,3.646l0.707,0.707L7,9.707l1.646,1.646h0.707L11,9.707l3.646,3.646l0.707-0.707L11.707,9L15.354,5.354z" fill-rule="evenodd"></path></svg>', MESSAGE = '<svg class="svg-messages" height="16px" version="1.1" viewBox="0 0 18 18" width="16px" x="0px" y="0px"><path clip-rule="evenodd" d="M1,15V3h16v12H1z M15.354,5.354l-0.707-0.707L9,10.293L3.354,4.646L2.646,5.354L6.293,9l-3.646,3.646l0.707,0.707L7,9.707l1.646,1.646h0.707L11,9.707l3.646,3.646l0.707-0.707L11.707,9L15.354,5.354z" fill-rule="evenodd"></path></svg>',
@ -89,6 +93,18 @@ FFZ.settings_info.highlight_messages_with_mod_card = {
}; };
FFZ.settings_info.logviewer_test = {
type: "boolean",
value: false,
no_bttv: true,
category: "Chat Moderation",
name: "Logviewer Integration <small>Beta</small>",
help: "Display information from CBenni's logviewer directly on moderation cards."
}
FFZ.settings_info.chat_mod_icon_visibility = { FFZ.settings_info.chat_mod_icon_visibility = {
type: "select", type: "select",
options: { options: {
@ -630,8 +646,19 @@ FFZ.prototype.modify_moderation_card = function(component) {
utils.ember_reopen_view(component, { utils.ember_reopen_view(component, {
ffzForceRedraw: function() { ffzForceRedraw: function() {
this.rerender(); this.rerender();
if ( f.settings.mod_card_history )
this.ffzRenderHistory(); 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');
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(this._lv_sock_room + '-' + this._lv_sock_user);
this._lv_sock_room = null;
this._lv_sock_user = null;
}
// Highlight this user's chat messages. // Highlight this user's chat messages.
if ( f.settings.highlight_messages_with_mod_card ) if ( f.settings.highlight_messages_with_mod_card )
@ -674,10 +701,124 @@ FFZ.prototype.modify_moderation_card = function(component) {
info.innerHTML = out; info.innerHTML = out;
}.observes("cardInfo.user.views"), }.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(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)
// Make sure:
// 1) It's a log-add command.
// 2) We have logs loaded already.
// 3) The loaded logs are for this user.
if ( cmd !== 'log-add' || ! this._lv_logs || ! this._lv_logs.data || data.nick !== this._lv_logs.data.user.nick )
return;
// Parse the message. Store the data.
var t,
message = f.lv_parse_message(data);
this._lv_logs.data.before.push(message);
this._lv_logs.data.user[message.is_ban ? 'timeouts' : 'messages'] += 1;
// 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('.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.toLocaleDateString();
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, t, 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');
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() { ffz_destroy: function() {
if ( f._mod_card === this ) if ( f._mod_card === this )
f._mod_card = undefined; f._mod_card = undefined;
if ( this._lv_sock_room && this._lv_sock_user ) {
f.lv_ws_unsub(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'); utils.update_css(f._chat_style, 'mod-card-highlight');
}, },
@ -687,6 +828,11 @@ FFZ.prototype.modify_moderation_card = function(component) {
f._mod_card = this; f._mod_card = this;
if ( f.settings.logviewer_test ) {
this._lv_callback = this.lvOnMessage.bind(this);
f.lv_ws_add_callback(this._lv_callback);
}
var el = this.get('element'), var el = this.get('element'),
controller = this.get('controller'), controller = this.get('controller'),
t = this, t = this,
@ -699,6 +845,7 @@ FFZ.prototype.modify_moderation_card = function(component) {
user = f.get_user(), user = f.get_user(),
room = chat && chat.get('currentRoom'), room = chat && chat.get('currentRoom'),
room_id = room && room.get('id'), room_id = room && room.get('id'),
ffz_room = f.rooms && f.rooms[room_id] || {},
is_broadcaster = user && room_id === user.login, is_broadcaster = user && room_id === user.login,
user_id = controller.get('cardInfo.user.id'), user_id = controller.get('cardInfo.user.id'),
@ -713,6 +860,23 @@ FFZ.prototype.modify_moderation_card = function(component) {
this.ffz_room_id = room_id; this.ffz_room_id = room_id;
// Should we be requesting a token? How about access levels?
if ( f.settings.logviewer_test && ffz_room.has_logs )
if ( ! ffz_room.logviewer_levels )
f.lv_get_token().then(function(token) {
if ( ! token )
return;
utils.logviewer.get("channel/" + room_id, token)
.then(utils.json).then(function(result) {
f.log("[LV] Channel Info: " + room_id, result);
ffz_room.logviewer_levels = result;
t.lvUpdateLevels(result);
});
})
else
t.lvUpdateLevels(ffz_room.logviewer_levels);
// Highlight this user's chat messages. // Highlight this user's chat messages.
if ( f.settings.highlight_messages_with_mod_card ) 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, user_id)); utils.update_css(f._chat_style, 'mod-card-highlight', styles['chat-user-bg'].replace(/{user_id}/g, user_id));
@ -848,6 +1012,18 @@ FFZ.prototype.modify_moderation_card = function(component) {
is_mod = controller.get('cardInfo.isModeratorOrHigher'), is_mod = controller.get('cardInfo.isModeratorOrHigher'),
room = utils.ember_lookup('controller:chat').get('currentRoom'); room = utils.ember_lookup('controller:chat').get('currentRoom');
if ( key === keycodes.C )
return t.ffzChangePage('default');
else if ( t.lv_view && key === keycodes.H )
return t.ffzChangePage('history');
else if ( t.lv_view && key === keycodes.S )
return t.ffzChangePage('stats');
else if ( t.lv_view_notes && key === keycodes.N )
return t.ffzChangePage('notes');
if ( is_mod && key == keycodes.P ) if ( is_mod && key == keycodes.P )
room.send("/timeout " + user_id + " 1" + ban_reason(), true); room.send("/timeout " + user_id + " 1" + ban_reason(), true);
@ -1057,9 +1233,42 @@ FFZ.prototype.modify_moderation_card = function(component) {
} }
// Tabbed Content
var tabs = utils.createElement('ul', 'interface menu clearfix'),
tab_container = utils.createElement('div', 'ffz-tab-container');
for(var page_id in FFZ.mod_card_pages) {
var page = FFZ.mod_card_pages[page_id];
if ( page && page.title && (page_id !== 'history' || f.settings.mod_card_history) ) {
var tab = utils.createElement('li', 'item', page.title);
if ( page_id === 'default' )
tab.classList.add('active');
tab.setAttribute('data-page', page_id);
tabs.appendChild(tab);
tab.addEventListener('click', function(e) {
t.ffzChangePage(this.getAttribute('data-page'), tabs, tab_container);
});
}
}
el.insertBefore(tab_container, el.querySelector('.interface'));
el.insertBefore(tabs, tab_container);
el.classList.add('ffz-default-tab');
// Message History // Message History
if ( f.settings.mod_card_history ) if ( f.settings.mod_card_history ) {
this.ffzRenderHistory(); var history = utils.createElement('ul', 'interface chat-history live-history');
el.appendChild(history);
var chat_history = ffz_room.user_history && ffz_room.user_history[user_id] || [];
for(var i=0; i < chat_history.length; i++)
history.appendChild(f._build_mod_card_history(chat_history[i], t, false));
setTimeout(function(){history.scrollTop = history.scrollHeight;});
}
// Reposition the menu if it's off-screen. // Reposition the menu if it's off-screen.
this.ffzReposition(); this.ffzReposition();
@ -1073,6 +1282,31 @@ FFZ.prototype.modify_moderation_card = function(component) {
el.focus(); el.focus();
}, },
ffzChangePage: function(page_id, tabs, tab_container) {
if ( ! tabs || ! tab_container ) {
var el = this.get('element');
tabs = el.querySelector('ul.menu');
tab_container = el.querySelector('.ffz-tab-container');
}
var active_page = tab_container.getAttribute('data-page');
if ( active_page === page_id )
return;
if ( page_id === null )
page_id = active_page;
jQuery('.item', tabs).removeClass('active');
jQuery('.item[data-page="' + page_id + '"]').addClass('active');
this.get('element').classList.toggle('ffz-default-tab', page_id === 'default');
tab_container.setAttribute('data-page', page_id);
tab_container.innerHTML = '';
FFZ.mod_card_pages[page_id].render.call(f, this, tab_container);
},
ffzReposition: function() { ffzReposition: function() {
var el = this.get('element'), var el = this.get('element'),
el_bound = el.getBoundingClientRect(), el_bound = el.getBoundingClientRect(),
@ -1097,7 +1331,7 @@ FFZ.prototype.modify_moderation_card = function(component) {
} }
}.observes('cardInfo.renderTop', 'cardInfo.renderLeft', 'cardInfo.renderRight', 'cardInfo.renderBottom'), }.observes('cardInfo.renderTop', 'cardInfo.renderLeft', 'cardInfo.renderRight', 'cardInfo.renderBottom'),
ffzRenderHistory: function() { /*ffzRenderHistory: function() {
var t = this, var t = this,
Chat = utils.ember_lookup('controller:chat'), Chat = utils.ember_lookup('controller:chat'),
room = Chat && Chat.get('currentRoom'), room = Chat && Chat.get('currentRoom'),
@ -1109,17 +1343,22 @@ FFZ.prototype.modify_moderation_card = function(component) {
user_history = ffz_room && ffz_room.user_history && ffz_room.user_history[user_id] || [], user_history = ffz_room && ffz_room.user_history && ffz_room.user_history[user_id] || [],
el = this.get('element'), el = this.get('element'),
history = el && el.querySelector('.chat-history'); history = el && el.querySelector('.ffz-tab-container');
if ( history && ! history.classList.contains('chat-history') ) {
history.parentElement.removeChild(history);
history = null;
}
if ( ! history ) { if ( ! history ) {
history = utils.createElement('ul', 'interface clearfix chat-history'); history = utils.createElement('ul', 'interface clearfix ffz-tab-container chat-history');
el.appendChild(history); el.appendChild(history);
} else { } else {
history.classList.remove('loading'); history.classList.remove('loading');
history.innerHTML = ''; history.innerHTML = '';
} }
if ( user_history.length < 50 ) { /*if ( user_history.length < 50 ) {
var before = (user_history.length > 0 && user_history[0].date ? user_history[0].date.getTime() : Date.now()) - (f._ws_server_offset || 0); var before = (user_history.length > 0 && user_history[0].date ? user_history[0].date.getTime() : Date.now()) - (f._ws_server_offset || 0);
f.ws_send("user_history", [room_id, user_id, 50 - user_history.length], function(success, data) { f.ws_send("user_history", [room_id, user_id, 50 - user_history.length], function(success, data) {
if ( ! success ) if ( ! success )
@ -1157,16 +1396,16 @@ FFZ.prototype.modify_moderation_card = function(component) {
if ( was_at_top ) if ( was_at_top )
setTimeout(function() { history.scrollTop = history.scrollHeight; }); setTimeout(function() { history.scrollTop = history.scrollHeight; });
}); });
} }*//*
for(var i=0; i < user_history.length; i++) for(var i=0; i < user_history.length; i++)
history.appendChild(f._build_mod_card_history(user_history[i], t)); history.appendChild(f._build_mod_card_history(user_history[i], t));
// Lazy scroll-to-bottom // Lazy scroll-to-bottom
history.scrollTop = history.scrollHeight; history.scrollTop = history.scrollHeight;
}, },*/
ffzAdjacentHistory: function(line) { /*ffzAdjacentHistory: function(line) {
var Chat = utils.ember_lookup('controller:chat'), var Chat = utils.ember_lookup('controller:chat'),
t = this, t = this,
@ -1267,12 +1506,12 @@ FFZ.prototype.modify_moderation_card = function(component) {
history.classList.remove('loading'); history.classList.remove('loading');
history.scrollTop = scroll_top; history.scrollTop = scroll_top;
} }
} }*/
}); });
} }
FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) { FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from, ts_click) {
var l_el = document.createElement('li'), var l_el = document.createElement('li'),
out = [], out = [],
f = this; f = this;
@ -1280,7 +1519,7 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) {
style = '', colored = ''; style = '', colored = '';
if ( helpers && helpers.getTime ) if ( helpers && helpers.getTime )
out.push('<span class="timestamp">' + helpers.getTime(msg.date) + '</span>'); out.push('<span class="timestamp' + (ts_click ? ' ts-action' : '') + '">' + helpers.getTime(msg.date) + '</span>');
if ( show_from ) { if ( show_from ) {
var alias = this.aliases[msg.from], var alias = this.aliases[msg.from],
@ -1315,7 +1554,7 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) {
(results[1] ? ' title="' + utils.quote_attr(results[1]) + '"' : '') + '>' (results[1] ? ' title="' + utils.quote_attr(results[1]) + '"' : '') + '>'
+ results[0] + '</span>'); + results[0] + '</span>');
out.push('<span class="colon">:</span> '); out.push(msg.style !== 'action' ? '<span class="colon">:</span> ' : ' ');
} }
@ -1356,6 +1595,8 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) {
l_el.setAttribute('data-room', msg.room); l_el.setAttribute('data-room', msg.room);
l_el.setAttribute('data-sender', msg.from); l_el.setAttribute('data-sender', msg.from);
l_el.setAttribute('data-id', msg.tags && msg.tags.id); l_el.setAttribute('data-id', msg.tags && msg.tags.id);
l_el.setAttribute('data-lv-id', msg.lv_id);
l_el.setAttribute('data-date', msg.date.toLocaleDateString());
l_el.setAttribute('data-deleted', msg.deleted || false); l_el.setAttribute('data-deleted', msg.deleted || false);
l_el.innerHTML = out.join(""); l_el.innerHTML = out.join("");
@ -1379,9 +1620,9 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) {
}); });
}); });
l_el.querySelector('.timestamp').addEventListener('click', function(e) { ts_click && l_el.querySelector('.timestamp').addEventListener('click', function(e) {
if ( e.button === 0 ) if ( e.button === 0 )
modcard.ffzAdjacentHistory(msg); return ts_click.call(this, e);
}); });
} }
@ -1482,4 +1723,329 @@ FFZ.chat_commands.u = function(room, args) {
} }
} }
FFZ.chat_commands.u.enabled = function() { return this.settings.short_commands; } FFZ.chat_commands.u.enabled = function() { return this.settings.short_commands; }
// ----------------
// Moderation Card Pages
// ----------------
FFZ.mod_card_pages = {};
FFZ.mod_card_pages.default = {
title: "<span>C</span>ontrols",
render: function(mod_card, el) { }
}
FFZ.mod_card_pages.history = {
title: "Chat <span>H</span>istory",
render_more: function(mod_card, el, history, ref_id, is_user, is_after) {
var f = this,
controller = utils.ember_lookup('controller:chat'),
user_id = mod_card.get('cardInfo.user.id'),
room_id = controller && controller.get('currentRoom.id'),
btn_more = utils.createElement('li', 'button ffz-load-more' + (is_after ? ' load-after' : ''), '<span class="ffz-chevron"></span> Load More <span class="ffz-chevron"></span>');
if ( is_after )
history.appendChild(btn_more);
else
history.insertBefore(btn_more, history.firstElementChild);
btn_more.addEventListener('click', function() {
history.scrollTop = 0;
history.classList.add('loading');
f.lv_get_logs(room_id, is_user ? user_id : null, ref_id, is_after ? 0 : 10, is_after ? 10 : 0).then(function(data) {
history.removeChild(btn_more);
history.classList.remove('loading');
var messages = is_after ? data.after : data.before,
last_message = history.querySelector('.chat-line:' + (is_after ? 'last' : 'first') + '-of-type'),
last_date = last_message ? last_message.getAttribute('data-date') : (new Date).toLocaleDateString();
if ( last_message.classList.contains('timestamp-line') )
last_message.parentElement.removeChild(last_message);
if ( ! is_after )
messages.reverse();
var original_message = history.querySelector('.original-msg'),
original_sender = original_message && original_message.getAttribute('data-sender');
for(var i=0; i < messages.length; i++) {
var new_message = messages[i],
date = new_message.date.toLocaleDateString(),
date_line = null;
new_message.original_sender = original_sender === new_message.from;
var new_line = f._build_mod_card_history(
new_message, mod_card, !is_user,
FFZ.mod_card_pages.history.render_adjacent.bind(f, mod_card, el, new_message)
);
if ( is_user )
new_line.classList.remove('ffz-mentioned');
new_message.original_sender = null;
if ( last_date !== date ) {
date_line = utils.createElement('li', 'chat-line timestamp-line', is_after ? date : last_date);
date_line.setAttribute('data-date', is_after ? date : last_date);
last_date = date;
if ( is_after )
history.appendChild(date_line);
else
history.insertBefore(date_line, history.firstElementChild);
}
if ( is_after )
history.appendChild(new_line);
else
history.insertBefore(new_line, history.firstElementChild);
}
if ( ! is_after && last_date !== (new Date).toLocaleDateString() ) {
var date_line = utils.createElement('li', 'chat-line timestamp-line', last_date);
date_line.setAttribute('data-date', last_date);
history.insertBefore(date_line, history.firstElementChild);
}
// Only add the button back if there are even more messages to load.
if ( messages.length >= 10 )
if ( is_after )
history.appendChild(btn_more);
else
history.insertBefore(btn_more, history.firstElementChild);
var original = history.querySelector('.chat-line[data-lv-id="' + ref_id + '"]');
if ( original )
setTimeout(function() {
history.scrollTop = (original.offsetTop - history.offsetTop) - (history.clientHeight - original.clientHeight) / 2;
});
ref_id = messages[messages.length-1].lv_id;
});
})
},
render_adjacent: function(mod_card, el, message) {
var f = this,
controller = utils.ember_lookup('controller:chat'),
user_id = mod_card.get('cardInfo.user.id'),
room_id = controller && controller.get('currentRoom.id'),
ffz_room = this.rooms[room_id],
old_history = el.querySelector('.chat-history'),
history = el.querySelector('.adjacent-history');
old_history.classList.add('hidden');
if ( history )
history.innerHTML = '';
else {
var btn_hide = utils.createElement('li', 'button ffz-back-button', '<span class="ffz-chevron"></span> Back'),
btn_container = utils.createElement('ul', 'interface chat-history chat-back-button', btn_hide);
btn_hide.addEventListener('click', function() {
el.removeChild(history);
el.removeChild(btn_container);
old_history.classList.remove('hidden');
})
history = utils.createElement('ul', 'interface chat-history adjacent-history');
el.appendChild(btn_container);
el.appendChild(history);
}
history.classList.add('loading');
f.lv_get_logs(room_id, null, message.lv_id, 10, 10).then(function(data) {
history.classList.remove('loading');
// Should we display more?
if ( data.before.length >= 10 )
FFZ.mod_card_pages.history.render_more.call(
f, mod_card, el, history, data.before[0].lv_id, false, false);
var last_date = (new Date).toLocaleDateString(),
messages = _.union(data.before, [message], data.after);
for(var i=0; i < messages.length; i++) {
var new_message = messages[i],
date = new_message.date.toLocaleDateString();
if ( date !== last_date ) {
var date_line = utils.createElement('li', 'chat-line timestamp-line', date);
date_line.setAttribute('data-date', date);
history.appendChild(date_line);
last_date = date;
}
new_message.is_original = new_message.lv_id === message.lv_id;
new_message.original_sender = new_message.from === message.from;
var msg_line = f._build_mod_card_history(new_message, mod_card, true,
FFZ.mod_card_pages.history.render_adjacent.bind(f, mod_card, el, new_message));
if ( new_message.is_original )
msg_line.classList.remove('ffz-mentioned');
history.appendChild(msg_line);
// These objects can be persistent, so clear these.
new_message.is_original = null;
new_message.original_sender = null;
}
if ( data.after.length >= 10 )
FFZ.mod_card_pages.history.render_more.call(
f, mod_card, el, history, data.after[data.after.length-1].lv_id, false, true);
setTimeout(function() {
var original = history.querySelector('.original-msg');
if ( original )
history.scrollTop = (original.offsetTop - history.offsetTop) - (history.clientHeight - original.clientHeight) / 2;
})
});
},
render: function(mod_card, el) {
var f = this,
controller = utils.ember_lookup('controller:chat'),
user_id = mod_card.get('cardInfo.user.id'),
room_id = controller && controller.get('currentRoom.id'),
ffz_room = this.rooms[room_id],
history = utils.createElement('ul', 'interface chat-history lv-history');
el.appendChild(history);
// Are we relying on LogViewer here?
if ( ! ffz_room.has_logs || ! mod_card.lv_view ) {
history.innerHTML = '<li class="chat-line admin"><span class="message">You do not have permission to view chat history in this channel.</span></li>';
return;
}
// Start loading!
history.classList.add('loading');
mod_card.lvGetLogs().then(function(data) {
f.log("[LV] Logs: " + user_id + " in " + room_id, data);
history.classList.remove('loading');
// Should we display more?
if ( data.before.length >= 10 )
FFZ.mod_card_pages.history.render_more.call(
f, mod_card, el, history, data.before[0].lv_id, true, false);
var last_date = (new Date).toLocaleDateString();
for(var i=0; i < data.before.length; i++) {
var message = data.before[i],
date = message.date.toLocaleDateString();
if ( date !== last_date ) {
var date_line = utils.createElement('li', 'chat-line timestamp-line', date);
date_line.setAttribute('data-date', date);
history.appendChild(date_line);
last_date = date;
}
var msg_line = f._build_mod_card_history(message, mod_card, false,
FFZ.mod_card_pages.history.render_adjacent.bind(f, mod_card, el, message));
msg_line.classList.remove('ffz-mentioned');
history.appendChild(msg_line);
}
history.scrollTop = history.scrollHeight;
});
}
}
FFZ.mod_card_pages.stats = {
title: "<span>S</span>tatistics",
render: function(mod_card, el) {
var f = this,
controller = utils.ember_lookup('controller:chat'),
room_id = controller && controller.get('currentRoom.id'),
user_id = mod_card.get('cardInfo.user.id'),
ffz_room = f.rooms && f.rooms[room_id];
var container = utils.createElement('ul', 'interface version-list');
el.appendChild(container);
if ( ffz_room.has_logs && mod_card.lv_view ) {
container.classList.add('loading');
mod_card.lvGetLogs().then(function(data) {
container.classList.remove('loading');
container.innerHTML = '<li>Messages <span>' + utils.number_commas(data.user.messages) + '</span></li><li>Timeouts <span> ' + utils.number_commas(data.user.timeouts) + '</span></li>';
});
var notice = utils.createElement('div', 'interface');
notice.innerHTML = 'Chat Log Source: <a target="_blank" href="https://cbenni.com/' + room_id + '?user=' + user_id + '">CBenni\'s logviewer</a>';
el.appendChild(notice);
}
}
}
FFZ.mod_card_pages.notes = {
title: "<span>N</span>otes",
render: function(mod_card, el) {
var f = this,
controller = utils.ember_lookup('controller:chat'),
room = controller && controller.get('currentRoom'),
tmiSession = room.tmiSession || (window.TMI && TMI._sessions && TMI._sessions[0]),
room_id = room && room.get('id'),
user_id = mod_card.get('cardInfo.user.id'),
ffz_room = this.rooms[room_id],
history = utils.createElement('ul', 'interface chat-history user-notes');
el.appendChild(history);
if ( ! ffz_room.has_logs || ! mod_card.lv_view_notes ) {
history.innerHTML = '<li class="chat-line admin"><span class="message">You do not have permission to view notes in this channel.</span></li>';
return;
}
history.classList.add('loading');
this.lv_get_token().then(function(token) {
utils.logviewer.get("comments/" + room_id + "?topic=" + user_id, token)
.then(utils.json).then(function(data) {
f.log("[LV] Comments: " + user_id + " in " + room_id, data);
history.classList.remove('loading');
if ( data.length )
for(var i=0; i < data.length; i++) {
var raw = data[i],
msg = {
date: new Date(raw.added * 1000),
from: raw.author,
room: raw.channel,
lv_id: raw.id,
message: raw.text,
tags: {},
color: tmiSession && raw.author ? tmiSession.getColor(raw.author) : "#755000"
};
f.tokenize_chat_line(msg, true, false);
history.appendChild(f._build_mod_card_history(msg, mod_card, true));
}
else
history.appendChild(utils.createElement('li', 'chat-line message-line admin',
'<span class="message">There are no notes on this user.</span>'));
history.appendChild(utils.createElement('li', 'chat-line message-line admin',
'<span clss="message">Create notes for this user at <a target="_blank" href="https://cbenni.com/' + room_id + '?user=' + user_id + '">CBenni\'s logviewer.</a></span>'));
});
});
}
}

View file

@ -827,6 +827,16 @@ FFZ.prototype.add_room = function(id, room) {
} }
} }
// Look up if the room has moderation logs.
var f = this;
this.ws_send("has_logs", id, function(success, response) {
if ( ! success )
return;
data.has_logs = response.has_logs;
data.log_source = response.log_source;
}, true);
// Is the room important? // Is the room important?
this.update_room_important(id); this.update_room_important(id);
@ -905,7 +915,7 @@ FFZ.prototype.remove_room = function(id) {
}*/ }*/
FFZ.prototype._show_deleted = function(room_id) { /*FFZ.prototype._show_deleted = function(room_id) {
var room = this.rooms[room_id]; var room = this.rooms[room_id];
if ( ! room || ! room.room ) if ( ! room || ! room.room )
return; return;
@ -1047,7 +1057,7 @@ FFZ.prototype._insert_history = function(room_id, data, from_server) {
} }
} }
} }
} }*/
// -------------------- // --------------------
@ -1084,8 +1094,15 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) {
data = data.room; 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. // Preserve the pointer to the Room instance.
if ( this.rooms[room_id] ) /*if ( this.rooms[room_id] )
data.room = this.rooms[room_id].room; data.room = this.rooms[room_id].room;
// Preserve everything else. // Preserve everything else.
@ -1096,26 +1113,26 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) {
data.needs_history = this.rooms[room_id] && this.rooms[room_id].needs_history || false; data.needs_history = this.rooms[room_id] && this.rooms[room_id].needs_history || false;
this.rooms[room_id] = data; this.rooms[room_id] = data;*/
if ( data.css || data.moderator_badge ) if ( model.css || model.moderator_badge )
utils.update_css(this._room_style, room_id, moderator_css(data) + (data.css||"")); utils.update_css(this._room_style, room_id, moderator_css(model) + (model.css || ""));
if ( ! this.emote_sets.hasOwnProperty(data.set) ) if ( ! this.emote_sets.hasOwnProperty(model.set) )
this.load_set(data.set, function(success, set) { this.load_set(model.set, function(success, set) {
if ( set.users.indexOf(room_id) === -1 ) if ( set.users.indexOf(room_id) === -1 )
set.users.push(room_id); set.users.push(room_id);
}); });
else if ( this.emote_sets[data.set].users.indexOf(room_id) === -1 ) else if ( this.emote_sets[model.set].users.indexOf(room_id) === -1 )
this.emote_sets[data.set].users.push(room_id); this.emote_sets[model.set].users.push(room_id);
this.update_ui_link(); this.update_ui_link();
if ( data.set ) if ( model.set )
this.rerender_feed_cards(data.set); this.rerender_feed_cards(model.set);
if ( callback ) if ( callback )
callback(true, data); callback(true, model);
} }
@ -1569,8 +1586,8 @@ FFZ.prototype._modify_room = function(room) {
t.set("messages", []); t.set("messages", []);
t.addMessage({ t.addMessage({
style: 'admin', style: 'admin',
message: i18n("Chat was cleared by a moderator"), message: i18n("Chat was cleared by a moderator")
ffz_old_messages: msgs //ffz_old_messages: msgs
}); });
} }
} }
@ -1962,11 +1979,12 @@ FFZ.prototype._modify_room = function(room) {
if ( f._mod_card && f._mod_card.ffz_room_id === msg.room && f._mod_card.get('cardInfo.user.id') === msg.from ) { if ( f._mod_card && f._mod_card.ffz_room_id === msg.room && f._mod_card.get('cardInfo.user.id') === msg.from ) {
var el = f._mod_card.get('element'), var el = f._mod_card.get('element'),
history = el && el.querySelector('.chat-history:not(.adjacent-history)'), history = el && el.querySelector('.chat-history.live-history');
was_at_top = history && history.scrollTop >= (history.scrollHeight - history.clientHeight);
if ( history ) { if ( history ) {
var el = f._build_mod_card_history(msg, f._mod_card); var was_at_top = history.scrollTop >= (history.scrollHeight - history.clientHeight),
el = f._build_mod_card_history(msg, f._mod_card);
if ( msg.tags && msg.tags.historical ) if ( msg.tags && msg.tags.historical )
history.insertBefore(el, history.firstElementChild); history.insertBefore(el, history.firstElementChild);
else else

View file

@ -34,7 +34,7 @@ FFZ.msg_commands = {};
// Version // Version
var VER = FFZ.version_info = { var VER = FFZ.version_info = {
major: 3, minor: 5, revision: 312, major: 3, minor: 5, revision: 313,
toString: function() { toString: function() {
return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || "");
} }
@ -202,6 +202,7 @@ require('./ext/emote_menu');
require('./featurefriday'); require('./featurefriday');
require('./ui/logviewer');
//require('./ui/chatpane'); //require('./ui/chatpane');
require('./ui/popups'); require('./ui/popups');
require('./ui/styles'); require('./ui/styles');

View file

@ -659,17 +659,8 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, del
//if ( helpers && helpers.tokenizeRichContent ) //if ( helpers && helpers.tokenizeRichContent )
// tokens = helpers.tokenizeRichContent(tokens, tags.content, delete_links); // tokens = helpers.tokenizeRichContent(tokens, tags.content, delete_links);
if ( helpers && helpers.linkifyMessage ) { if ( helpers && helpers.linkifyMessage )
var labels = msgObject.labels || [], tokens = helpers.linkifyMessage(tokens, delete_links && ! tags.mod);
mod_or_higher = labels.indexOf("owner") !== -1 ||
labels.indexOf("staff") !== -1 ||
labels.indexOf("admin") !== -1 ||
labels.indexOf("global_mod") !== -1 ||
labels.indexOf("mod") !== -1 ||
msgObject.style === 'admin';
tokens = helpers.linkifyMessage(tokens, delete_links && !mod_or_higher);
}
if ( user && user.login && helpers && helpers.mentionizeMessage ) { if ( user && user.login && helpers && helpers.mentionizeMessage ) {
@ -1317,7 +1308,7 @@ FFZ.prototype._deleted_link_click = function(e) {
// History Loading // History Loading
// --------------------- // ---------------------
FFZ.prototype.parse_history = function(history, purged, bad_ids, room_id, delete_links, tmiSession, per_line) { /*FFZ.prototype.parse_history = function(history, purged, bad_ids, room_id, delete_links, tmiSession, per_line) {
var i = history.length, was_cleared = false; var i = history.length, was_cleared = false;
purged = purged || {}; purged = purged || {};
bad_ids = bad_ids || {}; bad_ids = bad_ids || {};
@ -1396,4 +1387,4 @@ FFZ.prototype.parse_history = function(history, purged, bad_ids, room_id, delete
} }
return [history, purged, was_cleared]; return [history, purged, was_cleared];
} }*/

249
src/ui/logviewer.js Normal file
View file

@ -0,0 +1,249 @@
var FFZ = window.FrankerFaceZ,
utils = require('../utils'),
constants = require('../constants');
// ----------------
// Token Request
// ----------------
FFZ.prototype._lv_token_requests = null;
FFZ.prototype._lv_token = {"token": null, "expires": 0};
FFZ.prototype.lv_get_token = function() {
var f = this,
token = this._lv_token,
now = Date.now() / 1000;
return new Promise(function(succeed, fail) {
// Make sure the token will be valid for at least 5 more minutes.
if ( token.token && token.expires > (now + 300) )
return succeed(token.token);
// If we're already making a request, don't duplicate it.
if ( f._lv_token_requests )
return f._lv_token_requests.push([succeed, fail]);
// Nope, new request.
f._lv_token_requests = [[succeed, fail]];
f.ws_send("get_logviewer_token", undefined, function(succeeded, data) {
token = succeeded ? data : token = {"token": null, "expires": 0};
var requests = f._lv_token_requests;
f._lv_token = token;
f._lv_token_requests = null;
for(var i=0; i < requests.length; i++) {
requests[i][succeeded ? 0 : 1](token.token);
}
}, true);
});
}
// ----------------
// Log Requests
// ----------------
FFZ.prototype.lv_get_logs = function(room_id, user_id, ref_id, before_cnt, after_cnt) {
var f = this;
return new Promise(function(succeed, fail) {
f.lv_get_token().then(function(token) {
args = [];
user_id !== undefined && user_id !== null && args.push('nick=' + user_id);
ref_id !== undefined && ref_id !== null && args.push('id=' + ref_id);
before_cnt !== undefined && before_cnt !== null && args.push('before=' + before_cnt);
after_cnt !== undefined && after_cnt !== null && args.push('after=' + after_cnt)
utils.logviewer.get("logs/" + room_id + "?" + args.join('&'), token)
.then(utils.json).then(function(data) {
// Parse every message immediately.
var bound = f.lv_parse_message.bind(f);
data.before = _.map(data.before, bound);
data.after = _.map(data.after, bound);
succeed(data);
});
});
})
}
// ----------------
// Message Processing
// ----------------
FFZ.prototype.lv_parse_message = function(message) {
var parsed = utils.parse_irc_privmsg(message.text),
ffz_room = this.rooms && this.rooms[parsed.room],
room = ffz_room && ffz_room.room;
parsed.lv_id = message.id;
parsed.date = new Date(message.time * 1000);
// Check for ban notices. Those are identifiable via display-name.
parsed.is_ban = parsed.tags['display-name'] === 'jtv';
if ( parsed.is_ban )
parsed.style = 'admin';
if ( parsed.tags.color )
parsed.color = parsed.tags.color;
else {
var tmiSession = room && room.tmiSession || window.TMI && TMI._sessions && TMI._sessions[0];
parsed.color = tmiSession && parsed.from ? tmiSession.getColor(parsed.from) : "#755000";
}
if ( ! parsed.tags.hasOwnProperty('mod') ) {
var badges = parsed.tags.badges || {};
parsed.tags.mod = parsed.from === parsed.room || badges.hasOwnProperty('staff') || badges.hasOwnProperty('admin') || badges.hasOwnProperty('global_mod') || badges.hasOwnProperty('moderator');
}
this.tokenize_chat_line(parsed, true, room && room.get('roomProperties.hide_chat_links'));
return parsed;
}
// ---------------------
// WebSocket Connection
// ---------------------
FFZ.prototype.lv_ws_create = function() {
var f = this, ws,
server = constants.LV_SOCKET_SERVER + '?EIO=3&transport=websocket';
if ( this._lv_ws_recreate_timer ) {
clearTimeout(this._lv_ws_recreate_timer);
this._lv_ws_recreate_timer = null;
}
if ( this._lv_ws_connecting || this._lv_ws_open )
return;
this._lv_ws_topics = this._lv_ws_topics || [];
this._lv_ws_ping_timer = null;
this._lv_ws_connecting = true;
this.log('[LV] Using Socket Server: ' + server);
try {
ws = this._lv_ws_sock = new WebSocket(server);
} catch(err) {
this._lv_ws_sock = null;
return this.error('[LV] Error creating WebSocket', err);
}
ws.onopen = function(e) {
f._lv_ws_connecting = false;
f._lv_ws_open = true;
f.log('[LV] Socket connected.');
ws.send('2probe');
ws.send('5');
// Ping every 10 seconds just to be safe.
f._lv_ws_ping_timer = setInterval(function() {
ws.send('2');
}, 10000);
if ( f._lv_ws_topics.length )
for(var i=0; i < f._lv_ws_topics.length; i++)
ws.send('42' + JSON.stringify(["subscribe", f._lv_ws_topics[i]]));
else
f._lv_ws_close_timer = setTimeout(f.lv_ws_maybe_close.bind(f), 5000);
}
ws.onclose = function(e) {
var was_open = f._lv_ws_open;
f.log('[LV] Socket closed. (Code: ' + e.code + ', Reason: ' + e.reason + ')');
f._lv_ws_open = false;
f._lv_ws_connecting = false;
f._lv_ws_sock = null;
if ( f._lv_ws_ping_timer ) {
clearInterval(f._lv_ws_ping_timer);
f._lv_ws_ping_timer = null;
}
// Do we care about reconnecting? We only do if we have topics?
if ( ! f._lv_ws_topics || ! f._lv_ws_topics.length )
return;
f._lv_ws_recreate_timer = setTimeout(f.lv_ws_create.bind(f), 500 + Math.random() * 2000);
}
ws.onmessage = function(e) {
var message = e.data;
// We only care about the meaning of life.
if ( message.substr(0,2) !== '42' )
return;
var data = JSON.parse(message.substr(2));
if ( f._lv_ws_callbacks && f._lv_ws_callbacks.length )
for(var i=0; i < f._lv_ws_callbacks.length; i++)
f._lv_ws_callbacks[i](data[0], data[1]);
}
}
FFZ.prototype.lv_ws_add_callback = function(callback) {
this._lv_ws_callbacks = this._lv_ws_callbacks || [];
if ( this._lv_ws_callbacks.indexOf(callback) === -1 )
this._lv_ws_callbacks.push(callback);
}
FFZ.prototype.lv_ws_remove_callback = function(callback) {
this._lv_ws_callbacks = this._lv_ws_callbacks || [];
var ind = this._lv_ws_callbacks.indexOf(callback);
if ( ind !== -1 )
this._lv_ws_callbacks.splice(ind, 1);
}
FFZ.prototype.lv_ws_sub = function(topic) {
this._lv_ws_topics = this._lv_ws_topics || [];
if ( this._lv_ws_topics.indexOf(topic) !== -1 )
return;
this._lv_ws_topics.push(topic);
if ( this._lv_ws_close_timer ) {
clearTimeout(this._lv_ws_close_timer);
this._lv_ws_close_timer = null;
}
if ( this._lv_ws_open )
this._lv_ws_sock.send("42" + JSON.stringify(['subscribe', topic]));
else if ( ! this._lv_ws_connecting )
this.lv_ws_create();
}
FFZ.prototype.lv_ws_unsub = function(topic) {
this._lv_ws_topics = this._lv_ws_topics || [];
var ind = this._lv_ws_topics.indexOf(topic);
if ( ind === -1 )
return;
this._lv_ws_topics.splice(ind, 1);
if ( this._lv_ws_open ) {
this._lv_ws_sock.send("42" + JSON.stringify(['unsubscribe', topic]));
if ( this._lv_ws_topics.length === 0 )
this._lv_ws_close_timer = setTimeout(this.lv_ws_maybe_close.bind(this), 5000);
}
}
FFZ.prototype.lv_ws_maybe_close = function() {
if ( this._lv_ws_close_timer ) {
clearTimeout(this._lv_ws_close_timer);
this._lv_ws_close_timer = null;
}
if ( (! this._lv_ws_topics || ! this._lv_ws_topics.length) && this._lv_ws_open )
this._lv_ws_sock.close();
}

View file

@ -93,6 +93,162 @@ var createElement = function(tag, className, content) {
return new Date(unix); return new Date(unix);
}, },
/* IRC Processing */
irc_regex = /^(?:@([^ ]+) )?(?:[:](\S+) )?(\S+)(?: (?!:)(.+?))?(?: [:](.+))?$/,
tag_regex = /([^=;]+)=([^;]*)/g,
parse_badge_tag = function(tag) {
var badges = {},
values = tag.split(',');
for(var i=0; i < values.length; i++) {
var parts = values[i].split('/');
if ( parts.length === 2 )
badges[parts[0]] = parts[1];
}
return badges;
},
parse_emote_tag = function(tag) {
var emotes = {},
values = tag.split("/"),
i = values.length;
while(i--) {
var parts = values[i].split(":");
if ( parts.length !== 2 )
return {};
var emote_id = parts[0],
matches = emotes[emote_id] = [],
indices = parts[1].split(",");
for(var j=0, jl = indices.length; j < jl; j++) {
var pair = indices[j].split("-");
if ( pair.length !== 2 )
return {};
var start = parseInt(pair[0]),
end = parseInt(pair[1]);
matches.push([start,end]);
}
}
return emotes;
},
parse_tag = function(tag, value) {
switch (tag) {
case "badges":
return parse_badge_tag(value);
case "emotes":
return parse_emote_tag(value);
case "sent-ts":
case "sent-tmi-ts":
case "slow":
return +value;
case "subscriber":
case "mod":
case "turbo":
case "r9k":
case "subs-only":
case "historical":
return value === "1";
default:
// Try to unescape the value.
try {
return value;
} catch(err) {
return "";
}
}
},
parse_tags = function(raw_tags) {
var m, tags = {};
do {
m = tag_regex.exec(raw_tags);
if ( m )
tags[m[1]] = parse_tag(m[1], m[2]);
} while(m);
return tags;
},
parse_sender = function(prefix, tags) {
var ind = prefix.indexOf('!');
if ( ind !== -1 )
return prefix.substr(0, ind);
if ( prefix === "tmi.twitch.tv" && tags.login )
return tags.login;
return prefix;
},
parse_irc_message = function(message) {
var data = irc_regex.exec(message);
if ( ! data )
return null;
var m,
tags = {},
output = {
tags: tags,
prefix: data[2],
command: data[3],
params: data[4],
trailing: data[5]
};
if ( data[1] )
output.tags = parse_tags(data[1]);
return output;
},
parse_irc_privmsg = function(message) {
var parsed = parse_irc_message(message);
if ( parsed.command.toLowerCase() !== "privmsg" )
return null;
var params = (parsed.params || "").split(' '),
target = params.shift();
if ( target.charAt(0) !== '#' )
return null;
if ( parsed.trailing )
params.push(parsed.trailing);
var from = parse_sender(parsed.prefix),
message = params.join(' '),
style = '';
if ( from === 'jtv' )
style = 'admin';
else if ( from === 'twitchnotify' )
style = 'notification';
if ( message.substr(0,8) === '\u0001ACTION ' && message.charAt(message.length-1) === '\u0001' ) {
message = message.substr(8, message.length - 9);
style += (style ? ' ' : '') + 'action';
}
return {
tags: parsed.tags,
from: parse_sender(parsed.prefix),
room: target.substr(1),
message: message,
style: style
}
},
BADGE_REV = { BADGE_REV = {
'b': 'broadcaster', 'b': 'broadcaster',
's': 'staff', 's': 'staff',
@ -217,6 +373,16 @@ var createElement = function(tag, className, content) {
}, },
logviewer_call = function(method, url, token, info) {
info = info || {};
info['method'] = method;
if ( token )
url += (url.indexOf('?') === -1 ? '?' : '&') + 'token=' + token;
return fetch("https://cbenni.com/api/" + url, info);
},
// Dialogs // Dialogs
show_modal = function(contents, on_close, width) { show_modal = function(contents, on_close, width) {
var container = createElement('div', 'twitch_subwindow_container'), var container = createElement('div', 'twitch_subwindow_container'),
@ -354,6 +520,19 @@ module.exports = FFZ.utils = {
put: function(u,d,o,t) { return api_call('put', u,d,o,t); } put: function(u,d,o,t) { return api_call('put', u,d,o,t); }
}, },
logviewer: {
del: function(u,t,i) { return logviewer_call('del', u,t,i) },
get: function(u,t,i) { return logviewer_call('get', u,t,i) },
post: function(u,t,i) { return logviewer_call('post', u,t,i) },
put: function(u,t,i) { return logviewer_call('put', u,t,i) },
},
json: function(response) {
if ( ! response.ok )
return Promise.resolve(null);
return response.json();
},
find_parent: function(el, klass) { find_parent: function(el, klass) {
while (el && el.parentNode) { while (el && el.parentNode) {
@ -366,6 +545,13 @@ module.exports = FFZ.utils = {
}, },
parse_badge_tag: parse_badge_tag,
parse_emote_tag: parse_emote_tag,
parse_tags: parse_tags,
parse_irc_message: parse_irc_message,
parse_irc_privmsg: parse_irc_privmsg,
CMD_VAR_REGEX: CMD_VAR_REGEX, CMD_VAR_REGEX: CMD_VAR_REGEX,
replace_cmd_variables: function(command, user, room, message, args) { replace_cmd_variables: function(command, user, room, message, args) {

162
style.css
View file

@ -18,6 +18,10 @@ body > div.tipsy .tipsy-arrow { opacity: 0.8; }
cursor: pointer; cursor: pointer;
} }
.ffz-moderation-card:not(.lv-notes) ul.menu li[data-page="notes"],
.ffz-moderation-card:not(.lv-logs) ul.menu li[data-page="history"],
.ffz-moderation-card:not(.lv-logs) ul.menu li[data-page="stats"],
.ffz-moderation-card:not(.lv-tabs) ul.menu,
.ffz-hide-prime .drawer .warp__list.js-offers, .ffz-hide-prime .drawer .warp__list.js-offers,
.ffz-hide-prime-collapsed .drawer.closed .js-offers, .ffz-hide-prime-collapsed .drawer.closed .js-offers,
.ffz-hide-friends-collapsed .drawer.closed .friend-list, .ffz-hide-friends-collapsed .drawer.closed .friend-list,
@ -713,9 +717,12 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill:
.ffz-ui-sub-menu-page[data-page="about"], .ffz-ui-sub-menu-page[data-page="about"],
.ffz-ui-menu-page .chat-menu-content p { padding: 0 20px; } .ffz-ui-menu-page .chat-menu-content p { padding: 0 20px; }
.ffz-moderation-card .version-list span,
.ffz-ui-menu-page .version-list span { float: right } .ffz-ui-menu-page .version-list span { float: right }
.ffz-moderation-card .version-list li:not(:last-of-type),
.ffz-ui-menu-page .version-list li:not(:last-of-type) { .ffz-ui-menu-page .version-list li:not(:last-of-type) {
border-bottom: 1px dotted rgba(127,127,127,0.5); border-bottom: 1px dotted rgba(127,127,127,0.5);
} }
.ffz-ui-menu-page .version-list ul { .ffz-ui-menu-page .version-list ul {
@ -723,14 +730,14 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill:
} }
.ffz-ui-menu-page pre { .ffz-ui-menu-page pre {
box-shadow: none !important; box-shadow: none !important;
white-space: pre-wrap; white-space: pre-wrap;
} }
#ffz-old-news-button { #ffz-old-news-button {
text-align: center; text-align: center;
text-transform: none; text-transform: none;
padding-bottom: 15px; padding-bottom: 15px;
} }
#ffz-old-news { display: none } #ffz-old-news { display: none }
@ -809,7 +816,7 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill:
list-style-type: none; list-style-type: none;
border-top: 1px solid rgba(0,0,0,0.2); border-top: 1px solid rgba(0,0,0,0.2);
background-color: #eee; background-color: #eee;
margin: 0 1px 1px; margin: 0 1px 1px;
} }
.ffz-ui-popup ul.menu:not(.sub-menu) { .ffz-ui-popup ul.menu:not(.sub-menu) {
@ -824,12 +831,7 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill:
.ember-chat .chat-interface .ffz-ui-popup.emoticon-selector .emoticon-selector-box .emoticon-grid { background-color: transparent !important; } .ember-chat .chat-interface .ffz-ui-popup.emoticon-selector .emoticon-selector-box .emoticon-grid { background-color: transparent !important; }
.app-main.theatre .ffz-ui-popup ul.menu, .ffz-bttv-dark .ffz-ui-popup ul.menu {
.chat-container.dark .ffz-ui-popup ul.menu,
.chat-container.force-dark .ffz-ui-popup ul.menu,
.ember-chat-container.dark .ffz-ui-popup ul.menu,
.ember-chat-container.force-dark .ffz-ui-popup ul.menu,
body.ffz-bttv-dark .ffz-ui-popup ul.menu {
background-color: #282828; background-color: #282828;
} }
@ -845,8 +847,8 @@ body.ffz-bttv-dark .ffz-ui-popup ul.menu {
} }
.ffz-ui-popup ul.sub-menu { .ffz-ui-popup ul.sub-menu {
background-color: #dfdfdf; background-color: #dfdfdf;
margin: 0 1px; margin: 0 1px;
} }
.app-main.theatre .ffz-ui-popup ul.sub-menu, .app-main.theatre .ffz-ui-popup ul.sub-menu,
@ -1281,8 +1283,7 @@ img.channel_background[src="null"] { display: none; }
.ffz-moderation-card { .ffz-moderation-card {
border: 2px solid #cbcbcb; border: 2px solid #cbcbcb;
max-width: 350px; width: 340px;
min-width: 325px;
} }
.ember-chat .ffz-moderation-card .close-button { .ember-chat .ffz-moderation-card .close-button {
@ -1385,14 +1386,12 @@ img.channel_background[src="null"] { display: none; }
/*box-shadow: #000 0 0 5px;*/ /*box-shadow: #000 0 0 5px;*/
} }
.ffz-moderation-card ul.menu + .interface { border-top: none }
.ffz-moderation-card .interface:not(:last-of-type) { .ffz-moderation-card .interface:not(:last-of-type) {
border-bottom: none; border-bottom: none;
} }
.ffz-moderation-card .interface {
border-top: none;
}
.ffz-moderation-card h4.name { display: inline-block; } .ffz-moderation-card h4.name { display: inline-block; }
.ffz-moderation-card .info, .ffz-moderation-card .info,
@ -1410,6 +1409,60 @@ img.channel_background[src="null"] { display: none; }
width: 100%; width: 100%;
} }
.ffz-moderation-card ul.menu {
list-style-type: none;
padding: 0 !important;
border: none !important;
border-bottom: 1px solid rgba(0,0,0,0.2) !important;
}
.ffz-moderation-card ul.menu span { text-decoration: underline }
.ffz-dark .ffz-moderation-card ul.menu,
.theatre .ffz-moderation-card ul.menu,
.dark .ffz-moderation-card ul.menu,
.force-dark .ffz-moderation-card ul.menu {
border-bottom-color: rgba(255,255,255,0.2) !important;
}
.ffz-moderation-card ul.menu li:first-of-type { margin-left: 5px }
.ffz-moderation-card ul.menu li:last-of-type { margin-right: 5px }
.ffz-moderation-card ul.menu li {
display: block;
cursor: pointer;
float: left;
padding: 5px 5px 3px;
margin-bottom: -1px;
border-bottom: 2px solid transparent;
}
.ffz-moderation-card ul.menu li:hover {
color: #7d5bbe;
border-bottom-color: #7d5bbe;
}
.ffz-dark .ffz-moderation-card ul.menu li,
.theatre .ffz-moderation-card ul.menu li,
.dark .ffz-moderation-card ul.menu li,
.force-dark .ffz-moderation-card ul.menu li {
color: #999;
}
.ffz-dark .ffz-moderation-card ul.menu li:hover,
.theatre .ffz-moderation-card ul.menu li:hover,
.dark .ffz-moderation-card ul.menu li:hover,
.force-dark .ffz-moderation-card ul.menu li:hover {
color: #a68ed2;
border-bottom-color: #a68ed2;
}
.ffz-moderation-card ul.menu li.active { border-bottom-color: #6441a4 }
.ffz-moderation-card:not(.ffz-default-tab) > .interface:not(.menu) {
display: none
}
/* dark moderation card */ /* dark moderation card */
@ -1984,10 +2037,14 @@ body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textar
.chat-history { .chat-history {
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;
max-height: 200px; max-height: 320px;
overflow-y: auto; overflow-y: auto;
} }
.moderation-card .chat-history.live-history {
max-height: 200px;
}
.chat-history.interface li:first-child { padding-top: 10px; } .chat-history.interface li:first-child { padding-top: 10px; }
.chat-history.interface li:last-child { padding-bottom: 10px; } .chat-history.interface li:last-child { padding-bottom: 10px; }
@ -1998,6 +2055,12 @@ body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textar
list-style-position: unset; list-style-position: unset;
} }
.chat-history .timestamp-line {
text-align: center;
color: #8c8c8c;
font-size: 1.1rem;
}
.ember-chat .moderation-card .interface.chat-history .chat-line.action { .ember-chat .moderation-card .interface.chat-history .chat-line.action {
float: none; float: none;
margin-right: 0px; margin-right: 0px;
@ -2014,13 +2077,22 @@ body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textar
} }
.chat-history .chat-line:not(.original-sender) span.from:hover, .chat-history .chat-line:not(.original-sender) span.from:hover,
.chat-history .timestamp:hover { .chat-history .timestamp.ts-action:hover {
cursor: pointer; cursor: pointer;
text-decoration: underline; text-decoration: underline;
} }
.chat-history .chat-line.admin.original-msg .message { color: #000 }
.ffz-dark .chat-history .chat-line.admin.original-msg .message,
.theatre .chat-history .chat-line.admin.original-msg .message,
.dark .chat-history .chat-line.admin.original-msg .message,
.force-dark .chat-history .chat-line.admin.original-msg .message { color: #fff }
.chat-history.loading { .chat-history.loading {
position: relative; position: relative;
min-height: 160px;
overflow-y: hidden !important; overflow-y: hidden !important;
} }
.chat-history.loading li { pointer-events: none; } .chat-history.loading li { pointer-events: none; }
@ -2053,6 +2125,50 @@ body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textar
z-index: 1000; z-index: 1000;
} }
.moderation-card .ffz-back-button,
.moderation-card .ffz-load-more {
display: block;
text-align: center;
padding: 0 1rem !important;
margin: 10px;
}
.ffz-moderation-card .chat-back-button {
border-bottom: 1px solid rgba(0,0,0,0.2) !important;
}
.ffz-chevron {
position: relative;
display: inline-block;
height: 1rem;
width: 1rem;
margin: 0 .5rem;
}
.ffz-chevron:before {
position: absolute;
display: block;
content: '';
border: .5rem solid transparent;
}
.ffz-back-button .ffz-chevron {
margin-left: -1rem;
}
.ffz-back-button .ffz-chevron:before {
border-right-color: #fff;
}
.ffz-load-more:not(.load-after) .ffz-chevron:before {
top: -.25rem;
border-bottom-color: #fff;
}
.ffz-load-more.load-after .ffz-chevron:before {
top: .25rem;
border-top-color: #fff;
}
.ember-chat .moderation-card .back-button { .ember-chat .moderation-card .back-button {
border: 1px solid rgba(0,0,0,0.2); border: 1px solid rgba(0,0,0,0.2);
border-top: none; border-top: none;