You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
474 lines
11 KiB
474 lines
11 KiB
5 years ago
|
/* NOTE: This file is processed by server/state.js
|
||
|
and served by server/server.js (to auth'd users only) */
|
||
|
|
||
|
$('<link>', {rel: 'stylesheet', type: 'text/css', href: mediaURL+'css/mod.css'}).appendTo('head');
|
||
|
|
||
|
var $selectButton, $controls;
|
||
|
window.loggedInUser = IDENT.user;
|
||
|
window.x_csrf = IDENT.csrf;
|
||
|
|
||
|
function show_toolbox() {
|
||
|
var specs = [
|
||
|
{name: 'Lewd', kind: 7},
|
||
|
{name: 'Porn', kind: 8},
|
||
|
{name: 'Delete', kind: 9},
|
||
|
{name: 'Lock', kind: 11},
|
||
|
];
|
||
|
if (IDENT.auth == 'Admin')
|
||
|
specs.push({name: 'Panel', kind: 'panel'});
|
||
|
var $toolbox = $('<div/>', {id: 'toolbox', "class": 'mod'});
|
||
|
|
||
|
$selectButton = $('<input />', {
|
||
|
type: 'button', val: 'Select',
|
||
|
click: function (e) { toggle_multi_selecting(); },
|
||
|
});
|
||
|
$toolbox.append($selectButton, ' ');
|
||
|
|
||
|
$controls = $('<span></span>').hide();
|
||
|
_.each(specs, function (spec) {
|
||
|
$controls.append($('<input />', {
|
||
|
type: 'button',
|
||
|
val: spec.name,
|
||
|
data: {kind: spec.kind},
|
||
|
}), ' ');
|
||
|
});
|
||
|
$controls.on('click', 'input[type=button]', tool_action);
|
||
|
|
||
|
_.each(delayNames, function (when, i) {
|
||
|
var id = 'delay-' + when;
|
||
|
var $label = $('<label/>', {text: when, 'for': id});
|
||
|
var $radio = $('<input/>', {
|
||
|
id: id, type: 'radio', val: when, name: 'delay',
|
||
|
});
|
||
|
if (i == 0)
|
||
|
$radio.prop('checked', true);
|
||
|
$controls.append($radio, $label, ' ');
|
||
|
});
|
||
|
|
||
|
$toolbox.append($controls).insertBefore(THREAD ? 'hr:last' : $ceiling);
|
||
|
}
|
||
|
|
||
|
function tool_action(event) {
|
||
|
var ids = [];
|
||
|
var $sel = $('.selected');
|
||
|
$sel.each(function () {
|
||
|
var id = extract_num(parent_post($(this)));
|
||
|
if (id)
|
||
|
ids.push(id);
|
||
|
});
|
||
|
var $button = $(this);
|
||
|
var kind = $button.data('kind');
|
||
|
if (kind == 'panel')
|
||
|
return toggle_panel();
|
||
|
|
||
|
/* On a thread page there's only one thread to lock, so... */
|
||
|
if (kind == 11 && THREAD && !ids.length)
|
||
|
ids = [THREAD];
|
||
|
|
||
|
if (ids.length) {
|
||
|
var when = $('input:radio[name=delay]:checked').val();
|
||
|
ids.unshift(parseInt(kind, 10), {when: when});
|
||
|
send(ids);
|
||
|
$sel.removeClass('selected');
|
||
|
}
|
||
|
else {
|
||
|
var orig = $button.val();
|
||
|
var caption = _.bind($button.val, $button);
|
||
|
caption('Nope.');
|
||
|
if (orig != 'Nope.')
|
||
|
_.delay(caption, 2000, orig);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
readOnly.push('graveyard');
|
||
|
menuOptions.unshift('Select');
|
||
|
menuHandlers.Hide = function () { alert('nope.avi'); };
|
||
|
|
||
|
var multiSelecting = false;
|
||
|
|
||
|
function toggle_multi_selecting(model, $post) {
|
||
|
var oldTarget;
|
||
|
if ($post && model) {
|
||
|
oldTarget = lockTarget;
|
||
|
set_lock_target(model.id);
|
||
|
}
|
||
|
with_dom(function () {
|
||
|
multiSelecting = !multiSelecting;
|
||
|
if (multiSelecting) {
|
||
|
$('body').addClass('multi-select');
|
||
|
make_selection_handle().prependTo('article');
|
||
|
make_selection_handle().prependTo('section > header');
|
||
|
if ($post)
|
||
|
select_post($post);
|
||
|
$controls.show();
|
||
|
$selectButton.val('X');
|
||
|
}
|
||
|
else {
|
||
|
$('body').removeClass('multi-select');
|
||
|
$('.select-handle').remove();
|
||
|
$controls.hide();
|
||
|
$selectButton.val('Select');
|
||
|
}
|
||
|
});
|
||
|
if ($post)
|
||
|
set_lock_target(oldTarget);
|
||
|
}
|
||
|
|
||
|
menuHandlers.Select = toggle_multi_selecting;
|
||
|
|
||
|
function enable_multi_selecting() {
|
||
|
if (!multiSelecting)
|
||
|
toggle_multi_selecting();
|
||
|
}
|
||
|
|
||
|
function select_post($post) {
|
||
|
$post.find('.select-handle:first').addClass('selected');
|
||
|
}
|
||
|
|
||
|
function make_selection_handle() {
|
||
|
return $('<a class="select-handle" href="#"/>');
|
||
|
}
|
||
|
|
||
|
window.fun = function () {
|
||
|
send([33, THREAD]);
|
||
|
};
|
||
|
|
||
|
override(ComposerView.prototype, 'make_alloc_request',
|
||
|
function (orig, text, img) {
|
||
|
var msg = orig.call(this, text, img);
|
||
|
if ($('#authname').is(':checked'))
|
||
|
msg.auth = IDENT.auth;
|
||
|
return msg;
|
||
|
});
|
||
|
|
||
|
$DOC.on('click', '.select-handle', function (event) {
|
||
|
event.preventDefault();
|
||
|
$(event.target).toggleClass('selected');
|
||
|
});
|
||
|
|
||
|
with_dom(function () {
|
||
|
$('h1').text('Moderation - ' + $('h1').text());
|
||
|
var $authname = $('<input>', {type: 'checkbox', id: 'authname'});
|
||
|
var $label = $('<label/>', {text: ' '+IDENT.auth}).prepend($authname);
|
||
|
$name.after(' ', $label);
|
||
|
|
||
|
/* really ought to be done with model observation! */
|
||
|
$authname.change(function () {
|
||
|
if (postForm)
|
||
|
postForm.propagate_ident();
|
||
|
});
|
||
|
oneeSama.hook('fillMyName', function ($el) {
|
||
|
var auth = $('#authname').is(':checked');
|
||
|
$el.toggleClass(IDENT.auth.toLowerCase(), auth);
|
||
|
if (auth)
|
||
|
$el.append(' ## ' + IDENT.auth)
|
||
|
});
|
||
|
|
||
|
Backbone.on('afterInsert', function (model, $el) {
|
||
|
if (multiSelecting)
|
||
|
make_selection_handle().prependTo($el);
|
||
|
});
|
||
|
show_toolbox();
|
||
|
});
|
||
|
|
||
|
var Address = Backbone.Model.extend({
|
||
|
idAttribute: 'key',
|
||
|
defaults: {
|
||
|
count: 0,
|
||
|
},
|
||
|
});
|
||
|
|
||
|
var AddressView = Backbone.View.extend({
|
||
|
className: 'mod address',
|
||
|
events: {
|
||
|
'keydown .name': 'entered_name',
|
||
|
'click .sel-all': 'select_all',
|
||
|
'click .ban': 'ban',
|
||
|
},
|
||
|
|
||
|
initialize: function () {
|
||
|
var $el = this.$el;
|
||
|
$('<span/>', {"class": 'ip'}).appendTo($el);
|
||
|
$el.append(' ', $('<input/>', {
|
||
|
"class": 'sel-all',
|
||
|
type: 'button',
|
||
|
val: 'Sel All'
|
||
|
}));
|
||
|
if (IDENT.auth == 'Admin')
|
||
|
$el.append($('<input/>', {
|
||
|
"class": 'ban',
|
||
|
type: 'button',
|
||
|
val: 'Ban'
|
||
|
}));
|
||
|
|
||
|
$el.append(
|
||
|
'<br>',
|
||
|
$('<input>', {"class": 'name', placeholder: 'Name'})
|
||
|
);
|
||
|
this.listenTo(this.model, 'change', this.render);
|
||
|
},
|
||
|
|
||
|
render: function () {
|
||
|
var attrs = this.model.attributes;
|
||
|
if (attrs.shallow) {
|
||
|
this.$('.ip').text(attrs.ip ? attrs.ip + ' "\u2026"' : 'Loading...');
|
||
|
return this;
|
||
|
}
|
||
|
this.$('.ip').text(attrs.ip);
|
||
|
var $name = this.$('.name');
|
||
|
if (!this.focusedName) {
|
||
|
_.defer(function () {
|
||
|
$name.focus().prop({
|
||
|
selectionStart: 0,
|
||
|
selectionEnd: $name.val().length,
|
||
|
});
|
||
|
});
|
||
|
this.focusedName = true;
|
||
|
}
|
||
|
if (attrs.name != $name.val()) {
|
||
|
$name.val(attrs.name);
|
||
|
}
|
||
|
this.$('.ban')
|
||
|
.val(attrs.ban ? 'Unban' : 'Ban');
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
entered_name: function (event) {
|
||
|
if (event.which != 13)
|
||
|
return;
|
||
|
event.preventDefault();
|
||
|
var name = this.$('.name').val().trim();
|
||
|
var ip = this.model.get('ip');
|
||
|
send([SET_ADDRESS_NAME, ip, name]);
|
||
|
this.remove();
|
||
|
},
|
||
|
|
||
|
remove: function () {
|
||
|
this.trigger('preremove');
|
||
|
Backbone.View.prototype.remove.call(this);
|
||
|
},
|
||
|
|
||
|
select_all: function () {
|
||
|
if (!THREAD)
|
||
|
return alert('TODO');
|
||
|
// TODO: do where query by ip_key lookup
|
||
|
var models = Threads.get(THREAD).get('replies').where({
|
||
|
ip: this.model.get('ip'),
|
||
|
});
|
||
|
if (!models.length)
|
||
|
return;
|
||
|
enable_multi_selecting();
|
||
|
with_dom(function () {
|
||
|
$.each(models, function () {
|
||
|
select_post($('#' + this.id));
|
||
|
});
|
||
|
});
|
||
|
this.remove();
|
||
|
},
|
||
|
|
||
|
ban: function () {
|
||
|
var ip = this.model.get('ip');
|
||
|
var attrs = this.model.attributes;
|
||
|
var act, type;
|
||
|
if (!attrs.ban) {
|
||
|
act = 'Ban';
|
||
|
type = 'timeout';
|
||
|
} else {
|
||
|
act = 'Unban';
|
||
|
type = 'unban';
|
||
|
}
|
||
|
if (!confirm(act + ' ' + ip + '?'))
|
||
|
return;
|
||
|
|
||
|
send([BAN, ip, type]);
|
||
|
// show ... while processing
|
||
|
this.$('.ban').val('...');
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// basically just a link
|
||
|
var AddrView = Backbone.View.extend({
|
||
|
tagName: 'a',
|
||
|
className: 'mod addr',
|
||
|
|
||
|
events: {
|
||
|
click: 'toggle_expansion',
|
||
|
},
|
||
|
|
||
|
initialize: function () {
|
||
|
this.$el.attr('href', '#');
|
||
|
this.listenTo(this.model, 'change:name', this.render);
|
||
|
},
|
||
|
|
||
|
render: function () {
|
||
|
var attrs = this.model.attributes;
|
||
|
var text = ip_mnemonic(attrs.ip);
|
||
|
if (attrs.name)
|
||
|
text += ' "' + attrs.name + '"';
|
||
|
if (attrs.country)
|
||
|
text += ' ' + attrs.country;
|
||
|
this.$el.attr('title', attrs.ip).text(text);
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
toggle_expansion: function (event) {
|
||
|
if (event.target !== this.el)
|
||
|
return;
|
||
|
event.preventDefault();
|
||
|
|
||
|
if (this.expansion)
|
||
|
return this.expansion.remove();
|
||
|
var ip = this.model.get('ip');
|
||
|
if (!ip)
|
||
|
return;
|
||
|
|
||
|
this.expansion = new AddressView({model: this.model});
|
||
|
this.$el.after(this.expansion.render().el);
|
||
|
this.listenTo(this.expansion, 'preremove',
|
||
|
this.expansion_removed);
|
||
|
|
||
|
if (this.model.get('shallow'))
|
||
|
send([FETCH_ADDRESS, ip]);
|
||
|
},
|
||
|
|
||
|
remove: function () {
|
||
|
if (this.expansion)
|
||
|
this.expansion.remove();
|
||
|
Backbone.View.prototype.remove.call(this);
|
||
|
},
|
||
|
|
||
|
expansion_removed: function () {
|
||
|
this.expansion = null;
|
||
|
},
|
||
|
});
|
||
|
|
||
|
var Addresses = Backbone.Collection.extend({
|
||
|
model: Address,
|
||
|
comparator: function (a) { return ip_mnemonic(a.ip); },
|
||
|
});
|
||
|
|
||
|
window.addrs = new Addresses;
|
||
|
|
||
|
function hook_up_address(model, $post) {
|
||
|
var $a = $post.find('a.mod.addr:first');
|
||
|
if (!$a.length)
|
||
|
return;
|
||
|
var ip = $a.prop('title') || $a.text();
|
||
|
var givenName;
|
||
|
var m = $a.text().match(/^([\w'.:]+(?: [\w'.:]*)?) "(.+)"$/);
|
||
|
if (m) {
|
||
|
if (is_IPv4_ip(m[1]))
|
||
|
ip = m[1];
|
||
|
givenName = m[2];
|
||
|
}
|
||
|
if (!is_valid_ip(ip))
|
||
|
return;
|
||
|
|
||
|
/* Activate this address link */
|
||
|
var key = ip_key(ip);
|
||
|
var address = window.addrs.get(key);
|
||
|
if (!address) {
|
||
|
address = new Address({ip: ip, key: key});
|
||
|
address.set(givenName ? {name: givenName} : {shallow: true});
|
||
|
window.addrs.add(address);
|
||
|
}
|
||
|
var view = new AddrView({model: address, el: $a[0]});
|
||
|
if (address.get('name'))
|
||
|
view.render();
|
||
|
|
||
|
if (model && model.set && !model.has('ip'))
|
||
|
model.set('ip', ip);
|
||
|
}
|
||
|
Backbone.on('afterInsert', hook_up_address);
|
||
|
|
||
|
with_dom(function () {
|
||
|
$('section').each(function () {
|
||
|
var $section = $(this);
|
||
|
var thread = Threads.get(extract_num($section));
|
||
|
hook_up_address(thread, $section);
|
||
|
var replies = thread && thread.get('replies');
|
||
|
$section.find('article').each(function () {
|
||
|
var $post = $(this);
|
||
|
var model = replies && replies.get(extract_num($post));
|
||
|
hook_up_address(model, $post);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
if (/reported/.test(window.location.search))
|
||
|
enable_multi_selecting();
|
||
|
|
||
|
});
|
||
|
|
||
|
window.adminState = new Backbone.Model({
|
||
|
});
|
||
|
|
||
|
var PanelView = Backbone.View.extend({
|
||
|
id: 'panel',
|
||
|
|
||
|
initialize: function () {
|
||
|
this.listenTo(this.model, 'change:visible', this.renderVis);
|
||
|
this.listenTo(window.addrs, 'add change:count reset',
|
||
|
this.renderIPs);
|
||
|
this.listenTo(this.model, 'change:memoryUsage',
|
||
|
this.renderMemory);
|
||
|
this.listenTo(this.model, 'change:addrs change:bans',
|
||
|
this.renderCounts);
|
||
|
this.listenTo(this.model, 'change:uptime', this.renderUptime);
|
||
|
$('<div/>', {id: 'ips'}).appendTo(this.el);
|
||
|
$('<div/>', {id: 'mem'}).appendTo(this.el);
|
||
|
$('<div/>', {id: 'counts'}).appendTo(this.el);
|
||
|
$('<div/>', {id: 'uptime'}).appendTo(this.el);
|
||
|
},
|
||
|
|
||
|
renderVis: function (model, vis) {
|
||
|
this.$el.toggle(!!vis);
|
||
|
},
|
||
|
|
||
|
renderIPs: function () {
|
||
|
var $ips = this.$('#ips').empty();
|
||
|
window.addrs.forEach(function (addr) {
|
||
|
var n = addr.get('count');
|
||
|
if (!n)
|
||
|
return;
|
||
|
var el = new AddrView({model: addr}).render().el;
|
||
|
$ips.append(el, n>1 ? ' ('+n+')' : '', '<br>');
|
||
|
});
|
||
|
},
|
||
|
|
||
|
renderMemory: function (model, mem) {
|
||
|
function mb(n) {
|
||
|
return Math.round(n/1000000);
|
||
|
}
|
||
|
this.$('#mem').text(
|
||
|
mb(mem.heapUsed) + '/' + mb(mem.heapTotal) +
|
||
|
' MB heap used.'
|
||
|
);
|
||
|
},
|
||
|
|
||
|
renderCounts: function (model) {
|
||
|
var a = model.attributes;
|
||
|
this.$('#counts').text(pluralize(a.addrs, 'addr') + ', ' +
|
||
|
pluralize(a.bans, 'ban') + '.');
|
||
|
},
|
||
|
|
||
|
renderUptime: function (model, s) {
|
||
|
var m = Math.floor(s / 60) % 60;
|
||
|
var h = Math.floor(s / 3600) % 60;
|
||
|
var d = Math.floor(s / (3600*24));
|
||
|
h = h ? h+'h' : '';
|
||
|
d = d ? d+'d' : '';
|
||
|
this.$('#uptime').text('Up '+ d + h + m +'m.');
|
||
|
},
|
||
|
});
|
||
|
|
||
|
function toggle_panel() {
|
||
|
var show = !adminState.get('visible');
|
||
|
send([show ? 60 : 61, 'adminState']);
|
||
|
}
|
||
|
|
||
|
if (IDENT.auth == 'Admin') (function () {
|
||
|
var $panel = $('<div/>', {id: 'panel', "class": 'mod modal'}).hide();
|
||
|
var view = new PanelView({model: adminState, el: $panel[0]});
|
||
|
$panel.appendTo('body');
|
||
|
})();
|