diff --git a/acp.js b/acp.js index b69138fd..0a332608 100644 --- a/acp.js +++ b/acp.js @@ -9,10 +9,11 @@ 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. */ -var Auth = require("./auth"); -var ActionLog = require("./actionlog"); +var Logger = require("./logger"); module.exports = function (Server) { + var db = Server.db; + var ActionLog = Server.actionlog; return { init: function(user) { ActionLog.record(user.ip, user.name, "acp-init"); @@ -28,118 +29,84 @@ module.exports = function (Server) { }); user.socket.on("acp-global-ban", function(data) { - ActionLog.record(user.ip, user.name, "acp-global-ban", data.ip); - Server.db.globalBanIP(data.ip, data.note); - user.socket.emit("acp-global-banlist", Server.db.refreshGlobalBans()); + ActionLog.record(user.ip, user.name, "acp-global-ban", data.ip); + db.setGlobalIPBan(data.ip, data.note, function (err, res) { + db.listGlobalIPBans(function (err, res) { + res = res || []; + user.socket.emit("acp-global-banlist", res); + }); + }); }); user.socket.on("acp-global-unban", function(ip) { ActionLog.record(user.ip, user.name, "acp-global-unban", ip); - Server.db.globalUnbanIP(ip); - user.socket.emit("acp-global-banlist", Server.db.refreshGlobalBans()); + db.clearGlobalIPBan(ip, function (err, res) { + db.listGlobalIPBans(function (err, res) { + res = res || []; + user.socket.emit("acp-global-banlist", res); + }); + }); }); - user.socket.emit("acp-global-banlist", Server.db.refreshGlobalBans()); + db.listGlobalIPBans(function (err, res) { + res = res || []; + user.socket.emit("acp-global-banlist", res); + }); user.socket.on("acp-lookup-user", function(name) { - var db = Server.db.getConnection(); - if(!db) { - return; - } - - var query = Server.db.createQuery( - "SELECT id,uname,global_rank,profile_image,profile_text,email FROM registrations WHERE uname LIKE ?", - ["%"+name+"%"] - ); - - var res = db.querySync(query); - if(!res) - return; - - var rows = res.fetchAllSync(); - user.socket.emit("acp-userdata", rows); + db.searchUser(name, function (err, res) { + res = res || []; + user.socket.emit("acp-userdata", res); + }); }); user.socket.on("acp-lookup-channel", function (data) { - var db = Server.db.getConnection(); - if(!db) { - return; - } - - var query; - if(data.field === "owner") { - query = Server.db.createQuery( - "SELECT * FROM channels WHERE owner LIKE ?", - ["%" + data.value + "%"] - ); - } else if (data.field === "name") { - query = Server.db.createQuery( - "SELECT * FROM channels WHERE name LIKE ?", - ["%" + data.value + "%"] - ); - } else { - return; - } - - var results = db.querySync(query); - if(!results) - return; - - var rows = results.fetchAllSync(); - user.socket.emit("acp-channeldata", rows); + db.searchChannel(data.field, data.value, function (e, res) { + res = res || []; + user.socket.emit("acp-channeldata", res); + }); }); user.socket.on("acp-reset-password", function(data) { - if(Auth.getGlobalRank(data.name) >= user.global_rank) - return; - try { - var hash = Server.db.generatePasswordReset(user.ip, data.name, data.email); - ActionLog.record(user.ip, user.name, "acp-reset-password", data.name); - } - catch(e) { - user.socket.emit("acp-reset-password", { - success: false, - error: e - }); - return; - } - if(hash) { - user.socket.emit("acp-reset-password", { - success: true, - hash: hash - }); - } - else { - user.socket.emit("acp-reset-password", { - success: false, - error: "Reset failed" - }); - } + db.getGlobalRank(data.name, function (err, rank) { + if(err || rank >= user.global_rank) + return; + db.genPasswordReset(user.ip, data.name, data.email, + function (err, hash) { + var pkt = { + success: !err + }; + + if(err) { + pkt.error = err; + } else { + pkt.hash = hash; + } + + user.socket.emit("acp-reset-password", pkt); + ActionLog.record(user.ip, user.name, + "acp-reset-password", data.name); + }); + }); }); user.socket.on("acp-set-rank", function(data) { if(data.rank < 1 || data.rank >= user.global_rank) return; - if(Auth.getGlobalRank(data.name) >= user.global_rank) - return; + db.getGlobalRank(data.name, function (err, rank) { + if(err || rank >= user.global_rank) + return; - var db = Server.db.getConnection(); - if(!db) - return; - - ActionLog.record(user.ip, user.name, "acp-set-rank", data); - var query = Server.db.createQuery( - "UPDATE registrations SET global_rank=? WHERE uname=?", - [data.rank, data.name] - ); - - var res = db.querySync(query); - if(!res) - return; - - user.socket.emit("acp-set-rank", data); + db.setGlobalRank(data.name, data.rank, + function (err, res) { + ActionLog.record(user.ip, user.name, "acp-set-rank", + data); + if(!err) + user.socket.emit("acp-set-rank", data); + }); + }); }); user.socket.on("acp-list-loaded", function() { @@ -180,9 +147,10 @@ module.exports = function (Server) { }); user.socket.on("acp-actionlog-list", function () { - user.socket.emit("acp-actionlog-list", - ActionLog.getLogTypes() - ); + ActionLog.listActionTypes(function (err, types) { + if(!err) + user.socket.emit("acp-actionlog-list", types); + }); }); user.socket.on("acp-actionlog-clear", function(data) { @@ -196,13 +164,10 @@ module.exports = function (Server) { }); user.socket.on("acp-view-stats", function () { - var db = Server.db.getConnection(); - if(!db) - return; - var query = "SELECT * FROM stats ORDER BY time ASC"; - var results = db.querySync(query); - if(results) - user.socket.emit("acp-view-stats", results.fetchAllSync()); + db.listStats(function (err, res) { + if(!err) + user.socket.emit("acp-view-stats", res); + }); }); } } diff --git a/actionlog.js b/actionlog.js index 312a2f74..4fb864b6 100644 --- a/actionlog.js +++ b/actionlog.js @@ -9,143 +9,50 @@ 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. */ -var Database = require("./database"); var Logger = require("./logger"); -exports.record = function(ip, name, action, args) { - if(typeof args === "undefined" || args === null) { - args = ""; - } else { - try { - args = JSON.stringify(args); - } catch(e) { - args = ""; +module.exports = function (Server) { + var db = Server.db; + return { + record: function (ip, name, action, args) { + if(!args) + args = ""; + else { + try { + args = JSON.stringify(args); + } catch(e) { + args = ""; + } + } + + db.recordAction(ip, name, action, args); + }, + + clear: function (actions) { + db.clearActions(actions); + }, + + clearOne: function (item) { + db.clearSingleAction(item); + }, + + throttleRegistrations: function (ip, callback) { + db.recentRegistrationCount(ip, function (err, count) { + if(err) { + callback(err, null); + return; + } + + callback(null, count > 4); + }); + }, + + listActionTypes: function (callback) { + db.listActionTypes(callback); + }, + + listActions: function (types, callback) { + db.listActions(types, callback); } - } - - var db = Database.getConnection(); - if(!db) - return false; - - var query = Database.createQuery( - "INSERT INTO actionlog (ip, name, action, args, time) "+ - "VALUES (?, ?, ?, ?, ?)", - [ip, name, action, args, Date.now()] - ); - - var result = db.querySync(query); - if(!result) { - Logger.errlog.log("! Failed to record action"); - } - - return result; -} - -exports.clear = function(actions) { - var db = Database.getConnection(); - if(!db) - return false; - - var list = new Array(actions.length); - for(var i = 0; i < actions.length; i++) - list[i] = "?"; - - var query = Database.createQuery( - "DELETE FROM actionlog WHERE action IN ("+ - list.join(",")+ - ")", - actions - ); - - var result = db.querySync(query); - if(!result) { - Logger.errlog.log("! Failed to clear action log"); - } - - return result; -} - -exports.clearOne = function(e) { - var db = Database.getConnection(); - if(!db) - return false; - - var query = Database.createQuery( - "DELETE FROM actionlog WHERE ip=? AND time=?", - [e.ip, e.time] - ); - - var result = db.querySync(query); - if(!result) { - Logger.errlog.log("! Failed to clear action log"); - } - - return result; -} - -exports.tooManyRegistrations = function (ip) { - var db = Database.getConnection(); - if(!db) - return true; - - var query = Database.createQuery( - "SELECT * FROM actionlog WHERE ip=? AND action='register-success'"+ - "AND time > ?", - [ip, Date.now() - 48 * 3600 * 1000] - ); - - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to check tooManyRegistrations"); - return true; - } - - var rows = results.fetchAllSync(); - // TODO Config value for this - return rows.length > 4; -} - -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(); - if(!db) - return false; - - 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); - if(!result) { - Logger.errlog.log("! Failed to read action log"); - return []; - } - - return result.fetchAllSync(); -} + }; +}; diff --git a/api.js b/api.js index 607b8041..2c92e8f0 100644 --- a/api.js +++ b/api.js @@ -9,14 +9,12 @@ 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. */ -var Auth = require("./auth"); var Logger = require("./logger"); -var apilog = new Logger.Logger("api.log"); -var ActionLog = require("./actionlog"); var fs = require("fs"); - +var $util = require("./utilities"); module.exports = function (Server) { + var ActionLog = Server.actionlog; function getIP(req) { var raw = req.connection.remoteAddress; var forward = req.header("x-forwarded-for"); @@ -28,257 +26,317 @@ module.exports = function (Server) { return raw; } - var API = function () { + function getChannelData(channel) { + var data = { + name: channel.name, + loaded: true + }; + data.pagetitle = channel.opts.pagetitle; + data.media = channel.playlist.current ? + channel.playlist.current.media.pack() : + {}; + data.usercount = channel.users.length; + data.afkcount = channel.afkers.length; + data.users = []; + for(var i in channel.users) + if(channel.users[i].name !== "") + data.users.push(channel.users[i].name); + + data.chat = []; + for(var i in channel.chatbuffer) + data.chat.push(channel.chatbuffer[i]); + + return data; } - API.prototype = { - handle: function (path, req, res) { - var parts = path.split("/"); - var last = parts[parts.length - 1]; - var params = {}; - if(last.indexOf("?") != -1) { - parts[parts.length - 1] = last.substring(0, last.indexOf("?")); - var plist = last.substring(last.indexOf("?") + 1).split("&"); - for(var i = 0; i < plist.length; i++) { - var kv = plist[i].split("="); - if(kv.length != 2) { - res.send(400); - return; - } - params[unescape(kv[0])] = unescape(kv[1]); - } - } - for(var i = 0; i < parts.length; i++) { - parts[i] = unescape(parts[i]); - } - if(parts.length != 2) { - res.send(400); + var app = Server.app; + var db = Server.db; + + /* */ + app.get("/api/coffee", function (req, res) { + res.send(418); // 418 I'm a teapot + }); + + /* REGION channels */ + + /* data about a specific channel */ + app.get("/api/channels/:channel", function (req, res) { + var name = req.params.channel; + if(!name.match(/^[\w-_]+$/)) { + res.send(404); + return; + } + + var data = { + name: name, + loaded: false + }; + + if(Server.channelLoaded(name)) + data = getChannelData(Server.getChannel(name)); + + res.type("application/json"); + res.jsonp(data); + }); + + /* data about all channels (filter= public or all) */ + app.get("/api/allchannels/:filter", function (req, res) { + var filter = req.params.filter; + if(filter !== "public" && filter !== "all") { + res.send(400); + return; + } + + var query = req.query; + + // Listing non-public channels requires authenticating as an admin + if(filter !== "public") { + var name = query.name || ""; + var session = query.session || ""; + db.userLoginSession(name, session, function (err, row) { + if(err) { + if(err !== "Invalid session" && + err !== "Session expired") { + res.send(500); + } else { + res.send(403); + } + return; + } + + if(row.global_rank < 255) { + res.send(403); + return; + } + + var channels = []; + for(var key in Server.channels) { + var channel = Server.channels[key]; + channels.push(getChannelData(channel)); + } + + res.type("application/jsonp"); + res.jsonp(channels); + }); + } + + // If we get here, the filter is public channels + + var channels = []; + for(var key in Server.channels) { + var channel = Server.channels[key]; + if(channel.opts.show_public) + channels.push(getChannelData(channel)); + } + + res.type("application/jsonp"); + res.jsonp(channels); + }); + + /* ENDREGION channels */ + + /* REGION authentication, account management */ + + /* login */ + app.post("/api/login", function (req, res) { + res.type("application/jsonp"); + res.setHeader("Access-Control-Allow-Origin", "*"); + var name = req.body.name; + var pw = req.body.pw; + var session = req.body.session; + + // for some reason CyTube previously allowed guest logins + // over the API...wat + if(!pw && !session) { + res.jsonp({ + success: false, + error: "You must provide a password" + }); + return; + } + + db.userLogin(name, pw, session, function (err, row) { + if(err) { + if(err !== "Session expired") + ActionLog.record(getIP(req), name, "login-failure"); + res.jsonp({ + success: false, + error: err + }); + return; + } + + // Only record login-success for admins + if(row.global_rank >= 255) + ActionLog.record(getIP(req), name, "login-success"); + + res.jsonp({ + success: true, + name: name, + session: row.session_hash + }); + }); + }); + + /* register an account */ + app.post("/api/register", function (req, res) { + res.type("application/jsonp"); + res.setHeader("Access-Control-Allow-Origin", "*"); + var name = req.body.name; + var pw = req.body.pw; + var ip = getIP(req); + + // Limit registrations per IP within a certain time period + ActionLog.throttleRegistrations(ip, function (err, toomany) { + if(err) { + res.jsonp({ + success: false, + error: err + }); + return; + } + + if(toomany) { + ActionLog.record(ip, name, "register-failure", + "Too many recent registrations"); + res.jsonp({ + success: false, + error: "Your IP address has registered too many " + + "accounts in the past 48 hours. Please wait " + + "a while before registering another." + }); return; } - if(parts[0] == "json") { - res.callback = params.callback || false; - if(!(parts[1] in this.jsonHandlers)) { - res.end(JSON.stringify({ - error: "Unknown endpoint: " + parts[1] - }, null, 4)); - return; - } - this.jsonHandlers[parts[1]](params, req, res); - } - else if(parts[0] == "plain") { - if(!(parts[1] in this.plainHandlers)) { - res.send(404); - return; - } - this.plainHandlers[parts[1]](params, req, res); - } - else { - res.send(400); - } - }, - - sendJSON: function (res, obj) { - var response = JSON.stringify(obj, null, 4); - if(res.callback) { - response = res.callback + "(" + response + ")"; - } - var len = unescape(encodeURIComponent(response)).length; - - res.setHeader("Content-Type", "application/json"); - res.setHeader("Content-Length", len); - res.end(response); - }, - - sendPlain: function (res, str) { - if(res.callback) { - str = res.callback + "('" + str + "')"; - } - var len = unescape(encodeURIComponent(str)).length; - - res.setHeader("Content-Type", "text/plain"); - res.setHeader("Content-Length", len); - res.end(response); - }, - - handleChannelData: function (params, req, res) { - var clist = params.channel || ""; - clist = clist.split(","); - var data = []; - for(var j = 0; j < clist.length; j++) { - var cname = clist[j]; - if(!cname.match(/^[a-zA-Z0-9-_]+$/)) { - continue; - } - - var d = { - name: cname, - loaded: Server.channelLoaded(cname) - }; - - if(d.loaded) { - var chan = Server.getChannel(cname); - d.pagetitle = chan.opts.pagetitle; - d.media = chan.playlist.current ? chan.playlist.current.media.pack() : {}; - d.usercount = chan.users.length; - d.users = []; - for(var i = 0; i < chan.users.length; i++) { - if(chan.users[i].name) { - d.users.push(chan.users[i].name); - } - } - d.chat = []; - for(var i = 0; i < chan.chatbuffer.length; i++) { - d.chat.push(chan.chatbuffer[i]); - } - } - data.push(d); - } - - this.sendJSON(res, data); - }, - - handleChannelList: function (params, req, res) { - if(params.filter == "public") { - var all = Server.channels; - var clist = []; - for(var key in all) { - if(all[key].opts.show_public) { - clist.push(all[key].name); - } - } - this.handleChannelData({channel: clist.join(",")}, req, res); - } - var session = params.session || ""; - var name = params.name || ""; - var pw = params.pw || ""; - var row = Auth.login(name, pw, session); - if(!row || row.global_rank < 255) { - res.send(403); + if(!pw) { + // costanza.jpg + res.jsonp({ + success: false, + error: "You must provide a password" + }); return; } - var clist = []; - for(var key in Server.channels) { - clist.push(Server.channels[key].name); + + + if(!$util.isValidUserName(name)) { + ActionLog.record(ip, name, "register-failure", + "Invalid name"); + res.jsonp({ + success: false, + error: "Invalid username. Valid usernames must be " + + "1-20 characters long and consist only of " + + "alphanumeric characters and underscores (_)" + }); + return; } - this.handleChannelData({channel: clist.join(",")}, req, res); - }, - handleLogin: function (params, req, res) { - var session = params.session || ""; - var name = params.name || ""; - var pw = params.pw || ""; - - if(pw == "" && session == "") { - if(!Auth.isRegistered(name)) { - this.sendJSON(res, { - success: true, - session: "" - }); - return; - } - else { - this.sendJSON(res, { + // db.registerUser checks if the name is taken already + db.registerUser(name, pw, function (err, session) { + if(err) { + res.jsonp({ success: false, - error: "That username is already taken" + error: err }); return; } - } - var row = Auth.login(name, pw, session); - if(row) { - if(row.global_rank >= 255) - ActionLog.record(getIP(req), name, "login-success"); - this.sendJSON(res, { + ActionLog.record(ip, name, "register-success"); + res.jsonp({ success: true, - session: row.session_hash + session: session }); - } - else { - ActionLog.record(getIP(req), name, "login-failure"); - this.sendJSON(res, { - error: "Invalid username/password", - success: false - }); - } - }, + }); + }); + }); - handlePasswordChange: function (params, req, res) { - var name = params.name || ""; - var oldpw = params.oldpw || ""; - var newpw = params.newpw || ""; - if(oldpw == "" || newpw == "") { - this.sendJSON(res, { + /* password change */ + app.post("/api/account/passwordchange", function (req, res) { + res.type("application/jsonp"); + res.setHeader("Access-Control-Allow-Origin", "*"); + + var name = req.body.name; + var oldpw = req.body.oldpw; + var newpw = req.body.newpw; + + if(!oldpw || !newpw) { + res.jsonp({ + success: false, + error: "Password cannot be empty" + }); + return; + } + + db.userLoginPassword(name, oldpw, function (err, row) { + if(err) { + res.jsonp({ success: false, - error: "Old password and new password cannot be empty" + error: err }); return; } - var row = Auth.login(name, oldpw); - if(row) { + + db.setUserPassword(name, newpw, function (err, row) { + if(err) { + res.jsonp({ + success: false, + error: err + }); + return; + } + ActionLog.record(getIP(req), name, "password-change"); - var success = Auth.setUserPassword(name, newpw); - this.sendJSON(res, { - success: success, - error: success ? "" : "Change password failed", - session: row.session_hash + res.jsonp({ + success: true }); - } - else { - this.sendJSON(res, { - success: false, - error: "Invalid username/password" - }); - } - }, + }); + }); + }); - handlePasswordReset: function (params, req, res) { - var name = params.name || ""; - var email = params.email || ""; - var ip = getIP(req); + /* password reset */ + app.post("/api/account/passwordreset", function (req, res) { + res.type("application/jsonp"); + res.setHeader("Access-Control-Allow-Origin", "*"); + var name = req.body.name; + var email = req.body.email; + var ip = getIP(req); + var hash = false; - var hash = false; - try { - hash = Server.db.generatePasswordReset(ip, name, email); - ActionLog.record(ip, name, "password-reset-generate", email); - } - catch(e) { - this.sendJSON(res, { + db.genPasswordReset(ip, name, email, function (err, hash) { + if(err) { + res.jsonp({ success: false, - error: e + error: err }); return; } - + ActionLog.record(ip, name, "password-reset-generate", email); if(!Server.cfg["enable-mail"]) { - this.sendJSON(res, { + res.jsonp({ success: false, - error: "This server does not have email enabled. Contact an administrator" + error: "This server does not have email recovery " + + "enabled. Contact an administrator for " + + "assistance." }); return; } + if(!email) { - this.sendJSON(res, { + res.jsonp({ success: false, - error: "You don't have a recovery email address set. Contact an administrator" + error: "You don't have a recovery email address set. "+ + "Contact an administrator for assistance." }); return; } - var msg = [ - "A password reset request was issued for your account `", - name, - "` on ", - Server.cfg["domain"], - ". This request is valid for 24 hours. ", - "If you did not initiate this, there is no need to take action. ", - "To reset your password, copy and paste the following link into ", - "your browser: ", - Server.cfg["domain"], - "/reset.html?", - hash - ].join(""); + + var msg = "A password reset request was issued for your " + + "account '"+ name + "' on " + Server.cfg["domain"] + + ". This request is valid for 24 hours. If you did "+ + "not initiate this, there is no need to take action."+ + " To reset your password, copy and paste the " + + "following link into your browser: " + + Server.cfg["domain"] + "/reset.html?"+hash; var mail = { from: "CyTube Services <" + Server.cfg["mail-from"] + ">", @@ -286,334 +344,347 @@ module.exports = function (Server) { subject: "Password reset request", text: msg }; - var api = this; - Server.cfg["nodemailer"].sendMail(mail, function(err, response) { + + Server.cfg["nodemailer"].sendMail(mail, function (err, response) { if(err) { - Logger.errlog.log("Mail fail: " + err); - api.sendJSON(res, { + Logger.errlog.log("mail fail: " + err); + res.jsonp({ success: false, - error: "Email failed. Contact an admin if this persists." + error: "Email send failed. Contact an administrator "+ + "if this persists" }); - } - else { - api.sendJSON(res, { + } else { + res.jsonp({ success: true }); - - if(Server.cfg["debug"]) { - Logger.syslog.log(response); - } } }); - }, + }); + }); - handlePasswordRecover: function (params, req, res) { - var hash = params.hash || ""; - var ip = getIP(req); + /* password recovery */ + app.get("/api/account/passwordrecover", function (req, res) { + res.type("application/jsonp"); + var hash = req.query.hash; + var ip = getIP(req); - try { - var info = Server.db.recoverPassword(hash); - this.sendJSON(res, { - success: true, - name: info[0], - pw: info[1] - }); - ActionLog.record(ip, info[0], "password-recover-success"); - Logger.syslog.log(ip + " recovered password for " + info[0]); - return; - } - catch(e) { - ActionLog.record(ip, "", "password-recover-failure"); - this.sendJSON(res, { + db.recoverUserPassword(hash, function (err, auth) { + if(err) { + ActionLog.record(ip, "", "password-recover-failure", hash); + res.jsonp({ success: false, - error: e - }); - } - }, - - handleProfileGet: function (params, req, res) { - var name = params.name || ""; - - try { - var prof = Server.db.getProfile(name); - this.sendJSON(res, { - success: true, - profile_image: prof.profile_image, - profile_text: prof.profile_text - }); - } - catch(e) { - this.sendJSON(res, { - success: false, - error: e - }); - } - }, - - handleProfileChange: function (params, req, res) { - var name = params.name || ""; - var pw = params.pw || ""; - var session = params.session || ""; - var img = params.profile_image || ""; - var text = params.profile_text || ""; - - var row = Auth.login(name, pw, session); - if(!row) { - this.sendJSON(res, { - success: false, - error: "Invalid login" + error: err }); return; } - - var result = Server.db.setProfile(name, { - image: img, - text: text - }); - - this.sendJSON(res, { - success: result, - error: result ? "" : "Internal error. Contact an administrator" - }); - - var all = Server.channels; - for(var n in all) { - var chan = all[n]; - for(var i = 0; i < chan.users.length; i++) { - if(chan.users[i].name.toLowerCase() == name) { - chan.users[i].profile = { - image: img, - text: text - }; - chan.broadcastUserUpdate(chan.users[i]); - break; - } - } - } - }, - - handleEmailChange: function (params, req, res) { - var name = params.name || ""; - var pw = params.pw || ""; - var email = params.email || ""; - // perhaps my email regex isn't perfect, but there's no freaking way - // I'm implementing this monstrosity: - // - if(!email.match(/^[a-z0-9_\.]+@[a-z0-9_\.]+[a-z]+$/)) { - this.sendJSON(res, { - success: false, - error: "Invalid email" - }); - return; - } - - if(email.match(/.*@(localhost|127\.0\.0\.1)/i)) { - this.sendJSON(res, { - success: false, - error: "Nice try, but no." - }); - return; - } - - if(pw == "") { - this.sendJSON(res, { - success: false, - error: "Password cannot be empty" - }); - return; - } - var row = Auth.login(name, pw); - if(row) { - var success = Server.db.setUserEmail(name, email); - ActionLog.record(getIP(req), name, "email-update", email); - this.sendJSON(res, { - success: success, - error: success ? "" : "Email update failed", - session: row.session_hash - }); - } - else { - this.sendJSON(res, { - success: false, - error: "Invalid username/password" - }); - } - }, - - handleRegister: function (params, req, res) { - var name = params.name || ""; - var pw = params.pw || ""; - if(ActionLog.tooManyRegistrations(getIP(req))) { - ActionLog.record(getIP(req), name, "register-failure", - "Too many recent registrations from this IP"); - this.sendJSON(res, { - success: false, - error: "Your IP address has registered several accounts in "+ - "the past 48 hours. Please wait a while or ask an "+ - "administrator for assistance." - }); - return; - } - - if(pw == "") { - this.sendJSON(res, { - success: false, - error: "You must provide a password" - }); - return; - } - else if(Auth.isRegistered(name)) { - ActionLog.record(getIP(req), name, "register-failure", - "Name taken"); - this.sendJSON(res, { - success: false, - error: "That username is already taken" - }); - return false; - } - else if(!Auth.validateName(name)) { - ActionLog.record(getIP(req), name, "register-failure", - "Invalid name"); - this.sendJSON(res, { - success: false, - error: "Invalid username. Usernames must be 1-20 characters long and consist only of alphanumeric characters and underscores" - }); - } - else { - var session = Auth.register(name, pw); - if(session) { - ActionLog.record(getIP(req), name, "register-success"); - Logger.syslog.log(getIP(req) + " registered " + name); - this.sendJSON(res, { - success: true, - session: session - }); - } - else { - this.sendJSON(res, { - success: false, - error: "Registration error. Contact an admin for assistance." - }); - } - } - }, - - handleListUserChannels: function (params, req, res) { - var name = params.name || ""; - var pw = params.pw || ""; - var session = params.session || ""; - - var row = Auth.login(name, pw, session); - if(!row) { - this.sendJSON(res, { - success: false, - error: "Invalid login" - }); - return; - } - - var channels = Server.db.listUserChannels(name); - - this.sendJSON(res, { + ActionLog.record(ip, info[0], "password-recover-success"); + res.jsonp({ success: true, - channels: channels + name: auth.name, + pw: auth.pw }); - }, + }); + }); - handleAdmReports: function (params, req, res) { - this.sendJSON(res, { - error: "Not implemented" - }); - }, + /* profile retrieval */ + app.get("/api/users/:user/profile", function (req, res) { + res.type("application/jsonp"); + var name = req.params.user; - handleReadActionLog: function (params, req, res) { - var name = params.name || ""; - var pw = params.pw || ""; - var session = params.session || ""; - var types = params.actions || ""; - var row = Auth.login(name, pw, session); - if(!row || row.global_rank < 255) { - res.send(403); + db.getUserProfile(name, function (err, profile) { + if(err) { + res.jsonp({ + success: false, + error: err + }); return; } - var actiontypes = types.split(","); - var actions = ActionLog.readLog(actiontypes); - this.sendJSON(res, actions); - }, + res.jsonp({ + success: true, + profile_image: profile.profile_image, + profile_text: profile.profile_text + }); + }); + }); - // Helper function - pipeLast: function (res, file, len) { - fs.stat(file, function(err, data) { + /* profile change */ + app.post("/api/account/profile", function (req, res) { + res.type("application/jsonp"); + res.setHeader("Access-Control-Allow-Origin", "*"); + var name = req.body.name; + var session = req.body.session; + var img = req.body.profile_image; + var text = req.body.profile_text; + + db.userLoginSession(name, session, function (err, row) { + if(err) { + res.jsonp({ + success: false, + error: err + }); + return; + } + + db.setUserProfile(name, { image: img, text: text }, + function (err, dbres) { if(err) { - res.send(500); + res.jsonp({ + success: false, + error: err + }); return; } - var start = data.size - len; - if(start < 0) { - start = 0; - } - var end = data.size - 1; - fs.createReadStream(file, {start: start, end: end}).pipe(res); - }); - }, - handleReadLog: function (params, req, res) { - var name = params.name || ""; - var pw = params.pw || ""; - var session = params.session || ""; - var row = Auth.login(name, pw, session); - if(!row || row.global_rank < 255) { + res.jsonp({ success: true }); + name = name.toLowerCase(); + for(var i in Server.channels) { + var chan = Server.channels[i]; + for(var j in chan.users) { + var user = chan.users[j]; + if(user.name.toLowerCase() == name) { + user.profile = { + image: img, + text: text + }; + chan.broadcastUserUpdate(user); + } + } + } + }); + }); + }); + + /* set email */ + app.post("/api/account/email", function (req, res) { + res.type("application/jsonp"); + res.setHeader("Access-Control-Allow-Origin", "*"); + var name = req.body.name; + var pw = req.body.pw; + var email = req.body.email; + + if(!email.match(/^[\w_\.]+@[\w_\.]+[a-z]+$/i)) { + res.jsonp({ + success: false, + error: "Invalid email address" + }); + return; + } + + if(email.match(/.*@(localhost|127\.0\.0\.1)/i)) { + res.jsonp({ + success: false, + error: "Nice try, but no" + }); + return; + } + + db.userLoginPassword(name, pw, function (err, row) { + if(err) { + res.jsonp({ + success: false, + error: err + }); + return; + } + + db.setUserEmail(name, email, function (err, dbres) { + if(err) { + res.jsonp({ + success: false, + error: err + }); + return; + } + + ActionLog.record(getIP(req), name, "email-update", email); + res.jsonp({ + success: true, + session: row.session_hash + }); + }); + }); + }); + + /* my channels */ + app.get("/api/account/mychannels", function (req, res) { + res.type("application/jsonp"); + var name = req.query.name; + var session = req.query.session; + + db.userLoginSession(name, session, function (err, row) { + if(err) { + res.jsonp({ + success: false, + error: err + }); + return; + } + + db.listUserChannels(name, function (err, dbres) { + if(err) { + res.jsonp({ + success: false, + channels: [] + }); + return; + } + + res.jsonp({ + success: true, + channels: dbres + }); + }); + }); + + }); + + /* END REGION */ + + /* REGION log reading */ + + /* action log */ + app.get("/api/logging/actionlog", function (req, res) { + res.type("application/jsonp"); + var name = req.query.name; + var session = req.query.session; + var types = req.query.actions; + + db.userLoginSession(name, session, function (err, row) { + if(err) { + if(err !== "Invalid session" && + err !== "Session expired") { + res.send(500); + } else { + res.send(403); + } + return; + } + + if(row.global_rank < 255) { res.send(403); return; } - res.setHeader("Access-Control-Allow-Origin", "*"); - var type = params.type || ""; - if(type == "sys") { - this.pipeLast(res, "sys.log", 1024*1024); + types = types.split(","); + ActionLog.listActions(types, function (err, actions) { + if(err) + actions = []; + + res.jsonp(actions); + }); + }); + }); + + /* helper function to pipe the last N bytes of a file */ + function pipeLast(res, file, len) { + fs.stat(file, function (err, data) { + if(err) { + res.send(500); + return; } - else if(type == "err") { - this.pipeLast(res, "error.log", 1024*1024); + var start = data.size - len; + if(start < 0) { + start = 0; } - else if(type == "channel") { - var chan = params.channel || ""; - fs.exists("chanlogs/" + chan + ".log", function(exists) { - if(exists) { - this.pipeLast(res, "chanlogs/" + chan + ".log", 1024*1024); - } - else { - res.send(404); - } - }.bind(this)); + var end = data.size - 1; + fs.createReadStream(file, { start: start, end: end }) + .pipe(res); + }); + } + + app.get("/api/logging/syslog", function (req, res) { + res.type("text/plain"); + res.setHeader("Access-Control-Allow-Origin", "*"); + + var name = req.query.name; + var session = req.query.session; + + db.userLoginSession(name, session, function (err, row) { + if(err) { + if(err !== "Invalid session" && + err !== "Session expired") { + res.send(500); + } else { + res.send(403); + } + return; } - else { + + if(row.global_rank < 255) { + res.send(403); + return; + } + + pipeLast(res, "sys.log", 1048576); + }); + }); + + app.get("/api/logging/errorlog", function (req, res) { + res.type("text/plain"); + res.setHeader("Access-Control-Allow-Origin", "*"); + + var name = req.query.name; + var session = req.query.session; + + db.userLoginSession(name, session, function (err, row) { + if(err) { + if(err !== "Invalid session" && + err !== "Session expired") { + res.send(500); + } else { + res.send(403); + } + return; + } + + if(row.global_rank < 255) { + res.send(403); + return; + } + + pipeLast(res, "error.log", 1048576); + }); + }); + + app.get("/api/logging/channels/:channel", function (req, res) { + res.type("text/plain"); + res.setHeader("Access-Control-Allow-Origin", "*"); + + var name = req.query.name; + var session = req.query.session; + + db.userLoginSession(name, session, function (err, row) { + if(err) { + if(err !== "Invalid session" && + err !== "Session expired") { + res.send(500); + } else { + res.send(403); + } + return; + } + + if(row.global_rank < 255) { + res.send(403); + return; + } + + var chan = req.params.channel || ""; + if(!$util.isValidChannelName(chan)) { res.send(400); + return; } - } - }; - var api = new API(); + fs.exists("chanlogs/" + chan + ".log", function(exists) { + if(exists) { + pipeLast(res, "chanlogs/" + chan + ".log", 1048576); + } else { + res.send(404); + } + }); + }); + }); - api.plainHandlers = { - "readlog" : api.handleReadLog.bind(api) - }; - - api.jsonHandlers = { - "channeldata" : api.handleChannelData.bind(api), - "listloaded" : api.handleChannelList.bind(api), - "login" : api.handleLogin.bind(api), - "register" : api.handleRegister.bind(api), - "changepass" : api.handlePasswordChange.bind(api), - "resetpass" : api.handlePasswordReset.bind(api), - "recoverpw" : api.handlePasswordRecover.bind(api), - "setprofile" : api.handleProfileChange.bind(api), - "getprofile" : api.handleProfileGet.bind(api), - "listuserchannels": api.handleListUserChannels.bind(api), - "setemail" : api.handleEmailChange.bind(api), - "admreports" : api.handleAdmReports.bind(api), - "readactionlog" : api.handleReadActionLog.bind(api) - }; - - return api; + return null; } diff --git a/auth.js b/auth.js deleted file mode 100644 index 89fb8efc..00000000 --- a/auth.js +++ /dev/null @@ -1,226 +0,0 @@ -/* -The MIT License (MIT) -Copyright (c) 2013 Calvin Montgomery - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of 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. -*/ - -var mysql = require("mysql-libmysqlclient"); -var Database = require("./database.js"); -var bcrypt = require("bcrypt"); -var hashlib = require("node_hash"); -var Logger = require("./logger.js"); - -// Check if a name is taken -exports.isRegistered = function(name) { - var db = Database.getConnection(); - if(!db) { - throw "Database failure"; - } - var query = Database.createQuery( - "SELECT * FROM `registrations` WHERE uname=?", - [name] - ); - var results = db.querySync(query); - if(!results) { - return true; - } - - var rows = results.fetchAllSync(); - return rows.length > 0; -} - -// Check if a name is valid -// Valid names are 1-20 characters, alphanumeric and underscores -exports.validateName = function(name) { - if(name.length > 20) - return false; - const VALID_REGEX = /^[a-zA-Z0-9_]+$/; - return name.match(VALID_REGEX) != null; -} - -// Try to register a new account -exports.register = function(name, pw) { - if(!exports.validateName(name)) - return false; - if(exports.isRegistered(name)) - return false; - var db = Database.getConnection(); - if(!db) { - return false; - } - - var hash = bcrypt.hashSync(pw, 10); - var query = Database.createQuery( - ["INSERT INTO `registrations` VALUES ", - "(NULL, ?, ?, 1, '', 0, '', '', '')"].join(""), - [name, hash] - ); - var results = db.querySync(query); - if(results) { - return exports.createSession(name); - } - return false; -} - -exports.login = function(name, pw, session) { - if(session) { - var res = exports.loginSession(name, session); - if(res) { - return res; - } - else if(!pw) { - return false; - } - } - var row = exports.loginPassword(name, pw); - if(row) { - var hash = exports.createSession(name); - row.session_hash = hash; - return row; - } -} - -// Try to login -exports.loginPassword = function(name, pw) { - var db = Database.getConnection(); - if(!db) { - throw "Database failure"; - } - var query = Database.createQuery( - "SELECT * FROM `registrations` WHERE uname=?", - [name] - ); - var results = db.querySync(query); - if(!results) { - return false; - } - var rows = results.fetchAllSync(); - if(rows.length > 0) { - try { - if(bcrypt.compareSync(pw, rows[0].pw)) { - return rows[0]; - } - else { - // Check if the sha256 is in the database - // If so, migrate to bcrypt - var sha256 = hashlib.sha256(pw); - if(sha256 == rows[0].pw) { - var newhash = bcrypt.hashSync(pw, 10); - var query = Database.createQuery( - ["UPDATE `registrations` SET pw=?", - "WHERE uname=?"].join(""), - [newhash, name] - ); - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("Failed to migrate password! user=" + name); - return false; - } - return rows[0]; - } - return false; - } - } - catch(e) { - Logger.errlog.log("Auth.login fail"); - Logger.errlog.log(e); - } - } - return false; -} - -exports.createSession = function(name) { - var salt = sessionSalt(); - var hash = hashlib.sha256(salt + name); - var db = Database.getConnection(); - if(!db) { - throw "Database failure"; - } - var query = Database.createQuery( - ["UPDATE `registrations` SET ", - "`session_hash`=?,", - "`expire`=? ", - "WHERE uname=?"].join(""), - [hash, Date.now() + 604800000, name] - ); - var results = db.querySync(query); - return results ? hash : false; -} - -exports.loginSession = function(name, hash) { - var db = Database.getConnection(); - if(!db) { - throw "Database failure"; - } - var query = Database.createQuery( - "SELECT * FROM `registrations` WHERE `uname`=?", - [name] - ); - var results = db.querySync(query); - if(!results) { - return false; - } - var rows = results.fetchAllSync(); - if(rows.length != 1) { - return false; - } - - var dbhash = rows[0].session_hash; - if(hash != dbhash) { - return false; - } - var timeout = rows[0].expire; - if(timeout < new Date().getTime()) { - return false; - } - return rows[0]; -} - -function sessionSalt() { - var chars = "abcdefgihjklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - + "0123456789!@#$%^&*_+=~"; - var salt = []; - for(var i = 0; i < 32; i++) { - salt.push(chars[parseInt(Math.random()*chars.length)]); - } - return salt.join(''); -} - -exports.setUserPassword = function(name, pw) { - var db = Database.getConnection(); - if(!db) { - return false; - } - var hash = bcrypt.hashSync(pw, 10); - var query = Database.createQuery( - "UPDATE `registrations` SET `pw`=? WHERE `uname`=?", - [hash, name] - ); - var result = db.querySync(query); - return result; -} - -exports.getGlobalRank = function(name) { - var db = Database.getConnection(); - if(!db) { - return false; - } - var query = Database.createQuery( - "SELECT * FROM `registrations` WHERE `uname`=?", - [name] - ); - var results = db.querySync(query); - if(!results) { - return 0; - } - var rows = results.fetchAllSync(); - if(rows.length > 0) { - return rows[0].global_rank; - } - return 0; -} diff --git a/channel.js b/channel.js index c85ced24..eb932e62 100644 --- a/channel.js +++ b/channel.js @@ -16,34 +16,34 @@ var Media = require("./media.js").Media; var formatTime = require("./media.js").formatTime; var Logger = require("./logger.js"); var Rank = require("./rank.js"); -var Auth = require("./auth.js"); var ChatCommand = require("./chatcommand.js"); var Filter = require("./filter.js").Filter; -var ActionLog = require("./actionlog"); var Playlist = require("./playlist"); var sanitize = require("validator").sanitize; +var $util = require("./utilities"); var Channel = function(name, Server) { + var self = this; Logger.syslog.log("Opening channel " + name); - this.initialized = false; - this.server = Server; + self.initialized = false; + self.dbloaded = false; + self.server = Server; - this.name = name; - this.canonical_name = name.toLowerCase(); + self.name = name; + self.canonical_name = name.toLowerCase(); // Initialize defaults - this.registered = false; - this.users = []; - this.afkers = []; - this.playlist = new Playlist(this); - this.library = {}; - this.position = -1; - this.drinks = 0; - this.leader = null; - this.chatbuffer = []; - this.openqueue = false; - this.poll = false; - this.voteskip = false; - this.permissions = { + self.registered = false; + self.users = []; + self.afkers = []; + self.playlist = new Playlist(self); + self.position = -1; + self.drinks = 0; + self.leader = null; + self.chatbuffer = []; + self.openqueue = false; + self.poll = false; + self.voteskip = false; + self.permissions = { oplaylistadd: -1, oplaylistnext: 1.5, oplaylistmove: 1.5, @@ -74,11 +74,11 @@ var Channel = function(name, Server) { drink: 1.5, chat: 0 }; - this.opts = { + self.opts = { allow_voteskip: true, voteskip_ratio: 0.5, afk_timeout: 180, - pagetitle: this.name, + pagetitle: self.name, maxlength: 0, externalcss: "", externaljs: "", @@ -86,42 +86,44 @@ var Channel = function(name, Server) { show_public: false, enable_link_regex: true }; - this.filters = [ + self.filters = [ new Filter("monospace", "`([^`]+)`", "g", "$1"), new Filter("bold", "(^|\\s)\\*([^\\*]+)\\*", "g", "$1$2"), new Filter("italic", "(^| )_([^_]+)_", "g", "$1$2"), new Filter("strikethrough", "~~([^~]+)~~", "g", "$1"), new Filter("inline spoiler", "\\[spoiler\\](.*)\\[\\/spoiler\\]", "ig", "$1"), ]; - this.motd = { + self.motd = { motd: "", html: "" }; - this.ipbans = {}; - this.namebans = {}; - this.ip_alias = {}; - this.name_alias = {}; - this.login_hist = []; - this.logger = new Logger.Logger("chanlogs/" + this.canonical_name + ".log"); - this.i = 0; - this.time = new Date().getTime(); - this.plmeta = { + self.ipbans = {}; + self.namebans = {}; + self.ip_alias = {}; + self.name_alias = {}; + self.login_hist = []; + self.logger = new Logger.Logger("chanlogs/" + self.canonical_name + ".log"); + self.i = 0; + self.time = new Date().getTime(); + self.plmeta = { count: 0, time: "00:00" }; - this.css = ""; - this.js = ""; + self.css = ""; + self.js = ""; - this.ipkey = ""; + self.ipkey = ""; for(var i = 0; i < 15; i++) { - this.ipkey += "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[parseInt(Math.random() * 65)] + self.ipkey += "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[parseInt(Math.random() * 65)] } - Server.db.loadChannel(this); - if(this.registered) { - this.loadDump(); - } + Server.db.loadChannelData(self, function () { + self.dbloaded = true; + if(self.registered) { + self.loadDump(); + } + }); } /* REGION Permissions */ @@ -324,8 +326,11 @@ Channel.prototype.readLog = function (filterIp, callback) { rs.on("end", function () { if(filterIp) { buffer = buffer.replace( - /(\d{1,3}\.){2}(\d{1,3})\.(\d{1,3})/g, - "x.x.$2.$3" + /\d+\.\d+\.(\d+\.\d+)/g, + "x.x.$1" + ).replace( + /\d+\.\d+\.(\d+)/g, + "x.x.$1.*" ); } @@ -356,18 +361,19 @@ Channel.prototype.tryReadLog = function (user) { }); } -Channel.prototype.tryRegister = function(user) { - if(this.registered) { - ActionLog.record(user.ip, user.name, "channel-register-failure", [ - this.name, "Channel already registered"]); +Channel.prototype.tryRegister = function (user) { + var self = this; + if(self.registered) { + self.server.actionlog.record(user.ip, user.name, "channel-register-failure", + [self.name, "Channel already registered"]); user.socket.emit("registerChannel", { success: false, error: "This channel is already registered" }); } else if(!user.loggedIn) { - ActionLog.record(user.ip, user.name, "channel-register-failure", [ - this.name, "Not logged in"]); + self.server.actionlog.record(user.ip, user.name, "channel-register-failure", + [self.name, "Not logged in"]); user.socket.emit("registerChannel", { success: false, error: "You must log in to register a channel" @@ -375,220 +381,295 @@ Channel.prototype.tryRegister = function(user) { } else if(!Rank.hasPermission(user, "registerChannel")) { - ActionLog.record(user.ip, user.name, "channel-register-failure", [ - this.name, "Insufficient permissions"]); + self.server.actionlog.record(user.ip, user.name, "channel-register-failure", + [self.name, "Insufficient permissions"]); user.socket.emit("registerChannel", { success: false, - error: "You don't have permission to register this channel" + error: "You don't have permission to register self channel" }); } else { - if(this.server.db.registerChannel(this.name, user.name)) { - ActionLog.record(user.ip, user.name, "channel-register-success", this.name); - this.registered = true; - this.initialized = true; - this.saveDump(); - this.saveRank(user); + self.server.db.registerChannel(self.name, user.name, + function (err, res) { + if(err) { + user.socket.emit("registerChannel", { + success: false, + error: "Unable to register channel: " + err + }); + return; + } + + self.server.actionlog.record(user.ip, user.name, + "channel-register-success", self.name); + self.registered = true; + self.initialized = true; + self.saveDump(); + self.saveRank(user); user.socket.emit("registerChannel", { - success: true, + success: true }); - this.logger.log("*** " + user.name + " registered the channel"); - Logger.syslog.log("Channel " + this.name + " was registered by " + user.name); - } - else { - user.socket.emit("registerChannel", { + self.logger.log("*** " + user.name + " registered the channel"); + }); + } +} + +Channel.prototype.unregister = function (user) { + var self = this; + + if(!self.registered) { + user.socket.emit("unregisterChannel", { + success: false, + error: "This channel is already unregistered" + }); + return; + } + + if(user.rank < 10) { + user.socket.emit("unregisterChannel", { + success: false, + error: "You must be the channel owner to unregister it" + }); + return; + } + self.server.db.dropChannel(self.name, function (err, res) { + if(err) { + user.socket.emit("unregisterChannel", { success: false, - error: "Unable to register channel, see an admin" + error: "Unregistration failed: " + err }); + return; } - } -} - -Channel.prototype.unregister = function() { - if(this.server.db.deleteChannel(this.name)) { - this.registered = false; - return true; - } - return false; -} - -Channel.prototype.getRank = function(name) { - var global = Auth.getGlobalRank(name); - if(!this.registered) { - return global; - } - var local = this.server.db.getChannelRank(this.name, name); - return local > global ? local : global; -} - -Channel.prototype.saveRank = function(user) { - return this.server.db.setChannelRank(this.name, user.name, user.rank); -} - -Channel.prototype.getIPRank = function(ip) { - var names = []; - if(!(ip in this.ip_alias)) - this.ip_alias[ip] = this.server.db.getAliases(ip); - this.ip_alias[ip].forEach(function(name) { - names.push(name); + + self.registered = false; + user.socket.emit("unregisterChannel", { success: true }); }); +} - var ranks = this.server.db.getChannelRank(this.name, names); - var rank = 0; - for(var i = 0; i < ranks.length; i++) { - rank = (ranks[i] > rank) ? ranks[i] : rank; - } - return rank; +Channel.prototype.getRank = function (name, callback) { + var self = this; + self.server.db.getGlobalRank(name, function (err, global) { + if(err) { + callback(err, null); + return; + } + + if(!self.registered) { + callback(null, global); + return; + } + + self.server.db.getChannelRank(self.name, name, + function (err, rank) { + if(err) { + callback(err, null); + return; + } + + callback(null, rank > global ? rank : global); + }); + }); +} + +Channel.prototype.saveRank = function (user, callback) { + if(!this.registered) + return; + this.server.db.setChannelRank(this.name, user.name, user.rank, callback); +} + +Channel.prototype.getIPRank = function (ip, callback) { + var self = this; + self.server.db.listAliases(ip, function (err, names) { + self.server.db.listChannelUserRanks(self.name, names, + function (err, res) { + if(err) { + callback(err, null); + return; + } + + var rank = 0; + for(var i in res) { + rank = (res[i] > rank) ? res[i] : rank; + } + + self.server.db.listGlobalRanks(names, function (err, res) { + if(err) { + callback(err, null); + return; + } + + for(var i in res) { + rank = (res[i] > rank) ? res[i] : rank; + } + + callback(null, rank); + }); + }); + }); } Channel.prototype.cacheMedia = function(media) { + var self = this; // Prevent the copy in the playlist from messing with this one media = media.dup(); if(media.temp) { return; } - this.library[media.id] = media; - if(this.registered) { - return this.server.db.addToLibrary(this.name, media); + if(self.registered) { + self.server.db.addToLibrary(self.name, media); } - return false; } Channel.prototype.tryNameBan = function(actor, name) { - if(!this.hasPermission(actor, "ban")) { + var self = this; + if(!self.hasPermission(actor, "ban")) { return false; } name = name.toLowerCase(); - - var rank = this.getRank(name); - if(rank >= actor.rank) { - actor.socket.emit("errorMsg", {msg: "You don't have permission to ban this person."}); - return false; + if(name == actor.name.toLowerCase()) { + actor.socket.emit("costanza", { + msg: "Trying to ban yourself?" + }); + return; } - this.namebans[name] = actor.name; - for(var i = 0; i < this.users.length; i++) { - if(this.users[i].name.toLowerCase() == name) { - this.kick(this.users[i], "You're banned!"); - break; + self.getRank(name, function (err, rank) { + if(err) { + actor.socket.emit("errorMsg", { + msg: "Internal error " + err + }); + return; } - } - this.logger.log("*** " + actor.name + " namebanned " + name); - var chan = this; - this.users.forEach(function(u) { - chan.sendBanlist(u); - }); - if(!this.registered) { - return false; - } - return this.server.db.channelBan(this.name, "*", name, actor.name); + if(rank >= actor.rank) { + actor.socket.emit("errorMsg", { + msg: "You don't have permission to ban " + name + }); + return; + } + + self.namebans[name] = actor.name; + for(var i = 0; i < self.users.length; i++) { + if(self.users[i].name.toLowerCase() == name) { + self.kick(self.users[i], "You're banned!"); + break; + } + } + self.logger.log("*** " + actor.name + " namebanned " + name); + self.users.forEach(function(u) { + self.sendBanlist(u); + }); + + if(!self.registered) { + return; + } + + self.server.db.addChannelBan(self.name, "*", name, actor.name); + }); } Channel.prototype.unbanName = function(actor, name) { - if(!this.hasPermission(actor, "ban")) { + var self = this; + if(!self.hasPermission(actor, "ban")) { return false; } - this.namebans[name] = null; - delete this.namebans[name]; - this.logger.log("*** " + actor.name + " un-namebanned " + name); + self.namebans[name] = null; + delete self.namebans[name]; + self.logger.log("*** " + actor.name + " un-namebanned " + name); - var chan = this; - this.users.forEach(function(u) { - chan.sendBanlist(u); + self.server.db.clearChannelNameBan(self.name, name, function (err, res) { + + self.users.forEach(function(u) { + self.sendBanlist(u); + }); }); - return this.server.db.channelUnbanName(this.name, name); } Channel.prototype.tryIPBan = function(actor, name, range) { - if(!this.hasPermission(actor, "ban")) { - return false; + var self = this; + if(!self.hasPermission(actor, "ban")) { + return; } if(typeof name != "string") { - return false; + return; } - var ips = this.server.db.ipForName(name); - var chan = this; - ips.forEach(function(ip) { - if(chan.getIPRank(ip) >= actor.rank) { - actor.socket.emit("errorMsg", {msg: "You don't have permission to ban IP: x.x." + ip.replace(/\d+\.\d+\.(\d+\.\d+)/, "$1")}); - return false; + name = name.toLowerCase(); + if(name == actor.name.toLowerCase()) { + actor.socket.emit("costanza", { + msg: "Trying to ban yourself?" + }); + return; + } + self.server.db.listIPsForName(name, function (err, ips) { + if(err) { + actor.socket.emit("errorMsg", { + msg: "Internal error: " + err + }); + return; } - - if(range) { - ip = ip.replace(/(\d+)\.(\d+)\.(\d+)\.(\d+)/, "$1.$2.$3"); - for(var ip2 in chan.ip_alias) { - if(ip2.indexOf(ip) == 0 && chan.getIPRank(ip2) >= actor.rank) { - actor.socket.emit("errorMsg", {msg: "You don't have permission to ban IP: x.x." + ip2.replace(/\d+\.\d+\.(\d+\.\d+)/, "$1")}); - return false; + ips.forEach(function (ip) { + if(range) + ip = ip.replace(/(\d+)\.(\d+)\.(\d+)\.(\d+)/, "$1.$2.$3"); + self.getIPRank(ip, function (err, rank) { + if(err) { + actor.socket.emit("errorMsg", { + msg: "Internal error" + }); + return; } - } - } - chan.ipbans[ip] = [name, actor.name]; - //chan.broadcastBanlist(); - chan.logger.log("*** " + actor.name + " banned " + ip + " (" + name + ")"); - for(var i = 0; i < chan.users.length; i++) { - if(chan.users[i].ip.indexOf(ip) == 0) { - chan.kick(chan.users[i], "Your IP is banned!"); - i--; - } - } + if(rank >= actor.rank) { + actor.socket.emit("errorMsg", { + msg: "You don't have permission to ban IP: " + + $util.maskIP(ip) + }); + return; + } - if(!chan.registered) - return false; + self.ipbans[ip] = [name, actor.name]; + self.logger.log("*** " + actor.name + " banned " + ip + + " (" + name + ")"); - // Update database ban table - return chan.server.db.channelBan(chan.name, ip, name, actor.name); + for(var i = 0; i < self.users.length; i++) { + if(self.users[i].ip.indexOf(ip) == 0) { + self.kick(self.users[i], "Your IP is banned!"); + i--; + } + } + + if(!self.registered) + return; + + self.server.db.addChannelBan(self.name, ip, name, + actor.name, + function (err, res) { + self.users.forEach(function(u) { + self.sendBanlist(u); + }); + }); + }); + }); }); - - var chan = this; - this.users.forEach(function(u) { - chan.sendBanlist(u); - }); -} - -Channel.prototype.banIP = function(actor, receiver) { - if(!this.hasPermission(actor, "ban")) - return false; - - this.ipbans[receiver.ip] = [receiver.name, actor.name]; - try { - receiver.socket.disconnect(true); - } - catch(e) { - // Socket already disconnected - } - //this.broadcastBanlist(); - this.logger.log(receiver.ip + " (" + receiver.name + ") was banned by " + actor.name); - - if(!this.registered) - return false; - - // Update database ban table - return this.server.db.channelBanIP(this.name, receiver.ip, receiver.name, actor.name); } Channel.prototype.unbanIP = function(actor, ip) { - if(!this.hasPermission(actor, "ban")) + var self = this; + if(!self.hasPermission(actor, "ban")) return false; - this.ipbans[ip] = null; - var chan = this; - this.users.forEach(function(u) { - chan.sendBanlist(u); + self.ipbans[ip] = null; + self.users.forEach(function(u) { + self.sendBanlist(u); }); - this.logger.log("*** " + actor.name + " unbanned " + ip); + self.logger.log("*** " + actor.name + " unbanned " + ip); - if(!this.registered) + if(!self.registered) return false; - //this.broadcastBanlist(); // Update database ban table - return this.server.db.channelUnbanIP(this.name, ip); + self.server.db.clearChannelIPBan(self.name, ip); } Channel.prototype.tryUnban = function(actor, data) { @@ -602,34 +683,20 @@ Channel.prototype.tryUnban = function(actor, data) { } Channel.prototype.search = function(query, callback) { - // Search youtube - if(callback) { - if(query.trim() == "") { - return; + var self = this; + self.server.db.searchLibrary(self.name, query, function (err, res) { + if(err) { + res = []; } - this.server.infogetter.Getters["ytSearch"](query.split(" "), function(err, vids) { - if(!err) { - callback(vids); - } + + res.sort(function(a, b) { + var x = a.title.toLowerCase(); + var y = b.title.toLowerCase(); + + return (x == y) ? 0 : (x < y ? -1 : 1); }); - return; - } - - query = query.toLowerCase(); - var results = []; - for(var id in this.library) { - if(this.library[id].title.toLowerCase().indexOf(query) != -1) { - results.push(this.library[id]); - } - } - results.sort(function(a, b) { - var x = a.title.toLowerCase(); - var y = b.title.toLowerCase(); - - return (x == y) ? 0 : (x < y ? -1 : 1); + callback(res); }); - - return results; } /* REGION User interaction */ @@ -662,12 +729,6 @@ Channel.prototype.userJoin = function(user) { } } - // If the channel is empty and isn't registered, the first person - // gets ownership of the channel (temporarily) - if(this.users.length == 0 && !this.registered) { - user.rank = (user.rank < Rank.Owner) ? 10 : user.rank; - user.socket.emit("channelNotRegistered"); - } this.users.push(user); this.broadcastVoteskipUpdate(); if(user.name != "") { @@ -773,7 +834,7 @@ Channel.prototype.sendBanlist = function(user) { var ip_hidden = this.hideIP(ip); var disp = ip; if(user.rank < Rank.Siteadmin) { - disp = "x.x." + ip.replace(/\d+\.\d+\.(\d+\.\d+)/, "$1"); + disp = $util.maskIP(ip); } ents.push({ ip_displayed: disp, @@ -816,8 +877,16 @@ Channel.prototype.sendRankStuff = function(user) { } Channel.prototype.sendChannelRanks = function(user) { - if(Rank.hasPermission(user, "acl")) { - user.socket.emit("channelRanks", this.server.db.listChannelRanks(this.name)); + if(Rank.hasPermission(user, "acl") && this.registered) { + this.server.db.listChannelRanks(this.name, function (err, res) { + if(err) { + user.socket.emit("errorMsg", { + msg: "Internal error: " + err + }); + return; + } + user.socket.emit("channelRanks", res); + }); } } @@ -896,50 +965,62 @@ Channel.prototype.broadcastUsercount = function() { } Channel.prototype.broadcastNewUser = function(user) { - var aliases = this.server.db.getAliases(user.ip); - var chan = this; - this.ip_alias[user.ip] = aliases; - aliases.forEach(function(alias) { - chan.name_alias[alias] = aliases; - }); - - this.login_hist.unshift({ - name: user.name, - aliases: this.ip_alias[user.ip], - time: Date.now() - }); - if(this.login_hist.length > 20) - this.login_hist.pop(); - - if(user.name.toLowerCase() in this.namebans && - this.namebans[user.name.toLowerCase()] != null) { - this.kick(user, "You're banned!"); - return; + var self = this; + // If the channel is empty and isn't registered, the first person + // gets ownership of the channel (temporarily) + if(self.dbloaded && self.users.length == 1 && !self.registered) { + user.rank = (user.rank < Rank.Owner) ? 10 : user.rank; + user.socket.emit("channelNotRegistered"); } - this.sendAll("addUser", { - name: user.name, - rank: user.rank, - leader: this.leader == user, - meta: user.meta, - profile: user.profile - }); - //this.sendRankStuff(user); - if(user.rank > Rank.Guest) { - this.saveRank(user); - } - - var msg = user.name + " joined (aliases: "; - msg += this.ip_alias[user.ip].join(", ") + ")"; - var pkt = { - username: "[server]", - msg: msg, - msgclass: "server-whisper", - time: Date.now() - }; - this.users.forEach(function(u) { - if(u.rank >= 2) { - u.socket.emit("joinMessage", pkt); + self.server.db.listAliases(user.ip, function (err, aliases) { + if(err) { + aliases = []; } + + self.ip_alias[user.ip] = aliases; + aliases.forEach(function (alias) { + self.name_alias[alias] = aliases; + }); + + self.login_hist.unshift({ + name: user.name, + aliases: self.ip_alias[user.ip], + time: Date.now() + }); + + if(self.login_hist.length > 20) + self.login_hist.pop(); + + if(user.name.toLowerCase() in self.namebans && + self.namebans[user.name.toLowerCase()] !== null) { + self.kick(user, "You're banned!"); + return; + } + self.sendAll("addUser", { + name: user.name, + rank: user.rank, + leader: self.leader == user, + meta: user.meta, + profile: user.profile + }); + + if(user.rank > Rank.Guest) { + self.saveRank(user); + } + + var msg = user.name + " joined (aliases: "; + msg += self.ip_alias[user.ip].join(", ") + ")"; + var pkt = { + username: "[server]", + msg: msg, + msgclass: "server-whisper", + time: Date.now() + }; + self.users.forEach(function(u) { + if(u.rank >= 2) { + u.socket.emit("joinMessage", pkt); + } + }); }); } @@ -978,7 +1059,7 @@ Channel.prototype.broadcastBanlist = function() { var name = this.ipbans[ip][0]; var ip_hidden = this.hideIP(ip); ents.push({ - ip_displayed: "x.x." + ip.replace(/\d+\.\d+\.(\d+\.\d+)/, "$1"), + ip_displayed: $util.maskIP(ip), ip_hidden: ip_hidden, name: name, aliases: this.ip_alias[ip] || [], @@ -1023,13 +1104,6 @@ Channel.prototype.broadcastBanlist = function() { } } -Channel.prototype.broadcastRankTable = function() { - var ranks = this.server.db.listChannelRanks(this.name); - for(var i = 0; i < this.users.length; i++) { - this.sendACL(this.users[i]); - } -} - Channel.prototype.broadcastChatFilters = function() { var filts = new Array(this.filters.length); for(var i = 0; i < this.filters.length; i++) { @@ -1074,32 +1148,6 @@ Channel.prototype.onVideoChange = function () { this.broadcastDrinks(); } -// The server autolead function -function mediaUpdate(chan, id) { - // Bail cases - video changed, someone's leader, no video playing - if(chan.media == null || - id != chan.media.id || - chan.leader != null || - chan.users.length == 0) { - return; - } - - chan.media.currentTime += (new Date().getTime() - chan.time) / 1000.0; - chan.time = new Date().getTime(); - - // Show's over, move on to the next thing - if(chan.media.currentTime > chan.media.seconds + 1) { - chan.playNext(); - } - // Send updates about every 5 seconds - else if(chan.i % 5 == 0) { - chan.sendAll("mediaUpdate", chan.media.timeupdate()); - } - chan.i++; - - setTimeout(function() { mediaUpdate(chan, id); }, 1000); -} - function isLive(type) { return type == "li" // Livestream.com || type == "tw" // Twitch.tv @@ -1147,9 +1195,6 @@ Channel.prototype.tryQueue = function(user, data) { if(typeof data.id !== "string" && data.id !== false) { return; } - if(typeof data.type !== "string" && !(data.id in this.library)) { - return; - } if(data.pos == "next" && !this.hasPermission(user, "playlistnext")) { return; @@ -1183,69 +1228,87 @@ Channel.prototype.tryQueue = function(user, data) { } Channel.prototype.addMedia = function(data, user) { - if(data.type === "yp" && !this.hasPermission(user, "playlistaddlist")) { - user.socket.emit("queueFail", "You don't have permission to add playlists"); + var self = this; + if(data.type === "yp" && + !self.hasPermission(user, "playlistaddlist")) { + user.socket.emit("queueFail", "You don't have permission to add " + + "playlists"); return; } - if(data.type === "cu" && !this.hasPermission(user, "playlistaddcustom")) { - user.socket.emit("queueFail", "You don't have permission to add cusstom embeds"); + if(data.type === "cu" && + !self.hasPermission(user, "playlistaddcustom")) { + user.socket.emit("queueFail", "You don't have permission to add " + + "custom embeds"); return; } data.temp = data.temp || isLive(data.type); data.queueby = user ? user.name : ""; - data.maxlength = this.hasPermission(user, "exceedmaxlength") ? 0 : this.opts.maxlength; - var chan = this; - if(data.id in this.library) { - var m = this.library[data.id].dup(); - if(data.maxlength && m.seconds > data.maxlength) { - user.socket.emit("queueFail", "Media is too long!"); + data.maxlength = self.hasPermission(user, "exceedmaxlength") + ? 0 + : this.opts.maxlength; + self.server.db.getLibraryItem(self.name, data.id, + function (err, item) { + if(err) { + user.socket.emit("queueFail", "Internal error: " + err); return; } - data.media = m; - this.playlist.addCachedMedia(data, function (err, item) { - if(err) { - if(err === true) - err = false; - if(user) - user.socket.emit("queueFail", err); + if(item !== null) { + var m = new Media(item.id, item.title, item.seconds, item.type); + if(data.maxlength && m.seconds > data.maxlength) { + user.socket.emit("queueFail", "Media is too long!"); return; } - else { - chan.logger.log("### " + user.name + " queued " + item.media.title); - chan.sendAll("queue", { - item: item.pack(), - after: item.prev ? item.prev.uid : "prepend" - }); - chan.broadcastPlaylistMeta(); - } - }); - return; - } - if(isLive(data.type) && !this.hasPermission(user, "playlistaddlive")) { - user.socket.emit("queueFail", "You don't have permission to queue livestreams"); - return; - } - this.playlist.addMedia(data, function(err, item) { - if(err) { - if(err === true) - err = false; - if(user) - user.socket.emit("queueFail", err); - return; - } - else { - chan.logger.log("### " + user.name + " queued " + item.media.title); - chan.sendAll("queue", { - item: item.pack(), - after: item.prev ? item.prev.uid : "prepend" + data.media = m; + self.playlist.addCachedMedia(data, function (err, item) { + if(err) { + if(err === true) + err = false; + if(user) + user.socket.emit("queueFail", err); + return; + } + else { + self.logger.log("### " + user.name + " queued " + item.media.title); + self.sendAll("queue", { + item: item.pack(), + after: item.prev ? item.prev.uid : "prepend" + }); + self.broadcastPlaylistMeta(); + } + }); + return; + } else { + if(isLive(data.type) && + !self.hasPermission(user, "playlistaddlive")) { + user.socket.emit("queueFail", "You don't have " + + "permission to add livestreams"); + return; + } + self.playlist.addMedia(data, function(err, item) { + if(err) { + if(err === true) + err = false; + if(user) + user.socket.emit("queueFail", err); + return; + } + else { + self.logger.log("### " + user.name + " queued " + item.media.title); + self.sendAll("queue", { + item: item.pack(), + after: item.prev ? item.prev.uid : "prepend" + }); + self.broadcastPlaylistMeta(); + if(!item.temp) + self.cacheMedia(item.media); + } }); - chan.broadcastPlaylistMeta(); - if(!item.temp) - chan.cacheMedia(item.media); } + }); + } Channel.prototype.addMediaList = function(data, user) { @@ -1274,6 +1337,7 @@ Channel.prototype.addMediaList = function(data, user) { } Channel.prototype.tryQueuePlaylist = function(user, data) { + var self = this; if(!this.hasPermission(user, "playlistaddlist")) { return; } @@ -1287,11 +1351,19 @@ Channel.prototype.tryQueuePlaylist = function(user, data) { return; } - var pl = this.server.db.loadUserPlaylist(user.name, data.name); - data.list = pl; - data.queueby = user.name; - data.temp = !this.hasPermission(user, "addnontemp"); - this.addMediaList(data, user); + self.server.db.getUserPlaylist(user.name, data.name, + function (err, pl) { + if(err) { + user.socket.emit("errorMsg", { + msg: "Playlist load failed: " + err + }); + return; + } + data.list = pl; + data.queueby = user.name; + data.temp = !self.hasPermission(user, "addnontemp"); + self.addMediaList(data, user); + }); } Channel.prototype.setTemp = function(uid, temp) { @@ -1347,19 +1419,21 @@ Channel.prototype.tryDequeue = function(user, data) { } Channel.prototype.tryUncache = function(user, data) { + var self = this; if(!Rank.hasPermission(user, "uncache")) { return; } if(typeof data.id != "string") { return; } - if(this.server.db.removeFromLibrary(this.name, data.id)) { - var msg = data.id; - if(data.id in this.library) - msg = this.library[data.id]; - this.logger.log("*** " + user.name + " deleted " + msg + " from library"); - delete this.library[data.id]; - } + self.server.db.removeFromLibrary(self.name, data.id, + function (err, res) { + if(err) + return; + + self.logger.log("*** " + user.name + " deleted " + data.id + + " from library"); + }); } Channel.prototype.playNext = function() { @@ -1888,6 +1962,7 @@ Channel.prototype.sendMessage = function(username, msg, msgclass, data) { /* REGION Rank stuff */ Channel.prototype.trySetRank = function(user, data) { + var self = this; if(!Rank.hasPermission(user, "promote")) return; @@ -1901,9 +1976,9 @@ Channel.prototype.trySetRank = function(user, data) { return; var receiver; - for(var i = 0; i < this.users.length; i++) { - if(this.users[i].name == data.user) { - receiver = this.users[i]; + for(var i = 0; i < self.users.length; i++) { + if(self.users[i].name == data.user) { + receiver = self.users[i]; break; } } @@ -1913,94 +1988,27 @@ Channel.prototype.trySetRank = function(user, data) { return; receiver.rank = data.rank; if(receiver.loggedIn) { - this.saveRank(receiver); + self.saveRank(receiver, function (err, res) { + self.logger.log("*** " + user.name + " set " + + data.user + "'s rank to " + data.rank); + self.sendAllWithPermission("acl", "setChannelRank", data); + }); } - this.broadcastUserUpdate(receiver); - } - else { - var rrank = this.getRank(data.user); - if(rrank >= user.rank) - return; - this.server.db.setChannelRank(this.name, data.user, data.rank); - } - - this.logger.log("*** " + user.name + " set " + data.user + "'s rank to " + - data.rank); - this.sendAllWithPermission("acl", "setChannelRank", data); -} - -Channel.prototype.tryPromoteUser = function(actor, data) { - if(!Rank.hasPermission(actor, "promote")) { - return; - } - - if(data.name == undefined) { - return; - } - - var name = data.name; - - var receiver; - for(var i = 0; i < this.users.length; i++) { - if(this.users[i].name == name) { - receiver = this.users[i]; - break; - } - } - - var rank = receiver ? receiver.rank : this.getRank(data.name); - - if(actor.rank > rank + 1) { - rank++; - if(receiver) { - receiver.rank++; - if(receiver.loggedIn) { - this.saveRank(receiver); - } - this.broadcastUserUpdate(receiver); - } - else { - this.server.db.setChannelRank(this.name, data.name, rank); - } - this.logger.log("*** " + actor.name + " promoted " + data.name + " from " + (rank - 1) + " to " + rank); - //this.broadcastRankTable(); - } -} - -Channel.prototype.tryDemoteUser = function(actor, data) { - if(!Rank.hasPermission(actor, "promote")) { - return; - } - - if(data.name == undefined) { - return; - } - - var name = data.name; - var receiver; - for(var i = 0; i < this.users.length; i++) { - if(this.users[i].name == name) { - receiver = this.users[i]; - break; - } - } - - var rank = receiver ? receiver.rank : this.getRank(data.name); - - if(actor.rank > rank) { - rank--; - if(receiver) { - receiver.rank--; - if(receiver.loggedIn) { - this.saveRank(receiver); - } - this.broadcastUserUpdate(receiver); - } - else { - this.server.db.setChannelRank(this.name, data.name, rank); - } - this.logger.log("*** " + actor.name + " demoted " + data.name + " from " + (rank + 1) + " to " + rank); - //this.broadcastRankTable(); + self.broadcastUserUpdate(receiver); + } else if(self.registered) { + self.getRank(data.user, function (err, rrank) { + if(err) + return; + if(rrank >= user.rank) + return; + self.server.db.setChannelRank(self.name, data.user, + data.rank, function (err, res) { + + self.logger.log("*** " + user.name + " set " + + data.user + "'s rank to " + data.rank); + self.sendAllWithPermission("acl", "setChannelRank", data); + }); + }); } } diff --git a/chatcommand.js b/chatcommand.js index 388bee42..1ad2d946 100644 --- a/chatcommand.js +++ b/chatcommand.js @@ -145,10 +145,21 @@ function handleUnmute(chan, user, args) { function handleKick(chan, user, args) { if(chan.hasPermission(user, "kick") && args.length > 0) { args[0] = args[0].toLowerCase(); + if(args[0] == user.name.toLowerCase()) { + user.socket.emit("costanza", { + msg: "Kicking yourself?" + }); + return; + } var kickee; for(var i = 0; i < chan.users.length; i++) { - if(chan.users[i].name.toLowerCase() == args[0] && - chan.getRank(chan.users[i].name) < user.rank) { + if(chan.users[i].name.toLowerCase() == args[0]) { + if(chan.users[i].rank >= user.rank) { + user.socket.emit("errorMsg", { + msg: "You don't have permission to kick " + args[0] + }); + return; + } kickee = chan.users[i]; break; } diff --git a/config.js b/config.js index abea1605..4d21e58c 100644 --- a/config.js +++ b/config.js @@ -42,17 +42,14 @@ var defaults = { } function save(cfg, file) { + if(!cfg.loaded) + return; var x = {}; for(var k in cfg) { - if(k !== "nodemailer") + if(k !== "nodemailer" && k !== "loaded") 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); - } - }); + fs.writeFileSync(file, JSON.stringify(x, null, 4)); } exports.load = function (Server, file, callback) { @@ -92,6 +89,8 @@ exports.load = function (Server, file, callback) { ); } + cfg["loaded"] = true; + save(cfg, file); Server.cfg = cfg; callback(); diff --git a/database.js b/database.js index 816549b5..f00602b5 100644 --- a/database.js +++ b/database.js @@ -1,132 +1,88 @@ -/* -The MIT License (MIT) -Copyright (c) 2013 Calvin Montgomery - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of 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. -*/ - -var mysql = require("mysql-libmysqlclient"); -var Logger = require("./logger"); -var Media = require("./media").Media; -var bcrypt = require("bcrypt"); +var mysql = require("mysql"); var hashlib = require("node_hash"); +var bcrypt = require("bcrypt"); +var $util = require("./utilities"); +var Logger = require("./logger"); -var db = false; -var SERVER = ""; -var USER = ""; -var DATABASE = ""; -var PASSWORD = ""; -var CONFIG = {}; -var global_bans = {}; +var Database = function (cfg) { + this.cfg = cfg; + this.pool = mysql.createPool({ + host: cfg["mysql-server"], + user: cfg["mysql-user"], + password: cfg["mysql-pw"], + database: cfg["mysql-db"], + multipleStatements: true + }); -function setup(cfg) { - SERVER = cfg["mysql-server"]; - USER = cfg["mysql-user"]; - DATABASE = cfg["mysql-db"]; - PASSWORD = cfg["mysql-pw"]; - CONFIG = cfg; -} - -function getConnection() { - if(db && db.connectedSync()) { - return db; - } - db = mysql.createConnectionSync(); - db.connectSync(SERVER, USER, PASSWORD, DATABASE); - if(!db.connectedSync()) { - Logger.errlog.log("DB connection failed"); - return false; - } - db._querySync = db.querySync; - db.querySync = function(q) { - if(!this.connectedSync()) { - db = false; - return false; + // Test the connection + this.pool.getConnection(function (err, conn) { + if(err) { + Logger.errlog.log("! DB connection failed"); + return; } - var res = this._querySync(q); - if(!res) { - try { - db.closeSync(); - } catch(e) { - // already disconnected + conn.end(); + }); + + this.global_ipbans = {}; +}; + +Database.prototype.query = function (query, sub, callback) { + // 2nd argument is optional + if(typeof sub === "function") { + callback = sub; + sub = false; + } + + if(typeof callback !== "function") + callback = blackHole; + + var self = this; + self.pool.getConnection(function (err, conn) { + if(err) { + callback("Database failure", null); + } else { + function cback(err, res) { + if(err) { + if(self.cfg["debug"]) { + console.log(query); + console.log(err); + } + callback("Database failure", null); + } else { + callback(null, res); + } + conn.end(); + } + + if(sub) { + conn.query(query, sub, cback); + } else { + conn.query(query, cback); } - db = false; } - return res; - } - return db; + }); } -function sqlEscape(obj) { - if(obj === undefined || obj === null) - return "NULL"; +function blackHole() { - if(typeof obj === "boolean") - return obj ? "true" : "false"; - - if(typeof obj === "number") - return obj + ""; - - if(typeof obj === "object") - return "'object'"; - - if(typeof obj === "string") { - obj = obj.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function(s) { - switch(s) { - case "\0": return "\\0"; - case "\n": return "\\n"; - case "\r": return "\\r"; - case "\b": return "\\b"; - case "\t": return "\\t"; - case "\x1a": return "\\Z"; - default: return "\\" + s; - } - }); - return "'" + obj + "'"; - } } -function createQuery(template, args) { - var last = -1; - while(template.indexOf("?", last) >= 0) { - var idx = template.indexOf("?", last); - var arg = args.shift(); - arg = sqlEscape(arg); - // Stupid workaround because even if I call replace() with a string - // and not a regex, '$' still holds special meaning - // this actually replaces '$' with '$$' - // What the hell, Javascript? - arg = arg.replace(/\$/g, "$$$$"); - var first = template.substring(0, idx); - template = first + template.substring(idx).replace("?", arg); - last = idx + arg.length; - } - template = template.replace(/`'/g, "`"); - template = template.replace(/'`/g, "`"); - return template; -} - -function init() { - var db = getConnection(); - if(!db) { - return false; - } - +Database.prototype.init = function () { + var self = this; + var query; // Create channel table - var query = ["CREATE TABLE IF NOT EXISTS `channels` (", - "`id` INT NOT NULL AUTO_INCREMENT,", - "`name` VARCHAR(255) NOT NULL,", - "`owner` VARCHAR(20) NOT NULL,", - "PRIMARY KEY(`id`))", - "ENGINE = MyISAM;"].join(""); - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to create channels table"); - } + query = ["CREATE TABLE IF NOT EXISTS `channels` (", + "`id` INT NOT NULL AUTO_INCREMENT,", + "`name` VARCHAR(255) NOT NULL,", + "`owner` VARCHAR(20) NOT NULL,", + "PRIMARY KEY(`id`))", + "ENGINE = MyISAM;"].join(""); + + self.query(query, function (err, res) { + if(err) { + Logger.errlog.log("! Failed to create channels table"); + } + }); // Create registration table query = ["CREATE TABLE IF NOT EXISTS `registrations` (", @@ -142,10 +98,11 @@ function init() { "PRIMARY KEY (`id`))", "ENGINE = MyISAM;"].join(""); - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to create registrations table"); - } + self.query(query, function (err, res) { + if(err) { + Logger.errlog.log("! Failed to create registration table"); + } + }); // Create global bans table query = ["CREATE TABLE IF NOT EXISTS `global_bans` (", @@ -154,12 +111,11 @@ function init() { "PRIMARY KEY (`ip`))", "ENGINE = MyISAM;"].join(""); - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to create global ban table"); - } - - refreshGlobalBans(); + self.query(query, function (err, res) { + if(err) { + Logger.errlog.log("! Failed to create global ban table"); + } + }); // Create password reset table query = ["CREATE TABLE IF NOT EXISTS `password_reset` (", @@ -171,10 +127,11 @@ function init() { "PRIMARY KEY (`name`))", "ENGINE = MyISAM;"].join(""); - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to create password reset table"); - } + self.query(query, function (err, res) { + if(err) { + Logger.errlog.log("! Failed to create password reset table"); + } + }); // Create user playlist table query = ["CREATE TABLE IF NOT EXISTS `user_playlists` (", @@ -185,10 +142,12 @@ function init() { "`time` INT NOT NULL,", "PRIMARY KEY (`user`, `name`))", "ENGINE = MyISAM;"].join(""); - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to create playlist table"); - } + + self.query(query, function (err, res) { + if(err) { + Logger.errlog.log("! Failed to create user playlist table"); + } + }); // Create user aliases table query = ["CREATE TABLE IF NOT EXISTS `aliases` (", @@ -198,10 +157,12 @@ function init() { "`time` BIGINT NOT NULL,", "PRIMARY KEY (`visit_id`), INDEX (`ip`))", "ENGINE = MyISAM;"].join(""); - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to create aliases table"); - } + + self.query(query, function (err, res) { + if(err) { + Logger.errlog.log("! Failed to create user aliases table"); + } + }); // Create action log table query = ["CREATE TABLE IF NOT EXISTS `actionlog` (", @@ -212,10 +173,12 @@ function init() { "`time` BIGINT NOT NULL,", "PRIMARY KEY (`ip`, `time`), INDEX (`action`))", "ENGINE = MyISAM;"].join(""); - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to create actionlog table"); - } + + self.query(query, function (err, res) { + if(err) { + Logger.errlog.log("! Failed to create action log table"); + } + }); // Create stats table query = ["CREATE TABLE IF NOT EXISTS `stats` (", @@ -225,689 +188,1005 @@ function init() { "`mem` INT NOT NULL,", "PRIMARY KEY (`time`))", "ENGINE = MyISAM;"].join(""); - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to create stats table"); - } -} -/* REGION Global Bans */ + self.query(query, function (err, res) { + if(err) { + Logger.errlog.log("! Failed to create stats table"); + } + }); -function checkGlobalBan(ip) { + // Refresh global IP bans + self.listGlobalIPBans(); +}; + +/* REGION global bans */ + +Database.prototype.isGlobalIPBanned = function (ip, callback) { + if(typeof callback !== "function") + return; const re = /(\d+)\.(\d+)\.(\d+)\.(\d+)/; + // Account for range banning var s16 = ip.replace(re, "$1.$2"); var s24 = ip.replace(re, "$1.$2.$3"); - return (ip in global_bans || - s16 in global_bans || - s24 in global_bans); -} -function refreshGlobalBans() { - var db = getConnection(); - if(!db) { - return; - } + var banned = ip in this.global_ipbans || + s16 in this.global_ipbans || + s24 in this.global_ipbans; - var query = "SELECT * FROM `global_bans` WHERE 1"; - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to load global bans"); - } - else { - var rows = results.fetchAllSync(); - global_bans = {}; - for(var i = 0; i < rows.length; i++) { - global_bans[rows[i].ip] = rows[i].note; - } - } - return global_bans; -} + callback(null, banned); +}; -function globalBanIP(ip, reason) { - var db = getConnection(); - if(!db) { - return; - } +Database.prototype.listGlobalIPBans = function (callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; - var query = createQuery( - "INSERT INTO `global_bans` VALUES (?, ?)", - [ip, reason] - ); - return db.querySync(query); -} - -function globalUnbanIP(ip) { - var db = getConnection(); - if(!db) { - return; - } - - var query = createQuery( - "DELETE FROM `global_bans` WHERE ip=?", - [ip] - ); - - return db.querySync(query); -} - -/* REGION Channel Registration/Loading */ - -function registerChannel(name, owner) { - if(!name.match(/^[a-zA-Z0-9-_]+$/)) { - return false; - } - var db = getConnection(); - if(!db) { - return false; - } - - // Library table - var query = ["CREATE TABLE `?` (", - "`id` VARCHAR(255) NOT NULL,", - "`title` VARCHAR(255) NOT NULL,", - "`seconds` INT NOT NULL,", - "`type` VARCHAR(2) NOT NULL,", - "PRIMARY KEY (`id`))", - "ENGINE = MyISAM;"].join(""); - query = createQuery(query, ["chan_" + name + "_library"]); - - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to create table: chan_"+name+"_library"); - return false; - } - - // Rank table - query = ["CREATE TABLE `?` (", - "`name` VARCHAR(32) NOT NULL,", - "`rank` INT NOT NULL,", - "UNIQUE (`name`))", - "ENGINE = MyISAM;"].join(""); - query = createQuery(query, ["chan_" + name + "_ranks"]); - - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to create table: chan_"+name+"_ranks"); - return false; - } - - // Ban table - query = ["CREATE TABLE `?` (", - "`ip` VARCHAR(15) NOT NULL,", - "`name` VARCHAR(32) NOT NULL,", - "`banner` VARCHAR(32) NOT NULL,", - "PRIMARY KEY (`ip`))", - "ENGINE = MyISAM;"].join(""); - query = createQuery(query, ["chan_" + name + "_bans"]); - - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to create table: chan_"+name+"_bans"); - return false; - } - - // Insert into channel table - query = createQuery( - "INSERT INTO `channels` VALUES (NULL, ?, ?)", - [name, owner] - ); - - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to insert into channel table: " + name); - return false; - } - - return true; -} - -function loadChannel(chan) { - if(!chan.name.match(/^[a-zA-Z0-9-_]+$/)) { - return false; - } - var db = getConnection(); - if(!db) { - return false; - } - - var query = createQuery( - "SELECT * FROM `channels` WHERE name=?", - [chan.name] - ); - - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to query channel table"); - } - else { - var rows = results.fetchAllSync(); - if(rows.length == 0) { - // Unregistered - Logger.syslog.log("Channel " + chan.name + " is unregistered"); + self.query("SELECT * FROM global_bans WHERE 1", function (err, res) { + if(err) { + callback(err, null); return; } - // Database is case insensitive - else if(rows[0].name != chan.name) { - chan.name = rows[0].name; + + self.global_ipbans = {}; + for(var i in res) { + self.global_ipbans[res[i].ip] = res[i].note; } + + callback(null, self.global_ipbans); + }); +}; + +Database.prototype.setGlobalIPBan = function (ip, reason, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var query = "INSERT INTO global_bans VALUES (?, ?)" + + " ON DUPLICATE KEY UPDATE note=?"; + self.query(query, [ip, reason, reason], function (err, res) { + if(err) { + callback(err, null); + return; + } + + self.listGlobalIPBans(); + callback(null, res); + }); +}; + +Database.prototype.clearGlobalIPBan = function (ip, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + + var query = "DELETE FROM global_bans WHERE ip=?"; + self.query(query, [ip], function (err, res) { + if(err) { + callback(err, null); + return; + } + + callback(null, res); + }); +}; + +/* END REGION */ + +/* REGION channels */ + +Database.prototype.searchChannel = function (field, value, callback) { + var self = this; + if(typeof callback !== "function") + return; + + var query = "SELECT * FROM channels WHERE "; + if(field === "owner") + query += "owner LIKE ?"; + else if(field === "name") + query += "name LIKE ?"; + + self.query(query, ["%" + value + "%"], callback); +}; + +Database.prototype.channelExists = function (name, callback) { + var self = this; + if(typeof callback !== "function") + return; + if(!$util.isValidChannelName(name)) { + callback("Invalid channel name", null); + return; + } + + var query = "SELECT name FROM channels WHERE name=?"; + self.query(query, [name], function (err, res) { + callback(err, res.length > 0); + }); +}; + +Database.prototype.registerChannel = function (name, owner, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + if(!$util.isValidChannelName(name)) { + callback("Invalid channel name", null); + return; + } + + // Messy, but I can't think of a better async solution atm + var query = "SELECT * FROM channels WHERE name=?"; + self.query(query, [name], function (err, res) { + if(!err && res.length > 0) { + callback("Channel already exists", null); + return; + } + + // Library table + query = ["CREATE TABLE `chan_" + name + "_library` (", + "`id` VARCHAR(255) NOT NULL,", + "`title` VARCHAR(255) NOT NULL,", + "`seconds` INT NOT NULL,", + "`type` VARCHAR(2) NOT NULL,", + "PRIMARY KEY (`id`))", + "ENGINE = MyISAM;"].join(""); + self.query(query, function (err, res) { + if(err) { + callback(err, null); + return; + } + + // Rank table + query = ["CREATE TABLE `chan_" + name + "_ranks` (", + "`name` VARCHAR(32) NOT NULL,", + "`rank` INT NOT NULL,", + "UNIQUE (`name`))", + "ENGINE = MyISAM;"].join(""); + + self.query(query, function (err, res) { + if(err) { + callback(err, null); + return; + } + + // Ban table + query = ["CREATE TABLE `chan_" + name + "_bans` (", + "`ip` VARCHAR(15) NOT NULL,", + "`name` VARCHAR(32) NOT NULL,", + "`banner` VARCHAR(32) NOT NULL,", + "PRIMARY KEY (`ip`))", + "ENGINE = MyISAM;"].join(""); + + self.query(query, function (err, res) { + if(err) { + callback(err, null); + return; + } + + query = "INSERT INTO channels VALUES (NULL, ?, ?)"; + self.query(query, [name, owner], function (err, res) { + callback(err, res); + }); + }); + }); + }); + }); +}; + +Database.prototype.loadChannelData = function (chan, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + if(!$util.isValidChannelName(chan.name)) { + callback("Invalid channel name", null); + return; + } + + var query = "SELECT * FROM channels WHERE name=?"; + + self.query(query, [chan.name], function (err, res) { + if(err) { + callback(err, null); + return; + } + + if(res.length == 0) { + callback("Channel is unregistered", null); + return; + } + + if(res[0].name != chan.name) + chan.name = res[0].name; chan.registered = true; - } - // Load channel library - query = createQuery( - "SELECT * FROM `?`", - ["chan_" + chan.name + "_library"] - ); - - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to load channel library: " + chan.name); - } - else { - var rows = results.fetchAllSync(); - for(var i = 0; i < rows.length; i++) { - var r = rows[i]; - var m = new Media(r.id, r.title, r.seconds, r.type); - chan.library[r.id] = m; - } - } - - // Load channel bans - query = createQuery( - "SELECT * FROM `?`", - ["chan_" + chan.name + "_bans"] - ); - - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to load channel bans: " + chan.name); - } - else { - var rows = results.fetchAllSync(); - for(var i = 0; i < rows.length; i++) { - var r = rows[i]; - if(r.ip == "*") { - chan.namebans[r.name] = r.banner; + // Load bans + query = "SELECT * FROM `chan_" + chan.name + "_bans`"; + self.query(query, function (err, res) { + if(err) { + callback(err, null); + return; } - else { - chan.ipbans[r.ip] = [r.name, r.banner]; + + for(var i in res) { + var r = res[i]; + if(r.ip === "*") + chan.namebans[r.name] = r.banner; + else + chan.ipbans[r.ip] = [r.name, r.banner]; } - } + + chan.logger.log("*** Loaded channel from database"); + callback(null, true); + }); + }); +}; + +Database.prototype.dropChannel = function (name, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + if(!$util.isValidChannelName(name)) { + callback("Invalid channel name", null); + return; } - chan.logger.log("*** Loaded channel from database"); - Logger.syslog.log("Loaded channel " + chan.name + " from database"); -} - -function deleteChannel(name) { - if(!name.match(/^[a-zA-Z0-9-_]+$/)) { - return false; - } - - var db = getConnection(); - if(!db) { - return false; - } - - var query = "DROP TABLE `chan_?_bans`, `chan_?_ranks`, `chan_?_library`" + var query = "DROP TABLE `chan_?_bans`,`chan_?_ranks`,`chan_?_library`" .replace(/\?/g, name); - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to delete channel tables for " + name); - return false; - } - - query = createQuery( - "DELETE FROM `channels` WHERE name=?", - [name] - ); - - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to delete row from channel table: " + name); - return false; - } - - return true; -} - -/* REGION Channel data */ - -function getChannelRank(chan, name) { - if(!chan.match(/^[a-zA-Z0-9-_]+$/)) { - return false; - } - var db = getConnection(); - if(!db) { - return 0; - } - - var query; - if(typeof name == "object") { - var n = "(?"; - for(var i = 1; i < name.length; i++) { - n += ",?"; + self.query(query, function (err, res) { + if(err) { + Logger.errlog.log("! Failed to drop channel tables for "+name); + callback(err, null); + return; } - n += ")" - name.unshift("chan_" + chan + "_ranks"); - query = createQuery( - "SELECT * FROM `?` WHERE name IN " + n, - name - ); - } - else { - query = createQuery( - "SELECT * FROM `?` WHERE name=?", - ["chan_"+chan+"_ranks", name] - ); + + query = "DELETE FROM channels WHERE name=?"; + self.query(query, [name], function (err, res) { + callback(err, res); + if(err) { + Logger.errlog.log("! Failed to delete channel "+name); + } + }); + }); +}; + +Database.prototype.getChannelRank = function (channame, name, callback) { + var self = this; + if(typeof callback !== "function") + return; + + if(!$util.isValidChannelName(channame)) { + callback("Invalid channel name", null); + return; } - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to lookup chan_"+chan+"_ranks"); - return 0; - } + var query = "SELECT name, rank FROM `chan_" + channame + "_ranks`" + + "WHERE name=?"; - var rows = results.fetchAllSync(); - if(typeof name == "object") { - var ranks = []; - for(var i = 0; i < rows.length; i++) { - ranks.push(rows[i].rank); + self.query(query, [name], function (err, res) { + if(err) { + Logger.errlog.log("! Failed to lookup " + channame + " ranks"); + callback(err, null); + return; } - while(ranks.length < rows.length) { - ranks.push(0); + + if(res.length == 0) + callback(null, 0); + else + callback(null, res[0].rank); + }); +}; + +Database.prototype.listChannelUserRanks = function (channame, names, + callback) { + var self = this; + if(typeof callback !== "function") + return; + + if(!$util.isValidChannelName(channame)) { + callback("Invalid channel name", null); + return; + } + + if(typeof names === "string") + names = [names]; + + // Build the query template (?, ?, ?, ?, ...) + var nlist = []; + for(var i in names) + nlist.push("?"); + nlist = "(" + nlist.join(",") + ")"; + + var query = "SELECT name, rank FROM `chan_" + channame + "_ranks`" + + "WHERE name IN " + nlist; + + self.query(query, names, function (err, res) { + if(err) { + Logger.errlog.log("! Failed to lookup " + channame + " ranks"); + callback(err, null); + return; } - return ranks; - } - if(rows.length == 0) { - return 0; + + for(var i in res) + res[i] = res[i].rank; + + callback(null, res); + }); +}; + +Database.prototype.setChannelRank = function (channame, name, rank, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + if(!$util.isValidChannelName(channame)) { + callback("Invalid channel name", null); + return; } - return rows[0].rank; -} + var query = "INSERT INTO `chan_" + channame + "_ranks` " + + "(name, rank) VALUES (?, ?) " + + "ON DUPLICATE KEY UPDATE rank=?"; -function setChannelRank(chan, name, rank) { - if(!chan.match(/^[a-zA-Z0-9-_]+$/)) { - return false; - } - var db = getConnection(); - if(!db) { - return false; + self.query(query, [name, rank, rank], callback); +}; + +Database.prototype.listChannelRanks = function (channame, callback) { + var self = this; + if(typeof callback !== "function") + return; + + if(!$util.isValidChannelName(channame)) { + callback("Invalid channel name", null); + return; } - var query = createQuery( - ["INSERT INTO `?` ", - "(`name`, `rank`) ", - "VALUES ", - "(?, ?) ", - "ON DUPLICATE KEY UPDATE ", - "`rank`=?"].join(""), - ["chan_"+chan+"_ranks", name, rank, rank] - ); + var query = "SELECT * FROM `chan_" + channame + "_ranks` WHERE 1"; + self.query(query, callback); +}; - return db.querySync(query); -} +Database.prototype.addToLibrary = function (channame, media, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; -function listChannelRanks(chan) { - if(!chan.match(/^[a-zA-Z0-9-_]+$/)) { - return []; - } - var db = getConnection(); - if(!db) { - return []; + if(!$util.isValidChannelName(channame)) { + callback("Invalid channel name"); + return; } - var query = createQuery( - "SELECT * FROM `?` WHERE 1", - ["chan_"+chan+"_ranks"] - ); + var query = "INSERT INTO `chan_" + channame + "_library` " + + "(id, title, seconds, type) " + + "VALUES (?, ?, ?, ?)"; + var params = [ + media.id, + media.title, + media.seconds, + media.type + ]; + self.query(query, params, callback); +}; - var results = db.querySync(query); - if(!results) { - return []; +Database.prototype.removeFromLibrary = function (channame, id, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + if(!$util.isValidChannelName(channame)) { + callback("Invalid channel name", null); + return; } - return results.fetchAllSync(); -} + var query = "DELETE FROM `chan_" + channame + "_library` WHERE id=?"; + self.query(query, [id], callback); +}; -function addToLibrary(chan, media) { - if(!chan.match(/^[a-zA-Z0-9-_]+$/)) { - return false; - } - var db = getConnection(); - if(!db) { - return false; +Database.prototype.getLibraryItem = function (channame, id, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + if(!$util.isValidChannelName(channame)) { + callback("Invalid channel name", null); + return; } - var query = createQuery( - ["INSERT INTO `?` ", - "(`id`, `title`, `seconds`, `type`) ", - "VALUES ", - "(?, ?, ?, ?)"].join(""), - ["chan_"+chan+"_library", media.id, media.title, media.seconds, media.type] - ); + var query = "SELECT id, title, seconds, type FROM " + + "`chan_" + channame + "_library` WHERE id=?"; - return db.querySync(query); -} + self.query(query, [id], function (err, res) { + if(err) { + callback(err, null); + return; + } -function removeFromLibrary(chan, id) { - if(!chan.match(/^[a-zA-Z0-9-_]+$/)) { - return false; - } - var db = getConnection(); - if(!db) { - return false; + callback(null, res.length > 0 ? res[0] : null); + }); +}; + +Database.prototype.searchLibrary = function (channame, term, callback) { + var self = this; + if(typeof callback !== "function") + return; + + if(!$util.isValidChannelName(channame)) { + callback("Invalid channel name", null); + return; } - var query = createQuery( - "DELETE FROM `?` WHERE id=?", - ["chan_"+chan+"_library", id] - ); + var query = "SELECT id, title, seconds, type FROM " + + "`chan_" + channame + "_library` WHERE title LIKE ?"; - return db.querySync(query); -} + self.query(query, ["%" + term + "%"], callback); +}; -function channelBan(chan, ip, name, banby) { - if(!chan.match(/^[a-zA-Z0-9-_]+$/)) { - return false; - } - var db = getConnection(); - if(!db) { - return false; +Database.prototype.addChannelBan = function (channame, ip, name, banBy, + callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + if(!$util.isValidChannelName(channame)) { + callback("Invalid channel name", null); + return; } - var query = createQuery( - ["INSERT INTO `?` ", - "(`ip`, `name`, `banner`) ", - "VALUES ", - "(?, ?, ?)"].join(""), - ["chan_"+chan+"_bans", ip, name, banby] - ); + var query = "INSERT INTO `chan_" + channame + "_bans`" + + "(ip, name, banner) VALUES (?, ?, ?)"; - return db.querySync(query); -} + self.query(query, [ip, name, banBy], callback); +}; -function channelUnbanIP(chan, ip) { - if(!chan.match(/^[a-zA-Z0-9-_]+$/)) { - return false; - } - var db = getConnection(); - if(!db) { - return false; +Database.prototype.clearChannelIPBan = function (channame, ip, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + if(!$util.isValidChannelName(channame)) { + callback("Invalid channel name", null); + return; } - var query = createQuery( - "DELETE FROM `?` WHERE `ip`=?", - ["chan_"+chan+"_bans", ip] - ); + var query = "DELETE FROM `chan_" + channame + "_bans` WHERE ip=?"; + self.query(query, [ip], callback); +}; - return db.querySync(query); -} - -function channelUnbanName(chan, name) { - if(!chan.match(/^[a-zA-Z0-9-_]+$/)) { - return false; - } - var db = getConnection(); - if(!db) { - return false; +Database.prototype.clearChannelNameBan = function (channame, name, + callback) { + var self = this; + if(typeof callback !== "function") { + callback = blackHole; + return; } - var query = createQuery( - "DELETE FROM `?` WHERE `ip`='*' AND `name`=?", - ["chan_"+chan+"_bans", name] - ); + var query = "DELETE FROM `chan_" + channame + "_bans` WHERE ip='*'" + + "AND name=?"; - return db.querySync(query); -} + self.query(query, [name], callback); +}; -/* REGION Users */ +/* END REGION */ -function getProfile(name) { - var db = getConnection(); - if(!db) { - return false; +/* REGION Auth */ + +Database.prototype.isUsernameTaken = function (name, callback) { + var self = this; + if(typeof callback !== "function") + return; + + var query = "SELECT id FROM registrations WHERE uname=?"; + self.query(query, [name], function (err, res) { + if(err) { + callback(err, null); + return; + } + + callback(null, res.length > 0); + }); +}; + +Database.prototype.registerUser = function (name, pw, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + if(!$util.isValidUserName(name)) { + callback("Invalid username", null); + return; } - var query = createQuery( - "SELECT profile_image,profile_text FROM registrations WHERE uname=?", - [name] - ); + var postRegister = function (err, res) { + if(err) { + callback(err, null); + return; + } - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to retrieve user profile"); - throw "Database failure. Contact an administrator."; - } + self.createLoginSession(name, function (err, hash) { + if(err) { + // Don't confuse people into thinking the registration + // failed when it was the session that failed + callback(null, ""); + return; + } - var rows = results.fetchAllSync(); - if(rows.length == 0) { - throw "User not found"; - } - - return { - profile_image: rows[0].profile_image, - profile_text: rows[0].profile_text + callback(null, hash); + }); }; -} -function setProfile(name, data) { - var db = getConnection(); - if(!db) { - return false; + self.isUsernameTaken(name, function (err, taken) { + if(err) { + callback(err, null); + return; + } + + if(taken) { + callback("Username already taken", null); + return; + } + + bcrypt.hash(pw, 10, function (err, hash) { + if(err) { + callback(err, null); + return; + } + + var query = "INSERT INTO registrations VALUES " + + "(NULL, ?, ?, 1, '', 0, '', '', '')"; + + self.query(query, [name, hash], postRegister); + }); + }); +}; + +Database.prototype.userLogin = function (name, pw, session, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var postLogin = function (err, row) { + if(err) { + callback(err, null); + return; + } + + if(row.session_hash) { + callback(null, row); + return; + } + + self.createLoginSession(name, function (err, hash) { + if(err) { + callback(err, null); + return; + } + + row.session_hash = hash; + callback(null, row); + }); + }; + + if(session) { + self.userLoginSession(name, session, postLogin); + } else if(pw) { + self.userLoginPassword(name, pw, postLogin); + } else { + callback("Invalid login", null); } +}; - var query = createQuery( - ["UPDATE `registrations` SET ", - "`profile_image`=?,", - "`profile_text`=? ", - "WHERE uname=?"].join(""), - [data.image, data.text, name] - ); +Database.prototype.userLoginPassword = function (name, pw, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; - return db.querySync(query); -} + var query = "SELECT * FROM registrations WHERE uname=?"; + self.query(query, [name], function (err, res) { + if(err) { + callback(err, null); + return; + } -function setUserEmail(name, email) { - var db = getConnection(); - if(!db) { - return false; - } + if(res.length == 0) { + callback("User does not exist", null); + return; + } - var query = createQuery( - "UPDATE `registrations` SET `email`=? WHERE `uname`=?", - [email, name] - ); + var row = res[0]; - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to set user email"); - return false; - } - return true; -} + bcrypt.compare(pw, row.pw, function (err, valid) { + if(valid) { + // For security, erase the password field before returning + delete row["pw"]; + row.session_hash = ""; + callback(null, row); + return; + } -function genSalt() { - var chars = "abcdefgihjklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - + "0123456789!@#$%^&*_+=~"; - var salt = []; - for(var i = 0; i < 32; i++) { - salt.push(chars[parseInt(Math.random()*chars.length)]); - } - return salt.join(''); -} + // Possibly could be a SHA256 hash from an *ancient* version + // of CyTube -function generatePasswordReset(ip, name, email) { - var db = getConnection(); - if(!db) { - return false; - } + var sha = hashlib.sha256(pw); + if(sha == row.pw) { + // Replace it + bcrypt.hash(pw, 10, function (err, hash) { + if(!err) { + self.query("UPDATE registrations SET pw=? "+ + "WHERE uname=?", [hash, name]); + } + }); - var query = createQuery( - "SELECT `email` FROM `registrations` WHERE `uname`=?", - [name] - ); + // Remove password field before returning + delete row["pw"]; + row.session_hash = ""; + callback(null, row); + } else { + callback("Invalid username/password combination", null); + } + }); + }); +}; - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to retrieve user email"); - return false; - } +Database.prototype.userLoginSession = function (name, session, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; - var rows = results.fetchAllSync(); - if(rows.length == 0) { - throw "Provided username does not exist"; - } - if(rows[0].email != email) { - throw "Provided email does not match user's email"; - } + var query = "SELECT * FROM registrations WHERE uname=? AND " + + "session_hash=?"; - // Validation complete, now time to reset it - var hash = hashlib.sha256(genSalt() + name); - var exp = Date.now() + 24*60*60*1000; - query = createQuery( - ["INSERT INTO `password_reset` (", - "`ip`, `name`, `hash`, `email`, `expire`", - ") VALUES (", - "?, ?, ?, ?, ?", - ") ON DUPLICATE KEY UPDATE `hash`=?,`expire`=?"].join(""), - [ip, name, hash, email, exp, hash, exp] - ); + self.query(query, [name, session], function (err, res) { + if(err) { + callback(err, null); + return; + } - results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to insert password reset"); - return false; - } + if(res.length == 0) { + callback("Session expired", null); + return; + } - return hash; -} + var row = res[0]; -function recoverPassword(hash) { - var db = getConnection(); - if(!db) { - return false; - } + if(row.expire < Date.now()) { + callback("Session expired", null); + return; + } - var query = createQuery( - "SELECT * FROM password_reset WHERE hash=?", - [hash] - ); + callback(null, row); + }); +}; - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to retrieve from password_reset"); - throw "Database error. Contact an administrator"; - } +Database.prototype.createLoginSession = function (name, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; - var rows = results.fetchAllSync(); - if(rows.length == 0) { - throw "Invalid password reset link"; - } + var salt = $util.randomSalt(32); + var hash = hashlib.sha256(salt + name); - db.querySync(createQuery( - "DELETE FROM password_reset WHERE hash=?", - [hash] - )); + var query = "UPDATE registrations SET session_hash=?, expire=? " + + "WHERE uname=?"; - if(Date.now() > rows[0].expire) { - throw "Link expired. Password resets are valid for 24 hours"; - } + self.query(query, [hash, Date.now() + 604800000, name], + function (err, res) { + if(err) { + callback(err, null); + return; + } - var pw; - if(!(pw = resetPassword(rows[0].name))) { - throw "Operation failed. Contact an administrator."; - } + callback(null, hash); + }); +}; - return [rows[0].name, pw]; -} +Database.prototype.setUserPassword = function (name, pw, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; -function resetPassword(name) { - var db = getConnection(); - if(!db) { - return false; - } + bcrypt.hash(pw, 10, function (err, hash) { + if(err) { + callback(err, null); + return; + } + var query = "UPDATE registrations SET pw=? WHERE uname=?"; + self.query(query, [hash, name], callback); + }); +}; + +Database.prototype.getGlobalRank = function (name, callback) { + var self = this; + if(typeof callback !== "function") + return; + + var query = "SELECT global_rank FROM registrations WHERE uname=?"; + + self.query(query, [name], function (err, res) { + if(err) { + callback(err, null); + return; + } + + if(res.length == 0) { + callback("User does not exist", null); + return; + } + + callback(null, res[0].global_rank); + }); +}; + +Database.prototype.listGlobalRanks = function (names, callback) { + var self = this; + if(typeof callback !== "function") + return; + + if(typeof names === "string") + names = [names]; + + // Build the query template (?, ?, ?, ?, ...) + var nlist = []; + for(var i in names) + nlist.push("?"); + nlist = "(" + nlist.join(",") + ")"; + + var query = "SELECT global_rank FROM registrations WHERE uname IN " + + nlist; + self.query(query, names, function (err, res) { + if(err) { + callback(err, null); + return; + } + + if(res.length == 0) { + callback("User does not exist", null); + return; + } + + for(var i in res) + res[i] = res[i].global_rank; + + callback(null, res); + }); +}; + +/* END REGION */ + +/* REGION users */ + +Database.prototype.searchUser = function (name, callback) { + var self = this; + if(typeof callback !== "function") + return; + + // NOTE: No SELECT * here because I don't want to risk exposing + // the user's password hash + var query = "SELECT id, uname, global_rank, profile_image, " + + "profile_text, email FROM registrations WHERE " + + "uname LIKE ?"; + + self.query(query, ["%" + name + "%"], callback); +}; + +/* rank */ + +Database.prototype.setGlobalRank = function (name, rank, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var query = "UPDATE registrations SET global_rank=? WHERE uname=?"; + self.query(query, [rank, name], callback); +}; + +/* email and profile */ + +Database.prototype.getUserProfile = function (name, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var query = "SELECT profile_image, profile_text FROM registrations " + + "WHERE uname=?"; + + self.query(query, [name], function (err, res) { + if(err) { + callback(err, null); + return; + } + + var def = { + profile_image: "", + profile_text: "" + }; + + callback(null, res.length > 0 ? res[0] : def); + }); +}; + +Database.prototype.setUserProfile = function (name, data, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var query = "UPDATE registrations SET profile_image=?, profile_text=?" + + "WHERE uname=?"; + + self.query(query, [data.image, data.text, name], callback); +}; + +Database.prototype.setUserEmail = function (name, email, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var query = "UPDATE registrations SET email=? WHERE uname=?"; + + self.query(query, [email, name], callback); +}; + +/* password recovery */ + +Database.prototype.genPasswordReset = function (ip, name, email, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var query = "SELECT email FROM registrations WHERE uname=?"; + self.query(query, [name], function (err, res) { + if(err) { + callback(err, null); + return; + } + + if(res.length == 0) { + callback("Provided username does not exist", null); + return; + } + + if(res[0].email != email) { + callback("Provided email does not match user's email", null); + return; + } + + var hash = hashlib.sha256($util.randomSalt(32) + name); + var expire = Date.now() + 24*60*60*1000; + query = "INSERT INTO password_reset " + + "(ip, name, hash, email, expire) VALUES (?, ?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE hash=?, expire=?"; + self.query(query, [ip, name, hash, email, exp, hash, exp], + function (err, res) { + if(err) { + callback(err, null); + return; + } + + callback(null, hash); + }); + }); +}; + +Database.prototype.recoverUserPassword = function (hash, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var query = "SELECT * FROM password_reset WHERE hash=?"; + self.query(query, [hash], function (err, res) { + if(err) { + callback(err, null); + return; + } + + if(rows.length == 0) { + callback("Invalid password reset link", null); + return; + } + + if(Date.now() > res[0].expire) { + self.query("DELETE FROM password_reset WHERE hash=?", [hash]); + callback("Link expired. Password resets are valid for 24hr", + null); + return; + } + + var name = res[0].name; + + self.resetUserPassword(res[0].name, function (err, pw) { + if(err) { + callback(err, null); + return; + } + + self.query("DELETE FROM password_reset WHERE hash=?", [hash]); + callback(null, { + name: name, + pw: pw + }); + }); + }); +}; + +Database.prototype.resetUserPassword = function (name, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var pwChars = "abcdefghijkmnopqrstuvwxyz023456789"; var pw = ""; - for(var i = 0; i < 10; i++) { - pw += "abcdefghijklmnopqrstuvwxyz"[parseInt(Math.random() * 25)]; - } - var hash = bcrypt.hashSync(pw, 10); - var query = createQuery( - "UPDATE `registrations` SET `pw`=? WHERE `uname`=?", - [hash, name] - ); + for(var i = 0; i < 10; i++) + pw += pwChars[parseInt(Math.random() * 33)]; - var results = db.querySync(query); - if(!results) { - return false; - } + bcrypt.hash(pw, 10, function (err, data) { + if(err) { + Logger.errlog.log("bcrypt error: " + err); + callback("Password reset failure", null); + return; + } - return pw; -} + var query = "UPDATE registrations SET pw=? WHERE uname=?"; + self.query(query, [data, name], function (err, res) { + if(err) { + callback(err, null); + return; + } -/* REGION User Playlists */ -function getUserPlaylists(user) { - var db = getConnection(); - if(!db) { - []; - } + callback(null, pw); + }); + }); +}; - var query = createQuery( - "SELECT name,count,time FROM user_playlists WHERE user=?", - [user] - ); +/* user playlists */ - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to query user playlists"); - return []; - } +Database.prototype.listUserPlaylists = function (name, callback) { + var self = this; + if(typeof callback !== "function") + return; - return results.fetchAllSync(); -} + var query = "SELECT name, count, time FROM user_playlists WHERE user=?"; + self.query(query, [name], callback); +}; -function loadUserPlaylist(user, name) { - var db = getConnection(); - if(!db) { - []; - } +Database.prototype.getUserPlaylist = function (username, plname, callback) { + var self = this; + if(typeof callback !== "function") + return; - var query = createQuery( - "SELECT contents FROM user_playlists WHERE user=? AND name=?", - [user, name] - ); + var query = "SELECT contents FROM user_playlists WHERE " + + "user=? AND name=?"; - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to query playlists"); - return []; - } + self.query(query, [username, plname], function (err, res) { + if(err) { + callback(err, null); + return; + } - var row = results.fetchAllSync()[0]; - var pl; - try { - pl = JSON.parse(row.contents); - } - catch(e) { - Logger.errlog.log("! Failed to load playlist "+user+"."+name); - return []; - } + if(res.length == 0) { + callback("Playlist does not exist", null); + return; + } - return pl; -} + var pl = null; + try { + pl = JSON.parse(res[0].contents); + } catch(e) { + callback("Malformed playlist JSON", null); + return; + } + callback(null, pl); + }); +}; -function saveUserPlaylist(pl, user, name) { - var db = getConnection(); - if(!db) { - return false; - } +Database.prototype.saveUserPlaylist = function (pl, username, plname, + callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; - // Strip out unnecessary data - var pl2 = []; - var time = 0; - for(var i = 0; i < pl.length; i++) { + var tmp = [], time = 0; + for(var i in pl) { var e = { id: pl[i].media.id, title: pl[i].media.title, @@ -915,180 +1194,241 @@ function saveUserPlaylist(pl, user, name) { type: pl[i].media.type }; time += pl[i].media.seconds; - pl2.push(e); + tmp.push(e); } - var count = pl2.length; - var plstr = JSON.stringify(pl2); + var count = tmp.length; + var plText = JSON.stringify(tmp); - var query = createQuery( - "INSERT INTO user_playlists VALUES (?, ?, ?, ?, ?)" + - "ON DUPLICATE KEY UPDATE contents=?,count=?,time=?", - [user, name, plstr, count, time, plstr, count, time] - ); + var query = "INSERT INTO user_playlists VALUES (?, ?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE contents=?, count=?, time=?"; - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to insert into playlists"); - return false; - } + var params = [username, plname, plText, count, time, + plText, count, time]; - return true; -} + self.query(query, params, callback); +}; -function deleteUserPlaylist(user, name) { - var db = getConnection(); - if(!db) { - return false; - } +Database.prototype.deleteUserPlaylist = function (username, plname, + callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; - var query = createQuery( - "DELETE FROM user_playlists WHERE user=? AND name=?", - [user, name] - ); + var query = "DELETE FROM user_playlists WHERE user=? AND name=?"; + self.query(query, [username, plname], callback); +}; - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to delete from user_playlists"); - } - return results; -} +/* user channels */ -function listUserChannels(user) { - var db = getConnection(); - if(!db) { - return []; - } +Database.prototype.listUserChannels = function (username, callback) { + var self = this; + if(typeof callback !== "function") + return; - var query = createQuery( - "SELECT * FROM channels WHERE owner=? ORDER BY id ASC", - [user] - ); + var query = "SELECT * FROM channels WHERE owner=? ORDER BY id ASC"; + self.query(query, [username], callback); +}; - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to list user channels"); - return []; - } +/* aliases */ - return results.fetchAllSync(); -} - -/* User Aliases */ - -function recordVisit(ip, name) { - var db = getConnection(); - if(!db) { - return false; - } +Database.prototype.recordVisit = function (ip, name, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; var time = Date.now(); - db.querySync(createQuery( - "DELETE FROM aliases WHERE ip=? AND name=?", - [ip, name] - )); - var query = createQuery( - "INSERT INTO aliases VALUES (NULL, ?, ?, ?)", - [ip, name, time] - ); + var query = "DELETE FROM aliases WHERE ip=? AND name=?;" + + "INSERT INTO aliases VALUES (NULL, ?, ?, ?)"; - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to record visit"); - } + self.query(query, [ip, name, ip, name, time], function (err, res) { + if(err) { + callback(err, null); + return; + } - // Keep most recent 5 records per IP - results = db.querySync(createQuery( - ["DELETE FROM aliases WHERE ip=? AND visit_id NOT IN (", - "SELECT visit_id FROM (", - "SELECT visit_id,time FROM aliases WHERE ip=? ORDER BY time DESC LIMIT 5", - ") foo", - ");"].join(""), - [ip, ip] - )); + callback(null, res); + query = "DELETE FROM aliases WHERE ip=? AND visit_id NOT IN (" + + "SELECT visit_id FROM (" + + "SELECT visit_id, time FROM aliases WHERE ip=?" + + "ORDER BY time DESC LIMIT 5" + + ") foo" + // The 'foo' here is actually necessary + ")"; - return results; -} - -function getAliases(ip) { - var db = getConnection(); - if(!db) { - return []; - } - - var query = createQuery( - "SELECT name FROM aliases WHERE ip=?", - [ip] - ); - - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to retrieve aliases"); - return []; - } - - var names = []; - results.fetchAllSync().forEach(function(row) { - names.push(row.name); + self.query(query, [ip, ip]); }); +}; - return names; -} +Database.prototype.listAliases = function (ip, callback) { + var self = this; + if(typeof callback !== "function") + return; -function ipForName(name) { - var db = getConnection(); - if(!db) { - return []; + var query = "SELECT name FROM aliases WHERE ip"; + // Range + if(ip.match(/^\d+\.\d+\.\d+$/)) { + query += " LIKE ?"; + ip += ".%"; + } else { + query += "=?"; } - var query = createQuery( - "SELECT ip FROM aliases WHERE name=?", - [name] - ); + self.query(query, [ip], function (err, res) { + var names = null; + if(!err) { + names = []; + res.forEach(function (row) { + names.push(row.name); + }); + } - var results = db.querySync(query); - if(!results) { - Logger.errlog.log("! Failed to retrieve IP for name"); - return []; - } - - var ips = []; - results.fetchAllSync().forEach(function(row) { - ips.push(row.ip); + callback(err, names); }); +}; - return ips; -} +Database.prototype.listIPsForName = function (name, callback) { + var self = this; + if(typeof callback !== "function") + return; -exports.setup = setup; -exports.getConnection = getConnection; -exports.createQuery = createQuery; -exports.init = init; -exports.checkGlobalBan = checkGlobalBan; -exports.refreshGlobalBans = refreshGlobalBans; -exports.globalBanIP = globalBanIP; -exports.globalUnbanIP = globalUnbanIP; -exports.registerChannel = registerChannel; -exports.loadChannel = loadChannel; -exports.deleteChannel = deleteChannel; -exports.getChannelRank = getChannelRank; -exports.setChannelRank = setChannelRank; -exports.listChannelRanks = listChannelRanks; -exports.addToLibrary = addToLibrary; -exports.removeFromLibrary = removeFromLibrary; -exports.channelBan = channelBan; -exports.channelUnbanIP = channelUnbanIP; -exports.channelUnbanName = channelUnbanName; -exports.setProfile = setProfile; -exports.getProfile = getProfile; -exports.setUserEmail = setUserEmail; -exports.generatePasswordReset = generatePasswordReset; -exports.recoverPassword = recoverPassword; -exports.resetPassword = resetPassword; -exports.getUserPlaylists = getUserPlaylists; -exports.loadUserPlaylist = loadUserPlaylist; -exports.saveUserPlaylist = saveUserPlaylist; -exports.deleteUserPlaylist = deleteUserPlaylist; -exports.listUserChannels = listUserChannels; -exports.recordVisit = recordVisit; -exports.getAliases = getAliases; -exports.ipForName = ipForName; + var query = "SELECT ip FROM aliases WHERE name=?"; + self.query(query, [name], function (err, res) { + var ips = null; + if(!err) { + ips = []; + res.forEach(function (row) { + ips.push(row.ip); + }); + } + + callback(err, ips); + }); +}; + +/* END REGION */ + +/* REGION action log */ + +Database.prototype.recordAction = function (ip, name, action, args, + callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var query = "INSERT INTO actionlog (ip, name, action, args, time) " + + "VALUES (?, ?, ?, ?, ?)"; + + self.query(query, [ip, name, action, args, Date.now()], callback); +}; + +Database.prototype.clearActions = function (actions, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var list = []; + for(var i in actions) + list.push("?"); + + var actionlist = "(" + list.join(",") + ")"; + + var query = "DELETE FROM actionlog WHERE action IN " + actionlist; + self.query(query, actions, callback); +}; + +Database.prototype.clearSingleAction = function (item, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var query = "DELETE FROM actionlog WHERE ip=? AND time=?"; + self.query(query, [item.ip, item.time], callback); +}; + + +Database.prototype.recentRegistrationCount = function (ip, callback) { + var self = this; + if(typeof callback !== "function") + return; + + var query = "SELECT * FROM actionlog WHERE ip=? " + + "AND action='register-success' AND time > ?"; + + self.query(query, [ip, Date.now() - 48 * 3600 * 1000], + function (err, res) { + if(err) { + callback(err, null); + return; + } + + callback(null, res.length); + }); +}; + +Database.prototype.listActionTypes = function (callback) { + var self = this; + if(typeof callback !== "function") + return; + + var query = "SELECT DISTINCT action FROM actionlog"; + self.query(query, function (err, res) { + if(err) { + callback(err, null); + return; + } + + var types = []; + res.forEach(function (row) { + types.push(row.action); + }); + callback(null, types); + }); +}; + +Database.prototype.listActions = function (types, callback) { + var self = this; + if(typeof callback !== "function") + return; + + var list = []; + for(var i in types) + list.push("?"); + + var actionlist = "(" + list.join(",") + ")"; + var query = "SELECT * FROM actionlog WHERE action IN " + actionlist; + self.query(query, types, callback); +}; + +/* END REGION */ + +/* REGION stats */ + +Database.prototype.addStatPoint = function (time, ucount, ccount, mem, + callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var query = "INSERT INTO stats VALUES (?, ?, ?, ?)"; + self.query(query, [time, ucount, ccount, mem], callback); +}; + +Database.prototype.pruneStats = function (before, callback) { + var self = this; + if(typeof callback !== "function") + callback = blackHole; + + var query = "DELETE FROM stats WHERE time < ?"; + self.query(query, [before], callback); +}; + +Database.prototype.listStats = function (callback) { + var self = this; + if(typeof callback !== "function") + return; + + var query = "SELECT * FROM stats ORDER BY time ASC"; + self.query(query, callback); +}; + +/* END REGION */ +module.exports = Database; diff --git a/package.json b/package.json index 98fdc2dc..b5fe4dcb 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,14 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "2.3.3", + "version": "2.4.0", "repository": { "url": "http://github.com/calzoneman/sync" }, "dependencies": { "socket.io": ">=0.9", "express": ">=3.2", - "mysql-libmysqlclient": "*", + "mysql": "2.0.0-alpha8", "node_hash": "*", "bcrypt": "*", "nodemailer": "*", diff --git a/server.js b/server.js index 1ff32018..23094c96 100644 --- a/server.js +++ b/server.js @@ -6,7 +6,7 @@ var Logger = require("./logger"); var Channel = require("./channel"); var User = require("./user"); -const VERSION = "2.3.3"; +const VERSION = "2.4.0"; function getIP(req) { var raw = req.connection.remoteAddress; @@ -74,6 +74,7 @@ var Server = { ips: {}, acp: null, httpaccess: null, + actionlog: null, logHTTP: function (req, status) { if(status === undefined) status = 200; @@ -89,8 +90,14 @@ var Server = { this.httpaccess.log([ipstr, req.method, url, status, req.headers["user-agent"]].join(" ")); }, init: function () { + // init database + var Database = require("./database"); + this.db = new Database(this.cfg); + this.db.init(); + this.actionlog = require("./actionlog")(this); this.httpaccess = new Logger.Logger("httpaccess.log"); this.app = express(); + this.app.use(express.bodyParser()); // channel path this.app.get("/r/:channel(*)", function (req, res, next) { var c = req.params.channel; @@ -105,10 +112,12 @@ var Server = { // api path this.api = require("./api")(this); + /* this.app.get("/api/:apireq(*)", function (req, res, next) { this.logHTTP(req); this.api.handle(req.url.substring(5), req, res); }.bind(this)); + */ this.app.get("/", function (req, res, next) { this.logHTTP(req); @@ -167,14 +176,15 @@ var Server = { this.io.sockets.on("connection", function (socket) { var ip = getSocketIP(socket); socket._ip = ip; - if(this.db.checkGlobalBan(ip)) { - Logger.syslog.log("Disconnecting " + ip + " - gbanned"); - socket.emit("kick", { - reason: "You're globally banned." - }); - socket.disconnect(true); - return; - } + this.db.isGlobalIPBanned(ip, function (err, bant) { + if(bant) { + Logger.syslog.log("Disconnecting " + ip + " - gbanned"); + socket.emit("kick", { + reason: "You're globally banned." + }); + socket.disconnect(true); + } + }); socket.on("disconnect", function () { this.ips[ip]--; @@ -197,10 +207,6 @@ var Server = { new User(socket, this); }.bind(this)); - // init database - this.db = require("./database"); - this.db.setup(Server.cfg); - this.db.init(); // init ACP this.acp = require("./acp")(this); diff --git a/stats.js b/stats.js index 04349990..c8a061af 100644 --- a/stats.js +++ b/stats.js @@ -15,6 +15,7 @@ const STAT_INTERVAL = 60 * 60 * 1000; const STAT_EXPIRE = 24 * STAT_INTERVAL; module.exports = function (Server) { + var db = Server.db; setInterval(function () { var chancount = Server.channels.length; var usercount = 0; @@ -24,29 +25,8 @@ module.exports = function (Server) { var mem = process.memoryUsage().rss; - var db = Server.db.getConnection(); - if(!db) - return; - - var query = Server.db.createQuery( - "INSERT INTO stats VALUES (?, ?, ?, ?)", - [Date.now(), usercount, chancount, mem] - ); - - if(!db.querySync(query)) { - Logger.errlog.log("! Failed to record stats"); - Logger.errlog.log(query); - } - - query = Server.db.createQuery( - "DELETE FROM stats WHERE time this.rank) { - this.rank = chanrank; - } + self.channel = self.server.getChannel(data.name); + if(self.loggedIn) { + self.channel.getRank(self.name, function (err, rank) { + if(!err && rank > self.rank) + self.rank = rank; + }); } - this.channel.userJoin(this); - }.bind(this)); + self.channel.userJoin(self); + }); - this.socket.on("login", function(data) { + self.socket.on("login", function(data) { var name = data.name || ""; var pw = data.pw || ""; var session = data.session || ""; if(pw.length > 100) pw = pw.substring(0, 100); - if(this.name == "") - this.login(name, pw, session); - }.bind(this)); + if(self.name == "") + self.login(name, pw, session); + }); - this.socket.on("assignLeader", function(data) { - if(this.channel != null) { - this.channel.tryChangeLeader(this, data); + self.socket.on("assignLeader", function(data) { + if(self.channel != null) { + self.channel.tryChangeLeader(self, data); } - }.bind(this)); + }); - this.socket.on("promote", function(data) { - if(this.channel != null) { - this.channel.tryPromoteUser(this, data); + self.socket.on("promote", function(data) { + if(self.channel != null) { + self.channel.tryPromoteUser(self, data); } - }.bind(this)); + }); - this.socket.on("demote", function(data) { - if(this.channel != null) { - this.channel.tryDemoteUser(this, data); + self.socket.on("demote", function(data) { + if(self.channel != null) { + self.channel.tryDemoteUser(self, data); } - }.bind(this)); + }); - this.socket.on("setChannelRank", function(data) { - if(this.channel != null) { - this.channel.trySetRank(this, data); + self.socket.on("setChannelRank", function(data) { + if(self.channel != null) { + self.channel.trySetRank(self, data); } - }.bind(this)); + }); - this.socket.on("banName", function(data) { - if(this.channel != null) { - this.channel.banName(this, data.name || ""); + self.socket.on("banName", function(data) { + if(self.channel != null) { + self.channel.banName(self, data.name || ""); } - }.bind(this)); + }); - this.socket.on("banIP", function(data) { - if(this.channel != null) { - this.channel.tryIPBan(this, data); + self.socket.on("banIP", function(data) { + if(self.channel != null) { + self.channel.tryIPBan(self, data); } - }.bind(this)); + }); - this.socket.on("unban", function(data) { - if(this.channel != null) { - this.channel.tryUnban(this, data); + self.socket.on("unban", function(data) { + if(self.channel != null) { + self.channel.tryUnban(self, data); } - }.bind(this)); + }); - this.socket.on("chatMsg", function(data) { - if(this.channel != null) { + self.socket.on("chatMsg", function(data) { + if(self.channel != null) { if(data.msg.indexOf("/afk") != 0) { - this.setAFK(false); - this.autoAFK(); + self.setAFK(false); + self.autoAFK(); } - this.channel.tryChat(this, data); + self.channel.tryChat(self, data); } - }.bind(this)); + }); - this.socket.on("newPoll", function(data) { - if(this.channel != null) { - this.channel.tryOpenPoll(this, data); + self.socket.on("newPoll", function(data) { + if(self.channel != null) { + self.channel.tryOpenPoll(self, data); } - }.bind(this)); + }); - this.socket.on("playerReady", function() { - if(this.channel != null) { - this.channel.sendMediaUpdate(this); + self.socket.on("playerReady", function() { + if(self.channel != null) { + self.channel.sendMediaUpdate(self); } - }.bind(this)); + }); - this.socket.on("requestPlaylist", function() { - if(this.channel != null) { - this.channel.sendPlaylist(this); + self.socket.on("requestPlaylist", function() { + if(self.channel != null) { + self.channel.sendPlaylist(self); } - }.bind(this)); + }); - this.socket.on("queue", function(data) { - if(this.channel != null) { - this.channel.tryQueue(this, data); + self.socket.on("queue", function(data) { + if(self.channel != null) { + self.channel.tryQueue(self, data); } - }.bind(this)); + }); - this.socket.on("setTemp", function(data) { - if(this.channel != null) { - this.channel.trySetTemp(this, data); + self.socket.on("setTemp", function(data) { + if(self.channel != null) { + self.channel.trySetTemp(self, data); } - }.bind(this)); + }); - this.socket.on("delete", function(data) { - if(this.channel != null) { - this.channel.tryDequeue(this, data); + self.socket.on("delete", function(data) { + if(self.channel != null) { + self.channel.tryDequeue(self, data); } - }.bind(this)); + }); - this.socket.on("uncache", function(data) { - if(this.channel != null) { - this.channel.tryUncache(this, data); + self.socket.on("uncache", function(data) { + if(self.channel != null) { + self.channel.tryUncache(self, data); } - }.bind(this)); + }); - this.socket.on("moveMedia", function(data) { - if(this.channel != null) { - this.channel.tryMove(this, data); + self.socket.on("moveMedia", function(data) { + if(self.channel != null) { + self.channel.tryMove(self, data); } - }.bind(this)); + }); - this.socket.on("jumpTo", function(data) { - if(this.channel != null) { - this.channel.tryJumpTo(this, data); + self.socket.on("jumpTo", function(data) { + if(self.channel != null) { + self.channel.tryJumpTo(self, data); } - }.bind(this)); + }); - this.socket.on("playNext", function() { - if(this.channel != null) { - this.channel.tryPlayNext(this); + self.socket.on("playNext", function() { + if(self.channel != null) { + self.channel.tryPlayNext(self); } - }.bind(this)); + }); - this.socket.on("clearPlaylist", function() { - if(this.channel != null) { - this.channel.tryClearqueue(this); + self.socket.on("clearPlaylist", function() { + if(self.channel != null) { + self.channel.tryClearqueue(self); } - }.bind(this)); + }); - this.socket.on("shufflePlaylist", function() { - if(this.channel != null) { - this.channel.tryShufflequeue(this); + self.socket.on("shufflePlaylist", function() { + if(self.channel != null) { + self.channel.tryShufflequeue(self); } - }.bind(this)); + }); - this.socket.on("togglePlaylistLock", function() { - if(this.channel != null) { - this.channel.tryToggleLock(this); + self.socket.on("togglePlaylistLock", function() { + if(self.channel != null) { + self.channel.tryToggleLock(self); } - }.bind(this)); + }); - this.socket.on("mediaUpdate", function(data) { - if(this.channel != null) { - this.channel.tryUpdate(this, data); + self.socket.on("mediaUpdate", function(data) { + if(self.channel != null) { + self.channel.tryUpdate(self, data); } - }.bind(this)); + }); - this.socket.on("searchMedia", function(data) { - if(this.channel != null) { + self.socket.on("searchMedia", function(data) { + if(self.channel != null) { if(data.source == "yt") { - var callback = function(vids) { - this.socket.emit("searchResults", { + var searchfn = self.server.infogetter.Getters["ytSearch"]; + searchfn(data.query.split(" "), function (e, vids) { + if(!e) { + self.socket.emit("searchResults", { + results: vids + }); + } + }); + } else { + self.channel.search(data.query, function (vids) { + self.socket.emit("searchResults", { results: vids }); - }.bind(this); - this.channel.search(data.query, callback); - } - else { - this.socket.emit("searchResults", { - results: this.channel.search(data.query) }); } } - }.bind(this)); + }); - this.socket.on("closePoll", function() { - if(this.channel != null) { - this.channel.tryClosePoll(this); + self.socket.on("closePoll", function() { + if(self.channel != null) { + self.channel.tryClosePoll(self); } - }.bind(this)); + }); - this.socket.on("vote", function(data) { - if(this.channel != null) { - this.channel.tryVote(this, data); + self.socket.on("vote", function(data) { + if(self.channel != null) { + self.channel.tryVote(self, data); } - }.bind(this)); + }); - this.socket.on("registerChannel", function(data) { - if(this.channel == null) { - this.socket.emit("channelRegistration", { + self.socket.on("registerChannel", function(data) { + if(self.channel == null) { + self.socket.emit("channelRegistration", { success: false, error: "You're not in any channel!" }); } else { - this.channel.tryRegister(this); + self.channel.tryRegister(self); } - }.bind(this)); + }); - this.socket.on("unregisterChannel", function() { - if(this.channel == null) { + self.socket.on("unregisterChannel", function() { + if(self.channel == null) { return; } - if(!this.channel.registered) { - this.socket.emit("unregisterChannel", { - success: false, - error: "This channel is already unregistered" - }); - } - else if(this.rank < 10) { - this.socket.emit("unregisterChannel", { - success: false, - error: "You don't have permission to unregister" - }); - } - else if(this.channel.unregister()) { - this.socket.emit("unregisterChannel", { - success: true - }); - } - else { - this.socket.emit("unregisterChannel", { - success: false, - error: "Unregistration failed. Please see a site admin if this continues." - }); - } - }.bind(this)); + self.channel.unregister(self); + }); - this.socket.on("setOptions", function(data) { - if(this.channel != null) { - this.channel.tryUpdateOptions(this, data); + self.socket.on("setOptions", function(data) { + if(self.channel != null) { + self.channel.tryUpdateOptions(self, data); } - }.bind(this)); + }); - this.socket.on("setPermissions", function(data) { - if(this.channel != null) { - this.channel.tryUpdatePermissions(this, data); + self.socket.on("setPermissions", function(data) { + if(self.channel != null) { + self.channel.tryUpdatePermissions(self, data); } - }.bind(this)); + }); - this.socket.on("setChannelCSS", function(data) { - if(this.channel != null) { - this.channel.trySetCSS(this, data); + self.socket.on("setChannelCSS", function(data) { + if(self.channel != null) { + self.channel.trySetCSS(self, data); } - }.bind(this)); + }); - this.socket.on("setChannelJS", function(data) { - if(this.channel != null) { - this.channel.trySetJS(this, data); + self.socket.on("setChannelJS", function(data) { + if(self.channel != null) { + self.channel.trySetJS(self, data); } - }.bind(this)); + }); - this.socket.on("updateFilter", function(data) { - if(this.channel != null) { - this.channel.tryUpdateFilter(this, data); + self.socket.on("updateFilter", function(data) { + if(self.channel != null) { + self.channel.tryUpdateFilter(self, data); } - }.bind(this)); + }); - this.socket.on("removeFilter", function(data) { - if(this.channel != null) { - this.channel.tryRemoveFilter(this, data); + self.socket.on("removeFilter", function(data) { + if(self.channel != null) { + self.channel.tryRemoveFilter(self, data); } - }.bind(this)); + }); - this.socket.on("moveFilter", function(data) { - if(this.channel != null) { - this.channel.tryMoveFilter(this, data); + self.socket.on("moveFilter", function(data) { + if(self.channel != null) { + self.channel.tryMoveFilter(self, data); } - }.bind(this)); + }); - this.socket.on("setMotd", function(data) { - if(this.channel != null) { - this.channel.tryUpdateMotd(this, data); + self.socket.on("setMotd", function(data) { + if(self.channel != null) { + self.channel.tryUpdateMotd(self, data); } - }.bind(this)); + }); - this.socket.on("requestLoginHistory", function() { - if(this.channel != null) { - this.channel.sendLoginHistory(this); + self.socket.on("requestLoginHistory", function() { + if(self.channel != null) { + self.channel.sendLoginHistory(self); } - }.bind(this)); + }); - this.socket.on("requestBanlist", function() { - if(this.channel != null) { - this.channel.sendBanlist(this); + self.socket.on("requestBanlist", function() { + if(self.channel != null) { + self.channel.sendBanlist(self); } - }.bind(this)); + }); - this.socket.on("requestChatFilters", function() { - if(this.channel != null) { - this.channel.sendChatFilters(this); + self.socket.on("requestChatFilters", function() { + if(self.channel != null) { + self.channel.sendChatFilters(self); } - }.bind(this)); + }); - this.socket.on("requestChannelRanks", function() { - if(this.channel != null) { - if(this.noflood("requestChannelRanks", 0.25)) + self.socket.on("requestChannelRanks", function() { + if(self.channel != null) { + if(self.noflood("requestChannelRanks", 0.25)) return; - this.channel.sendChannelRanks(this); + self.channel.sendChannelRanks(self); } - }.bind(this)); + }); - this.socket.on("voteskip", function(data) { - if(this.channel != null) { - this.channel.tryVoteskip(this); + self.socket.on("voteskip", function(data) { + if(self.channel != null) { + self.channel.tryVoteskip(self); } - }.bind(this)); + }); - this.socket.on("listPlaylists", function(data) { - if(this.name == "" || this.rank < 1) { - this.socket.emit("listPlaylists", { + self.socket.on("listPlaylists", function(data) { + if(self.name == "" || self.rank < 1) { + self.socket.emit("listPlaylists", { pllist: [], error: "You must be logged in to manage playlists" }); return; } - var list = this.server.db.getUserPlaylists(this.name); - for(var i = 0; i < list.length; i++) { - list[i].time = formatTime(list[i].time); - } - this.socket.emit("listPlaylists", { - pllist: list, + self.server.db.listUserPlaylists(self.name, function (err, list) { + if(err) + list = []; + for(var i = 0; i < list.length; i++) { + list[i].time = formatTime(list[i].time); + } + self.socket.emit("listPlaylists", { + pllist: list, + }); }); - }.bind(this)); + }); - this.socket.on("savePlaylist", function(data) { - if(this.rank < 1) { - this.socket.emit("savePlaylist", { + self.socket.on("savePlaylist", function(data) { + if(self.rank < 1) { + self.socket.emit("savePlaylist", { success: false, error: "You must be logged in to manage playlists" }); return; } - if(this.channel == null) { - this.socket.emit("savePlaylist", { + if(self.channel == null) { + self.socket.emit("savePlaylist", { success: false, error: "Not in a channel" }); @@ -487,79 +471,100 @@ User.prototype.initCallbacks = function() { return; } - var pl = this.channel.playlist.items.toArray(); - var result = this.server.db.saveUserPlaylist(pl, this.name, data.name); - this.socket.emit("savePlaylist", { - success: result, - error: result ? false : "Unknown" - }); - var list = this.server.db.getUserPlaylists(this.name); - for(var i = 0; i < list.length; i++) { - list[i].time = formatTime(list[i].time); - } - this.socket.emit("listPlaylists", { - pllist: list, - }); - }.bind(this)); + var pl = self.channel.playlist.items.toArray(); + self.server.db.saveUserPlaylist(pl, self.name, data.name, + function (err, res) { + if(err) { + self.socket.emit("savePlaylist", { + success: false, + error: err + }); + return; + } + + self.socket.emit("savePlaylist", { + success: true + }); - this.socket.on("queuePlaylist", function(data) { - if(this.channel != null) { - this.channel.tryQueuePlaylist(this, data); - } - }.bind(this)); + self.server.db.listUserPlaylists(self.name, + function (err, list) { + if(err) + list = []; + for(var i = 0; i < list.length; i++) { + list[i].time = formatTime(list[i].time); + } + self.socket.emit("listPlaylists", { + pllist: list, + }); + }); + }); + }); - this.socket.on("deletePlaylist", function(data) { + self.socket.on("queuePlaylist", function(data) { + if(self.channel != null) { + self.channel.tryQueuePlaylist(self, data); + } + }); + + self.socket.on("deletePlaylist", function(data) { if(typeof data.name != "string") { return; } - this.server.db.deleteUserPlaylist(this.name, data.name); - var list = this.server.db.getUserPlaylists(this.name); - for(var i = 0; i < list.length; i++) { - list[i].time = formatTime(list[i].time); - } - this.socket.emit("listPlaylists", { - pllist: list, + self.server.db.deleteUserPlaylist(self.name, data.name, + function () { + self.server.db.listUserPlaylists(self.name, + function (err, list) { + if(err) + list = []; + for(var i = 0; i < list.length; i++) { + list[i].time = formatTime(list[i].time); + } + self.socket.emit("listPlaylists", { + pllist: list, + }); + }); }); - }.bind(this)); + }); - this.socket.on("readChanLog", function () { - if(this.channel !== null) { - this.channel.tryReadLog(this); + self.socket.on("readChanLog", function () { + if(self.channel !== null) { + self.channel.tryReadLog(self); } - }.bind(this)); + }); - this.socket.on("acp-init", function() { - if(this.global_rank >= Rank.Siteadmin) - this.server.acp.init(this); - }.bind(this)); + self.socket.on("acp-init", function() { + if(self.global_rank >= Rank.Siteadmin) + self.server.acp.init(self); + }); - this.socket.on("borrow-rank", function(rank) { - if(this.global_rank < 255) + self.socket.on("borrow-rank", function(rank) { + if(self.global_rank < 255) return; - if(rank > this.global_rank) + if(rank > self.global_rank) return; - this.rank = rank; - this.socket.emit("rank", rank); - if(this.channel != null) - this.channel.broadcastUserUpdate(this); + self.rank = rank; + self.socket.emit("rank", rank); + if(self.channel != null) + self.channel.broadcastUserUpdate(self); - }.bind(this)); + }); } var lastguestlogin = {}; // Attempt to login User.prototype.login = function(name, pw, session) { + var self = this; // No password => try guest login if(pw == "" && session == "") { - if(this.ip in lastguestlogin) { - var diff = (Date.now() - lastguestlogin[this.ip])/1000; - if(diff < this.server.cfg["guest-login-delay"]) { - this.socket.emit("login", { + if(self.ip in lastguestlogin) { + var diff = (Date.now() - lastguestlogin[self.ip])/1000; + if(diff < self.server.cfg["guest-login-delay"]) { + self.socket.emit("login", { success: false, error: ["Guest logins are restricted to one per ", - this.server.cfg["guest-login-delay"] + self.server.cfg["guest-login-delay"] + " seconds per IP. ", "This restriction does not apply to registered users." ].join("") @@ -567,149 +572,111 @@ User.prototype.login = function(name, pw, session) { return false; } } - try { - // Sorry bud, can't take that name - if(Auth.isRegistered(name)) { - this.socket.emit("login", { - success: false, - error: "That username is already taken" - }); - return false; - } - // YOUR ARGUMENT IS INVALID - else if(!Auth.validateName(name)) { - this.socket.emit("login", { - success: false, - error: "Invalid username. Usernames must be 1-20 characters long and consist only of alphanumeric characters and underscores" - }); - } - else { - if(this.channel != null) { - for(var i = 0; i < this.channel.users.length; i++) { - if(this.channel.users[i].name == name) { - this.socket.emit("login", { - success: false, - error: "That name is already taken on this channel" - }); - return; - } - } - } - lastguestlogin[this.ip] = Date.now(); - this.rank = Rank.Guest; - Logger.syslog.log(this.ip + " signed in as " + name); - this.server.db.recordVisit(this.ip, name); - this.name = name; - this.loggedIn = false; - this.socket.emit("login", { - success: true, - name: name - }); - this.socket.emit("rank", this.rank); - if(this.channel != null) { - this.channel.logger.log(this.ip + " signed in as " + name); - this.channel.broadcastNewUser(this); - } - } - } - catch(e) { - this.socket.emit("login", { + if(!$util.isValidUserName(name)) { + self.socket.emit("login", { success: false, - error: e + error: "Invalid username. Usernames must be 1-20 characters long and consist only of alphanumeric characters and underscores" }); + return; } - } - else { - try { - var row; - if((row = Auth.login(name, pw, session))) { - if(this.channel != null) { - for(var i = 0; i < this.channel.users.length; i++) { - if(this.channel.users[i].name == name) { - this.channel.kick(this.channel.users[i], "Duplicate login"); - } - } - } - if(this.global_rank >= 255) - ActionLog.record(this.ip, name, "login-success"); - this.loggedIn = true; - this.socket.emit("login", { - success: true, - session: row.session_hash, - name: name - }); - Logger.syslog.log(this.ip + " logged in as " + name); - this.server.db.recordVisit(this.ip, name); - this.profile = { - image: row.profile_image, - text: row.profile_text - }; - var chanrank = (this.channel != null) ? this.channel.getRank(name) - : Rank.Guest; - var rank = (chanrank > row.global_rank) ? chanrank - : row.global_rank; - this.rank = (this.rank > rank) ? this.rank : rank; - this.global_rank = row.global_rank; - this.socket.emit("rank", this.rank); - this.name = name; - if(this.channel != null) { - this.channel.logger.log(this.ip + " logged in as " + name); - this.channel.broadcastNewUser(this); - } - } - // Wrong password - else { - ActionLog.record(this.ip, this.name, "login-failure"); - this.socket.emit("login", { - success: false, - error: "Invalid session" - }); - return false; - } - } - catch(e) { - this.socket.emit("login", { - success: false, - error: e - }); - } - } -} -// Attempt to register a user account -User.prototype.register = function(name, pw) { - if(pw == "") { - // Sorry bud, password required - this.socket.emit("register", { - success: false, - error: "You must provide a password" + self.server.db.isUsernameTaken(name, function (err, taken) { + if(err) { + self.socket.emit("login", { + success: false, + error: "Internal error: " + err + }); + return; + } + + if(taken) { + self.socket.emit("login", { + success: false, + error: "That username is taken" + }); + return; + } + + if(self.channel != null) { + for(var i = 0; i < self.channel.users.length; i++) { + if(self.channel.users[i].name == name) { + self.socket.emit("login", { + success: false, + error: "That name is already taken on self channel" + }); + return; + } + } + } + lastguestlogin[self.ip] = Date.now(); + self.rank = Rank.Guest; + Logger.syslog.log(self.ip + " signed in as " + name); + self.server.db.recordVisit(self.ip, name); + self.name = name; + self.loggedIn = false; + self.socket.emit("login", { + success: true, + name: name + }); + self.socket.emit("rank", self.rank); + if(self.channel != null) { + self.channel.logger.log(self.ip + " signed in as " + name); + self.channel.broadcastNewUser(self); + } }); - return false; - } - else if(Auth.isRegistered(name)) { - this.socket.emit("register", { - success: false, - error: "That username is already taken" - }); - return false; - } - else if(!Auth.validateName(name)) { - this.socket.emit("register", { - success: false, - error: "Invalid username. Usernames must be 1-20 characters long and consist only of alphanumeric characters and underscores" - }); - } - else if(Auth.register(name, pw)) { - console.log(this.ip + " registered " + name); - this.socket.emit("register", { - success: true - }); - this.login(name, pw); - } - else { - this.socket.emit("register", { - success: false, - error: "[](/ppshrug) Registration Failed." + } else { + self.server.db.userLogin(name, pw, session, function (err, row) { + if(err) { + self.server.actionlog.record(self.ip, name, "login-failure"); + self.socket.emit("login", { + success: false, + error: err + }); + return; + } + if(self.channel != null) { + for(var i = 0; i < self.channel.users.length; i++) { + if(self.channel.users[i].name.toLowerCase() == name.toLowerCase()) { + self.channel.kick(self.channel.users[i], "Duplicate login"); + } + } + } + if(self.global_rank >= 255) + self.server.actionlog.record(self.ip, name, "login-success"); + self.loggedIn = true; + self.socket.emit("login", { + success: true, + session: row.session_hash, + name: name + }); + Logger.syslog.log(self.ip + " logged in as " + name); + self.server.db.recordVisit(self.ip, name); + self.profile = { + image: row.profile_image, + text: row.profile_text + }; + self.global_rank = row.global_rank; + var afterRankLookup = function () { + self.socket.emit("rank", self.rank); + self.name = name; + if(self.channel != null) { + self.channel.logger.log(self.ip + " logged in as " + + name); + self.channel.broadcastNewUser(self); + } + }; + if(self.channel !== null) { + self.channel.getRank(name, function (err, rank) { + if(!err) + self.rank = rank; + else + self.rank = self.global_rank; + afterRankLookup(); + }); + } else { + self.rank = self.global_rank; + afterRankLookup(); + } }); } } diff --git a/utilities.js b/utilities.js new file mode 100644 index 00000000..0e401cb4 --- /dev/null +++ b/utilities.js @@ -0,0 +1,29 @@ +module.exports = { + isValidChannelName: function (name) { + return name.match(/^[\w-_]{1,30}$/); + }, + + isValidUserName: function (name) { + return name.match(/^[\w-_]{1,20}$/); + }, + + randomSalt: function (length) { + var chars = "abcdefgihjklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "0123456789!@#$%^&*_+=~"; + var salt = []; + for(var i = 0; i < length; i++) { + salt.push(chars[parseInt(Math.random()*chars.length)]); + } + return salt.join(''); + }, + + maskIP: function (ip) { + if(ip.match(/^\d+\.\d+\.\d+\.\d+$/)) { + // standard 32 bit IP + return ip.replace(/\d+\.\d+\.(\d+\.\d+)/, "x.x.$1"); + } else if(ip.match(/^\d+\.\d+\.\d+/)) { + // /24 range + return ip.replace(/\d+\.\d+\.(\d+)/, "x.x.$1.*"); + } + } +}; diff --git a/www/assets/js/account.js b/www/assets/js/account.js index 1f1d8fe0..c65cf7f9 100644 --- a/www/assets/js/account.js +++ b/www/assets/js/account.js @@ -9,19 +9,44 @@ 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. */ +/* + So, it turns out that $.post causes Firefox to use a GET request + on cross-site requests. What the hell? I'd understand if they just + made it error instead, but why give me chicken tenders if I ordered a + cheeseburger and act like everything's peachy? +*/ +function postJSON(url, data, callback) { + $.ajax(url, { + method: "POST", + crossDomain: true, + data: data, + success: function (data) { + try { + data = data.substring(data.indexOf("{")); + data = data.substring(0, data.lastIndexOf("}") + 1); + data = JSON.parse(data); + callback(data); + } catch(e) { + return; + } + }, + dataType: "text" + }); +} + var uname = readCookie("cytube_uname") || ""; var session = readCookie("cytube_session") || ""; -var api = WEB_URL + "/api/json/"; var loggedin = false; if(uname && session) { - var loginstr = "name=" + encodeURIComponent(uname) - + "&session=" + session; - var url = api + "login?" + loginstr + "&callback=?"; - $.getJSON(url, function(data) { - if(data.success) { + var data = { + name: uname, + session: session + }; + postJSON(WEB_URL + "/api/login?callback=?", data, function (data) { + console.log(data); + if(data.success) onLogin(); - } }); } @@ -57,7 +82,8 @@ $("#email").click(makeTabCallback("#email", "#changeemailpane")); $("#profile").click(makeTabCallback("#profile", "#profilepane")); $("#profile").click(function() { if(uname != "") { - $.getJSON(api + "getprofile?name=" + encodeURIComponent(uname) + "&callback=?", function(data) { + $.getJSON(WEB_URL+"/api/users/"+uname+"/profile?callback=?", + function (data) { if(data.success) { $("#profiletext").val(data.profile_text); $("#profileimg").val(data.profile_image); @@ -82,8 +108,10 @@ $("#channels").click(function () { return; } - $.getJSON(api + "listuserchannels?name=" + encodeURIComponent(uname) + - "&session=" + session + "&callback=?", function(data) { + var auth = "name=" + encodeURIComponent(uname) + "&session=" + + encodeURIComponent(session); + $.getJSON(WEB_URL+"/api/account/mychannels?"+auth+"&callback=?", + function (data) { $("#channellist tbody").remove(); data.channels.forEach(function (chan) { var tr = $("").appendTo($("#channellist")); @@ -134,12 +162,12 @@ $("#registerbtn").click(function() { } // Input valid, try registering - var url = api + "register?" + [ - "name=" + encodeURIComponent(name), - "pw=" + encodeURIComponent(pw) - ].join("&") + "&callback=?"; - - $.getJSON(url, function(data) { + var data = { + name: name, + pw: pw + }; + + postJSON(WEB_URL + "/api/register?callback=?", data, function (data) { if(data.success) { uname = name; session = data.session; @@ -170,10 +198,12 @@ $("#loginbtn").click(function() { return; } uname = $("#loginusername").val(); - var loginstr = "name=" + encodeURIComponent(uname) - + "&pw=" + encodeURIComponent($("#loginpw").val()); - var url = api + "login?" + loginstr + "&callback=?"; - $.getJSON(url, function(data) { + var data = { + name: uname, + pw: $("#loginpw").val() + }; + + postJSON(WEB_URL+"/api/login?callback=?", data, function(data) { if(data.success) { session = data.session; onLogin(); @@ -230,12 +260,13 @@ $("#cpwbtn").click(function() { } // Input valid, try changing password - var url = api + "changepass?" + [ - "name=" + encodeURIComponent(name), - "oldpw=" + encodeURIComponent(oldpw), - "newpw=" + encodeURIComponent(newpw) - ].join("&") + "&callback=?"; - $.getJSON(url, function(data) { + var data = { + name: name, + oldpw: oldpw, + newpw: newpw + }; + postJSON(WEB_URL + "/api/account/passwordchange?callback=?", data, + function (data) { if(data.success) { $("
").addClass("alert alert-success") .text("Password changed.") @@ -266,7 +297,7 @@ $("#cebtn").click(function() { return; } - if(!email.match(/^[a-z0-9_\.]+@[a-z0-9_\.]+[a-z]+$/)) { + if(!email.match(/^[\w_\.]+@[\w_\.]+[a-zA-Z]+$/)) { $("
").addClass("alert alert-error") .text("Invalid email") .insertAfter($("#ceemail").parent().parent()); @@ -282,12 +313,13 @@ $("#cebtn").click(function() { return; } - var url = api + "setemail?" + [ - "name=" + encodeURIComponent(name), - "pw=" + encodeURIComponent(pw), - "email=" + encodeURIComponent(email) - ].join("&") + "&callback=?"; - $.getJSON(url, function(data) { + var data = { + name: name, + pw: pw, + email: email + }; + postJSON(WEB_URL + "/api/account/email?callback=?", data, + function (data) { if(data.success) { $("
").addClass("alert alert-success") .text("Email updated") @@ -312,11 +344,12 @@ $("#rpbtn").click(function() { var name = $("#rpusername").val(); var email = $("#rpemail").val(); - var url = api + "resetpass?" + [ - "name=" + encodeURIComponent(name), - "email=" + encodeURIComponent(email) - ].join("&") + "&callback=?"; - $.getJSON(url, function(data) { + var data = { + name: name, + email: email + }; + postJSON(WEB_URL + "/api/account/passwordreset?callback=?", data, + function (data) { $("#rpbtn").text("Send Reset"); if(data.success) { $("
").addClass("alert alert-success") @@ -336,20 +369,16 @@ $("#profilesave").click(function() { $("#profilepane").find(".alert-error").remove(); $("#profilepane").find(".alert-success").remove(); var img = $("#profileimg").val(); - /* - img = escape(img).replace(/\//g, "%2F") - .replace(/&/g, "%26") - .replace(/=/g, "%3D") - .replace(/\?/g, "%3F"); - */ - var url = api + "setprofile?" + [ - "name=" + encodeURIComponent(uname), - "session=" + session, - "profile_image=" + encodeURIComponent(img), - "profile_text=" + encodeURIComponent($("#profiletext").val()) - ].join("&") + "&callback=?"; + var text = $("#profiletext").val(); + var data = { + name: uname, + session: session, + profile_image: img, + profile_text: text + }; - $.getJSON(url, function(data) { + postJSON(WEB_URL+"/api/account/profile?callback=?", data, + function (data) { if(data.success) { $("
").addClass("alert alert-success") .text("Profile updated.") diff --git a/www/assets/js/acp.js b/www/assets/js/acp.js index babc69eb..f96eb1f7 100644 --- a/www/assets/js/acp.js +++ b/www/assets/js/acp.js @@ -1,4 +1,3 @@ -var BASE = WEB_URL + "/api/json/"; var AUTH = ""; var NO_WEBSOCKETS = false; @@ -156,20 +155,21 @@ function reverseLog() { $("#log_reverse").click(reverseLog); function getSyslog() { - $.ajax(WEB_URL+"/api/plain/readlog?type=sys&"+AUTH).done(function(data) { + $.ajax(WEB_URL+"/api/logging/syslog?"+AUTH).done(function(data) { $("#log").text(data); }); } $("#syslog").click(getSyslog); function getErrlog() { - $.ajax(WEB_URL+"/api/plain/readlog?type=err&"+AUTH).done(function(data) { + $.ajax(WEB_URL+"/api/logging/errorlog?"+AUTH).done(function(data) { $("#log").text(data); }); } $("#errlog").click(getErrlog); function getActionLog() { var types = "&actions=" + $("#actionlog_filter").val().join(","); - $.getJSON(WEB_URL+"/api/json/readactionlog?"+AUTH+types+"&callback=?").done(function(entries) { + $.getJSON(WEB_URL+"/api/logging/actionlog?"+AUTH+types+"&callback=?") + .done(function(entries) { var tbl = $("#actionlog table"); entries.forEach(function (e) { e.time = parseInt(e.time); @@ -217,7 +217,8 @@ function getActionLog() { } function getChanlog() { var chan = $("#channame").val(); - $.ajax(WEB_URL+"/api/plain/readlog?type=channel&channel="+chan+"&"+AUTH).done(function(data) { + $.ajax(WEB_URL+"/api/logging/channels/"+chan+"?"+AUTH) + .done(function(data) { $("#log").text(data); }); } diff --git a/www/assets/js/callbacks.js b/www/assets/js/callbacks.js index 974c6bbc..36656138 100644 --- a/www/assets/js/callbacks.js +++ b/www/assets/js/callbacks.js @@ -13,6 +13,10 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI Callbacks = { error: function (reason) { + // Don't show the error for when the server goes down + if(reason && reason.returnValue === true) + return; + var d = $("
").addClass("alert alert-error span12") .appendTo($("#announcements")); $("

").text("Uh-oh!").appendTo(d); @@ -74,6 +78,33 @@ Callbacks = { alert(data.msg); }, + costanza: function (data) { + hidePlayer(); + $("#costanza-modal").modal("hide"); + var modal = $("
").addClass("modal hide fade") + .attr("id", "costanza-modal") + .appendTo($("body")); + + + var body = $("
").addClass("modal-body").appendTo(modal); + $(" - -
- -
-

-
- - -
- -
-
-
-
-

-
-
- -
-
- -
- -
- -
- -

Not connected

-
- -
-
- -
-
- - -
- -
- -

Nothing playing

- -
-
-
-
- -
- -
-
- -
- - -
- -
- -

Show Library

-
-
-
- -
-
- - -
-
- -
- -

Show Playlist Manager

-
-
-
- -
-
- -
-
    -
-
-
-
- -
-
- -
- -
- -

Show Playlist Controls

-
-
-
- -
-
- - -
-
- - - - -
-
- -
    -
-
-
-
-
-
- -
- - - - - - - - - - - - - - - - - - - diff --git a/www/channel.html b/www/channel.html index ba8ca7ce..cdca4f8a 100644 --- a/www/channel.html +++ b/www/channel.html @@ -247,12 +247,12 @@ - - - - - - + + + + + + diff --git a/www/channelwidget.html b/www/channelwidget.html deleted file mode 100644 index a4e918b1..00000000 --- a/www/channelwidget.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - CyTube - - - - - - - - - - - - - - -
ChannelConnectedPlaying
- - - - - diff --git a/www/index.html b/www/index.html index 65a2fc90..07fd8d7b 100644 --- a/www/index.html +++ b/www/index.html @@ -130,7 +130,8 @@ return entry; } function refresh() { - $.getJSON(WEB_URL+"/api/json/listloaded?filter=public&callback=?", function(data) { + $.getJSON(WEB_URL+"/api/allchannels/public?callback=?", + function(data) { $("#channeldata").find("tbody").remove(); data.sort(function(a, b) { var x = a.usercount; diff --git a/www/login.html b/www/login.html index 02fe6b72..0e48e3c8 100644 --- a/www/login.html +++ b/www/login.html @@ -37,6 +37,30 @@