Merge branch 'dev'

This commit is contained in:
Calvin Montgomery 2013-07-31 22:26:40 -04:00
commit ec1b28b8cd
22 changed files with 754 additions and 447 deletions

6
acp.js
View File

@ -150,6 +150,12 @@ module.exports = function (Server) {
} }
}); });
user.socket.on("acp-actionlog-list", function () {
user.socket.emit("acp-actionlog-list",
ActionLog.getLogTypes()
);
});
user.socket.on("acp-actionlog-clear", function(data) { user.socket.on("acp-actionlog-clear", function(data) {
ActionLog.clear(data); ActionLog.clear(data);
ActionLog.record(user.ip, user.name, "acp-actionlog-clear", data); ActionLog.record(user.ip, user.name, "acp-actionlog-clear", data);

View File

@ -105,12 +105,42 @@ exports.tooManyRegistrations = function (ip) {
return rows.length > 4; return rows.length > 4;
} }
exports.readLog = function () { exports.getLogTypes = function () {
var db = Database.getConnection();
if(!db)
return false;
var query = "SELECT DISTINCT action FROM actionlog";
var result = db.querySync(query);
if(!result) {
Logger.errlog.log("! Failed to read action log");
return [];
}
result = result.fetchAllSync();
var actions = [];
for(var i in result)
actions.push(result[i].action);
return actions;
}
exports.readLog = function (actions) {
var db = Database.getConnection(); var db = Database.getConnection();
if(!db) if(!db)
return false; return false;
var query = "SELECT * FROM actionlog"; var query = "SELECT * FROM actionlog";
if(actions !== undefined) {
var list = new Array(actions.length);
for(var i in actions)
list[i] = "?";
list = list.join(",");
query += Database.createQuery(
" WHERE action IN ("+list+")",
actions
);
}
var result = db.querySync(query); var result = db.querySync(query);
if(!result) { if(!result) {
Logger.errlog.log("! Failed to read action log"); Logger.errlog.log("! Failed to read action log");

47
api.js
View File

@ -12,22 +12,22 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
var Auth = require("./auth"); var Auth = require("./auth");
var Logger = require("./logger"); var Logger = require("./logger");
var apilog = new Logger.Logger("api.log"); var apilog = new Logger.Logger("api.log");
var Config = require("./config");
var ActionLog = require("./actionlog"); var ActionLog = require("./actionlog");
var fs = require("fs"); var fs = require("fs");
function getIP(req) {
var raw = req.connection.remoteAddress;
var forward = req.header("x-forwarded-for");
if(Config.REVERSE_PROXY && forward) {
var ip = forward.split(",")[0];
Logger.syslog.log("REVPROXY " + raw + " => " + ip);
return ip;
}
return raw;
}
module.exports = function (Server) { module.exports = function (Server) {
function getIP(req) {
var raw = req.connection.remoteAddress;
var forward = req.header("x-forwarded-for");
if(Server.cfg["trust-x-forward"] && forward) {
var ip = forward.split(",")[0];
Logger.syslog.log("REVPROXY " + raw + " => " + ip);
return ip;
}
return raw;
}
var API = function () { var API = function () {
} }
@ -189,7 +189,8 @@ module.exports = function (Server) {
var row = Auth.login(name, pw, session); var row = Auth.login(name, pw, session);
if(row) { if(row) {
ActionLog.record(getIP(req), name, "login-success"); if(row.global_rank >= 255)
ActionLog.record(getIP(req), name, "login-success");
this.sendJSON(res, { this.sendJSON(res, {
success: true, success: true,
session: row.session_hash session: row.session_hash
@ -251,7 +252,7 @@ module.exports = function (Server) {
return; return;
} }
if(!Config.MAIL) { if(!Server.cfg["enable-mail"]) {
this.sendJSON(res, { this.sendJSON(res, {
success: false, success: false,
error: "This server does not have email enabled. Contact an administrator" error: "This server does not have email enabled. Contact an administrator"
@ -269,24 +270,24 @@ module.exports = function (Server) {
"A password reset request was issued for your account `", "A password reset request was issued for your account `",
name, name,
"` on ", "` on ",
Config.DOMAIN, Server.cfg["domain"],
". This request is valid for 24 hours. ", ". This request is valid for 24 hours. ",
"If you did not initiate this, there is no need to take action. ", "If you did not initiate this, there is no need to take action. ",
"To reset your password, copy and paste the following link into ", "To reset your password, copy and paste the following link into ",
"your browser: ", "your browser: ",
Config.DOMAIN, Server.cfg["domain"],
"/reset.html?", "/reset.html?",
hash hash
].join(""); ].join("");
var mail = { var mail = {
from: "CyTube Services <" + Config.MAIL_FROM + ">", from: "CyTube Services <" + Server.cfg["mail-from"] + ">",
to: email, to: email,
subject: "Password reset request", subject: "Password reset request",
text: msg text: msg
}; };
var api = this; var api = this;
Config.MAIL.sendMail(mail, function(err, response) { Server.cfg["nodemailer"].sendMail(mail, function(err, response) {
if(err) { if(err) {
Logger.errlog.log("Mail fail: " + err); Logger.errlog.log("Mail fail: " + err);
api.sendJSON(res, { api.sendJSON(res, {
@ -299,7 +300,7 @@ module.exports = function (Server) {
success: true success: true
}); });
if(Config.DEBUG) { if(Server.cfg["debug"]) {
Logger.syslog.log(response); Logger.syslog.log(response);
} }
} }
@ -317,12 +318,12 @@ module.exports = function (Server) {
name: info[0], name: info[0],
pw: info[1] pw: info[1]
}); });
ActionLog.record(ip, name, "password-recover-success"); ActionLog.record(ip, info[0], "password-recover-success");
Logger.syslog.log(ip + " recovered password for " + name); Logger.syslog.log(ip + " recovered password for " + info[0]);
return; return;
} }
catch(e) { catch(e) {
ActionLog.record(ip, name, "password-recover-failure"); ActionLog.record(ip, "", "password-recover-failure");
this.sendJSON(res, { this.sendJSON(res, {
success: false, success: false,
error: e error: e
@ -510,13 +511,15 @@ module.exports = function (Server) {
var name = params.name || ""; var name = params.name || "";
var pw = params.pw || ""; var pw = params.pw || "";
var session = params.session || ""; var session = params.session || "";
var types = params.actions || "";
var row = Auth.login(name, pw, session); var row = Auth.login(name, pw, session);
if(!row || row.global_rank < 255) { if(!row || row.global_rank < 255) {
res.send(403); res.send(403);
return; return;
} }
var actions = ActionLog.readLog(); var actiontypes = types.split(",");
var actions = ActionLog.readLog(actiontypes);
this.sendJSON(res, actions); this.sendJSON(res, actions);
}, },

View File

@ -11,7 +11,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
var mysql = require("mysql-libmysqlclient"); var mysql = require("mysql-libmysqlclient");
var Database = require("./database.js"); var Database = require("./database.js");
var Config = require("./config.js");
var bcrypt = require("bcrypt"); var bcrypt = require("bcrypt");
var hashlib = require("node_hash"); var hashlib = require("node_hash");
var Logger = require("./logger.js"); var Logger = require("./logger.js");

View File

@ -34,6 +34,7 @@ var Channel = function(name, Server) {
// Initialize defaults // Initialize defaults
this.registered = false; this.registered = false;
this.users = []; this.users = [];
this.afkcount = 0;
this.playlist = new Playlist(this); this.playlist = new Playlist(this);
this.library = {}; this.library = {};
this.position = -1; this.position = -1;
@ -76,6 +77,7 @@ var Channel = function(name, Server) {
this.opts = { this.opts = {
allow_voteskip: true, allow_voteskip: true,
voteskip_ratio: 0.5, voteskip_ratio: 0.5,
afk_timeout: 180,
pagetitle: this.name, pagetitle: this.name,
maxlength: 0, maxlength: 0,
externalcss: "", externalcss: "",
@ -201,6 +203,9 @@ Channel.prototype.loadDump = function() {
} }
this.sendAll("setPermissions", this.permissions); this.sendAll("setPermissions", this.permissions);
this.broadcastOpts(); this.broadcastOpts();
this.users.forEach(function (u) {
u.autoAFK();
});
if(data.filters) { if(data.filters) {
for(var i = 0; i < data.filters.length; i++) { for(var i = 0; i < data.filters.length; i++) {
var f = data.filters[i]; var f = data.filters[i];
@ -263,7 +268,6 @@ Channel.prototype.saveDump = function() {
}; };
var text = JSON.stringify(dump); var text = JSON.stringify(dump);
fs.writeFileSync("chandump/" + this.name, text); fs.writeFileSync("chandump/" + this.name, text);
this.logger.flush();
} }
// Save channel dumps every 5 minutes, in case of crash // Save channel dumps every 5 minutes, in case of crash
@ -958,6 +962,7 @@ Channel.prototype.broadcastChatFilters = function() {
Channel.prototype.broadcastVoteskipUpdate = function() { Channel.prototype.broadcastVoteskipUpdate = function() {
var amt = this.voteskip ? this.voteskip.counts[0] : 0; var amt = this.voteskip ? this.voteskip.counts[0] : 0;
var need = this.voteskip ? parseInt(this.users.length * this.opts.voteskip_ratio) : 0; var need = this.voteskip ? parseInt(this.users.length * this.opts.voteskip_ratio) : 0;
need -= this.afkcount;
for(var i = 0; i < this.users.length; i++) { for(var i = 0; i < this.users.length; i++) {
if(Rank.hasPermission(this.users[i], "seeVoteskip") || if(Rank.hasPermission(this.users[i], "seeVoteskip") ||
this.leader == this.users[i]) { this.leader == this.users[i]) {
@ -1536,12 +1541,18 @@ Channel.prototype.tryVoteskip = function(user) {
if(!this.opts.allow_voteskip) { if(!this.opts.allow_voteskip) {
return; return;
} }
// Voteskip = auto-unafk
if(user.meta.afk) {
user.setAFK(false);
}
if(!this.voteskip) { if(!this.voteskip) {
this.voteskip = new Poll("voteskip", "voteskip", ["yes"]); this.voteskip = new Poll("voteskip", "voteskip", ["yes"]);
} }
this.voteskip.vote(user.ip, 0); this.voteskip.vote(user.ip, 0);
this.broadcastVoteskipUpdate(); this.broadcastVoteskipUpdate();
if(this.voteskip.counts[0] >= parseInt(this.users.length * this.opts.voteskip_ratio)) { var need = parseInt(this.users.length * this.opts.voteskip_ratio);
need -= this.afkcount;
if(this.voteskip.counts[0] >= need) {
this.playNext(); this.playNext();
} }
} }
@ -1681,6 +1692,11 @@ Channel.prototype.tryUpdateOptions = function(user, data) {
if(key in adminonly && user.rank < Rank.Owner) { if(key in adminonly && user.rank < Rank.Owner) {
continue; continue;
} }
if(key === "afk_timeout" && this.opts[key] != data[key]) {
this.users.forEach(function (u) {
u.autoAFK();
});
}
this.opts[key] = data[key]; this.opts[key] = data[key];
} }
} }
@ -1829,7 +1845,8 @@ Channel.prototype.sendMessage = function(username, msg, msgclass, data) {
this.chatbuffer.push(msgobj); this.chatbuffer.push(msgobj);
if(this.chatbuffer.length > 15) if(this.chatbuffer.length > 15)
this.chatbuffer.shift(); this.chatbuffer.shift();
this.logger.log("<" + username + "." + msgclass + "> " + msg); var unescaped = sanitize(msg).entityDecode();
this.logger.log("<" + username + "." + msgclass + "> " + unescaped);
}; };
/* REGION Rank stuff */ /* REGION Rank stuff */

View File

@ -24,8 +24,7 @@ function handle(chan, user, msg, data) {
} }
} }
else if(msg.indexOf("/afk") == 0) { else if(msg.indexOf("/afk") == 0) {
user.meta.afk = !user.meta.afk; user.setAFK(!user.meta.afk);
chan.broadcastUserUpdate(user);
} }
else if(msg.indexOf("/m ") == 0) { else if(msg.indexOf("/m ") == 0) {
if(user.rank >= Rank.Moderator) { if(user.rank >= Rank.Moderator) {

116
config.js
View File

@ -9,38 +9,88 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
exports.MYSQL_SERVER = ""; var fs = require("fs");
exports.MYSQL_DB = ""; var Logger = require("./logger");
exports.MYSQL_USER = "";
exports.MYSQL_PASSWORD = "";
exports.IO_PORT = 1337; // Socket.IO port, DO NOT USE PORT 80.
exports.WEBSERVER_PORT = 8080; // Webserver port. Binding port 80 requires root permissions
exports.MAX_PER_IP = 10;
exports.GUEST_LOGIN_DELAY = 60; // Seconds
/*
Set to true if your IO_URL and WEB_URL are behind a reverse proxy
(e.g. Cloudflare) so that client IPs are passed through correctly.
If you are not behind a reverse proxy, leave it as false, otherwise
clients can fake their IP address in the x-forwarded-for header
*/
exports.REVERSE_PROXY = false;
var nodemailer = require("nodemailer"); var nodemailer = require("nodemailer");
exports.MAIL = false;
/* Example for setting up email:
exports.MAIL = nodemailer.createTransport("SMTP", {
service: "Gmail",
auth: {
user: "some.user@gmail.com",
pass: "supersecretpassword"
}
});
See https://github.com/andris9/Nodemailer var defaults = {
*/ "mysql-server" : "localhost",
exports.MAIL_FROM = "some.user@gmail.com"; "mysql-db" : "cytube",
// Domain for password reset link "mysql-user" : "cytube",
// Email sent goes to exports.DOMAIN/reset.html?resethash "mysql-pw" : "supersecretpass",
exports.DOMAIN = "http://localhost"; "express-host" : "0.0.0.0",
"asset-cache-ttl" : 0,
"web-port" : 8080,
"io-port" : 1337,
"ip-connection-limit" : 10,
"guest-login-delay" : 60,
"trust-x-forward" : false,
"enable-mail" : false,
"mail-transport" : "SMTP",
"mail-config" : {
"service" : "Gmail",
"auth" : {
"user" : "some.user@gmail.com",
"pass" : "supersecretpassword"
}
},
"mail-from" : "some.user@gmail.com",
"domain" : "http://localhost"
}
function save(cfg, file) {
var x = {};
for(var k in cfg) {
if(k !== "nodemailer")
x[k] = cfg[k];
}
fs.writeFile(file, JSON.stringify(x, null, 4), function (err) {
if(err) {
Logger.errlog.log("Failed to save config");
Logger.errlog.log(err);
}
});
}
exports.load = function (Server, file, callback) {
var cfg = {};
for(var k in defaults)
cfg[k] = defaults[k];
fs.readFile(file, function (err, data) {
if(err) {
if(err.code == "ENOENT") {
Logger.syslog.log("Config file not found, generating default");
Logger.syslog.log("Edit cfg.json to configure");
data = "{}";
}
else {
Logger.errlog.log("Config load failed");
Logger.errlog.log(err);
return;
}
}
try {
data = JSON.parse(data + "");
} catch(e) {
Logger.errlog.log("Config JSON is invalid: ");
Logger.errlog.log(e);
return;
}
for(var k in data)
cfg[k] = data[k];
if(cfg["enable-mail"]) {
cfg["nodemailer"] = nodemailer.createTransport(
cfg["mail-transport"],
cfg["mail-config"]
);
}
save(cfg, file);
Server.cfg = cfg;
callback();
});
}

View File

@ -24,10 +24,10 @@ var CONFIG = {};
var global_bans = {}; var global_bans = {};
function setup(cfg) { function setup(cfg) {
SERVER = cfg.MYSQL_SERVER; SERVER = cfg["mysql-server"];
USER = cfg.MYSQL_USER; USER = cfg["mysql-user"];
DATABASE = cfg.MYSQL_DB; DATABASE = cfg["mysql-db"];
PASSWORD = cfg.MYSQL_PASSWORD; PASSWORD = cfg["mysql-pw"];
CONFIG = cfg; CONFIG = cfg;
} }
@ -41,7 +41,7 @@ function getConnection() {
Logger.errlog.log("DB connection failed"); Logger.errlog.log("DB connection failed");
return false; return false;
} }
if(CONFIG.DEBUG) { if(CONFIG["debug"]) {
db._querySync = db.querySync; db._querySync = db.querySync;
db.querySync = function(q) { db.querySync = function(q) {
Logger.syslog.log("DEBUG: " + q); Logger.syslog.log("DEBUG: " + q);
@ -900,6 +900,8 @@ function saveUserPlaylist(pl, user, name) {
for(var i = 0; i < pl.length; i++) { for(var i = 0; i < pl.length; i++) {
var e = { var e = {
id: pl[i].media.id, id: pl[i].media.id,
title: pl[i].media.title,
seconds: pl[i].media.seconds,
type: pl[i].media.type type: pl[i].media.type
}; };
time += pl[i].media.seconds; time += pl[i].media.seconds;

View File

@ -18,28 +18,37 @@ function getTimeString() {
var Logger = function(filename) { var Logger = function(filename) {
this.filename = filename; this.filename = filename;
this.buffer = []; this.writer = fs.createWriteStream(filename, {
flags: "a",
setInterval(function() { encoding: "utf-8"
this.flush(); });
}.bind(this), 15000);
} }
Logger.prototype.log = function(what) { Logger.prototype.log = function () {
this.buffer.push("[" + getTimeString() + "] " + what); var msg = "";
} for(var i in arguments)
msg += arguments[i];
Logger.prototype.flush = function() { if(this.dead) {
if(this.buffer.length == 0)
return; return;
var text = this.buffer.join("\n") + "\n"; }
this.buffer = [];
fs.appendFile(this.filename, text, function(err) { var str = "[" + getTimeString() + "] " + msg + "\n";
if(err) { try {
errlog.log("Append to " + this.filename + " failed: "); this.writer.write(str);
errlog.log(err); } catch(e) {
} errlog.log("WARNING: Attempted logwrite failed: " + this.filename);
}.bind(this)); errlog.log("Message was: " + msg);
errlog.log(e);
}
}
Logger.prototype.close = function () {
try {
this.writer.end();
} catch(e) {
errlog.log("Log close failed: " + this.filename);
}
} }
var errlog = new Logger("error.log"); var errlog = new Logger("error.log");

View File

@ -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": "2.1.2", "version": "2.1.4",
"repository": { "repository": {
"url": "http://github.com/calzoneman/sync" "url": "http://github.com/calzoneman/sync"
}, },

View File

@ -239,6 +239,19 @@ Playlist.prototype.addMedia = function(data, callback) {
}; };
this.queueAction(action); this.queueAction(action);
// Pre-cached data
if(typeof data.title === "string" &&
typeof data.seconds === "number") {
if(data.maxlength && data.seconds > data.maxlength) {
action.expire = 0;
callback("Media is too long!", null);
return;
}
it.media = new Media(data.id, data.title, data.seconds, data.type);
action.waiting = false;
return;
}
InfoGetter.getMedia(data.id, data.type, function(err, media) { InfoGetter.getMedia(data.id, data.type, function(err, media) {
if(err) { if(err) {
action.expire = 0; action.expire = 0;

View File

@ -1,16 +1,17 @@
var path = require("path"); var path = require("path");
var fs = require("fs");
var express = require("express"); var express = require("express");
var Config = require("./config"); var Config = require("./config");
var Logger = require("./logger"); var Logger = require("./logger");
var Channel = require("./channel"); var Channel = require("./channel");
var User = require("./user"); var User = require("./user");
const VERSION = "2.1.2"; const VERSION = "2.1.4";
function getIP(req) { function getIP(req) {
var raw = req.connection.remoteAddress; var raw = req.connection.remoteAddress;
var forward = req.header("x-forwarded-for"); var forward = req.header("x-forwarded-for");
if(Config.REVERSE_PROXY && forward) { if(Server.cfg["trust-x-forward"] && forward) {
var ip = forward.split(",")[0]; var ip = forward.split(",")[0];
Logger.syslog.log("REVPROXY " + raw + " => " + ip); Logger.syslog.log("REVPROXY " + raw + " => " + ip);
return ip; return ip;
@ -20,7 +21,7 @@ function getIP(req) {
function getSocketIP(socket) { function getSocketIP(socket) {
var raw = socket.handshake.address.address; var raw = socket.handshake.address.address;
if(Config.REVERSE_PROXY) { if(Server.cfg["trust-x-forward"]) {
if(typeof socket.handshake.headers["x-forwarded-for"] == "string") { if(typeof socket.handshake.headers["x-forwarded-for"] == "string") {
var ip = socket.handshake.headers["x-forwarded-for"] var ip = socket.handshake.headers["x-forwarded-for"]
.split(",")[0]; .split(",")[0];
@ -54,6 +55,7 @@ var Server = {
if(chan.registered) if(chan.registered)
chan.saveDump(); chan.saveDump();
chan.playlist.die(); chan.playlist.die();
chan.logger.close();
for(var i in this.channels) { for(var i in this.channels) {
if(this.channels[i].canonical_name == chan.canonical_name) { if(this.channels[i].canonical_name == chan.canonical_name) {
this.channels.splice(i, 1); this.channels.splice(i, 1);
@ -70,34 +72,57 @@ var Server = {
db: null, db: null,
ips: {}, ips: {},
acp: null, acp: null,
httpaccess: null,
logHTTP: function (req, status) {
if(status === undefined)
status = 200;
var ip = req.connection.remoteAddress;
var ip2 = false;
if(this.cfg["trust-x-forward"])
ip2 = req.header("x-forwarded-for") || req.header("cf-connecting-ip");
var ipstr = !ip2 ? ip : ip + " (X-Forwarded-For " + ip2 + ")";
var url = req.url;
// Remove query
if(url.indexOf("?") != -1)
url = url.substring(0, url.lastIndexOf("?"));
this.httpaccess.log([ipstr, req.method, url, status, req.headers["user-agent"]].join(" "));
},
init: function () { init: function () {
this.httpaccess = new Logger.Logger("httpaccess.log");
this.app = express(); this.app = express();
// channel path // channel path
this.app.get("/r/:channel(*)", function (req, res, next) { this.app.get("/r/:channel(*)", function (req, res, next) {
var c = req.params.channel; var c = req.params.channel;
if(!c.match(/^[\w-_]+$/)) if(!c.match(/^[\w-_]+$/)) {
res.redirect("/" + c); res.redirect("/" + c);
else }
else {
this.logHTTP(req);
res.sendfile(__dirname + "/www/channel.html"); res.sendfile(__dirname + "/www/channel.html");
}); }
}.bind(this));
// api path // api path
this.api = require("./api")(this); this.api = require("./api")(this);
this.app.get("/api/:apireq(*)", function (req, res, next) { this.app.get("/api/:apireq(*)", function (req, res, next) {
this.logHTTP(req);
this.api.handle(req.url.substring(5), req, res); this.api.handle(req.url.substring(5), req, res);
}.bind(this)); }.bind(this));
this.app.get("/", function (req, res, next) { this.app.get("/", function (req, res, next) {
this.logHTTP(req);
res.sendfile(__dirname + "/www/index.html"); res.sendfile(__dirname + "/www/index.html");
}); }.bind(this));
// default path // default path
this.app.get("/:thing(*)", function (req, res, next) { this.app.get("/:thing(*)", function (req, res, next) {
var opts = { var opts = {
root: __dirname + "/www", root: __dirname + "/www",
maxAge: this.cfg["asset-cache-ttl"]
} }
res.sendfile(req.params.thing, opts, function (err) { res.sendfile(req.params.thing, opts, function (err) {
if(err) { if(err) {
this.logHTTP(req, err.status);
// Damn path traversal attacks // Damn path traversal attacks
if(req.params.thing.indexOf("%2e") != -1) { if(req.params.thing.indexOf("%2e") != -1) {
res.send("Don't try that again, I'll ban you"); res.send("Don't try that again, I'll ban you");
@ -113,21 +138,27 @@ var Server = {
res.send(err.status); res.send(err.status);
} }
} }
}); else {
}); this.logHTTP(req);
}
}.bind(this));
}.bind(this));
// fallback // fallback
this.app.use(function (err, req, res, next) { this.app.use(function (err, req, res, next) {
this.logHTTP(req, err.status);
if(err.status == 404) { if(err.status == 404) {
res.send(404); res.send(404);
} else { } else {
next(err); next(err);
} }
}); }.bind(this));
// bind servers // bind servers
this.httpserv = this.app.listen(Config.WEBSERVER_PORT); this.httpserv = this.app.listen(Server.cfg["web-port"],
this.ioserv = express().listen(Config.IO_PORT); Server.cfg["express-host"]);
this.ioserv = express().listen(Server.cfg["io-port"],
Server.cfg["express-host"]);
// init socket.io // init socket.io
this.io = require("socket.io").listen(this.ioserv); this.io = require("socket.io").listen(this.ioserv);
@ -152,7 +183,7 @@ var Server = {
this.ips[ip] = 0; this.ips[ip] = 0;
this.ips[ip]++; this.ips[ip]++;
if(this.ips[ip] > Config.MAX_PER_IP) { if(this.ips[ip] > Server.cfg["ip-connection-limit"]) {
socket.emit("kick", { socket.emit("kick", {
reason: "Too many connections from your IP address" reason: "Too many connections from your IP address"
}); });
@ -167,7 +198,7 @@ var Server = {
// init database // init database
this.db = require("./database"); this.db = require("./database");
this.db.setup(Config); this.db.setup(Server.cfg);
this.db.init(); this.db.init();
// init ACP // init ACP
@ -190,15 +221,25 @@ var Server = {
}; };
Logger.syslog.log("Starting CyTube v" + VERSION); Logger.syslog.log("Starting CyTube v" + VERSION);
Server.init();
if(!Config.DEBUG) { fs.exists("chanlogs", function (exists) {
process.on("uncaughtException", function (err) { exists || fs.mkdir("chanlogs");
Logger.errlog.log("[SEVERE] Uncaught Exception: " + err); });
Logger.errlog.log(err.stack);
});
process.on("SIGINT", function () { fs.exists("chandump", function (exists) {
Server.shutdown(); exists || fs.mkdir("chandump");
}); });
}
Config.load(Server, "cfg.json", function () {
Server.init();
if(!Server.cfg["debug"]) {
process.on("uncaughtException", function (err) {
Logger.errlog.log("[SEVERE] Uncaught Exception: " + err);
Logger.errlog.log(err.stack);
});
process.on("SIGINT", function () {
Server.shutdown();
});
}
});

55
user.js
View File

@ -14,7 +14,6 @@ var Auth = require("./auth.js");
var Channel = require("./channel.js").Channel; var Channel = require("./channel.js").Channel;
var formatTime = require("./media.js").formatTime; var formatTime = require("./media.js").formatTime;
var Logger = require("./logger.js"); var Logger = require("./logger.js");
var Config = require("./config.js");
var ActionLog = require("./actionlog"); var ActionLog = require("./actionlog");
// Represents a client connected via socket.io // Represents a client connected via socket.io
@ -38,6 +37,8 @@ var User = function(socket, Server) {
image: "", image: "",
text: "" text: ""
}; };
this.awaytimer = false;
this.autoAFK();
this.initCallbacks(); this.initCallbacks();
if(Server.announcement != null) { if(Server.announcement != null) {
@ -79,6 +80,46 @@ User.prototype.noflood = function(name, hz) {
} }
} }
User.prototype.setAFK = function (afk) {
if(this.channel === null)
return;
var changed = this.meta.afk != afk;
var chan = this.channel;
this.meta.afk = afk;
if(!afk)
this.autoAFK();
if(changed) {
if(this.meta.afk)
chan.afkcount++;
else
chan.afkcount--;
}
if(chan.voteskip) {
chan.voteskip.unvote(this.ip);
var need = parseInt(chan.users.length * chan.opts.voteskip_ratio);
need -= chan.afkcount;
if(chan.voteskip.counts[0] >= need) {
chan.playNext();
}
else {
chan.broadcastVoteskipUpdate();
}
}
chan.broadcastUserUpdate(this);
}
User.prototype.autoAFK = function () {
if(this.awaytimer)
clearTimeout(this.awaytimer);
if(this.channel === null || this.channel.opts.afk_timeout == 0)
return;
this.awaytimer = setTimeout(function () {
this.setAFK(true);
}.bind(this), this.channel.opts.afk_timeout * 1000);
}
User.prototype.initCallbacks = function() { User.prototype.initCallbacks = function() {
this.socket.on("disconnect", function() { this.socket.on("disconnect", function() {
if(this.channel != null) if(this.channel != null)
@ -166,6 +207,10 @@ User.prototype.initCallbacks = function() {
this.socket.on("chatMsg", function(data) { this.socket.on("chatMsg", function(data) {
if(this.channel != null) { if(this.channel != null) {
if(data.msg.indexOf("/afk") == -1) {
this.setAFK(false);
this.autoAFK();
}
this.channel.tryChat(this, data); this.channel.tryChat(this, data);
} }
}.bind(this)); }.bind(this));
@ -506,11 +551,12 @@ User.prototype.login = function(name, pw, session) {
if(pw == "" && session == "") { if(pw == "" && session == "") {
if(this.ip in lastguestlogin) { if(this.ip in lastguestlogin) {
var diff = (Date.now() - lastguestlogin[this.ip])/1000; var diff = (Date.now() - lastguestlogin[this.ip])/1000;
if(diff < Config.GUEST_LOGIN_DELAY) { if(diff < this.server.cfg["guest-login-delay"]) {
this.socket.emit("login", { this.socket.emit("login", {
success: false, success: false,
error: ["Guest logins are restricted to one per ", error: ["Guest logins are restricted to one per ",
Config.GUEST_LOGIN_DELAY + " seconds per IP. ", this.server.cfg["guest-login-delay"]
+ " seconds per IP. ",
"This restriction does not apply to registered users." "This restriction does not apply to registered users."
].join("") ].join("")
}); });
@ -580,7 +626,8 @@ User.prototype.login = function(name, pw, session) {
} }
} }
} }
ActionLog.record(this.ip, name, "login-success"); if(this.global_rank >= 255)
ActionLog.record(this.ip, name, "login-success");
this.loggedIn = true; this.loggedIn = true;
this.socket.emit("login", { this.socket.emit("login", {
success: true, success: true,

View File

@ -254,6 +254,7 @@
<!-- Mine --> <!-- Mine -->
<script src="./assets/js/iourl.js"></script> <script src="./assets/js/iourl.js"></script>
<script src="./assets/js/data.js"></script> <script src="./assets/js/data.js"></script>
<script src="./assets/js/paginator.js"></script>
<script src="./assets/js/util.js"></script> <script src="./assets/js/util.js"></script>
<script src="./assets/js/acp.js"></script> <script src="./assets/js/acp.js"></script>
</body> </body>

View File

@ -154,7 +154,12 @@ html, body {
border-left: 0; border-left: 0;
} }
#messagebuffer div, #messagebuffer code, #filteredit code { #channeldata td {
max-width: 200px;
}
#messagebuffer div, #messagebuffer code, #filteredit code,
#channeldata td, #currenttitle {
white-space: pre-wrap; /* css-3 */ white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */ white-space: -pre-wrap; /* Opera 4-6 */

View File

@ -47,7 +47,25 @@ function tableResort(tbl, sortby) {
tbl.data("sort_desc", !tbl.data("sort_desc")); tbl.data("sort_desc", !tbl.data("sort_desc"));
else else
tbl.data("sortby", sortby) tbl.data("sortby", sortby)
loadPage(tbl, 0); var sort_field = tbl.data("sortby");
var sort_desc = tbl.data("sort_desc");
var p = tbl.data("paginator");
if(sort_field) {
p.items.sort(function(a, b) {
var x = a[sort_field];
if(typeof x == "string")
x = x.toLowerCase();
var y = b[sort_field];
if(typeof y == "string")
y = y.toLowerCase();
var z = x == y ? 0 : (x < y ? -1 : 1);
if(sort_desc)
z = -z;
return z;
});
}
p.loadPage(0);
} }
$("#userlookup_uid").click(function() { $("#userlookup_uid").click(function() {
tableResort($("#userlookup table"), "id"); tableResort($("#userlookup table"), "id");
@ -70,87 +88,33 @@ $("#listloaded_refresh").click(function() {
socket.emit("acp-list-loaded"); socket.emit("acp-list-loaded");
}); });
menuHandler("#show_actionlog", "#actionlog"); menuHandler("#show_actionlog", "#actionlog");
$("#show_actionlog").click(getActionLog); $("#show_actionlog").click(function () {
$("#actionlog_filter").click(function() { socket.emit("acp-actionlog-list");
var tbl = $("#actionlog table");
var actions = $(this).val();
$("#actionlog tbody").remove();
var entries = [];
tbl.data("allentries").forEach(function(e) {
if(actions.indexOf(e.action) == -1)
return;
entries.push(e);
});
$("#actionlog_pagination").remove();
if(entries.length > 20) {
var pag = $("<div/>").addClass("pagination")
.attr("id", "actionlog_pagination")
.insertAfter($("#actionlog table"));
var btns = $("<ul/>").appendTo(pag);
for(var i = 0; i < entries.length / 20; i++) {
var li = $("<li/>").appendTo(btns);
(function(i) {
$("<a/>").attr("href", "javascript:void(0)")
.text(i+1)
.click(function() {
loadPage(tbl, i);
})
.appendTo(li);
})(i);
}
tbl.data("pagination", pag);
}
$("#actionlog table").data("entries", entries);
loadPage($("#actionlog table"), 0);
}); });
$("#actionlog_filter").click(getActionLog);
$("#actionlog_searchbtn").click(function() { $("#actionlog_searchbtn").click(function() {
var tbl = $("#actionlog table"); var tbl = $("#actionlog table");
$("#actionlog tbody").remove();
var actions = $("#actionlog_filter").val();
var sfield = $("#actionlog_sfield").val(); var sfield = $("#actionlog_sfield").val();
var sval = $("#actionlog_search").val().toLowerCase(); var sval = $("#actionlog_search").val().toLowerCase();
var sort = $("#actionlog_sort").val(); var sort = $("#actionlog_sort").val();
var desc = $("#actionlog_sortorder").val() === "true"; var desc = $("#actionlog_sortorder").val() === "true";
tbl.data("sort_desc", desc); tbl.data("sort_desc", desc);
tbl.data("sortby", sort); tbl.data("sortby", sort);
var entries = []; var entries = tbl.data("allentries");
tbl.data("allentries").forEach(function(e) {
if(actions.indexOf(e.action) == -1)
return;
entries.push(e);
});
entries = entries.filter(function (item, i, arr) { entries = entries.filter(function (item, i, arr) {
var f = item[sfield]; var f = item[sfield];
if(sfield === "time") if(sfield === "time")
f = new Date(f).toString().toLowerCase(); f = new Date(f).toString().toLowerCase();
return f.indexOf(sval) > -1; return f.indexOf(sval) > -1;
}); });
$("#actionlog_pagination").remove(); tbl.data("entries", entries);
if(entries.length > 20) { var p = tbl.data("paginator");
var pag = $("<div/>").addClass("pagination") p.items = entries;
.attr("id", "actionlog_pagination") tableResort(tbl);
.insertAfter($("#actionlog table"));
var btns = $("<ul/>").appendTo(pag);
for(var i = 0; i < entries.length / 20; i++) {
var li = $("<li/>").appendTo(btns);
(function(i) {
$("<a/>").attr("href", "javascript:void(0)")
.text(i+1)
.click(function() {
loadPage(tbl, i);
})
.appendTo(li);
})(i);
}
tbl.data("pagination", pag);
}
$("#actionlog table").data("entries", entries);
loadPage($("#actionlog table"), 0);
}); });
$("#actionlog_clear").click(function() { $("#actionlog_clear").click(function() {
socket.emit("acp-actionlog-clear", $("#actionlog_filter").val()); socket.emit("acp-actionlog-clear", $("#actionlog_filter").val());
socket.emit("acp-actionlog-list");
getActionLog(); getActionLog();
}); });
$("#actionlog_refresh").click(function() { $("#actionlog_refresh").click(function() {
@ -193,47 +157,51 @@ function getErrlog() {
} }
$("#errlog").click(getErrlog); $("#errlog").click(getErrlog);
function getActionLog() { function getActionLog() {
$.getJSON(WEB_URL+"/api/json/readactionlog?"+AUTH+"&callback=?").done(function(data) { var types = "&actions=" + $("#actionlog_filter").val().join(",");
var entries = data; $.getJSON(WEB_URL+"/api/json/readactionlog?"+AUTH+types+"&callback=?").done(function(entries) {
var actions = []; var tbl = $("#actionlog table");
entries.forEach(function (e) { entries.forEach(function (e) {
if(actions.indexOf(e.action) == -1)
actions.push(e.action);
e.time = parseInt(e.time); e.time = parseInt(e.time);
}); });
var tbl = $("#actionlog table"); var p = tbl.data("paginator");
if(p) {
p.items = entries;
}
else {
var opts = {
preLoadPage: function () {
$("#actionlog tbody").remove();
},
generator: function (e, page, index) {
var tr = $("<tr/>").appendTo($("#actionlog table"));
var rem = $("<td/>").appendTo(tr);
$("<button/>").addClass("btn btn-mini btn-danger")
.html("<i class='icon-trash'></i>")
.appendTo(rem)
.click(function () {
socket.emit("acp-actionlog-clear-one", e);
tr.hide("blind", function () {
tr.remove();
getActionLog();
});
});
$("<td/>").text(e.ip).appendTo(tr);
$("<td/>").text(e.name).appendTo(tr);
$("<td/>").text(e.action).appendTo(tr);
$("<td/>").text(e.args).appendTo(tr);
$("<td/>").text(new Date(e.time).toString()).appendTo(tr);
}
};
p = Paginate(entries, opts);
p.paginator.insertBefore($("#actionlog table"));
tbl.data("paginator", p);
}
tbl.data("sortby", "time"); tbl.data("sortby", "time");
tbl.data("sort_desc", true); tbl.data("sort_desc", true);
tbl.data("entries", entries); tbl.data("entries", entries);
tbl.data("allentries", entries); tbl.data("allentries", entries);
tbl.data("generator", function(e) { tableResort(tbl);
var tr = $("<tr/>").appendTo($("#actionlog table"));
var rem = $("<td/>").appendTo(tr);
$("<button/>").addClass("btn btn-mini btn-danger")
.html("<i class='icon-trash'></i>")
.appendTo(rem)
.click(function () {
socket.emit("acp-actionlog-clear-one", e);
tr.hide("blind", function () {
tr.remove();
getActionLog();
});
});
$("<td/>").text(e.ip).appendTo(tr);
$("<td/>").text(e.name).appendTo(tr);
$("<td/>").text(e.action).appendTo(tr);
$("<td/>").text(e.args).appendTo(tr);
$("<td/>").text(new Date(e.time).toString()).appendTo(tr);
});
$("#actionlog table").data("entries", entries);
$("#actionlog_filter").html("");
actions.sort(function(a, b) {
return a == b ? 0 : (a < b ? -1 : 1);
});
actions.forEach(function(a) {
$("<option/>").text(a).val(a).appendTo($("#actionlog_filter"));
});
tbl.find("tbody").remove();
}); });
} }
function getChanlog() { function getChanlog() {
@ -272,41 +240,6 @@ $("#userlookup_submit").click(function() {
socket.emit("acp-lookup-user", $("#userlookup_name").val()); socket.emit("acp-lookup-user", $("#userlookup_name").val());
}); });
function loadPage(tbl, page) {
var sort_field = tbl.data("sortby");
var sort_desc = tbl.data("sort_desc");
var generator = tbl.data("generator");
var pag = tbl.data("pagination");
if(pag) {
pag.find("li").each(function() {
$(this).removeClass("active");
});
$(pag.find("li")[page]).addClass("active");
}
var e = tbl.data("entries");
tbl.find("tbody").remove();
if(sort_field) {
e.sort(function(a, b) {
var x = a[sort_field];
if(typeof x == "string")
x = x.toLowerCase();
var y = b[sort_field];
if(typeof y == "string")
y = y.toLowerCase();
var z = x == y ? 0 : (x < y ? -1 : 1);
if(sort_desc)
z = -z;
return z;
});
}
for(var i = page * 20; i < page * 20 + 20 && i < e.length; i++) {
generator(e[i]);
}
}
function setupCallbacks() { function setupCallbacks() {
socket.on("connect", function() { socket.on("connect", function() {
if(NAME && SESSION) { if(NAME && SESSION) {
@ -371,75 +304,70 @@ function setupCallbacks() {
socket.on("acp-userdata", function(data) { socket.on("acp-userdata", function(data) {
var tbl = $("#userlookup table"); var tbl = $("#userlookup table");
if(data.length > 20) { var p = tbl.data("paginator");
var pag = $("<div/>").addClass("pagination") if(p) {
.attr("id", "userlookup_pagination") p.items = data;
.insertAfter($("#userlookup table")); }
var btns = $("<ul/>").appendTo(pag); else {
for(var i = 0; i < data.length / 20; i++) { var opts = {
var li = $("<li/>").appendTo(btns); preLoadPage: function () {
(function(i) { tbl.find("tbody").remove();
$("<a/>").attr("href", "javascript:void(0)") },
.text(i+1) generator: function (u, page, index) {
.click(function() { var tr = $("<tr/>").appendTo(tbl);
loadPage(tbl, i); $("<td/>").text(u.id).appendTo(tr);
}) $("<td/>").text(u.uname).appendTo(tr);
.appendTo(li); var rank = $("<td/>").text(u.global_rank).appendTo(tr);
})(i); $("<td/>").text(u.email).appendTo(tr);
} $("<button/>").addClass("btn btn-mini")
tbl.data("pagination", pag); .text("Reset password")
.appendTo($("<td/>").appendTo(tr))
.click(function() {
var reset = confirm("Really reset password?");
if(reset) {
socket.emit("acp-reset-password", {
name: u.uname,
email: u.email
});
}
});
rank.click(function() {
if(this.find(".rank-edit").length > 0)
return;
var r = this.text();
this.text("");
var edit = $("<input/>").attr("type", "text")
.attr("placeholder", r)
.addClass("rank-edit")
.appendTo(this)
.focus();
function save() {
var r = this.val();
var r2 = r;
if(r.trim() == "")
r = this.attr("placeholder");
this.parent().text(this.attr("placeholder"));
socket.emit("acp-set-rank", {
name: u.uname,
rank: parseInt(r)
});
}
edit.blur(save.bind(edit));
edit.keydown(function(ev) {
if(ev.keyCode == 13)
save.bind(edit)();
});
}.bind(rank));
}
};
p = Paginate(data, opts);
p.paginator.insertBefore(tbl);
tbl.data("paginator", p);
} }
tbl.data("entries", data);
tbl.data("sortby", "uname"); tbl.data("sortby", "uname");
tbl.data("sort_desc", false); tbl.data("sort_desc", false);
tbl.data("generator", function(u) { tableResort(tbl);
var tr = $("<tr/>").appendTo($("#userlookup table"));
$("<td/>").text(u.id).appendTo(tr);
$("<td/>").text(u.uname).appendTo(tr);
var rank = $("<td/>").text(u.global_rank).appendTo(tr);
$("<td/>").text(u.email).appendTo(tr);
$("<button/>").addClass("btn btn-mini")
.text("Reset password")
.appendTo($("<td/>").appendTo(tr))
.click(function() {
var reset = confirm("Really reset password?");
if(reset) {
socket.emit("acp-reset-password", {
name: u.uname,
email: u.email
});
}
});
rank.click(function() {
if(this.find(".rank-edit").length > 0)
return;
var r = this.text();
this.text("");
var edit = $("<input/>").attr("type", "text")
.attr("placeholder", r)
.addClass("rank-edit")
.appendTo(this)
.focus();
function save() {
var r = this.val();
var r2 = r;
if(r.trim() == "")
r = this.attr("placeholder");
this.parent().text(this.attr("placeholder"));
socket.emit("acp-set-rank", {
name: u.uname,
rank: parseInt(r)
});
}
edit.blur(save.bind(edit));
edit.keydown(function(ev) {
if(ev.keyCode == 13)
save.bind(edit)();
});
}.bind(rank));
});
loadPage($("#userlookup table"), 0);
}); });
socket.on("acp-set-rank", function(data) { socket.on("acp-set-rank", function(data) {
@ -564,6 +492,16 @@ function setupCallbacks() {
new Chart($("#stat_channels")[0].getContext("2d")).Line(chan_data); new Chart($("#stat_channels")[0].getContext("2d")).Line(chan_data);
new Chart($("#stat_mem")[0].getContext("2d")).Line(mem_data); new Chart($("#stat_mem")[0].getContext("2d")).Line(mem_data);
}); });
socket.on("acp-actionlog-list", function (alist) {
$("#actionlog_filter").html("");
alist.sort(function(a, b) {
return a == b ? 0 : (a < b ? -1 : 1);
});
alist.forEach(function(a) {
$("<option/>").text(a).val(a).appendTo($("#actionlog_filter"));
});
});
} }
/* cookie util */ /* cookie util */

View File

@ -426,25 +426,61 @@ Callbacks = {
if(tbl.children().length > 1) { if(tbl.children().length > 1) {
$(tbl.children()[1]).remove(); $(tbl.children()[1]).remove();
} }
$("#channelranks_pagination").remove(); var p = tbl.data("paginator");
if(entries.length > 20) { if(p) {
var pag = $("<div/>").addClass("pagination span12") p.items = entries;
.attr("id", "channelranks_pagination") p.loadPage(0);
.prependTo($("#channelranks")); }
var btns = $("<ul/>").appendTo(pag); else {
for(var i = 0; i < entries.length / 20; i++) { var opts = {
var li = $("<li/>").appendTo(btns); preLoadPage: function (p) {
(function(i) { tbl.find("tbody").remove();
$("<a/>").attr("href", "javascript:void(0)") tbl.data("page", p);
.text(i+1) },
.click(function() { generator: function (item, page, index) {
loadChannelRanksPage(i); var tr = $("<tr/>").appendTo(tbl);
}) var name = $("<td/>").text(item.name).appendTo(tr);
.appendTo(li); name.addClass(getNameColor(item.rank));
})(i); var rank = $("<td/>").text(item.rank)
} .css("min-width", "220px")
.appendTo(tr);
rank.click(function() {
if(this.find(".rank-edit").length > 0)
return;
var r = this.text();
this.text("");
var edit = $("<input/>").attr("type", "text")
.attr("placeholder", r)
.addClass("rank-edit")
.appendTo(this)
.focus();
if(parseInt(r) >= CLIENT.rank) {
edit.attr("disabled", true);
}
function save() {
var r = this.val();
var r2 = r;
if(r.trim() == "" || parseInt(r) >= CLIENT.rank || parseInt(r) < 1)
r = this.attr("placeholder");
r = parseInt(r) + "";
this.parent().text(r);
socket.emit("setChannelRank", {
user: item.name,
rank: parseInt(r)
});
}
edit.blur(save.bind(edit));
edit.keydown(function(ev) {
if(ev.keyCode == 13)
save.bind(edit)();
});
}.bind(rank));
}
};
var p = Paginate(entries, opts);
p.paginator.insertBefore($("#channelranks table"));
tbl.data("paginator", p);
} }
loadChannelRanksPage(0);
}, },
setChannelRank: function(data) { setChannelRank: function(data) {
@ -456,7 +492,9 @@ Callbacks = {
} }
} }
$("#channelranks").data("entries", ents); $("#channelranks").data("entries", ents);
loadChannelRanksPage($("#channelranks").data("page")); $("#channelranks table").data("paginator").loadPage(
$("#channelranks table").data("page")
);
}, },
voteskip: function(data) { voteskip: function(data) {
@ -842,23 +880,6 @@ Callbacks = {
$("#search_clear").remove(); $("#search_clear").remove();
clearSearchResults(); clearSearchResults();
$("#library").data("entries", data.results); $("#library").data("entries", data.results);
if(data.results.length > 100) {
var pag = $("<div/>").addClass("pagination")
.attr("id", "search_pagination")
.insertAfter($("#library"));
var btns = $("<ul/>").appendTo(pag);
for(var i = 0; i < data.results.length / 100; i++) {
var li = $("<li/>").appendTo(btns);
(function(i) {
$("<a/>").attr("href", "javascript:void(0)")
.text(i+1)
.click(function() {
loadSearchPage(i);
})
.appendTo(li);
})(i);
}
}
$("<button/>").addClass("btn btn-block") $("<button/>").addClass("btn btn-block")
.addClass("span12") .addClass("span12")
.css("margin-left", "0") .css("margin-left", "0")
@ -868,7 +889,38 @@ Callbacks = {
clearSearchResults(); clearSearchResults();
}) })
.insertBefore($("#library")); .insertBefore($("#library"));
loadSearchPage(0); var p = $("#library").data("paginator");
if(p) {
p.items = data.results;
p.loadPage(0);
}
else {
var opts = {
preLoadPage: function () {
$("#library").html("");
},
generator: function (item, page, index) {
var li = makeSearchEntry(item, false);
if(hasPermission("playlistadd")) {
if(item.thumb) {
addLibraryButtons(li, item.id, "yt");
}
else {
addLibraryButtons(li, item.id);
}
}
$(li).appendTo($("#library"));
},
itemsPerPage: 100
};
p = Paginate(data.results, opts);
p.paginator.insertBefore($("#library"))
.attr("id", "search_pagination");
$("#library").data("paginator", p);
}
}, },
/* REGION Polls */ /* REGION Polls */

View File

@ -69,7 +69,8 @@
externaljs: $("#opt_externaljs").val(), externaljs: $("#opt_externaljs").val(),
chat_antiflood: $("#opt_chat_antiflood").prop("checked"), chat_antiflood: $("#opt_chat_antiflood").prop("checked"),
show_public: $("#opt_show_public").prop("checked"), show_public: $("#opt_show_public").prop("checked"),
enable_link_regex: $("#opt_enable_link_regex").prop("checked") enable_link_regex: $("#opt_enable_link_regex").prop("checked"),
afk_timeout: parseInt($("#opt_afktimeout").val())
}); });
}); });
@ -141,4 +142,60 @@
$("#newfilter_flags").val("g"); $("#newfilter_flags").val("g");
$("#newfilter_replace").val(""); $("#newfilter_replace").val("");
}); });
function splitreEntry(str) {
var split = [];
var current = [];
for(var i = 0; i < str.length; i++) {
if(str[i] == "\\" && i+1 < str.length && str[i+1].match(/\s/)) {
current.push(str[i+1]);
i++;
continue;
}
else if(str[i].match(/\s/)) {
split.push(current.join(""));
current = [];
}
else {
current.push(str[i]);
}
}
split.push(current.join(""));
return split;
}
$("#multifiltersubmit").click(function () {
var lines = $("#multifiltereditor").val().split("\n");
for(var i in lines) {
var ln = lines[i];
var fields = splitreEntry(ln);
if(fields.length < 3 || fields.length > 4) {
makeAlert("Error on line "+(parseInt(i)+1)+". Format: name regex flags replacement", "alert-error")
.insertBefore($("#multifiltereditor"));
return;
}
var name = "", re = "", f = "", replace = "";
if(fields.length == 3) {
name = fields[0];
re = fields[0];
f = fields[1];
replace = fields[2];
}
else if(fields.length == 4) {
name = fields[0];
re = fields[1];
f = fields[2];
replace = fields[3];
}
socket.emit("updateFilter", {
name: name,
source: re,
flags: f,
replace: replace,
filterlinks: false,
active: true
});
}
});
})(); })();

103
www/assets/js/paginator.js Normal file
View File

@ -0,0 +1,103 @@
(function () {
var defaults = {
preLoadPage: function () { },
postLoadPage: function () { },
generator: function () { },
itemsPerPage: 20,
maxPages: 5
};
function P(items, opts) {
this.items = items;
this.opts = opts || {};
for(var k in defaults)
if(!this.opts[k])
this.opts[k] = defaults[k];
this.paginator = $("<div/>").addClass("pagination");
this.loadPage(0);
}
P.prototype.loadButtons = function (p) {
var pages = parseInt(this.items.length / this.opts.itemsPerPage+0.5);
var endcaps = pages > this.opts.maxPages;
this.paginator.html("");
if(this.items.length < this.opts.itemsPerPage)
return;
var ul = $("<ul/>").appendTo(this.paginator);
var s = p - parseInt(this.opts.maxPages / 2);
s = s < 0 ? 0 : s;
s = s + this.opts.maxPages < pages ? s : pages - this.opts.maxPages;
if(endcaps) {
var li = $("<li/>").appendTo(ul);
$("<a/>").attr("href", "javascript:void(0)")
.html("&laquo;")
.click(function () {
this.loadPage(0);
}.bind(this))
.appendTo(li);
if(p == 0)
li.addClass("disabled");
if(s > 0) {
var sep = $("<li/>").addClass("disabled")
.appendTo(ul);
$("<a/>").attr("href", "javascript:void(0)")
.html("&hellip;")
.appendTo(sep);
}
}
for(var i = s; i < s + this.opts.maxPages; i++) {
(function (i) {
var li = $("<li/>").appendTo(ul);
if(i == p)
li.addClass("active");
$("<a/>").attr("href", "javascript:void(0)")
.text(i + 1)
.click(function () {
this.loadPage(i);
}.bind(this))
.appendTo(li);
}.bind(this))(i);
}
if(endcaps) {
if(s + this.opts.maxPages < pages) {
var sep = $("<li/>").addClass("disabled")
.appendTo(ul);
$("<a/>").attr("href", "javascript:void(0)")
.html("&hellip;")
.appendTo(sep);
}
var li = $("<li/>").appendTo(ul);
$("<a/>").attr("href", "javascript:void(0)")
.html("&raquo;")
.click(function () {
this.loadPage(pages - 1);
}.bind(this))
.appendTo(li);
if(p == pages - 1)
li.addClass("disabled");
}
}
P.prototype.loadPage = function (page) {
this.opts.preLoadPage(page);
this.loadButtons(page);
var s = page * this.opts.itemsPerPage;
var e = s + this.opts.itemsPerPage;
if(e > this.items.length)
e = this.items.length;
for(var i = s; i < e; i++) {
this.opts.generator(this.items[i], page, i);
}
this.opts.postLoadPage();
}
window.Paginate = function (items, opts) {
var p = new P(items, opts);
return p;
};
})();

View File

@ -779,6 +779,7 @@ function handleModPermissions() {
$("#opt_show_public").prop("checked", CHANNEL.opts.show_public); $("#opt_show_public").prop("checked", CHANNEL.opts.show_public);
$("#opt_show_public").attr("disabled", CLIENT.rank < 3); $("#opt_show_public").attr("disabled", CLIENT.rank < 3);
$("#opt_enable_link_regex").prop("checked", CHANNEL.opts.enable_link_regex); $("#opt_enable_link_regex").prop("checked", CHANNEL.opts.enable_link_regex);
$("#opt_afktimeout").val(CHANNEL.opts.afk_timeout);
$("#opt_allow_voteskip").prop("checked", CHANNEL.opts.allow_voteskip); $("#opt_allow_voteskip").prop("checked", CHANNEL.opts.allow_voteskip);
$("#opt_voteskip_ratio").val(CHANNEL.opts.voteskip_ratio); $("#opt_voteskip_ratio").val(CHANNEL.opts.voteskip_ratio);
(function() { (function() {
@ -836,7 +837,7 @@ function handlePermissionChange() {
hasPermission("playlistjump") || hasPermission("playlistjump") ||
hasPermission("playlistdelete") || hasPermission("playlistdelete") ||
hasPermission("settemp")) { hasPermission("settemp")) {
if(USEROPTS.first_visit) { if(USEROPTS.first_visit && $("#plonotification").length == 0) {
var al = makeAlert("Playlist Options", [ var al = makeAlert("Playlist Options", [
"From the Options menu, you can choose to automatically", "From the Options menu, you can choose to automatically",
" hide the buttons on each entry (and show them when", " hide the buttons on each entry (and show them when",
@ -844,6 +845,7 @@ function handlePermissionChange() {
" style of playlist buttons.", " style of playlist buttons.",
"<br>"].join("")) "<br>"].join(""))
.addClass("span12") .addClass("span12")
.attr("id", "plonotification")
.insertBefore($("#queue")); .insertBefore($("#queue"));
al.find(".close").remove(); al.find(".close").remove();
@ -906,30 +908,9 @@ function handlePermissionChange() {
function clearSearchResults() { function clearSearchResults() {
$("#library").html(""); $("#library").html("");
$("#search_clear").remove(); $("#search_clear").remove();
$("#search_pagination").remove(); var p = $("#library").data("paginator");
} if(p) {
p.paginator.html("");
function loadSearchPage(page) {
$("#library").html("");
var results = $("#library").data("entries");
var start = page * 100;
for(var i = start; i < start + 100 && i < results.length; i++) {
var li = makeSearchEntry(results[i], false);
if(hasPermission("playlistadd")) {
if(results[i].thumb) {
addLibraryButtons(li, results[i].id, "yt");
}
else {
addLibraryButtons(li, results[i].id);
}
}
$(li).appendTo($("#library"));
}
if($("#search_pagination").length > 0) {
$("#search_pagination").find("li").each(function() {
$(this).removeClass("active");
});
$($("#search_pagination").find("li")[page]).addClass("active");
} }
} }
@ -1417,63 +1398,6 @@ function genPermissionsEditor() {
}); });
} }
function loadChannelRanksPage(page) {
var entries = $("#channelranks").data("entries");
$("#channelranks").data("page", page);
var start = page * 20;
var tbl = $("#channelranks table");
if(tbl.children().length > 1) {
$(tbl.children()[1]).remove();
}
for(var i = start; i < start + 20 && i < entries.length; i++) {
var tr = $("<tr/>").appendTo(tbl);
var name = $("<td/>").text(entries[i].name).appendTo(tr);
name.addClass(getNameColor(entries[i].rank));
var rank = $("<td/>").text(entries[i].rank)
.css("min-width", "220px")
.appendTo(tr);
(function(name) {
rank.click(function() {
if(this.find(".rank-edit").length > 0)
return;
var r = this.text();
this.text("");
var edit = $("<input/>").attr("type", "text")
.attr("placeholder", r)
.addClass("rank-edit")
.appendTo(this)
.focus();
if(parseInt(r) >= CLIENT.rank) {
edit.attr("disabled", true);
}
function save() {
var r = this.val();
var r2 = r;
if(r.trim() == "" || parseInt(r) >= CLIENT.rank || parseInt(r) < 1)
r = this.attr("placeholder");
r = parseInt(r) + "";
this.parent().text(r);
socket.emit("setChannelRank", {
user: name,
rank: parseInt(r)
});
}
edit.blur(save.bind(edit));
edit.keydown(function(ev) {
if(ev.keyCode == 13)
save.bind(edit)();
});
}.bind(rank));
})(entries[i].name);
}
if($("#channelranks_pagination").length > 0) {
$("#channelranks_pagination").find("li").each(function() {
$(this).removeClass("active");
});
$($("#channelranks_pagination").find("li")[page]).addClass("active");
}
}
function waitUntilDefined(obj, key, fn) { function waitUntilDefined(obj, key, fn) {
if(typeof obj[key] === "undefined") { if(typeof obj[key] === "undefined") {
setTimeout(function () { setTimeout(function () {

View File

@ -227,6 +227,7 @@
<script src="./assets/js/iourl.js"></script> <script src="./assets/js/iourl.js"></script>
<script src="./assets/js/player.js"></script> <script src="./assets/js/player.js"></script>
<script src="./assets/js/notwebsocket.js"></script> <script src="./assets/js/notwebsocket.js"></script>
<script src="./assets/js/paginator.js"></script>
<script src="./assets/js/util.js"></script> <script src="./assets/js/util.js"></script>
<script src="./assets/js/ui.js"></script> <script src="./assets/js/ui.js"></script>
<script src="./assets/js/callbacks.js"></script> <script src="./assets/js/callbacks.js"></script>

View File

@ -56,6 +56,13 @@
<input type="text" id="opt_maxlength" placeholder="HH:MM:SS"> <input type="text" id="opt_maxlength" placeholder="HH:MM:SS">
</div> </div>
</div> </div>
<!-- auto afk -->
<div class="control-group">
<label class="control-label" for="opt_afktimeout">Auto AFK Delay (0 to disable)</label>
<div class="controls">
<input type="text" id="opt_afktimeout" placeholder="0">
</div>
</div>
<hr> <hr>
<strong>Admin-Only Controls</strong> <strong>Admin-Only Controls</strong>
<!-- page title --> <!-- page title -->
@ -107,19 +114,6 @@
</div> </div>
<div id="filteredit" class="span12"> <div id="filteredit" class="span12">
<p>Filters will be processed in the order that they are listed here. Click and drag a row to rearrange the order. Click a regex, flags, or replacement field to edit a filter. Changes are automatically saved when you finish editing.</p> <p>Filters will be processed in the order that they are listed here. Click and drag a row to rearrange the order. Click a regex, flags, or replacement field to edit a filter. Changes are automatically saved when you finish editing.</p>
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Delete</th>
<th>Name</th>
<th>Regex</th>
<th>Flags</th>
<th>Replacement</th>
<th>Affects Links</th>
<th>Active</th>
</tr>
</thead>
</table>
<strong>Add Filter</strong> <strong>Add Filter</strong>
<form class="form-horizontal" action="javascript:void(0)"> <form class="form-horizontal" action="javascript:void(0)">
<div class="control-group"> <div class="control-group">
@ -168,6 +162,22 @@
</div> </div>
</div> </div>
</form> </form>
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Delete</th>
<th>Name</th>
<th>Regex</th>
<th>Flags</th>
<th>Replacement</th>
<th>Affects Links</th>
<th>Active</th>
</tr>
</thead>
</table>
<p>Multi-Filter Editor (format: name regex flags replacement). Only use this if you know what you are doing</p>
<textarea id="multifiltereditor" class="input-block-level" rows="10"></textarea>
<button class="btn btn-primary" id="multifiltersubmit">Add/Update</button>
</div> </div>
<div id="cssedit" class="span12"> <div id="cssedit" class="span12">
<p>Max 20KB. If you need more space, host the file externally and use the External CSS option</p> <p>Max 20KB. If you need more space, host the file externally and use the External CSS option</p>