diff --git a/lib/channel-new.js b/lib/channel-new.js index 8be02c72..f4aed6eb 100644 --- a/lib/channel-new.js +++ b/lib/channel-new.js @@ -1,3 +1,6 @@ +var util = require("./utilities"); +var Playlist = require("./playlist"); + var DEFAULT_FILTERS = [ new Filter("monospace", "`(.+?)`", "g", "$1"), new Filter("bold", "\\*(.+?)\\*", "g", "$1"), @@ -7,7 +10,7 @@ var DEFAULT_FILTERS = [ ]; function Channel(name) { - var self = this; // Alias `this` to prevent scoping issues + var self = this; // Alias `this` to prevent scoping issues Logger.syslog.log("Loading channel " + name); // Defaults @@ -16,7 +19,7 @@ function Channel(name) { self.uniqueName = name.toLowerCase(); // To prevent casing issues self.registered = false; // set to true if the channel exists in the database self.users = []; - self.mutedUsers = new $util.Set(); + self.mutedUsers = new util.Set(); self.playlist = new Playlist(self); self.plqueue = new AsyncQueue(); // For synchronizing playlist actions self.drinks = 0; @@ -81,12 +84,13 @@ function Channel(name) { html: "" // Filtered MOTD text (XSS removed; \n replaced by
) }; self.filters = DEFAULT_FILTERS; - self.banlist = new Banlist(); + self.ipbans = {}; + self.namebans = {}; self.logger = new Logger.Logger(path.join(__dirname, "../chanlogs", self.uniqueName + ".log")); self.css = ""; // Up to 20KB of inline CSS self.js = ""; // Up to 20KB of inline Javascript - + self.error = false; // Set to true if something bad happens => don't save state self.on("ready", function () { @@ -150,7 +154,7 @@ Channel.prototype.loadState = function () { return; } - fs.readFile(path.join(__dirname, "../chandump", self.name), + fs.readFile(path.join(__dirname, "../chandump", self.uniqueName), function (err, data) { if (err) { // File didn't exist => start fresh @@ -160,7 +164,7 @@ Channel.prototype.loadState = function () { } else { Logger.errlog.log("Failed to open channel dump " + self.uniqueName); Logger.errlog.log(err); - self.setMOTD("Internal error when loading channel"); + self.setMOTD("Channel state load failed. Contact an administrator."); self.error = true; self.emit("ready"); } @@ -173,11 +177,16 @@ Channel.prototype.loadState = function () { // Load the playlist if ("playlist" in data) { + self.playlist.load(data.playlist, function () { + self.sendPlaylist(self.users); + self.sendPlaylistMeta(self.users); + self.playlist.startPlayback(data.playlist.time); + }); } // Playlist lock self.setLock(data.playlistLock || false); - + // Configurables if ("opts" in data) { for (var key in data.opts) { @@ -203,8 +212,330 @@ Channel.prototype.loadState = function () { } } - } catch (e) { + // MOTD + if ("motd" in data) { + self.motd = { + motd: data.motd.motd, + html: data.motd.html + }; + } + // Chat history + if ("chatbuffer" in data) { + data.chatbuffer.forEach(function (msg) { + self.chatbuffer.push(msg); + }); + } + + // Inline CSS/JS + self.css = data.css || ""; + self.js = data.js || ""; + self.emit("ready"); + + } catch (e) { + self.error = true; + Logger.errlog.log("Channel dump load failed (" + self.uniqueName + "): " + e); + self.setMOTD("Channel state load failed. Contact an administrator."); + self.emit("ready"); } }); }; + +Channel.prototype.saveState = function () { + var self = this; + + if (self.error) { + return; + } + + if (!self.registered || self.uniqueName === "") { + return; + } + + var filters = self.filters.map(function (f) { + return f.pack(); + }); + + var data = { + playlist: self.playlist.dump(), + opts: self.opts, + permissions: self.permissions, + filters: filters, + motd: self.motd, + playlistLock: self.playlistLock, + chatbuffer: self.chatbuffer, + css: self.css, + js: self.js + }; + + var text = JSON.stringify(data); + fs.writeFileSync(path.join(__dirname, "../chandump", self.uniqueName), text); +}; + +/** + * Checks whether a user has the given permission node + */ +Channel.prototype.hasPermission = function (user, key) { + // Special case: you can have separate permissions for when playlist is unlocked + if (key.indexOf("playlist") === 0 && !this.playlistLock) { + var key2 = "o" + key; + var v = this.permissions[key2]; + if (typeof v === "number" && user.rank >= v) { + return true; + } + } + + var v = this.permissions[key]; + if (typeof v !== "number") { + return false; + } + + return user.rank >= v; +}; + +/** + * Defer a callback to complete when the channel is ready to accept users. + * Called immediately if the ready flag is already set + */ +Channel.prototype.whenReady = function (fn) { + var self = this; + if (self.ready) { + setImmediate(fn); + } else { + self.on("ready", fn); + } +}; + +/** + * Looks up a user's rank in the channel. Computed as max(global_rank, channel rank) + */ +Channel.prototype.getRank = function (name, callback) { + var self = this; + db.users.getGlobalRank(name, function (err, global) { + if (self.dead) { + return; + } + + if (err) { + callback(err, null); + return; + } + + if (!self.registered) { + callback(null, global); + return; + } + + db.channels.getRank(self.name, name, function (err, rank) { + if (self.dead) { + return; + } + + if (err) { + callback(err, null); + return; + } + + callback(null, Math.max(rank, global)); + }); + }); +}; + +/** + * Called when a user joins a channel + */ +Channel.prototype.join = function (user, password) { + var self = this; + self.whenReady(function () { + if (self.opts.password !== false && user.rank < 2) { + if (password !== self.opts.password) { + user.socket.emit("needPassword", typeof password === "undefined"); + return; + } + } + + user.socket.emit("cancelNeedPassword"); + var range = user.ip.replace(/(\d+)\.(\d+)\.(\d+)\.(\d+)/, "$1.$2.$3"); + if (user.ip in self.ipbans || range in self.ipbans || + user.name.toLowerCase() in self.namebans) { + user.kick("You're banned!"); + return; + } + + user.autoAFK(); + user.socket.join(self.uniqueName); + user.channel = self; + + self.users.push(user); + self.sendVoteskipUpdate(self.users); + self.sendUsercount(self.users); + + user.whenLoggedIn(function () { + var lname = user.name.toLowerCase(); + for (var i = 0; i < self.users.length; i++) { + if (self.users[i] === user) { + Logger.errlog.log("Wat: join() called on user already in channel"); + break; + } + self.users[i].kick("Duplicate login"); + } + + self.getRank(user.name, function (err, rank) { + if (self.dead) { + return; + } + + if (err) { + user.rank = user.global_rank; + } else { + user.rank = Math.max(rank, user.global_rank); + } + + user.socket.emit("rank", user.rank); + self.sendUserJoin(self.users); + }); + }); + + self.sendPlaylist([user]); + self.sendMediaUpdate([user]); + self.sendPlaylistLock([user]); + self.sendUserlist([user]); + self.sendRecentchat([user]); + self.sendCSSJS([user]); + self.sendPoll([user]); + self.sendOpts([user]); + self.sendPermissions([user]); + self.sendMotd([user]); + self.sendDrinkCount([user]); + + self.logger.log("+++ " + user.ip + " joined"); + Logger.syslog.log(user.ip + " joined channel " + self.name); + }); +}; + +/** + * Called when a user leaves the channel. + * Cleans up and sends appropriate updates to other users + */ +Channel.prototype.part = function (user) { + user.channel = null; + + // Clear poll vote + if (self.poll) { + self.poll.unvote(user.ip); + self.sendPoll(self.users); + } + + // Clear voteskip vote + if (self.voteskip) { + self.voteskip.unvote(user.ip); + self.sendVoteskipUpdate(self.users); + } + + // Return video lead to server if necessary + if (self.leader === user) { + self.changeLeader(""); + } + + // Remove from users array + var idx = self.users.indexOf(user); + if (idx >= 0 && idx < self.users.length) { + self.users.splice(idx, 1); + } + + // A change in usercount might cause a voteskip result to change + self.checkVoteskipPass(); + self.sendUsercount(self.users); + + if (user.loggedIn) { + self.sendUserLeave(self.users, user); + } + + self.logger.log("--- " + user.ip + " (" + user.name + ") left"); + if (self.users.length === 0) { + self.emit("empty"); + return; + } +}; + +/** + * Set the MOTD and broadcast it to connected users + */ +Channel.prototype.setMOTD = function (message) { + var self = this; + self.motd.motd = message; + // TODO XSS filter + self.motd.html = message.replace(/\n/g, "
"); + self.sendMOTD(self.users); +}; + +/** + * Send the MOTD to the given users + */ +Channel.prototype.sendMOTD = function (users) { + var self = this; + users.forEach(function (u) { + u.socket.emit("setMotd", self.motd); + }); +}; + +Channel.prototype.tryReadLog = function (user) { + if (user.rank < 3) { + user.kick("Attempted readChanLog with insufficient permission"); + return; + } + + var filterIp = user.global_rank < 255; + this.readLog(filterIp, function (err, data) { + if (err) { + user.socket.emit("readChanLog", { + success: false, + data: "Reading channel log failed." + }); + } else { + user.socket.emit("readChanLog", { + success: true, + data: data + }); + } + }); +}; + +Channel.prototype.readLog = function (filterIp, callback) { + var maxLen = 102400; // Limit to last 100KiB + var file = this.logger.filename; + + fs.stat(file, function (err, data) { + if (err) { + callback(err, null); + return; + } + + var start = Math.max(data.size - maxLen, 0); + var end = data.size - 1; + + var rs = fs.createReadStream(file, { + start: start, + end: end + }); + + var buffer = ""; + rs.on("data", function (data) { + buffer += data; + }); + + rs.on("end", function () { + if (filterIp) { + buffer = buffer.replace( + /\d+\.\d+\.(\d+\.\d+)/g, + "x.x.$1" + ).replace( + /\d+\.\d+\.(\d+)/g, + "x.x.$.*" + ); + } + + callback(null, buffer); + }); + }); +};