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);
+ });
+ });
+};