Work on channel.js

This commit is contained in:
calzoneman 2013-12-27 16:38:06 -05:00
parent ba0664641e
commit 9611f86d55

View file

@ -1,3 +1,6 @@
var util = require("./utilities");
var Playlist = require("./playlist");
var DEFAULT_FILTERS = [
new Filter("monospace", "`(.+?)`", "g", "<code>$1</code>"),
new Filter("bold", "\\*(.+?)\\*", "g", "<strong>$1</strong>"),
@ -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 <br>)
};
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, "<br>");
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);
});
});
};