diff --git a/lib/channel-new.js b/lib/channel-new.js
new file mode 100644
index 00000000..8be02c72
--- /dev/null
+++ b/lib/channel-new.js
@@ -0,0 +1,210 @@
+var DEFAULT_FILTERS = [
+ new Filter("monospace", "`(.+?)`", "g", "$1"),
+ new Filter("bold", "\\*(.+?)\\*", "g", "$1"),
+ new Filter("italic", "_(.+?)_", "g", "$1"),
+ new Filter("strike", "~~(.+?)~~", "g", "$1"),
+ new Filter("inline spoiler", "\\[sp\\](.*?)\\[\\/sp\\]", "ig", "$1")
+];
+
+function Channel(name) {
+ var self = this; // Alias `this` to prevent scoping issues
+ Logger.syslog.log("Loading channel " + name);
+
+ // Defaults
+ self.ready = false;
+ self.name = 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.playlist = new Playlist(self);
+ self.plqueue = new AsyncQueue(); // For synchronizing playlist actions
+ self.drinks = 0;
+ self.leader = null;
+ self.chatbuffer = [];
+ self.playlistLock = true;
+ self.poll = null;
+ self.voteskip = null;
+ self.permissions = {
+ playlistadd: 1.5, // Add video to the playlist
+ playlistnext: 1.5, // TODO I don't think this is used
+ playlistmove: 1.5, // Move a video on the playlist
+ playlistdelete: 2, // Delete a video from the playlist
+ playlistjump: 1.5, // Start a different video on the playlist
+ playlistaddlist: 1.5, // Add a list of videos to the playlist
+ oplaylistadd: -1, // Same as above, but for open (unlocked) playlist
+ oplaylistnext: 1.5,
+ oplaylistmove: 1.5,
+ oplaylistdelete: 2,
+ oplaylistjump: 1.5,
+ oplaylistaddlist: 1.5,
+ playlistaddcustom: 3, // Add custom embed to the playlist
+ playlistaddlive: 1.5, // Add a livestream to the playlist
+ exceedmaxlength: 2, // Add a video longer than the maximum length set
+ addnontemp: 2, // Add a permanent video to the playlist
+ settemp: 2, // Toggle temporary status of a playlist item
+ playlistgeturl: 1.5, // TODO is this even used?
+ playlistshuffle: 2, // Shuffle the playlist
+ playlistclear: 2, // Clear the playlist
+ pollctl: 1.5, // Open/close polls
+ pollvote: -1, // Vote in polls
+ viewhiddenpoll: 1.5, // View results of hidden polls
+ voteskip: -1, // Vote to skip the current video
+ mute: 1.5, // Mute other users
+ kick: 1.5, // Kick other users
+ ban: 2, // Ban other users
+ motdedit: 3, // Edit the MOTD
+ filteredit: 3, // Control chat filters
+ drink: 1.5, // Use the /d command
+ chat: 0 // Send chat messages
+ };
+ self.opts = {
+ allow_voteskip: true, // Allow users to voteskip
+ voteskip_ratio: 0.5, // Ratio of skip votes:non-afk users needed to skip the video
+ afk_timeout: 600, // Number of seconds before a user is automatically marked afk
+ pagetitle: self.name, // Title of the browser tab
+ maxlength: 0, // Maximum length (in seconds) of a video queued
+ externalcss: "", // Link to external stylesheet
+ externaljs: "", // Link to external script
+ chat_antiflood: false, // Throttle chat messages
+ chat_antiflood_params: {
+ burst: 4, // Number of messages to allow with no throttling
+ sustained: 1, // Throttle rate (messages/second)
+ cooldown: 4 // Number of seconds with no messages before burst is reset
+ },
+ show_public: false, // List the channel on the index page
+ enable_link_regex: true, // Use the built-in link filter
+ password: false // Channel password (false -> no password required for entry)
+ };
+ self.motd = {
+ motd: "", // Raw MOTD text
+ html: "" // Filtered MOTD text (XSS removed; \n replaced by
)
+ };
+ self.filters = DEFAULT_FILTERS;
+ self.banlist = new Banlist();
+ 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 () {
+ self.ready = true;
+ });
+
+ // Load from database
+ db.channels.load(self, function (err) {
+ if (err && err !== "Channel is not registered") {
+ return;
+ } else {
+ // Load state from JSON blob
+ self.tryLoadState();
+ }
+ });
+};
+
+Channel.prototype = EventEmitter;
+
+Channel.prototype.tryLoadState = function () {
+ var self = this;
+ if (self.name === "") {
+ return;
+ }
+
+ // Don't load state if the channel isn't registered
+ if (!self.registered) {
+ self.emit("ready");
+ return;
+ }
+
+ var file = path.join(__dirname, "../chandump", self.uniqueName);
+ fs.stat(file, function (err, stats) {
+ if (!err) {
+ var mb = stats.size / 1048576;
+ mb = Math.floor(mb * 100) / 100;
+ if (mb > 1) {
+ Logger.errlog.log("Large chandump detected: " + self.uniqueName +
+ " (" + mb + " MiB)");
+ self.setMOTD("Your channel file has exceeded the maximum size of 1MB " +
+ "and cannot be loaded. Please ask an administrator for " +
+ "assistance in restoring it.");
+ self.error = true;
+ self.emit("ready");
+ return;
+ }
+ }
+
+ self.loadState();
+ });
+};
+
+/**
+ * Load the channel state from disk.
+ *
+ * SHOULD ONLY BE CALLED FROM tryLoadState
+ */
+Channel.prototype.loadState = function () {
+ var self = this;
+ if (self.error) {
+ return;
+ }
+
+ fs.readFile(path.join(__dirname, "../chandump", self.name),
+ function (err, data) {
+ if (err) {
+ // File didn't exist => start fresh
+ if (err.code === "ENOENT") {
+ self.emit("ready");
+ self.saveState();
+ } else {
+ Logger.errlog.log("Failed to open channel dump " + self.uniqueName);
+ Logger.errlog.log(err);
+ self.setMOTD("Internal error when loading channel");
+ self.error = true;
+ self.emit("ready");
+ }
+ return;
+ }
+
+ try {
+ self.logger.log("*** Loading channel state");
+ data = JSON.parse(data);
+
+ // Load the playlist
+ if ("playlist" in data) {
+ }
+
+ // Playlist lock
+ self.setLock(data.playlistLock || false);
+
+ // Configurables
+ if ("opts" in data) {
+ for (var key in data.opts) {
+ self.opts[key] = data.opts;
+ }
+ }
+
+ // Permissions
+ if ("permissions" in data) {
+ for (var key in data.permissions) {
+ self.permissions[key] = data.permissions[key];
+ }
+ }
+
+ // Chat filters
+ if ("filters" in data) {
+ for (var i = 0; i < data.filters.length; i++) {
+ var f = data.filters[i];
+ var filt = new Filter(f.name, f.source, f.flags, f.replace);
+ filt.active = f.active;
+ filt.filterlinks = f.filterlinks;
+ self.updateFilter(filt, false);
+ }
+ }
+
+ } catch (e) {
+
+ }
+ });
+};
diff --git a/lib/channel.js b/lib/channel.js
index 19ffc145..16d2e0de 100644
--- a/lib/channel.js
+++ b/lib/channel.js
@@ -1315,20 +1315,25 @@ Channel.prototype.calcVoteskipMax = function () {
}, 0);
};
-Channel.prototype.broadcastVoteskipUpdate = function() {
+Channel.prototype.getVoteskipPacket = function() {
var amt = this.voteskip ? this.voteskip.counts[0] : 0;
var count = this.calcVoteskipMax();
var need = this.voteskip ? Math.ceil(count * this.opts.voteskip_ratio) : 0;
- for(var i = 0; i < this.users.length; i++) {
- if(this.users[i].rank >= 1.5) {
- this.users[i].socket.emit("voteskip", {
- count: amt,
- need: need
- });
- }
- }
+ return {
+ count: amt,
+ need: need
+ };
}
+Channel.prototype.broadcastVoteskipUpdate = function () {
+ var pkt = this.getVoteskipPacket();
+ this.users.forEach(function (u) {
+ if (u.rank >= 1.5) {
+ u.socket.emit("voteskip", pkt);
+ }
+ });
+};
+
Channel.prototype.broadcastMotd = function() {
this.sendAll("setMotd", this.motd);
}
diff --git a/lib/user.js b/lib/user.js
index 9646b0cf..2a607fdf 100644
--- a/lib/user.js
+++ b/lib/user.js
@@ -32,7 +32,8 @@ var User = function (socket) {
this.name = "";
this.meta = {
afk: false,
- icon: false
+ icon: false,
+ aliases: []
};
this.queueLimiter = $util.newRateLimiter();
this.chatLimiter = $util.newRateLimiter();
diff --git a/lib/web/auth.js b/lib/web/auth.js
index e4182390..ff4d1aaa 100644
--- a/lib/web/auth.js
+++ b/lib/web/auth.js
@@ -67,7 +67,8 @@ function handleLoginPage(req, res) {
}
}
sendJade(res, 'login', {
- loggedIn: false
+ loggedIn: false,
+ redirect: req.header('Referrer')
});
}
diff --git a/templates/login.jade b/templates/login.jade
index cfc1c672..3f965dba 100644
--- a/templates/login.jade
+++ b/templates/login.jade
@@ -12,13 +12,15 @@ html(lang="en")
ul.nav.navbar-nav
mixin navdefaultlinks("/login")
if loggedIn
- mixin navlogoutform("/login")
+ if redirect
+ mixin navlogoutform(redirect)
+ else
+ mixin navlogoutform("/login")
section#mainpage.container
if wasAlreadyLoggedIn
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
.alert.alert-info.messagebox.center
h3(style="margin: 5px auto") Logged in as #{loginName}
- // TODO Link to My Account page
else if !loggedIn
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
if loginError
@@ -27,6 +29,8 @@ html(lang="en")
p= loginError
h2 Login
form(role="form", action="/login", method="post")
+ if redirect
+ input(type="hidden", name="redirect", value=redirect)
.form-group
label(for="username") Username
input#username.form-control(type="text", name="name")
diff --git a/templates/nav.jade b/templates/nav.jade
index 7c85f05a..9a81e666 100644
--- a/templates/nav.jade
+++ b/templates/nav.jade
@@ -55,7 +55,7 @@ mixin navloginform(redirect)
mixin navlogoutform(redirect)
if redirect
- - url = "/logout?redirect=#{redirect}"
+ - url = "/logout?redirect=" + redirect
else
- url = "/logout"
p#logoutform.navbar-text.pull-right