diff --git a/acp.js b/acp.js new file mode 100644 index 00000000..0abdce94 --- /dev/null +++ b/acp.js @@ -0,0 +1,145 @@ +var Server = require("./server"); +var Auth = require("./auth"); +var Database = require("./database"); +var ActionLog = require("./actionlog"); + +module.exports = { + init: function(user) { + ActionLog.record(user.ip, user.name, "acp-init"); + user.socket.on("acp-announce", function(data) { + ActionLog.record(user.ip, user.name, "acp-announce", [data]); + Server.announcement = data; + Server.io.sockets.emit("announcement", data); + }); + + user.socket.on("acp-announce-clear", function() { + ActionLog.record(user.ip, user.name, "acp-announce-clear"); + Server.announcement = null; + }); + + user.socket.on("acp-global-ban", function(data) { + ActionLog.record(user.ip, user.name, "acp-global-ban", [data.ip]); + Database.globalBanIP(data.ip, data.note); + user.socket.emit("acp-global-banlist", Database.refreshGlobalBans()); + }); + + user.socket.on("acp-global-unban", function(ip) { + ActionLog.record(user.ip, user.name, "acp-global-unban", [data.ip]); + Database.globalUnbanIP(ip); + user.socket.emit("acp-global-banlist", Database.refreshGlobalBans()); + }); + + user.socket.emit("acp-global-banlist", Database.refreshGlobalBans()); + + user.socket.on("acp-lookup-user", function(name) { + var db = Database.getConnection(); + if(!db) { + return; + } + + var query = Database.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); + }); + + user.socket.on("acp-reset-password", function(data) { + if(Auth.getGlobalRank(data.name) >= user.global_rank) + return; + try { + var hash = Database.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" + }); + } + + }); + + 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; + + var db = Database.getConnection(); + if(!db) + return; + + ActionLog.record(user.ip, user.name, "acp-set-rank", [data]); + var query = Database.createQuery( + "UPDATE registrations SET global_rank=? WHERE uname=?", + [data.name, data.rank] + ); + + var res = db.querySync(query); + if(!res) + return; + + user.socket.emit("acp-set-rank", data); + }); + + user.socket.on("acp-list-loaded", function() { + var chans = []; + for(var c in Server.channels) { + var chan = Server.channels[c]; + if(!chan) + continue; + + chans.push({ + name: c, + title: chan.opts.pagetitle, + usercount: chan.users.length, + mediatitle: chan.media ? chan.media.title : "-", + is_public: chan.opts.show_public, + registered: chan.registered + }); + } + + user.socket.emit("acp-list-loaded", chans); + }); + + user.socket.on("acp-channel-unload", function(data) { + if(data.name in Server.channels) { + var c = Server.channels[data.name]; + if(!c) + return; + ActionLog.record(user.ip, user.name, "acp-channel-unload"); + c.initialized = data.save; + c.users.forEach(function(u) { + c.kick(u, "Channel shutting down"); + }); + Server.unload(c); + } + }); + + user.socket.on("acp-actionlog-clear", function(data) { + ActionLog.clear(data); + ActionLog.record(user.ip, user.name, "acp-actionlog-clear", data); + }); + } +} diff --git a/actionlog.js b/actionlog.js new file mode 100644 index 00000000..c2aec0c9 --- /dev/null +++ b/actionlog.js @@ -0,0 +1,65 @@ +var fs = require("fs"); +var Logger = require("./logger"); + +var buffer = []; + +exports.record = function(ip, name, action, args) { + buffer.push(JSON.stringify({ + ip: ip, + name: name, + action: action, + args: args ? args : [], + time: Date.now() + })); +} + +exports.flush = function() { + if(buffer.length == 0) + return; + var text = buffer.join("\n") + "\n"; + buffer = []; + fs.appendFile("action.log", text, function(err) { + if(err) { + errlog.log("Append to actionlog failed: "); + errlog.log(err); + } + }); +} + +exports.clear = function(actions) { + clearInterval(FLUSH_TMR); + var rs = fs.createReadStream("action.log"); + var ws = fs.createWriteStream("action.log.tmp"); + function handleLine(ln) { + try { + js = JSON.parse(ln); + if(actions.indexOf(js.action) == -1) + ws.write(ln + "\n"); + } + catch(e) { } + } + var buffer = ""; + rs.on("data", function(chunk) { + buffer += chunk; + if(buffer.indexOf("\n") != -1) { + var lines = buffer.split("\n"); + buffer = lines[lines.length - 1]; + lines.length = lines.length - 1; + lines.forEach(handleLine); + } + }); + rs.on("end", function() { + handleLine(buffer); + ws.end(); + }); + try { + fs.renameSync("action.log.tmp", "action.log"); + } + catch(e) { + Logger.errlog.log("Failed to move action.log.tmp => action.log"); + Logger.errlog.log(e); + } + FLUSH_TMR = setInterval(exports.flush, 15000); +} + +var FLUSH_TMR = setInterval(exports.flush, 15000); diff --git a/api.js b/api.js index 8726b0fd..67da6a5b 100644 --- a/api.js +++ b/api.js @@ -15,6 +15,7 @@ var Logger = require("./logger.js"); var apilog = new Logger.Logger("api.log"); var Database = require("./database.js"); var Config = require("./config.js"); +var ActionLog = require("./actionlog.js"); var fs = require("fs"); var plainHandlers = { @@ -32,11 +33,21 @@ var jsonHandlers = { "setprofile" : handleProfileChange, "getprofile" : handleProfileGet, "setemail" : handleEmailChange, - "globalbans" : handleGlobalBans, "admreports" : handleAdmReports, - "acppwreset" : handleAcpPasswordReset }; +function getClientIP(req) { + var ip; + var forward = req.header("x-forwarded-for"); + if(forward) { + ip = forward.split(",")[0]; + } + if(!ip) { + ip = req.connection.remoteAddress; + } + return ip; +} + function handle(path, req, res) { var parts = path.split("/"); var last = parts[parts.length - 1]; @@ -193,12 +204,14 @@ function handleLogin(params, req, res) { var row = Auth.login(name, pw, session); if(row) { + ActionLog.record(getClientIP(req), name, "login-success"); sendJSON(res, { success: true, session: row.session_hash }); } else { + ActionLog.record(getClientIP(req), name, "login-failure"); sendJSON(res, { error: "Invalid username/password", success: false @@ -219,6 +232,7 @@ function handlePasswordChange(params, req, res) { } var row = Auth.login(name, oldpw); if(row) { + ActionLog.record(getClientIP(req), name, "password-change"); var success = Auth.setUserPassword(name, newpw); sendJSON(res, { success: success, @@ -237,11 +251,12 @@ function handlePasswordChange(params, req, res) { function handlePasswordReset(params, req, res) { var name = params.name || ""; var email = unescape(params.email || ""); - var ip = req.socket.address().address; + var ip = getClientIP(req); var hash = false; try { hash = Database.generatePasswordReset(ip, name, email); + ActionLog.record(ip, name, "password-reset-generate"); } catch(e) { sendJSON(res, { @@ -301,7 +316,7 @@ function handlePasswordReset(params, req, res) { function handlePasswordRecover(params, req, res) { var hash = params.hash || ""; - var ip = req.socket.address().address; + var ip = getClientIP(req); try { var info = Database.recoverPassword(hash); @@ -310,10 +325,12 @@ function handlePasswordRecover(params, req, res) { name: info[0], pw: info[1] }); + ActionLog.record(ip, name, "password-recover-success"); Logger.syslog.log(ip + " recovered password for " + name); return; } catch(e) { + ActionLog.record(ip, name, "password-recover-failure"); sendJSON(res, { success: false, error: e @@ -412,6 +429,7 @@ function handleEmailChange(params, req, res) { var row = Auth.login(name, pw); if(row) { var success = Database.setUserEmail(name, email); + ActionLog.record(getClientIP(req), name, "email-update", [email]); sendJSON(res, { success: success, error: success ? "" : "Email update failed", @@ -438,6 +456,7 @@ function handleRegister(params, req, res) { return; } else if(Auth.isRegistered(name)) { + ActionLog.record(getClientIP(req), name, "register-failure"); sendJSON(res, { success: false, error: "That username is already taken" @@ -445,6 +464,7 @@ function handleRegister(params, req, res) { return false; } else if(!Auth.validateName(name)) { + ActionLog.record(getClientIP(req), name, "register-failure"); sendJSON(res, { success: false, error: "Invalid username. Usernames must be 1-20 characters long and consist only of alphanumeric characters and underscores" @@ -453,7 +473,8 @@ function handleRegister(params, req, res) { else { var session = Auth.register(name, pw); if(session) { - Logger.syslog.log(this.ip + " registered " + name); + ActionLog.record(getClientIP(req), name, "register-success"); + Logger.syslog.log(getClientIP(req) + " registered " + name); sendJSON(res, { success: true, session: session @@ -468,103 +489,12 @@ function handleRegister(params, req, res) { } } -function handleGlobalBans(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.send(403); - return; - } - - var action = params.action || "list"; - if(action == "list") { - var gbans = Database.refreshGlobalBans(); - sendJSON(res, gbans); - } - else if(action == "add") { - var ip = params.ip || ""; - var reason = params.reason || ""; - if(!ip.match(/\d+\.\d+\.(\d+\.(\d+)?)?/)) { - sendJSON(res, { - error: "Invalid IP address" - }); - return; - } - var result = Database.globalBanIP(ip, reason); - sendJSON(res, { - success: result, - ip: ip, - reason: reason - }); - } - else if(action == "remove") { - var ip = params.ip || ""; - if(!ip.match(/\d+\.\d+\.(\d+\.(\d+)?)?/)) { - sendJSON(res, { - error: "Invalid IP address" - }); - return; - } - var result = Database.globalUnbanIP(ip); - sendJSON(res, { - success: result, - ip: ip, - }); - } - else { - sendJSON(res, { - error: "Invalid action: " + action - }); - } -} - function handleAdmReports(params, req, res) { sendJSON(res, { error: "Not implemented" }); } -function handleAcpPasswordReset(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.send(403); - return; - } - - var action = params.action || ""; - if(action == "reset") { - var uname = params.reset_name; - if(Auth.getGlobalRank(uname) > row.global_rank) { - sendJSON(res, { - success: false - }); - return; - } - var new_pw = Database.resetPassword(uname); - if(new_pw) { - sendJSON(res, { - success: true, - pw: new_pw - }); - } - else { - sendJSON(res, { - success: false - }); - } - } - else { - sendJSON(res, { - success: false - }); - } -} - // Helper function function pipeLast(res, file, len) { fs.stat(file, function(err, data) { @@ -599,6 +529,10 @@ function handleReadLog(params, req, res) { else if(type == "err") { pipeLast(res, "error.log", 1024*1024); } + else if(type == "action") { + ActionLog.flush(); + pipeLast(res, "action.log", 1024*1024*100); + } else if(type == "channel") { var chan = params.channel || ""; fs.exists("chanlogs/" + chan + ".log", function(exists) { diff --git a/auth.js b/auth.js index 51cedbe1..171c7532 100644 --- a/auth.js +++ b/auth.js @@ -20,7 +20,7 @@ var Logger = require("./logger.js"); exports.isRegistered = function(name) { var db = Database.getConnection(); if(!db) { - return true; + throw "Database failure"; } var query = Database.createQuery( "SELECT * FROM `registrations` WHERE uname=?", @@ -90,7 +90,7 @@ exports.login = function(name, pw, session) { exports.loginPassword = function(name, pw) { var db = Database.getConnection(); if(!db) { - return false; + throw "Database failure"; } var query = Database.createQuery( "SELECT * FROM `registrations` WHERE uname=?", @@ -140,7 +140,7 @@ exports.createSession = function(name) { var hash = hashlib.sha256(salt + name); var db = Database.getConnection(); if(!db) { - return false; + throw "Database failure"; } var query = Database.createQuery( ["UPDATE `registrations` SET ", @@ -156,7 +156,7 @@ exports.createSession = function(name) { exports.loginSession = function(name, hash) { var db = Database.getConnection(); if(!db) { - return false; + throw "Database failure"; } var query = Database.createQuery( "SELECT * FROM `registrations` WHERE `uname`=?", diff --git a/channel.js b/channel.js index c17767e4..555fcdaf 100644 --- a/channel.js +++ b/channel.js @@ -24,6 +24,7 @@ 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 Channel = function(name) { Logger.syslog.log("Opening channel " + name); @@ -68,14 +69,15 @@ var Channel = function(name) { ban: 2, motdedit: 3, filteredit: 3, - drink: 1.5 + drink: 1.5, + chat: 0 }; this.opts = { allow_voteskip: true, voteskip_ratio: 0.5, pagetitle: this.name, - customcss: "", - customjs: "", + externalcss: "", + externaljs: "", chat_antiflood: false, show_public: false, enable_link_regex: true @@ -93,7 +95,9 @@ var Channel = function(name) { }; this.ipbans = {}; this.namebans = {}; - this.logins = {}; + this.ip_alias = {}; + this.name_alias = {}; + this.login_hist = []; this.logger = new Logger.Logger("chanlogs/" + this.name + ".log"); this.i = 0; this.time = new Date().getTime(); @@ -136,13 +140,15 @@ Channel.prototype.hasPermission = function(user, key) { Channel.prototype.loadDump = function() { fs.readFile("chandump/" + this.name, function(err, data) { if(err) { - if(err.code === "ENOENT") { + if(err.code == "ENOENT") { Logger.errlog.log("WARN: missing dump for " + this.name); this.initialized = true; this.saveDump(); } - Logger.errlog.log("Failed to open channel dump " + this.name); - Logger.errlog.log(err); + else { + Logger.errlog.log("Failed to open channel dump " + this.name); + Logger.errlog.log(err); + } return; } try { @@ -158,9 +164,7 @@ Channel.prototype.loadDump = function() { } this.queue.push(m); } - this.sendAll("playlist", { - pl: this.queue - }); + this.sendAll("playlist", this.queue); this.broadcastPlaylistMeta(); // Backwards compatibility if(data.currentPosition != undefined) { @@ -177,7 +181,14 @@ Channel.prototype.loadDump = function() { this.media.currentTime = data.currentTime; } for(var key in data.opts) { - this.opts[key] = data.opts[key]; + // Gotta love backwards compatibility + if(key == "customcss" || key == "customjs") { + var k = key.substring(6); + this.opts[k] = data.opts[key]; + } + else { + this.opts[key] = data.opts[key]; + } } for(var key in data.permissions) { this.permissions[key] = data.permissions[key]; @@ -205,12 +216,6 @@ Channel.prototype.loadDump = function() { this.motd = data.motd; this.broadcastMotd(); } - data.logins = data.logins || {}; - for(var ip in data.logins) { - for(var i = 0; i < data.logins.length; i++) { - this.logins[ip].push(data.logins[ip][i]); - } - } this.setLock(!(data.openqueue || false)); this.chatbuffer = data.chatbuffer || []; for(var i = 0; i < this.chatbuffer.length; i++) { @@ -244,7 +249,6 @@ Channel.prototype.saveDump = function() { permissions: this.permissions, filters: filts, motd: this.motd, - logins: this.logins, openqueue: this.openqueue, chatbuffer: this.chatbuffer, css: this.css, @@ -265,12 +269,14 @@ function incrementalDump(chan) { Channel.prototype.tryRegister = function(user) { if(this.registered) { + ActionLog.record(user.ip, user.name, "channel-register-failure", [this.name]); 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]); user.socket.emit("registerChannel", { success: false, error: "You must log in to register a channel" @@ -278,6 +284,7 @@ Channel.prototype.tryRegister = function(user) { } else if(!Rank.hasPermission(user, "registerChannel")) { + ActionLog.record(user.ip, user.name, "channel-register-failure", [this.name]); user.socket.emit("registerChannel", { success: false, error: "You don't have permission to register this channel" @@ -285,6 +292,7 @@ Channel.prototype.tryRegister = function(user) { } else { if(Database.registerChannel(this.name)) { + ActionLog.record(user.ip, user.name, "channel-register-success", [this.name]); this.registered = true; this.initialized = true; this.saveDump(); @@ -327,11 +335,9 @@ Channel.prototype.saveRank = function(user) { Channel.prototype.getIPRank = function(ip) { var names = []; - if(this.logins[ip] === undefined || this.logins[ip].length == 0) { - return 0; - } - - this.logins[ip].forEach(function(name) { + if(!(ip in this.ip_alias)) + this.ip_alias = Database.getAliases(ip); + this.ip_alias[ip].forEach(function(name) { names.push(name); }); @@ -343,17 +349,9 @@ Channel.prototype.getIPRank = function(ip) { return rank; } -Channel.prototype.seen = function(ip, name) { - name = name.toLowerCase(); - for(var i = 0; i < this.logins[ip].length; i++) { - if(this.logins[ip][i].toLowerCase() == name) { - return true; - } - } - return false; -} - Channel.prototype.cacheMedia = function(media) { + // Prevent the copy in the playlist from messing with this one + media = media.dup(); if(media.temp) { return; } @@ -364,7 +362,7 @@ Channel.prototype.cacheMedia = function(media) { return false; } -Channel.prototype.banName = function(actor, name) { +Channel.prototype.tryNameBan = function(actor, name) { if(!this.hasPermission(actor, "ban")) { return false; } @@ -372,11 +370,6 @@ Channel.prototype.banName = function(actor, name) { name = name.toLowerCase(); var rank = this.getRank(name); - if(rank < 1) { - actor.socket.emit("errorMsg", {msg: "You can't ban guest names. Use a kick or IP ban."}); - return false; - } - if(rank >= actor.rank) { actor.socket.emit("errorMsg", {msg: "You don't have permission to ban this person."}); return false; @@ -389,8 +382,11 @@ Channel.prototype.banName = function(actor, name) { break; } } - this.broadcastBanlist(); this.logger.log(name + " was banned by " + actor.name); + var chan = this; + this.users.forEach(function(u) { + chan.sendBanlist(u); + }); if(!this.registered) { return false; } @@ -405,53 +401,61 @@ Channel.prototype.unbanName = function(actor, name) { this.namebans[name] = null; delete this.namebans[name]; - this.broadcastBanlist(); this.logger.log(name + " was unbanned by " + actor.name); + var chan = this; + this.users.forEach(function(u) { + chan.sendBanlist(u); + }); return Database.channelUnbanName(this.name, name); } -Channel.prototype.tryIPBan = function(actor, data) { +Channel.prototype.tryIPBan = function(actor, name, range) { if(!this.hasPermission(actor, "ban")) { return false; } - if(typeof data.id != "string") { - return false; - } - if(typeof data.name != "string") { - return false; - } - var ip = this.hideIP(data.id); - if(this.getIPRank(ip) >= actor.rank) { - actor.socket.emit("errorMsg", {msg: "You don't have permission to ban this IP"}); + if(typeof name != "string") { return false; } + var ips = Database.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; + } - if(data.range) { - ip = ip.replace(/(\d+)\.(\d+)\.(\d+)\.(\d+)/, "$1.$2.$3"); - for(var ip2 in this.logins) { - if(ip2.indexOf(ip) == 0 && this.getIPRank(ip2) >= actor.rank) { - actor.socket.emit("errorMsg", {msg: "You don't have permission to ban this IP"}); - return false; + 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; + } } } - } - this.ipbans[ip] = [data.name, actor.name]; - this.broadcastBanlist(); - this.logger.log(ip + " (" + data.name + ") was banned by " + actor.name); + chan.ipbans[ip] = [name, actor.name]; + //chan.broadcastBanlist(); + chan.logger.log(ip + " (" + name + ") was banned by " + actor.name); - for(var i = 0; i < this.users.length; i++) { - if(this.users[i].ip.indexOf(ip) == 0) { - this.kick(this.users[i], "Your IP is banned!"); - i--; + 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(!this.registered) - return false; + if(!chan.registered) + return false; - // Update database ban table - return Database.channelBan(this.name, ip, data.name, actor.name); + // Update database ban table + return Database.channelBan(chan.name, ip, name, actor.name); + }); + + var chan = this; + this.users.forEach(function(u) { + chan.sendBanlist(u); + }); } Channel.prototype.banIP = function(actor, receiver) { @@ -465,7 +469,7 @@ Channel.prototype.banIP = function(actor, receiver) { catch(e) { // Socket already disconnected } - this.broadcastBanlist(); + //this.broadcastBanlist(); this.logger.log(receiver.ip + " (" + receiver.name + ") was banned by " + actor.name); if(!this.registered) @@ -480,18 +484,22 @@ Channel.prototype.unbanIP = function(actor, ip) { return false; this.ipbans[ip] = null; + var chan = this; + this.users.forEach(function(u) { + chan.sendBanlist(u); + }); if(!this.registered) return false; - this.broadcastBanlist(); + //this.broadcastBanlist(); // Update database ban table return Database.channelUnbanIP(this.name, ip); } Channel.prototype.tryUnban = function(actor, data) { - if(data.id) { - var ip = this.hideIP(data.id); + if(data.ip_hidden) { + var ip = this.hideIP(data.ip_hidden); this.unbanIP(actor, ip); } else if(data.name) { @@ -533,9 +541,6 @@ Channel.prototype.search = function(query, callback) { /* REGION User interaction */ Channel.prototype.userJoin = function(user) { - if(!(user.ip in this.logins)) { - this.logins[user.ip] = []; - } var parts = user.ip.split("."); var slash24 = parts[0] + "." + parts[1] + "." + parts[2]; // GTFO @@ -579,7 +584,7 @@ Channel.prototype.userJoin = function(user) { // Set the new guy up this.sendPlaylist(user); this.sendMediaUpdate(user); - user.socket.emit("queueLock", {locked: !this.openqueue}); + user.socket.emit("setPlaylistLocked", {locked: !this.openqueue}); this.sendUserlist(user); this.sendRecentChat(user); user.socket.emit("channelCSSJS", {css: this.css, js: this.js}); @@ -588,8 +593,8 @@ Channel.prototype.userJoin = function(user) { } user.socket.emit("channelOpts", this.opts); user.socket.emit("setPermissions", this.permissions); - user.socket.emit("updateMotd", this.motd); - user.socket.emit("drinkCount", {count: this.drinks}); + user.socket.emit("setMotd", this.motd); + user.socket.emit("drinkCount", this.drinks); // Send things that require special permission this.sendRankStuff(user); @@ -655,26 +660,29 @@ Channel.prototype.hideIP = function(ip) { return chars.join(""); } -Channel.prototype.sendRankStuff = function(user) { +Channel.prototype.sendLoginHistory = function(user) { + if(user.rank < 2) + return; + + user.socket.emit("recentLogins", this.login_hist); +} + +Channel.prototype.sendBanlist = function(user) { if(this.hasPermission(user, "ban")) { var ents = []; for(var ip in this.ipbans) { if(this.ipbans[ip] != null) { - var name = []; - if(ip in this.logins) { - name = this.logins[ip]; - } - name.push(this.ipbans[ip][0]); - name = name.join(", "); - var id = this.hideIP(ip); + var name = this.ipbans[ip][0]; + var ip_hidden = this.hideIP(ip); var disp = ip; if(user.rank < Rank.Siteadmin) { - disp = "(Hidden)"; + disp = "x.x." + ip.replace(/\d+\.\d+\.(\d+\.\d+)/, "$1"); } ents.push({ - ip: disp, - id: id, + ip_displayed: disp, + ip_hidden: ip_hidden, name: name, + aliases: this.ip_alias[ip] || [], banner: this.ipbans[ip][1] }); } @@ -682,79 +690,44 @@ Channel.prototype.sendRankStuff = function(user) { for(var name in this.namebans) { if(this.namebans[name] != null) { ents.push({ - ip: "*", + ip_displayed: "*", + ip_hidden: false, name: name, + aliases: this.name_alias[name] || [], banner: this.namebans[name] }); } } - user.socket.emit("banlist", {entries: ents}); - } - if(Rank.hasPermission(user, "seenlogins")) { - var ents = []; - for(var ip in this.logins) { - var disp = ip; - if(user.rank < Rank.Siteadmin) { - disp = "(Hidden)"; - } - var banned = (ip in this.ipbans && this.ipbans[ip] != null); - var range = ip.replace(/(\d+)\.(\d+)\.(\d+)\.(\d+)/, "$1.$2.$3"); - banned = banned || (range in this.ipbans && this.ipbans[range] != null); - ents.push({ - ip: disp, - id: this.hideIP(ip), - names: this.logins[ip], - banned: banned - }); - } - user.socket.emit("seenlogins", {entries: ents}); + user.socket.emit("banlist", ents); } +} + +Channel.prototype.sendChatFilters = function(user) { if(this.hasPermission(user, "filteredit")) { var filts = new Array(this.filters.length); for(var i = 0; i < this.filters.length; i++) { filts[i] = this.filters[i].pack(); } - user.socket.emit("chatFilters", {filters: filts}); - } - this.sendACL(user); -} - -Channel.prototype.sendSeenLogins = function(user) { - if(Rank.hasPermission(user, "seenlogins")) { - var ents = []; - for(var ip in this.logins) { - var disp = ip; - if(user.rank < Rank.Siteadmin) { - disp = "(Hidden)"; - } - var banned = (ip in this.ipbans && this.ipbans[ip] != null); - var range = ip.replace(/(\d+)\.(\d+)\.(\d+)\.(\d+)/, "$1.$2.$3"); - banned = banned || (range in this.ipbans && this.ipbans[range] != null); - ents.push({ - ip: disp, - id: this.hideIP(ip), - names: this.logins[ip], - banned: banned - }); - } - user.socket.emit("seenlogins", {entries: ents}); + user.socket.emit("chatFilters", filts); } } -Channel.prototype.sendACL = function(user) { +Channel.prototype.sendRankStuff = function(user) { + this.sendBanlist(user); + this.sendChatFilters(user); + this.sendChannelRanks(user); +} + +Channel.prototype.sendChannelRanks = function(user) { if(Rank.hasPermission(user, "acl")) { - user.socket.emit("acl", Database.listChannelRanks(this.name)); + user.socket.emit("channelRanks", Database.listChannelRanks(this.name)); } } Channel.prototype.sendPlaylist = function(user) { - user.socket.emit("playlist", { - pl: this.queue - }); - user.socket.emit("updatePlaylistIdx", { - idx: this.position - }); - user.socket.emit("updatePlaylistMeta", this.plmeta); + user.socket.emit("playlist", this.queue); + user.socket.emit("setPosition", this.position); + user.socket.emit("setPlaylistMeta", this.plmeta); } Channel.prototype.sendMediaUpdate = function(user) { @@ -813,19 +786,29 @@ Channel.prototype.broadcastPlaylistMeta = function() { time: timestr }; this.plmeta = packet; - this.sendAll("updatePlaylistMeta", packet); + this.sendAll("setPlaylistMeta", packet); } Channel.prototype.broadcastUsercount = function() { - this.sendAll("usercount", { - count: this.users.length - }); + this.sendAll("usercount", this.users.length); } Channel.prototype.broadcastNewUser = function(user) { - if(!this.seen(user.ip, user.name)) { - this.logins[user.ip].push(user.name); - } + var aliases = Database.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!"); @@ -838,10 +821,24 @@ Channel.prototype.broadcastNewUser = function(user) { meta: user.meta, profile: user.profile }); - this.sendRankStuff(user); + //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); + } + }); } Channel.prototype.broadcastUserUpdate = function(user) { @@ -876,24 +873,20 @@ Channel.prototype.broadcastBanlist = function() { var adminents = []; for(var ip in this.ipbans) { if(this.ipbans[ip] != null) { - var name; - if(ip in this.logins) { - name = this.logins[ip].join(", "); - } - else { - name = this.ipbans[ip][0]; - } - var id = this.hideIP(ip); + var name = this.ipbans[ip][0]; + var ip_hidden = this.hideIP(ip); ents.push({ - ip: "(Hidden)", - id: id, + ip_displayed: "x.x." + ip.replace(/\d+\.\d+\.(\d+\.\d+)/, "$1"), + ip_hidden: ip_hidden, name: name, + aliases: this.ip_alias[ip] || [], banner: this.ipbans[ip][1] }); adminents.push({ - ip: ip, - id: id, + ip_displayed: ip, + ip_hidden: ip_hidden, name: name, + aliases: this.ip_alias[ip] || [], banner: this.ipbans[ip][1] }); } @@ -901,13 +894,17 @@ Channel.prototype.broadcastBanlist = function() { for(var name in this.namebans) { if(this.namebans[name] != null) { ents.push({ - ip: "*", + ip_displayed: "*", + ip_hidden: false, name: name, + aliases: this.name_alias[name] || [], banner: this.namebans[name] }); adminents.push({ - ip: "*", + ip_displayed: "*", + ip_hidden: false, name: name, + aliases: this.name_alias[name] || [], banner: this.namebans[name] }); } @@ -915,10 +912,10 @@ Channel.prototype.broadcastBanlist = function() { for(var i = 0; i < this.users.length; i++) { if(this.hasPermission(this.users[i], "ban")) { if(this.users[i].rank >= Rank.Siteadmin) { - this.users[i].socket.emit("banlist", {entries: adminents}); + this.users[i].socket.emit("banlist", adminents); } else { - this.users[i].socket.emit("banlist", {entries: ents}); + this.users[i].socket.emit("banlist", ents); } } } @@ -938,7 +935,7 @@ Channel.prototype.broadcastChatFilters = function() { } for(var i = 0; i < this.users.length; i++) { if(this.hasPermission(this.users[i], "filteredit")) { - this.users[i].socket.emit("chatFilters", {filters: filts}); + this.users[i].socket.emit("chatFilters", filts); } } } @@ -958,11 +955,11 @@ Channel.prototype.broadcastVoteskipUpdate = function() { } Channel.prototype.broadcastMotd = function() { - this.sendAll("updateMotd", this.motd); + this.sendAll("setMotd", this.motd); } Channel.prototype.broadcastDrinks = function() { - this.sendAll("drinkCount", {count: this.drinks}); + this.sendAll("drinkCount", this.drinks); } /* REGION Playlist Stuff */ @@ -1024,7 +1021,7 @@ Channel.prototype.autoTemp = function(media, user) { } } -Channel.prototype.enqueue = function(data, user) { +Channel.prototype.enqueue = function(data, user, callback) { var idx = data.pos == "next" ? this.position + 1 : this.queue.length; if(isLive(data.type) && !this.hasPermission(user, "playlistaddlive")) { @@ -1039,6 +1036,8 @@ Channel.prototype.enqueue = function(data, user) { this.autoTemp(media, user); this.queueAdd(media, idx); this.logger.log("*** Queued from cache: id=" + data.id); + if(callback) + callback(); } else { switch(data.type) { @@ -1049,6 +1048,8 @@ Channel.prototype.enqueue = function(data, user) { case "sc": InfoGetter.getMedia(data.id, data.type, function(err, media) { if(err) { + if(callback) + callback(); user.socket.emit("queueFail"); return; } @@ -1058,6 +1059,8 @@ Channel.prototype.enqueue = function(data, user) { this.cacheMedia(media); if(data.type == "yp") idx++; + if(callback) + callback(); }.bind(this)); break; case "li": @@ -1065,18 +1068,24 @@ Channel.prototype.enqueue = function(data, user) { media.queueby = user ? user.name : ""; this.autoTemp(media, user); this.queueAdd(media, idx); + if(callback) + callback(); break; case "tw": var media = new Media(data.id, "Twitch - " + data.id, "--:--", "tw"); media.queueby = user ? user.name : ""; this.autoTemp(media, user); this.queueAdd(media, idx); + if(callback) + callback(); break; case "jt": var media = new Media(data.id, "JustinTV - " + data.id, "--:--", "jt"); media.queueby = user ? user.name : ""; this.autoTemp(media, user); this.queueAdd(media, idx); + if(callback) + callback(); break; case "us": InfoGetter.getUstream(data.id, function(id) { @@ -1084,6 +1093,8 @@ Channel.prototype.enqueue = function(data, user) { media.queueby = user ? user.name : ""; this.autoTemp(media, user); this.queueAdd(media, idx); + if(callback) + callback(); }.bind(this)); break; case "rt": @@ -1091,18 +1102,24 @@ Channel.prototype.enqueue = function(data, user) { media.queueby = user ? user.name : ""; this.autoTemp(media, user); this.queueAdd(media, idx); + if(callback) + callback(); break; case "jw": var media = new Media(data.id, "JWPlayer Stream - " + data.id, "--:--", "jw"); media.queueby = user ? user.name : ""; this.autoTemp(media, user); this.queueAdd(media, idx); + if(callback) + callback(); break; case "im": var media = new Media(data.id, "Imgur Album", "--:--", "im"); media.queueby = user ? user.name : ""; this.autoTemp(media, user); this.queueAdd(media, idx); + if(callback) + callback(); break; default: break; @@ -1114,10 +1131,13 @@ Channel.prototype.tryQueue = function(user, data) { if(!this.hasPermission(user, "playlistadd")) { return; } - if(data.pos == undefined || data.id == undefined) { + if(typeof data.pos !== "string") { return; } - if(data.type == undefined && !(data.id in this.library)) { + if(typeof data.id !== "string" && data.id !== false) { + return; + } + if(typeof data.type !== "string" && !(data.id in this.library)) { return; } @@ -1131,7 +1151,38 @@ Channel.prototype.tryQueue = function(user, data) { return; } - this.enqueue(data, user); + if(data.list) + this.enqueueList(data, user); + else + this.enqueue(data, user); +} + +Channel.prototype.enqueueList = function(data, user) { + var pl = data.list; + var chan = this; + // Queue in reverse order for qnext + if(data.pos == "next") { + var i = pl.length; + var cback = function() { + i--; + if(i > 0) { + pl[i].pos = "next"; + chan.enqueue(pl[i], user, cback); + } + } + this.enqueue(pl[0], user, cback); + } + else { + var i = 0; + var cback = function() { + i++; + if(i < pl.length) { + pl[i].pos = "end"; + chan.enqueue(pl[i], user, cback); + } + } + this.enqueue(pl[i], user, cback); + } } Channel.prototype.tryQueuePlaylist = function(user, data) { @@ -1149,26 +1200,15 @@ Channel.prototype.tryQueuePlaylist = function(user, data) { } var pl = Database.loadUserPlaylist(user.name, data.name); - // Queue in reverse order for qnext - if(data.pos == "next") { - for(var i = pl.length - 1; i >= 0; i--) { - pl[i].pos = "next"; - this.enqueue(pl[i], user); - } - } - else { - for(var i = 0; i < pl.length; i++) { - pl[i].pos = "end"; - this.enqueue(pl[i], user); - } - } + data.list = pl; + this.enqueueList(data, user); } Channel.prototype.setTemp = function(idx, temp) { var med = this.queue[idx]; med.temp = temp; this.sendAll("setTemp", { - idx: idx, + position: idx, temp: temp }); @@ -1181,36 +1221,40 @@ Channel.prototype.trySetTemp = function(user, data) { if(!this.hasPermission(user, "settemp")) { return; } - if(typeof data.idx != "number" || typeof data.temp != "boolean") { + if(typeof data.position != "number" || typeof data.temp != "boolean") { return; } - if(data.idx < 0 || data.idx >= this.queue.length) { + if(data.position < 0 || data.position >= this.queue.length) { return; } - this.setTemp(data.idx, data.temp); + this.setTemp(data.position, data.temp); } -Channel.prototype.dequeue = function(data) { - if(data.pos < 0 || data.pos >= this.queue.length) { + +Channel.prototype.dequeue = function(position, removeonly) { + if(position < 0 || position >= this.queue.length) { return; } - this.queue.splice(data.pos, 1); - this.sendAll("unqueue", { - pos: data.pos + this.queue.splice(position, 1); + this.sendAll("delete", { + position: position }); this.broadcastPlaylistMeta(); + if(removeonly) + return; + // If you remove the currently playing video, play the next one - if(data.pos == this.position && !data.removeonly) { + if(position == this.position) { this.position--; this.playNext(); return; } // If you remove a video whose position is before the one currently // playing, you have to reduce the position of the one playing - if(data.pos < this.position) { + if(position < this.position) { this.position--; } } @@ -1220,9 +1264,8 @@ Channel.prototype.tryDequeue = function(user, data) { return; } - if(data.pos === undefined) { + if(typeof data !== "number") return; - } this.dequeue(data); } @@ -1265,7 +1308,7 @@ Channel.prototype.jumpTo = function(pos) { var old = this.position; if(this.media && this.media.temp && old != pos) { - this.dequeue({pos: old, removeonly: true}); + this.dequeue(old, true); if(pos > old && pos > 0) { pos--; } @@ -1284,10 +1327,7 @@ Channel.prototype.jumpTo = function(pos) { this.media.paused = false; this.sendAll("changeMedia", this.media.fullupdate()); - this.sendAll("updatePlaylistIdx", { - old: old, - idx: this.position - }); + this.sendAll("setPosition", this.position); // If it's not a livestream, enable autolead if(this.leader == null && !isLive(this.media.type)) { @@ -1303,11 +1343,11 @@ Channel.prototype.tryJumpTo = function(user, data) { return; } - if(data.pos === undefined) { + if(typeof data !== "number") { return; } - this.jumpTo(data.pos); + this.jumpTo(data); } Channel.prototype.clearqueue = function() { @@ -1338,9 +1378,9 @@ Channel.prototype.shufflequeue = function() { this.queue.splice(i, 1); } this.queue = n; - for(var i = 0; i < this.users.length; i++) { - this.sendPlaylist(this.users[i]); - } + this.sendAll("playlist", this.queue); + this.sendAll("setPosition", this.position); + this.sendAll("setPlaylistMeta", this.plmeta); } Channel.prototype.tryShufflequeue = function(user) { @@ -1377,34 +1417,38 @@ Channel.prototype.tryUpdate = function(user, data) { this.sendAll("mediaUpdate", this.media.timeupdate()); } -Channel.prototype.move = function(data) { - if(data.src < 0 || data.src >= this.queue.length) { +Channel.prototype.move = function(data, user) { + if(data.from < 0 || data.from >= this.queue.length) { return; } - if(data.dest < 0 || data.dest > this.queue.length) { + if(data.to < 0 || data.to > this.queue.length) { return; } - var media = this.queue[data.src]; - var dest = data.dest > data.src ? data.dest + 1 : data.dest; - var src = data.dest > data.src ? data.src : data.src + 1; + var media = this.queue[data.from]; + var to = data.to > data.from ? data.to + 1 : data.to; + var from = data.to > data.from ? data.from : data.from + 1; + var moveby = user && user.name ? user.name : null; + if(typeof data.moveby !== "undefined") + moveby = data.moveby; - this.queue.splice(dest, 0, media); - this.queue.splice(src, 1); + this.queue.splice(to, 0, media); + this.queue.splice(from, 1); this.sendAll("moveVideo", { - src: data.src, - dest: data.dest + from: data.from, + to: data.to, + moveby: moveby }); // Account for moving things around the active video - if(data.src < this.position && data.dest >= this.position) { + if(data.from < this.position && data.to >= this.position) { this.position--; } - else if(data.src > this.position && data.dest < this.position) { + else if(data.from > this.position && data.to < this.position) { this.position++ } - else if(data.src == this.position) { - this.position = data.dest; + else if(data.from == this.position) { + this.position = data.to; } } @@ -1413,11 +1457,11 @@ Channel.prototype.tryMove = function(user, data) { return; } - if(data.src == undefined || data.dest == undefined) { + if(typeof data.from !== "number" || typeof data.to !== "number") { return; } - this.move(data); + this.move(data, user); } /* REGION Polls */ @@ -1453,7 +1497,7 @@ Channel.prototype.tryVote = function(user, data) { if(!this.hasPermission(user, "pollvote")) { return; } - if(data.option == undefined) { + if(typeof data.option !== "number") { return; } @@ -1482,7 +1526,7 @@ Channel.prototype.tryVoteskip = function(user) { Channel.prototype.setLock = function(locked) { this.openqueue = !locked; - this.sendAll("queueLock", {locked: locked}); + this.sendAll("setPlaylistLocked", {locked: locked}); } Channel.prototype.trySetLock = function(user, data) { @@ -1497,17 +1541,40 @@ Channel.prototype.trySetLock = function(user, data) { this.setLock(data.locked); } +Channel.prototype.tryToggleLock = function(user) { + if(!Rank.hasPermission(user, "qlock")) { + return; + } + + this.setLock(this.openqueue); +} + +Channel.prototype.tryRemoveFilter = function(user, f) { + if(!this.hasPermission(user, "filteredit")) + return false; + + this.removeFilter(f); +} + +Channel.prototype.removeFilter = function(filter) { + for(var i = 0; i < this.filters.length; i++) { + if(this.filters[i].name == filter.name) { + this.filters.splice(i, 1); + break; + } + } + this.broadcastChatFilters(); +} + Channel.prototype.updateFilter = function(filter) { + if(filter.name == "") + filter.name = filter.source; var found = false; for(var i = 0; i < this.filters.length; i++) { - if(this.filters[i].name == "" && filter.name == "" - && this.filters[i].source == filter.source) { - found = true; - this.filters[i] = filter; - } - else if(filter.name != "" && this.filters[i].name == filter.name) { + if(this.filters[i].name == filter.name) { found = true; this.filters[i] = filter; + break; } } if(!found) { @@ -1516,45 +1583,45 @@ Channel.prototype.updateFilter = function(filter) { this.broadcastChatFilters(); } -Channel.prototype.removeFilter = function(name, source) { - for(var i = 0; i < this.filters.length; i++) { - if(this.filters[i].name == name - && this.filters[i].source == source) { - this.filters.splice(i, 1); - break; - } - } - this.broadcastChatFilters(); -} - -Channel.prototype.tryChangeFilter = function(user, data) { +Channel.prototype.tryUpdateFilter = function(user, f) { if(!this.hasPermission(user, "filteredit")) { return; } - if(data.cmd == undefined || data.filter == undefined) { + var re = f.source; + var flags = f.flags; + try { + new RegExp(re, flags); + } + catch(e) { return; } + var filter = new Filter(f.name, f.source, f.flags, f.replace); + filter.active = f.active; + filter.filterlinks = f.filterlinks; + this.updateFilter(filter); +} - if(data.cmd == "update") { - var re = data.filter.source; - var flags = data.filter.flags; - try { - new RegExp(re, flags); - } - catch(e) { - return; - } - var f = new Filter(data.filter.name, - data.filter.source, - data.filter.flags, - data.filter.replace); - f.active = data.filter.active; - this.updateFilter(f); - } - else if(data.cmd == "remove") { - this.removeFilter(data.filter.name, data.filter.source); +Channel.prototype.moveFilter = function(data) { + if(data.from < 0 || data.to < 0 || data.from >= this.filters.length || + data.to > this.filters.length) { + return; } + var f = this.filters[data.from]; + var to = data.to > data.from ? data.to + 1 : data.to; + var from = data.to > data.from ? data.from : data.from + 1; + this.filters.splice(to, 0, f); + this.filters.splice(from, 1); + this.broadcastChatFilters(); +} + +Channel.prototype.tryMoveFilter = function(user, data) { + if(!this.hasPermission(user, "filteredit")) + return; + + if(typeof data.to !== "number" || typeof data.from !== "number") + return; + this.moveFilter(data); } Channel.prototype.tryUpdatePermissions = function(user, perms) { @@ -1574,8 +1641,8 @@ Channel.prototype.tryUpdateOptions = function(user, data) { const adminonly = { pagetitle: true, - customcss: true, - customjs: true, + externalcss: true, + externaljs: true, show_public: true }; @@ -1649,7 +1716,10 @@ Channel.prototype.tryChat = function(user, data) { return; } - if(data.msg == undefined) { + if(!this.hasPermission(user, "chat")) + return; + + if(typeof data.msg !== "string") { return; } @@ -1682,6 +1752,11 @@ Channel.prototype.filterMessage = function(msg) { for(var j = 0; j < subs.length; j++) { if(this.opts.enable_link_regex && subs[j].match(link)) { subs[j] = subs[j].replace(link, "$1"); + for(var i = 0; i < this.filters.length; i++) { + if(!this.filters[i].filterlinks || !this.filters[i].active) + continue; + subs[j] = this.filters[i].filter(subs[j]); + } continue; } for(var i = 0; i < this.filters.length; i++) { diff --git a/chatcommand.js b/chatcommand.js index 59a0ade0..fd497dfc 100644 --- a/chatcommand.js +++ b/chatcommand.js @@ -32,6 +32,29 @@ function handle(chan, user, msg, data) { chan.chainMessage(user, msg.substring(3), {modflair: user.rank}) } } + else if(msg.indexOf("/a ") == 0) { + if(user.rank >= Rank.Siteadmin) { + var flair = { + superadminflair: { + labelclass: "label-important", + icon: "icon-globe" + } + }; + var args = msg.substring(3).split(" "); + var cargs = []; + for(var i = 0; i < args.length; i++) { + var a = args[i]; + if(a.indexOf("!icon-") == 0) + flair.superadminflair.icon = a.substring(1); + else if(a.indexOf("!label-") == 0) + flair.superadminflair.labelclass = a.substring(1); + else { + cargs.push(a); + } + } + chan.chainMessage(user, cargs.join(" "), flair); + } + } else if(msg.indexOf("/kick ") == 0) { handleKick(chan, user, msg.substring(6).split(" ")); } @@ -77,29 +100,13 @@ function handleKick(chan, user, args) { } function handleIPBan(chan, user, args) { - if(chan.hasPermission(user, "ban") && args.length > 0) { - args[0] = args[0].toLowerCase(); - var kickee; - for(var i = 0; i < chan.users.length; i++) { - if(chan.users[i].name.toLowerCase() == args[0]) { - kickee = chan.users[i]; - break; - } - } - if(kickee) { - chan.tryIPBan(user, { - id: chan.hideIP(kickee.ip), - name: kickee.name - }); - } - } + chan.tryIPBan(user, args[0], args[1]); + // Ban the name too for good measure + chan.tryNameBan(user, args[0]); } function handleBan(chan, user, args) { - if(chan.hasPermission(user, "ban") && args.length > 0) { - args[0] = args[0].toLowerCase(); - chan.banName(user, args[0]); - } + chan.tryNameBan(user, args[0]); } function handleUnban(chan, user, args) { diff --git a/database.js b/database.js index c734695e..6855b1e9 100644 --- a/database.js +++ b/database.js @@ -38,7 +38,7 @@ function getConnection() { db = mysql.createConnectionSync(); db.connectSync(SERVER, USER, PASSWORD, DATABASE); if(!db.connectedSync()) { - //Logger.errlog.log("DB connection failed"); + Logger.errlog.log("DB connection failed"); return false; } if(CONFIG.DEBUG) { @@ -173,6 +173,19 @@ function init() { if(!results) { Logger.errlog.log("! Failed to create playlist table"); } + + // Create user aliases table + query = ["CREATE TABLE IF NOT EXISTS `aliases` (", + "`visit_id` INT NOT NULL AUTO_INCREMENT,", + "`ip` VARCHAR(15) NOT NULL,", + "`name` VARCHAR(20) NOT NULL,", + "`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"); + } } /* REGION Global Bans */ @@ -672,6 +685,16 @@ function setUserEmail(name, email) { return true; } +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(''); +} + function generatePasswordReset(ip, name, email) { var db = getConnection(); if(!db) { @@ -698,7 +721,7 @@ function generatePasswordReset(ip, name, email) { } // Validation complete, now time to reset it - var hash = hashlib.sha256(Date.now() + name); + var hash = hashlib.sha256(genSalt() + name); var exp = Date.now() + 24*60*60*1000; query = createQuery( ["INSERT INTO `password_reset` (", @@ -885,6 +908,92 @@ function deleteUserPlaylist(user, name) { return results; } +/* User Aliases */ + +function recordVisit(ip, name) { + var db = getConnection(); + if(!db) { + return false; + } + + 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 results = db.querySync(query); + if(!results) { + Logger.errlog.log("! Failed to record visit"); + } + + // 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] + )); + + 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); + }); + + return names; +} + +function ipForName(name) { + var db = getConnection(); + if(!db) { + return []; + } + + var query = createQuery( + "SELECT ip FROM aliases WHERE name=?", + [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); + }); + + return ips; +} + exports.setup = setup; exports.getConnection = getConnection; exports.createQuery = createQuery; @@ -914,3 +1023,6 @@ exports.getUserPlaylists = getUserPlaylists; exports.loadUserPlaylist = loadUserPlaylist; exports.saveUserPlaylist = saveUserPlaylist; exports.deleteUserPlaylist = deleteUserPlaylist; +exports.recordVisit = recordVisit; +exports.getAliases = getAliases; +exports.ipForName = ipForName; diff --git a/filter.js b/filter.js index c2586abf..f7213fe1 100644 --- a/filter.js +++ b/filter.js @@ -16,6 +16,7 @@ var Filter = function(name, regex, flags, replace) { this.regex = new RegExp(this.source, this.flags); this.replace = replace; this.active = true; + this.filterlinks = false; } Filter.prototype.pack = function() { @@ -24,7 +25,8 @@ Filter.prototype.pack = function() { source: this.source, flags: this.flags, replace: this.replace, - active: this.active + active: this.active, + filterlinks: this.filterlinks } } diff --git a/notwebsocket.js b/notwebsocket.js index c4862ea4..6390cdab 100644 --- a/notwebsocket.js +++ b/notwebsocket.js @@ -141,6 +141,7 @@ function newConnection(req, res) { exports.newConnection = newConnection; function msgReceived(req, res) { + res.callback = req.query.callback; var h = req.params.hash; if(h in clients && clients[h] != null) { var str = req.params.str; diff --git a/package.json b/package.json index c1f359b3..f121b664 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "1.9.5", + "version": "2.0.0", "repository": { "url": "http://github.com/calzoneman/sync" }, diff --git a/server.js b/server.js index 19f9b4bb..7338f94d 100644 --- a/server.js +++ b/server.js @@ -9,7 +9,7 @@ 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. */ -const VERSION = "1.9.5"; +const VERSION = "2.0.0"; var fs = require("fs"); var Logger = require("./logger.js"); diff --git a/user.js b/user.js index 8b84c76c..b2dee674 100644 --- a/user.js +++ b/user.js @@ -17,6 +17,8 @@ var Server = require("./server.js"); var Database = require("./database.js"); var Logger = require("./logger.js"); var Config = require("./config.js"); +var ACP = require("./acp"); +var ActionLog = require("./actionlog"); // Represents a client connected via socket.io var User = function(socket, ip) { @@ -24,6 +26,7 @@ var User = function(socket, ip) { this.socket = socket; this.loggedIn = false; this.rank = Rank.Anonymous; + this.global_rank = Rank.Anonymous; this.channel = null; this.name = ""; this.meta = { @@ -206,7 +209,7 @@ User.prototype.initCallbacks = function() { } }.bind(this)); - this.socket.on("unqueue", function(data) { + this.socket.on("delete", function(data) { if(this.channel != null) { this.channel.tryDequeue(this, data); } @@ -236,21 +239,21 @@ User.prototype.initCallbacks = function() { } }.bind(this)); - this.socket.on("clearqueue", function() { + this.socket.on("clearPlaylist", function() { if(this.channel != null) { this.channel.tryClearqueue(this); } }.bind(this)); - this.socket.on("shufflequeue", function() { + this.socket.on("shufflePlaylist", function() { if(this.channel != null) { this.channel.tryShufflequeue(this); } }.bind(this)); - this.socket.on("queueLock", function(data) { + this.socket.on("togglePlaylistLock", function() { if(this.channel != null) { - this.channel.trySetLock(this, data); + this.channel.tryToggleLock(this); } }.bind(this)); @@ -260,18 +263,18 @@ User.prototype.initCallbacks = function() { } }.bind(this)); - this.socket.on("searchLibrary", function(data) { + this.socket.on("searchMedia", function(data) { if(this.channel != null) { - if(data.yt) { + if(data.source == "yt") { var callback = function(vids) { - this.socket.emit("librarySearchResults", { + this.socket.emit("searchResults", { results: vids }); }.bind(this); this.channel.search(data.query, callback); } else { - this.socket.emit("librarySearchResults", { + this.socket.emit("searchResults", { results: this.channel.search(data.query) }); } @@ -331,12 +334,6 @@ User.prototype.initCallbacks = function() { } }.bind(this)); - this.socket.on("adm", function(data) { - if(Rank.hasPermission(this, "acp")) { - this.handleAdm(data); - } - }.bind(this)); - this.socket.on("announce", function(data) { if(Rank.hasPermission(this, "announce")) { if(data.clear) { @@ -349,7 +346,7 @@ User.prototype.initCallbacks = function() { } }.bind(this)); - this.socket.on("channelOpts", function(data) { + this.socket.on("setOptions", function(data) { if(this.channel != null) { this.channel.tryUpdateOptions(this, data); } @@ -373,31 +370,53 @@ User.prototype.initCallbacks = function() { } }.bind(this)); - this.socket.on("chatFilter", function(data) { + this.socket.on("updateFilter", function(data) { if(this.channel != null) { - this.channel.tryChangeFilter(this, data); + this.channel.tryUpdateFilter(this, data); } }.bind(this)); - this.socket.on("updateMotd", function(data) { + this.socket.on("removeFilter", function(data) { + if(this.channel != null) { + this.channel.tryRemoveFilter(this, data); + } + }.bind(this)); + + this.socket.on("moveFilter", function(data) { + if(this.channel != null) { + this.channel.tryMoveFilter(this, data); + } + }.bind(this)); + + this.socket.on("setMotd", function(data) { if(this.channel != null) { this.channel.tryUpdateMotd(this, data); } }.bind(this)); - this.socket.on("requestAcl", function() { + this.socket.on("requestLoginHistory", function() { if(this.channel != null) { - this.channel.sendACL(this); - this.noflood("requestAcl", 0.25); + this.channel.sendLoginHistory(this); } }.bind(this)); - this.socket.on("requestSeenlogins", function() { + this.socket.on("requestBanlist", function() { if(this.channel != null) { - if(this.noflood("requestSeenLogins", 0.25)) { + this.channel.sendBanlist(this); + } + }.bind(this)); + + this.socket.on("requestChatFilters", function() { + if(this.channel != null) { + this.channel.sendChatFilters(this); + } + }.bind(this)); + + this.socket.on("requestChannelRanks", function() { + if(this.channel != null) { + if(this.noflood("requestChannelRanks", 0.25)) return; - } - this.channel.sendSeenLogins(this); + this.channel.sendChannelRanks(this); } }.bind(this)); @@ -498,39 +517,29 @@ User.prototype.initCallbacks = function() { pllist: list, }); }.bind(this)); -} -// Handle administration -User.prototype.handleAdm = function(data) { - if(data.cmd == "listchannels") { - var chans = []; - for(var chan in Server.channels) { - var nowplaying = "-"; - if(Server.channels[chan].media != null) - nowplaying = Server.channels[chan].media.title; - chans.push({ - name: chan, - usercount: Server.channels[chan].users.length, - nowplaying: nowplaying - }); - } - this.socket.emit("adm", { - cmd: "listchannels", - chans: chans - }); - } -}; + this.socket.on("acp-init", function() { + if(this.global_rank >= Rank.Siteadmin) + ACP.init(this); + }.bind(this)); + + this.socket.on("borrow-rank", function(rank) { + if(this.global_rank < 255) + return; + if(rank > this.global_rank) + return; + + this.rank = rank; + this.socket.emit("rank", rank); + if(this.channel != null) + this.channel.broadcastUserUpdate(this); + + }.bind(this)); +} var lastguestlogin = {}; // Attempt to login User.prototype.login = function(name, pw, session) { - if(this.channel != null && name != "") { - 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"); - } - } - } // No password => try guest login if(pw == "" && session == "") { if(this.ip in lastguestlogin) { @@ -546,73 +555,99 @@ User.prototype.login = function(name, pw, session) { return false; } } - // 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 { - lastguestlogin[this.ip] = Date.now(); - this.rank = Rank.Guest; - Logger.syslog.log(this.ip + " signed in as " + name); - this.name = name; - this.loggedIn = false; - this.socket.emit("login", { - success: true - }); - this.socket.emit("rank", { - rank: this.rank - }); - if(this.channel != null) { - this.channel.logger.log(this.ip + " signed in as " + name); - this.channel.broadcastNewUser(this); + 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 { + lastguestlogin[this.ip] = Date.now(); + this.rank = Rank.Guest; + Logger.syslog.log(this.ip + " signed in as " + name); + Database.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", { + success: false, + error: e + }); } } else { - var row; - if((row = Auth.login(name, pw, session))) { - this.loggedIn = true; - this.socket.emit("login", { - success: true, - session: row.session_hash - }); - Logger.syslog.log(this.ip + " logged in as " + 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.socket.emit("rank", { - rank: this.rank - }); - this.name = name; - if(this.channel != null) { - this.channel.logger.log(this.ip + " logged in as " + name); - this.channel.broadcastNewUser(this); + 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"); + } + } + } + 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); + Database.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; } } - // Wrong password - else { + catch(e) { this.socket.emit("login", { success: false, - error: "Invalid session" + error: e }); - return false; } } } diff --git a/www/account.html b/www/account.html index d3f865d8..fb4e3678 100644 --- a/www/account.html +++ b/www/account.html @@ -27,7 +27,7 @@
diff --git a/www/acp.html b/www/acp.html index dd4de0a4..8b5b36e7 100644 --- a/www/acp.html +++ b/www/acp.html @@ -13,13 +13,6 @@ body { padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */ } - .loginform { - margin: 100px auto 20px; - padding: 19px 29px 29px; - border-radius: 5px 5px 5px 5px; - border: 1px solid #dedede; - max-width: 300px; - } #log { max-height: 500px; @@ -39,31 +32,43 @@
+ +
+
+
- -
-
+ +
+

Log Viewer

@@ -75,30 +80,124 @@

             
-
-

Password Reset

-
-
- - +
+

Current Announcement

+

New Announcement

+ +
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
-
-
-
+

Global Bans

- +
- - - + + + + + +
IPReasonRemoveIP AddressNote
+

Add global ban

+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+

Users

+
+ + +
+ + + + + + + + + + +
UIDNameGlobal RankEmailPassword Reset
+
+
+

Loaded Channels

+ + + + + + + + + + + + +
TitleUser CountNow PlayingRegisteredPublicForce Unload
+
+
+

Action Log

+ + + + + + + + + + +
IP AddressNameActionArgsTime
+
+
+
+
+ + +
+ +
+
+
+
+

+
+
+ +
+
+ +
+ +
+ +
+ +

Not connected

+
+ +
+
+ +
+
+ + +
+ +
+ +

Nothing playing

+ +
+
+
+
+ +
+ +
+
+ +
+ + +
+ +
+ +

Show Library

+
+
+
+ +
+
+ + +
+
+ +
+ +

Show Playlist Manager

+
+
+
+ +
+
+ +
+
    +
+
+
+
+ +
+
+ +
+ +
+ +

Show Playlist Controls

+
+
+
+ +
+
+ + +
+
+ + + + +
+
+ +
    +
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + diff --git a/www/channel.html b/www/channel.html index 53ecf038..0850022e 100644 --- a/www/channel.html +++ b/www/channel.html @@ -4,334 +4,241 @@ CyTube - + + + - - -
-