mirror of https://github.com/calzoneman/sync.git
Merge pull request #614 from calzoneman/ip-session-age
Restrict chat messages from newer accounts/IPs
This commit is contained in:
commit
b34a8fce3c
|
@ -2,7 +2,7 @@
|
||||||
"author": "Calvin Montgomery",
|
"author": "Calvin Montgomery",
|
||||||
"name": "CyTube",
|
"name": "CyTube",
|
||||||
"description": "Online media synchronizer and chat",
|
"description": "Online media synchronizer and chat",
|
||||||
"version": "3.20.0",
|
"version": "3.21.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "http://github.com/calzoneman/sync"
|
"url": "http://github.com/calzoneman/sync"
|
||||||
},
|
},
|
||||||
|
|
|
@ -121,6 +121,27 @@ ChatModule.prototype.shadowMutedUsers = function () {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ChatModule.prototype.restrictNewAccount = function restrictNewAccount(user, data) {
|
||||||
|
if (user.account.effectiveRank < 2 && this.channel.modules.options) {
|
||||||
|
const firstSeen = user.getFirstSeenTime();
|
||||||
|
const opts = this.channel.modules.options;
|
||||||
|
if (firstSeen > Date.now() - opts.get("new_user_chat_delay")*1000) {
|
||||||
|
user.socket.emit("spamFiltered", {
|
||||||
|
reason: "NEW_USER_CHAT"
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} else if ((firstSeen > Date.now() - opts.get("new_user_chat_link_delay")*1000)
|
||||||
|
&& data.msg.match(LINK)) {
|
||||||
|
user.socket.emit("spamFiltered", {
|
||||||
|
reason: "NEW_USER_CHAT_LINK"
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
ChatModule.prototype.handleChatMsg = function (user, data) {
|
ChatModule.prototype.handleChatMsg = function (user, data) {
|
||||||
var self = this;
|
var self = this;
|
||||||
counters.add("chat:incoming");
|
counters.add("chat:incoming");
|
||||||
|
@ -131,6 +152,12 @@ ChatModule.prototype.handleChatMsg = function (user, data) {
|
||||||
|
|
||||||
// Limit to 240 characters
|
// Limit to 240 characters
|
||||||
data.msg = data.msg.substring(0, 240);
|
data.msg = data.msg.substring(0, 240);
|
||||||
|
|
||||||
|
// Restrict new accounts/IPs from chatting and posting links
|
||||||
|
if (this.restrictNewAccount(user, data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If channel doesn't permit them, strip ASCII control characters
|
// If channel doesn't permit them, strip ASCII control characters
|
||||||
if (!this.channel.modules.options ||
|
if (!this.channel.modules.options ||
|
||||||
!this.channel.modules.options.get("allow_ascii_control")) {
|
!this.channel.modules.options.get("allow_ascii_control")) {
|
||||||
|
@ -174,6 +201,11 @@ ChatModule.prototype.handlePm = function (user, data) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restrict new accounts/IPs from chatting and posting links
|
||||||
|
if (this.restrictNewAccount(user, data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.msg.match(Config.get("link-domain-blacklist-regex"))) {
|
if (data.msg.match(Config.get("link-domain-blacklist-regex"))) {
|
||||||
this.channel.logger.log(user.displayip + " (" + user.getName() + ") was kicked for " +
|
this.channel.logger.log(user.displayip + " (" + user.getName() + ") was kicked for " +
|
||||||
"blacklisted domain");
|
"blacklisted domain");
|
||||||
|
|
|
@ -25,7 +25,9 @@ function OptionsModule(channel) {
|
||||||
allow_dupes: false, // Allow duplicate videos on the playlist
|
allow_dupes: false, // Allow duplicate videos on the playlist
|
||||||
torbanned: false, // Block connections from Tor exit nodes
|
torbanned: false, // Block connections from Tor exit nodes
|
||||||
allow_ascii_control: false,// Allow ASCII control characters (\x00-\x1f)
|
allow_ascii_control: false,// Allow ASCII control characters (\x00-\x1f)
|
||||||
playlist_max_per_user: 0 // Maximum number of playlist items per user
|
playlist_max_per_user: 0, // Maximum number of playlist items per user
|
||||||
|
new_user_chat_delay: 10 * 60, // Minimum account/IP age to chat
|
||||||
|
new_user_chat_link_delay: 60 * 60 // Minimum account/IP age to post links
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,6 +273,20 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("new_user_chat_delay" in data) {
|
||||||
|
var delay = data.new_user_chat_delay;
|
||||||
|
if (!isNaN(delay) && delay >= 0) {
|
||||||
|
this.opts.new_user_chat_delay = delay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("new_user_chat_link_delay" in data) {
|
||||||
|
var delay = data.new_user_chat_link_delay;
|
||||||
|
if (!isNaN(delay) && delay >= 0) {
|
||||||
|
this.opts.new_user_chat_link_delay = delay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.channel.logger.log("[mod] " + user.getName() + " updated channel options");
|
this.channel.logger.log("[mod] " + user.getName() + " updated channel options");
|
||||||
this.sendOpts(this.channel.users);
|
this.sendOpts(this.channel.users);
|
||||||
};
|
};
|
||||||
|
|
|
@ -207,7 +207,7 @@ module.exports = {
|
||||||
the hashes match.
|
the hashes match.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
db.query("SELECT name,password,global_rank FROM `users` WHERE name=?",
|
db.query("SELECT name,password,global_rank,time FROM `users` WHERE name=?",
|
||||||
[name],
|
[name],
|
||||||
function (err, rows) {
|
function (err, rows) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -15,6 +15,7 @@ var crypto = require("crypto");
|
||||||
var isTorExit = require("../tor").isTorExit;
|
var isTorExit = require("../tor").isTorExit;
|
||||||
var session = require("../session");
|
var session = require("../session");
|
||||||
import counters from '../counters';
|
import counters from '../counters';
|
||||||
|
import { verifyIPSessionCookie } from '../web/middleware/ipsessioncookie';
|
||||||
|
|
||||||
var CONNECT_RATE = {
|
var CONNECT_RATE = {
|
||||||
burst: 5,
|
burst: 5,
|
||||||
|
@ -25,33 +26,54 @@ var ipThrottle = {};
|
||||||
// Keep track of number of connections per IP
|
// Keep track of number of connections per IP
|
||||||
var ipCount = {};
|
var ipCount = {};
|
||||||
|
|
||||||
|
function parseCookies(socket, accept) {
|
||||||
|
var req = socket.request;
|
||||||
|
if (req.headers.cookie) {
|
||||||
|
cookieParser(req, null, () => {
|
||||||
|
accept(null, true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
req.cookies = {};
|
||||||
|
req.signedCookies = {};
|
||||||
|
accept(null, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Called before an incoming socket.io connection is accepted.
|
* Called before an incoming socket.io connection is accepted.
|
||||||
*/
|
*/
|
||||||
function handleAuth(socket, accept) {
|
function handleAuth(socket, accept) {
|
||||||
var data = socket.request;
|
|
||||||
|
|
||||||
socket.user = false;
|
socket.user = false;
|
||||||
if (data.headers.cookie) {
|
var auth = socket.request.signedCookies.auth;
|
||||||
cookieParser(data, null, function () {
|
if (!auth) {
|
||||||
var auth = data.signedCookies.auth;
|
return accept(null, true);
|
||||||
if (!auth) {
|
|
||||||
return accept(null, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
session.verifySession(auth, function (err, user) {
|
|
||||||
if (!err) {
|
|
||||||
socket.user = {
|
|
||||||
name: user.name,
|
|
||||||
global_rank: user.global_rank
|
|
||||||
};
|
|
||||||
}
|
|
||||||
accept(null, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
accept(null, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.verifySession(auth, function (err, user) {
|
||||||
|
if (!err) {
|
||||||
|
socket.user = {
|
||||||
|
name: user.name,
|
||||||
|
global_rank: user.global_rank,
|
||||||
|
registrationTime: new Date(user.time)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
accept(null, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleIPSessionCookie(socket, accept) {
|
||||||
|
var cookie = socket.request.signedCookies['ip-session'];
|
||||||
|
if (!cookie) {
|
||||||
|
socket.ipSessionFirstSeen = new Date();
|
||||||
|
return accept(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessionMatch = verifyIPSessionCookie(socket._realip, cookie);
|
||||||
|
if (sessionMatch) {
|
||||||
|
socket.ipSessionFirstSeen = sessionMatch.date;
|
||||||
|
} else {
|
||||||
|
socket.ipSessionFirstSeen = new Date();
|
||||||
|
}
|
||||||
|
accept(null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function throttleIP(sock) {
|
function throttleIP(sock) {
|
||||||
|
@ -214,6 +236,7 @@ function handleConnection(sock) {
|
||||||
user.setFlag(Flags.U_REGISTERED);
|
user.setFlag(Flags.U_REGISTERED);
|
||||||
user.clearFlag(Flags.U_READY);
|
user.clearFlag(Flags.U_READY);
|
||||||
user.account.name = sock.user.name;
|
user.account.name = sock.user.name;
|
||||||
|
user.registrationTime = sock.user.registrationTime;
|
||||||
user.refreshAccount(function (err, account) {
|
user.refreshAccount(function (err, account) {
|
||||||
if (err) {
|
if (err) {
|
||||||
user.clearFlag(Flags.U_REGISTERED);
|
user.clearFlag(Flags.U_REGISTERED);
|
||||||
|
@ -247,8 +270,10 @@ module.exports = {
|
||||||
};
|
};
|
||||||
var io = sio.instance = sio();
|
var io = sio.instance = sio();
|
||||||
|
|
||||||
io.use(handleAuth);
|
|
||||||
io.use(ipForwardingMiddleware(webConfig));
|
io.use(ipForwardingMiddleware(webConfig));
|
||||||
|
io.use(parseCookies);
|
||||||
|
io.use(handleIPSessionCookie);
|
||||||
|
io.use(handleAuth);
|
||||||
io.on("connection", handleConnection);
|
io.on("connection", handleConnection);
|
||||||
|
|
||||||
Config.get("listen").forEach(function (bind) {
|
Config.get("listen").forEach(function (bind) {
|
||||||
|
|
15
src/user.js
15
src/user.js
|
@ -288,6 +288,7 @@ User.prototype.login = function (name, pw) {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.account.name = user.name;
|
self.account.name = user.name;
|
||||||
|
self.registrationTime = new Date(user.time);
|
||||||
self.setFlag(Flags.U_REGISTERED);
|
self.setFlag(Flags.U_REGISTERED);
|
||||||
self.refreshAccount(function (err, account) {
|
self.refreshAccount(function (err, account) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -449,4 +450,18 @@ User.prototype.refreshAccount = function (cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
User.prototype.getFirstSeenTime = function getFirstSeenTime() {
|
||||||
|
if (this.registrationTime && this.socket.ipSessionFirstSeen) {
|
||||||
|
return Math.min(this.registrationTime.getTime(), this.socket.ipSessionFirstSeen.getTime());
|
||||||
|
} else if (this.registrationTime) {
|
||||||
|
return this.registrationTime.getTime();
|
||||||
|
} else if (this.socket.ipSessionFirstSeen) {
|
||||||
|
return this.socket.ipSessionFirstSeen.getTime();
|
||||||
|
} else {
|
||||||
|
Logger.errlog.log(`User "${this.getName()}" (IP: ${this.realip}) has neither ` +
|
||||||
|
"an IP sesion first seen time nor a registered account.");
|
||||||
|
return Date.now();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = User;
|
module.exports = User;
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
const STATE_FOLDER_PATH = path.resolve(__dirname, '..', '..', '..', 'state');
|
||||||
|
const SALT_PATH = path.resolve(__dirname, '..', '..', '..', 'state', 'ipsessionsalt.json');
|
||||||
|
|
||||||
|
const NO_EXPIRATION = new Date('Fri, 31 Dec 9999 23:59:59 GMT');
|
||||||
|
var SALT;
|
||||||
|
try {
|
||||||
|
SALT = require(SALT_PATH);
|
||||||
|
} catch (error) {
|
||||||
|
SALT = crypto.randomBytes(32).toString('base64');
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(STATE_FOLDER_PATH);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 'EEXIST') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync(SALT_PATH, JSON.stringify(SALT));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256(input) {
|
||||||
|
var hash = crypto.createHash("sha256");
|
||||||
|
hash.update(input);
|
||||||
|
return hash.digest("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createIPSessionCookie(ip, date) {
|
||||||
|
const hashInput = [
|
||||||
|
ip,
|
||||||
|
date.getTime(),
|
||||||
|
SALT
|
||||||
|
].join(':');
|
||||||
|
|
||||||
|
return [
|
||||||
|
date.getTime(),
|
||||||
|
sha256(hashInput)
|
||||||
|
].join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyIPSessionCookie(ip, cookie) {
|
||||||
|
const parts = cookie.split(':');
|
||||||
|
if (parts.length !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = parseInt(parts[0], 10);
|
||||||
|
if (isNaN(timestamp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const expected = createIPSessionCookie(ip, date);
|
||||||
|
if (expected !== cookie) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: date,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ipSessionCookieMiddleware(req, res, next) {
|
||||||
|
var firstSeen = new Date();
|
||||||
|
var hasSession = false;
|
||||||
|
if (req.signedCookies && req.signedCookies['ip-session']) {
|
||||||
|
var sessionMatch = verifyIPSessionCookie(req.realIP, req.signedCookies['ip-session']);
|
||||||
|
if (sessionMatch) {
|
||||||
|
hasSession = true;
|
||||||
|
firstSeen = sessionMatch.date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSession) {
|
||||||
|
res.cookie('ip-session', createIPSessionCookie(req.realIP, firstSeen), {
|
||||||
|
signed: true,
|
||||||
|
httpOnly: true,
|
||||||
|
expires: NO_EXPIRATION
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.ipSessionFirstSeen = firstSeen;
|
||||||
|
next();
|
||||||
|
}
|
|
@ -146,6 +146,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
app.use(cookieParser(webConfig.getCookieSecret()));
|
app.use(cookieParser(webConfig.getCookieSecret()));
|
||||||
app.use(csrf.init(webConfig.getCookieDomain()));
|
app.use(csrf.init(webConfig.getCookieDomain()));
|
||||||
|
app.use('/r/:channel', require('./middleware/ipsessioncookie').ipSessionCookieMiddleware);
|
||||||
initializeLog(app);
|
initializeLog(app);
|
||||||
require('./middleware/authorize')(app, session);
|
require('./middleware/authorize')(app, session);
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,15 @@ mixin textbox-auto(id, label, placeholder)
|
||||||
else
|
else
|
||||||
input.form-control.cs-textbox(id=id, type="text")
|
input.form-control.cs-textbox(id=id, type="text")
|
||||||
|
|
||||||
|
mixin textbox-timeinput-auto(id, label, placeholder)
|
||||||
|
.form-group
|
||||||
|
label.control-label.col-sm-4(for=id)= label
|
||||||
|
.col-sm-8
|
||||||
|
if placeholder
|
||||||
|
input.form-control.cs-textbox-timeinput(id=id, type="text", placeholder=placeholder)
|
||||||
|
else
|
||||||
|
input.form-control.cs-textbox-timeinput(id=id, type="text")
|
||||||
|
|
||||||
mixin miscoptions
|
mixin miscoptions
|
||||||
#cs-miscoptions.tab-pane.active
|
#cs-miscoptions.tab-pane.active
|
||||||
h4 General Settings
|
h4 General Settings
|
||||||
|
@ -63,6 +72,11 @@ mixin miscoptions
|
||||||
+rcheckbox-auto("cs-chat_antiflood", "Throttle chat")
|
+rcheckbox-auto("cs-chat_antiflood", "Throttle chat")
|
||||||
+textbox-auto("cs-chat_antiflood_burst", "# of messages allowed before throttling")
|
+textbox-auto("cs-chat_antiflood_burst", "# of messages allowed before throttling")
|
||||||
+textbox-auto("cs-chat_antiflood_sustained", "# of messages (after burst) allowed per second")
|
+textbox-auto("cs-chat_antiflood_sustained", "# of messages (after burst) allowed per second")
|
||||||
|
+textbox-timeinput-auto("cs-new_user_chat_delay", "Delay before new accounts can chat", "0")
|
||||||
|
.form-group
|
||||||
|
.col-sm-8.col-sm-offset-4
|
||||||
|
span.text-info Restrictions to new accounts can be disabled by setting the delay to 0.
|
||||||
|
+textbox-timeinput-auto("cs-new_user_chat_link_delay", "Delay before new accounts can post links in chat", "0")
|
||||||
.form-group
|
.form-group
|
||||||
.col-sm-8.col-sm-offset-4
|
.col-sm-8.col-sm-offset-4
|
||||||
span.text-info Changes are automatically saved.
|
span.text-info Changes are automatically saved.
|
||||||
|
|
|
@ -88,6 +88,22 @@ Callbacks = {
|
||||||
scrollChat();
|
scrollChat();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
spamFiltered: function(data) {
|
||||||
|
var message = "Spam Filtered.";
|
||||||
|
switch (data.reason) {
|
||||||
|
case "NEW_USER_CHAT":
|
||||||
|
message = "Your account is too new to chat in this channel. " +
|
||||||
|
"Please wait a while and try again.";
|
||||||
|
break;
|
||||||
|
case "NEW_USER_CHAT_LINK":
|
||||||
|
message = "Your account is too new to post links in this channel. " +
|
||||||
|
"Please wait a while and try again.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
errDialog(message);
|
||||||
|
},
|
||||||
|
|
||||||
needPassword: function (wrongpw) {
|
needPassword: function (wrongpw) {
|
||||||
var div = $("<div/>");
|
var div = $("<div/>");
|
||||||
$("<strong/>").text("Channel Password")
|
$("<strong/>").text("Channel Password")
|
||||||
|
|
30
www/js/ui.js
30
www/js/ui.js
|
@ -636,6 +636,36 @@ $(".cs-textbox").keyup(function () {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".cs-textbox-timeinput").keyup(function (event) {
|
||||||
|
var box = $(this);
|
||||||
|
var key = box.attr("id").replace("cs-", "");
|
||||||
|
var value = box.val();
|
||||||
|
var lastkey = Date.now();
|
||||||
|
box.data("lastkey", lastkey);
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
if (box.data("lastkey") !== lastkey || box.val() !== value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#cs-textbox-timeinput-validation-error-" + key).remove();
|
||||||
|
$(event.target).parent().removeClass("has-error");
|
||||||
|
var data = {};
|
||||||
|
try {
|
||||||
|
data[key] = parseTimeout(value);
|
||||||
|
} catch (error) {
|
||||||
|
var msg = "Invalid timespan value '" + value + "'. Please use the format " +
|
||||||
|
"HH:MM:SS or enter a single number for the number of seconds.";
|
||||||
|
var validationError = $("<p/>").addClass("text-danger").text(msg)
|
||||||
|
.attr("id", "cs-textbox-timeinput-validation-error-" + key);
|
||||||
|
validationError.insertAfter(event.target);
|
||||||
|
$(event.target).parent().addClass("has-error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.emit("setOptions", data);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
$("#cs-chanlog-refresh").click(function () {
|
$("#cs-chanlog-refresh").click(function () {
|
||||||
socket.emit("readChanLog");
|
socket.emit("readChanLog");
|
||||||
});
|
});
|
||||||
|
|
|
@ -761,18 +761,23 @@ function applyOpts() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPollMenu() {
|
function parseTimeout(t) {
|
||||||
function parseTimeout(t) {
|
var m;
|
||||||
var m;
|
if (m = t.match(/^(\d+):(\d+):(\d+)$/)) {
|
||||||
if (m = t.match(/^(\d+):(\d+)$/)) {
|
// HH:MM:SS
|
||||||
return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
|
return parseInt(m[1], 10) * 3600 + parseInt(m[2], 10) * 60 + parseInt(m[3], 10);
|
||||||
} else if (m = t.match(/^(\d+)$/)) {
|
} else if (m = t.match(/^(\d+):(\d+)$/)) {
|
||||||
return parseInt(m[1], 10);
|
// MM:SS
|
||||||
} else {
|
return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
|
||||||
throw new Error("Invalid timeout value '" + t + "'");
|
} else if (m = t.match(/^(\d+)$/)) {
|
||||||
}
|
// Seconds
|
||||||
|
return parseInt(m[1], 10);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid timeout value '" + t + "'");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPollMenu() {
|
||||||
$("#pollwrap .poll-menu").remove();
|
$("#pollwrap .poll-menu").remove();
|
||||||
var menu = $("<div/>").addClass("well poll-menu")
|
var menu = $("<div/>").addClass("well poll-menu")
|
||||||
.prependTo($("#pollwrap"));
|
.prependTo($("#pollwrap"));
|
||||||
|
@ -932,6 +937,8 @@ function handleModPermissions() {
|
||||||
$("#cs-torbanned").prop("checked", CHANNEL.opts.torbanned);
|
$("#cs-torbanned").prop("checked", CHANNEL.opts.torbanned);
|
||||||
$("#cs-allow_ascii_control").prop("checked", CHANNEL.opts.allow_ascii_control);
|
$("#cs-allow_ascii_control").prop("checked", CHANNEL.opts.allow_ascii_control);
|
||||||
$("#cs-playlist_max_per_user").val(CHANNEL.opts.playlist_max_per_user || 0);
|
$("#cs-playlist_max_per_user").val(CHANNEL.opts.playlist_max_per_user || 0);
|
||||||
|
$("#cs-new_user_chat_delay").val(formatTime(CHANNEL.opts.new_user_chat_delay || 0));
|
||||||
|
$("#cs-new_user_chat_link_delay").val(formatTime(CHANNEL.opts.new_user_chat_link_delay || 0));
|
||||||
(function() {
|
(function() {
|
||||||
if(typeof CHANNEL.opts.maxlength != "number") {
|
if(typeof CHANNEL.opts.maxlength != "number") {
|
||||||
$("#cs-maxlength").val("");
|
$("#cs-maxlength").val("");
|
||||||
|
|
Loading…
Reference in New Issue