package: build with babel for ES2015 support

* Rename lib/ -> src/
* Add `postinstall` npm target for compiling src files to lib
* Add `build-watch` npm target for development with babel --watch
* Add `lib/` to .gitignore
* Add `source-map-support` module for babel-generated sourcemaps
This commit is contained in:
calzoneman 2015-09-20 22:06:53 -07:00
parent d042619b21
commit 0109a87e55
55 changed files with 9 additions and 3 deletions

564
src/database/accounts.js Normal file
View file

@ -0,0 +1,564 @@
var $util = require("../utilities");
var bcrypt = require("bcrypt");
var db = require("../database");
var Config = require("../config");
var Logger = require("../logger");
var registrationLock = {};
var blackHole = function () { };
/**
* Replaces look-alike characters with "_" (single character wildcard) for
* use in LIKE queries. This prevents guests from taking names that look
* visually identical to existing names in certain fonts.
*/
function wildcardSimilarChars(name) {
return name.replace(/_/g, "\\_").replace(/[Il1oO0]/g, "_");
}
module.exports = {
init: function () {
},
/**
* Check if a username is taken
*/
isUsernameTaken: function (name, callback) {
db.query("SELECT name FROM `users` WHERE name LIKE ? ESCAPE '\\\\'",
[wildcardSimilarChars(name)],
function (err, rows) {
if (err) {
callback(err, true);
return;
}
callback(null, rows.length > 0);
});
},
/**
* Search for a user by name
*/
search: function (name, fields, callback) {
/* This bit allows it to accept varargs
Function can be called as (name, callback) or
(name, fields, callback)
*/
if (typeof callback !== "function") {
if (typeof fields === "function") {
callback = fields;
fields = ["name"];
} else {
return;
}
}
// Don't allow search to return password hashes
if (fields.indexOf("password") !== -1) {
fields.splice(fields.indexOf("password"));
}
db.query("SELECT " + fields.join(",") + " FROM `users` WHERE name LIKE ?",
["%"+name+"%"],
function (err, rows) {
if (err) {
callback(err, true);
return;
}
callback(null, rows);
});
},
getUser: function (name, callback) {
if (typeof callback !== "function") {
return;
}
db.query("SELECT * FROM `users` WHERE name = ?", [name], function (err, rows) {
if (err) {
callback(err, true);
return;
}
if (rows.length !== 1) {
return callback("User does not exist");
}
callback(null, rows[0]);
});
},
/**
* Registers a new user account
*/
register: function (name, pw, email, ip, callback) {
// Start off with a boatload of error checking
if (typeof callback !== "function") {
callback = blackHole;
}
if (typeof name !== "string" || typeof pw !== "string") {
callback("You must provide a nonempty username and password", null);
return;
}
var lname = name.toLowerCase();
if (registrationLock[lname]) {
callback("There is already a registration in progress for "+name,
null);
return;
}
if (!$util.isValidUserName(name)) {
callback("Invalid username. Usernames may consist of 1-20 " +
"characters a-z, A-Z, 0-9, -, _, and accented letters.",
null);
return;
}
if (typeof email !== "string") {
email = "";
}
if (typeof ip !== "string") {
ip = "";
}
// From this point forward, actual registration happens
// registrationLock prevents concurrent database activity
// on the same user account
registrationLock[lname] = true;
this.getAccounts(ip, function (err, accts) {
if (err) {
delete registrationLock[lname];
callback(err, null);
return;
}
if (accts.length >= Config.get("max-accounts-per-ip")) {
delete registrationLock[lname];
callback("You have registered too many accounts from this "+
"computer.", null);
return;
}
module.exports.isUsernameTaken(name, function (err, taken) {
if (err) {
delete registrationLock[lname];
callback(err, null);
return;
}
if (taken) {
delete registrationLock[lname];
callback("Username is already registered", null);
return;
}
bcrypt.hash(pw, 10, function (err, hash) {
if (err) {
delete registrationLock[lname];
callback(err, null);
return;
}
db.query("INSERT INTO `users` " +
"(`name`, `password`, `global_rank`, `email`, `profile`, `ip`, `time`)" +
" VALUES " +
"(?, ?, ?, ?, '', ?, ?)",
[name, hash, 1, email, ip, Date.now()],
function (err, res) {
delete registrationLock[lname];
if (err) {
callback(err, null);
} else {
callback(null, {
name: name,
hash: hash
});
}
});
});
});
});
},
/**
* Verify a username/password pair
*/
verifyLogin: function (name, pw, callback) {
if (typeof callback !== "function") {
return;
}
if (typeof name !== "string" || typeof pw !== "string") {
callback("Invalid username/password combination", null);
return;
}
/* Passwords are capped at 100 characters to prevent a potential
denial of service vector through causing the server to hash
ridiculously long strings.
*/
pw = pw.substring(0, 100);
/* Note: rather than hash the password and then query based on name and
password, I query by name, then use bcrypt.compare() to check that
the hashes match.
*/
db.query("SELECT name,password,global_rank FROM `users` WHERE name=?",
[name],
function (err, rows) {
if (err) {
callback(err, null);
return;
}
if (rows.length === 0) {
callback("User does not exist", null);
return;
}
bcrypt.compare(pw, rows[0].password, function (err, match) {
if (err) {
callback(err, null);
} else if (!match) {
callback("Invalid username/password combination", null);
} else {
callback(null, rows[0]);
}
});
});
},
/**
* Verify an auth string of the form name:hash
*/
verifyAuth: function (auth, callback) {
if (typeof callback !== "function") {
return;
}
if (typeof auth !== "string") {
callback("Invalid auth string", null);
return;
}
var split = auth.split(":");
if (split.length !== 2) {
callback("Invalid auth string", null);
return;
}
var name = split[0];
var hash = split[1];
db.query("SELECT name,password,global_rank FROM `users` WHERE " +
"name=? and password=?", [name, hash],
function (err, rows) {
if (err) {
callback(err, null);
return;
}
if (rows.length === 0) {
callback("Auth string does not match an existing user", null);
return;
}
callback(null, {
name: rows[0].name,
hash: rows[0].password,
global_rank: rows[0].global_rank
});
});
},
/**
* Change a user's password
*/
setPassword: function (name, pw, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (typeof name !== "string" || typeof pw !== "string") {
callback("Invalid username/password combination", null);
return;
}
/* Passwords are capped at 100 characters to prevent a potential
denial of service vector through causing the server to hash
ridiculously long strings.
*/
pw = pw.substring(0, 100);
bcrypt.hash(pw, 10, function (err, hash) {
if (err) {
callback(err, null);
return;
}
db.query("UPDATE `users` SET password=? WHERE name=?",
[hash, name],
function (err, result) {
callback(err, err ? null : true);
});
});
},
/**
* Lookup a user's global rank
*/
getGlobalRank: function (name, callback) {
if (typeof callback !== "function") {
return;
}
if (typeof name !== "string") {
callback("Invalid username", null);
return;
}
if (!name) {
callback(null, -1);
return;
}
db.query("SELECT global_rank FROM `users` WHERE name=?", [name],
function (err, rows) {
if (err) {
callback(err, null);
} else if (rows.length === 0) {
callback(null, 0);
} else {
callback(null, rows[0].global_rank);
}
});
},
/**
* Updates a user's global rank
*/
setGlobalRank: function (name, rank, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (typeof name !== "string") {
callback("Invalid username", null);
return;
}
if (typeof rank !== "number") {
callback("Invalid rank", null);
return;
}
db.query("UPDATE `users` SET global_rank=? WHERE name=?", [rank, name],
function (err, result) {
callback(err, err ? null : true);
});
},
/**
* Lookup multiple users' global rank in one query
*/
getGlobalRanks: function (names, callback) {
if (typeof callback !== "function") {
return;
}
if (!(names instanceof Array)) {
callback("Expected array of names, got " + typeof names, null);
return;
}
if (names.length === 0) {
return callback(null, []);
}
var list = "(" + names.map(function () { return "?";}).join(",") + ")";
db.query("SELECT global_rank FROM `users` WHERE name IN " + list, names,
function (err, rows) {
if (err) {
callback(err, null);
} else if (rows.length === 0) {
callback(null, []);
} else {
callback(null, rows.map(function (x) { return x.global_rank; }));
}
});
},
/**
* Lookup a user's email
*/
getEmail: function (name, callback) {
if (typeof callback !== "function") {
return;
}
if (typeof name !== "string") {
callback("Invalid username", null);
return;
}
db.query("SELECT email FROM `users` WHERE name=?", [name],
function (err, rows) {
if (err) {
callback(err, null);
} else if (rows.length === 0) {
callback("User does not exist", null);
} else {
callback(null, rows[0].email);
}
});
},
/**
* Updates a user's email
*/
setEmail: function (name, email, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (typeof name !== "string") {
callback("Invalid username", null);
return;
}
if (typeof email !== "string") {
callback("Invalid email", null);
return;
}
db.query("UPDATE `users` SET email=? WHERE name=?", [email, name],
function (err, result) {
callback(err, err ? null : true);
});
},
/**
* Lookup a user's profile
*/
getProfile: function (name, callback) {
if (typeof callback !== "function") {
return;
}
if (typeof name !== "string") {
callback("Invalid username", null);
return;
}
db.query("SELECT profile FROM `users` WHERE name=?", [name],
function (err, rows) {
if (err) {
callback(err, null);
} else if (rows.length === 0) {
callback("User does not exist", null);
} else {
var userprof = {
image: "",
text: ""
};
if (rows[0].profile === "") {
callback(null, userprof);
return;
}
try {
var profile = JSON.parse(rows[0].profile);
userprof.image = profile.image || "";
userprof.text = profile.text || "";
callback(null, userprof);
} catch (e) {
Logger.errlog.log("Corrupt profile: " + rows[0].profile +
" (user: " + name + ")");
callback(null, userprof);
}
}
});
},
/**
* Updates a user's profile
*/
setProfile: function (name, profile, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (typeof name !== "string") {
callback("Invalid username", null);
return;
}
if (typeof profile !== "object") {
callback("Invalid profile", null);
return;
}
// Cast to string to guarantee string type
profile.image += "";
profile.text += "";
// Limit size
profile.image = profile.image.substring(0, 255);
profile.text = profile.text.substring(0, 255);
// Stringify the literal to guarantee I only get the keys I want
var profilejson = JSON.stringify({
image: profile.image,
text: profile.text
});
db.query("UPDATE `users` SET profile=? WHERE name=?", [profilejson, name],
function (err, result) {
callback(err, err ? null : true);
});
},
generatePasswordReset: function (ip, name, email, callback) {
if (typeof callback !== "function") {
return;
}
callback("generatePasswordReset is not implemented", null);
},
recoverPassword: function (hash, callback) {
if (typeof callback !== "function") {
return;
}
callback("recoverPassword is not implemented", null);
},
/**
* Retrieve a list of channels owned by a user
*/
getChannels: function (name, callback) {
if (typeof callback !== "function") {
return;
}
db.query("SELECT * FROM `channels` WHERE owner=?", [name], callback);
},
/**
* Retrieves all names registered from a given IP
*/
getAccounts: function (ip, callback) {
if (typeof callback !== "function") {
return;
}
db.query("SELECT name,global_rank FROM `users` WHERE `ip`=?", [ip],
callback);
}
};

621
src/database/channels.js Normal file
View file

@ -0,0 +1,621 @@
var db = require("../database");
var valid = require("../utilities").isValidChannelName;
var fs = require("fs");
var path = require("path");
var Logger = require("../logger");
var tables = require("./tables");
var Flags = require("../flags");
var util = require("../utilities");
var blackHole = function () { };
function dropTable(name, callback) {
db.query("DROP TABLE `" + name + "`", callback);
}
function initTables(name, owner, callback) {
if (!valid(name)) {
callback("Invalid channel name", null);
return;
}
}
module.exports = {
init: function () {
},
/**
* Checks if the given channel name is registered
*/
isChannelTaken: function (name, callback) {
if (typeof callback !== "function") {
return;
}
if (!valid(name)) {
callback("Invalid channel name", null);
return;
}
db.query("SELECT name FROM `channels` WHERE name=?",
[name],
function (err, rows) {
if (err) {
callback(err, true);
return;
}
callback(null, rows.length > 0);
});
},
/**
* Looks up a channel
*/
lookup: function (name, callback) {
if (typeof callback !== "function") {
return;
}
if (!valid(name)) {
callback("Invalid channel name", null);
return;
}
db.query("SELECT * FROM `channels` WHERE name=?",
[name],
function (err, rows) {
if (err) {
callback(err, null);
return;
}
if (rows.length === 0) {
callback("No such channel", null);
} else {
callback(null, rows[0]);
}
});
},
/**
* Searches for a channel
*/
search: function (name, callback) {
if (typeof callback !== "function") {
return;
}
db.query("SELECT * FROM `channels` WHERE name LIKE ?",
["%" + name + "%"],
function (err, rows) {
if (err) {
callback(err, null);
return;
}
callback(null, rows);
});
},
/**
* Searches for a channel by owner
*/
searchOwner: function (name, callback) {
if (typeof callback !== "function") {
return;
}
db.query("SELECT * FROM `channels` WHERE owner LIKE ?",
["%" + name + "%"],
function (err, rows) {
if (err) {
callback(err, null);
return;
}
callback(null, rows);
});
},
/**
* Validates and registers a new channel
*/
register: function (name, owner, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (typeof name !== "string" || typeof owner !== "string") {
callback("Name and owner are required for channel registration", null);
return;
}
if (!valid(name)) {
callback("Invalid channel name. Channel names may consist of 1-30 " +
"characters a-z, A-Z, 0-9, -, and _", null);
return;
}
module.exports.isChannelTaken(name, function (err, taken) {
if (err) {
callback(err, null);
return;
}
if (taken) {
callback("Channel name " + name + " is already taken", null);
return;
}
db.query("INSERT INTO `channels` " +
"(`name`, `owner`, `time`) VALUES (?, ?, ?)",
[name, owner, Date.now()],
function (err, res) {
if (err) {
callback(err, null);
return;
}
db.users.getGlobalRank(owner, function (err, rank) {
if (err) {
callback(err, null);
return;
}
rank = Math.max(rank, 5);
module.exports.setRank(name, owner, rank, function (err) {
if (err) {
callback(err, null);
return;
}
callback(null, { name: name });
});
});
});
});
},
/**
* Unregisters a channel
*/
drop: function (name, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (!valid(name)) {
callback("Invalid channel name", null);
return;
}
db.query("DELETE FROM `channels` WHERE name=?", [name], function (err) {
module.exports.deleteBans(name, function (err) {
if (err) {
Logger.errlog.log("Failed to delete bans for " + name + ": " + err);
}
});
module.exports.deleteLibrary(name, function (err) {
if (err) {
Logger.errlog.log("Failed to delete library for " + name + ": " + err);
}
});
module.exports.deleteAllRanks(name, function (err) {
if (err) {
Logger.errlog.log("Failed to delete ranks for " + name + ": " + err);
}
});
fs.unlink(path.join(__dirname, "..", "..", "chandump", name),
function (err) {
if (err && err.code !== "ENOENT") {
Logger.errlog.log("Deleting chandump failed:");
Logger.errlog.log(err);
}
});
callback(err, !Boolean(err));
});
},
/**
* Looks up channels registered by a given user
*/
listUserChannels: function (owner, callback) {
if (typeof callback !== "function") {
return;
}
db.query("SELECT * FROM `channels` WHERE owner=?", [owner],
function (err, res) {
if (err) {
callback(err, []);
return;
}
callback(err, res);
});
},
/**
* Loads the channel from the database
*/
load: function (chan, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (!valid(chan.name)) {
callback("Invalid channel name", null);
return;
}
db.query("SELECT * FROM `channels` WHERE name=?", chan.name, function (err, res) {
if (err) {
callback(err, null);
return;
}
if (res.length === 0) {
callback("Channel is not registered", null);
return;
}
if (chan.dead) {
callback("Channel is dead", null);
return;
}
// Note that before this line, chan.name might have a different capitalization
// than the database has stored. Update accordingly.
chan.name = res[0].name;
chan.uniqueName = chan.name.toLowerCase();
chan.setFlag(Flags.C_REGISTERED);
chan.logger.log("[init] Loaded channel from database");
callback(null, true);
});
},
/**
* Looks up a user's rank
*/
getRank: function (chan, name, callback) {
if (typeof callback !== "function") {
return;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("SELECT * FROM `channel_ranks` WHERE name=? AND channel=?",
[name, chan],
function (err, rows) {
if (err) {
callback(err, -1);
return;
}
if (rows.length === 0) {
callback(null, 1);
return;
}
callback(null, rows[0].rank);
});
},
/**
* Looks up multiple users' ranks at once
*/
getRanks: function (chan, names, callback) {
if (typeof callback !== "function") {
return;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
var replace = "(" + names.map(function () { return "?"; }).join(",") + ")";
/* Last substitution is the channel to select ranks for */
names.push(chan);
db.query("SELECT * FROM `channel_ranks` WHERE name IN " +
replace + " AND channel=?", names,
function (err, rows) {
if (err) {
callback(err, []);
return;
}
callback(null, rows.map(function (r) { return r.rank; }));
});
},
/**
* Query all user ranks at once
*/
allRanks: function (chan, callback) {
if (typeof callback !== "function") {
return;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("SELECT * FROM `channel_ranks` WHERE channel=?", [chan], callback);
},
/**
* Updates a user's rank
*/
setRank: function (chan, name, rank, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (rank < 2) {
module.exports.deleteRank(chan, name, callback);
return;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("INSERT INTO `channel_ranks` VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE rank=?",
[name, rank, chan, rank, chan], callback);
},
/**
* Removes a user's rank entry
*/
deleteRank: function (chan, name, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("DELETE FROM `channel_ranks` WHERE name=? AND channel=?", [name, chan],
callback);
},
/**
* Removes all ranks for a channel
*/
deleteAllRanks: function (chan, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("DELETE FROM `channel_ranks` WHERE channel=?", [chan], callback);
},
/**
* Adds a media item to the library
*/
addToLibrary: function (chan, media, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
var meta = JSON.stringify({
bitrate: media.meta.bitrate,
codec: media.meta.codec,
scuri: media.meta.scuri,
embed: media.meta.embed
});
db.query("INSERT INTO `channel_libraries` " +
"(id, title, seconds, type, meta, channel) " +
"VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE id=id",
[media.id, media.title, media.seconds, media.type, meta, chan], callback);
},
/**
* Retrieves a media item from the library by id
*/
getLibraryItem: function (chan, id, callback) {
if (typeof callback !== "function") {
return;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("SELECT * FROM `channel_libraries` WHERE id=? AND channel=?", [id, chan],
function (err, rows) {
if (err) {
callback(err, null);
return;
}
if (rows.length === 0) {
callback("Item not in library", null);
} else {
callback(null, rows[0]);
}
});
},
/**
* Search the library by title
*/
searchLibrary: function (chan, search, callback) {
if (typeof callback !== "function") {
return;
}
db.query("SELECT * FROM `channel_libraries` WHERE title LIKE ? AND channel=?",
["%" + search + "%", chan], callback);
},
/**
* Deletes a media item from the library
*/
deleteFromLibrary: function (chan, id, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("DELETE FROM `channel_libraries` WHERE id=? AND channel=?",
[id, chan], callback);
},
/**
* Deletes all library entries for a channel
*/
deleteLibrary: function (chan, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("DELETE FROM `channel_libraries` WHERE channel=?", [chan], callback);
},
/**
* Add a ban to the banlist
*/
ban: function (chan, ip, name, note, bannedby, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("INSERT INTO `channel_bans` (ip, name, reason, bannedby, channel) " +
"VALUES (?, ?, ?, ?, ?)",
[ip, name, note, bannedby, chan], callback);
},
/**
* Check if an IP address or range is banned
*/
isIPBanned: function (chan, ip, callback) {
if (typeof callback !== "function") {
return;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
var range = util.getIPRange(ip);
var wrange = util.getWideIPRange(ip);
db.query("SELECT * FROM `channel_bans` WHERE ip IN (?, ?, ?) AND channel=?",
[ip, range, wrange, chan],
function (err, rows) {
callback(err, err ? false : rows.length > 0);
});
},
/**
* Check if a username is banned
*/
isNameBanned: function (chan, name, callback) {
if (typeof callback !== "function") {
return;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("SELECT * FROM `channel_bans` WHERE name=? AND channel=?", [name, chan],
function (err, rows) {
callback(err, err ? false : rows.length > 0);
});
},
/**
* Lists all bans
*/
listBans: function (chan, callback) {
if (typeof callback !== "function") {
return;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("SELECT * FROM `channel_bans` WHERE channel=?", [chan], callback);
},
/**
* Removes a ban from the banlist
*/
unbanId: function (chan, id, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("DELETE FROM `channel_bans` WHERE id=? AND channel=?",
[id, chan], callback);
},
/**
* Removes all bans from a channel
*/
deleteBans: function (chan, id, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
if (!valid(chan)) {
callback("Invalid channel name", null);
return;
}
db.query("DELETE FROM `channel_bans` WHERE channel=?", [chan], callback);
}
};

141
src/database/tables.js Normal file
View file

@ -0,0 +1,141 @@
const TBL_USERS = "" +
"CREATE TABLE IF NOT EXISTS `users` (" +
"`id` INT NOT NULL AUTO_INCREMENT," +
"`name` VARCHAR(20) NOT NULL," +
"`password` VARCHAR(64) NOT NULL," +
"`global_rank` INT NOT NULL," +
"`email` VARCHAR(255) NOT NULL," +
"`profile` TEXT CHARACTER SET utf8mb4 NOT NULL," +
"`ip` VARCHAR(39) NOT NULL," +
"`time` BIGINT NOT NULL," +
"PRIMARY KEY(`id`)," +
"UNIQUE(`name`)) " +
"CHARACTER SET utf8";
const TBL_CHANNELS = "" +
"CREATE TABLE IF NOT EXISTS `channels` (" +
"`id` INT NOT NULL AUTO_INCREMENT," +
"`name` VARCHAR(30) NOT NULL," +
"`owner` VARCHAR(20) NOT NULL," +
"`time` BIGINT NOT NULL," +
"PRIMARY KEY (`id`), UNIQUE(`name`), INDEX(`owner`))" +
"CHARACTER SET utf8";
const TBL_GLOBAL_BANS = "" +
"CREATE TABLE IF NOT EXISTS `global_bans` (" +
"`ip` VARCHAR(39) NOT NULL," +
"`reason` VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL," +
"PRIMARY KEY (`ip`)) " +
"CHARACTER SET utf8";
const TBL_PASSWORD_RESET = "" +
"CREATE TABLE IF NOT EXISTS `password_reset` (" +
"`ip` VARCHAR(39) NOT NULL," +
"`name` VARCHAR(20) NOT NULL," +
"`hash` VARCHAR(64) NOT NULL," +
"`email` VARCHAR(255) NOT NULL," +
"`expire` BIGINT NOT NULL," +
"PRIMARY KEY (`name`))" +
"CHARACTER SET utf8";
const TBL_USER_PLAYLISTS = "" +
"CREATE TABLE IF NOT EXISTS `user_playlists` (" +
"`user` VARCHAR(20) NOT NULL," +
"`name` VARCHAR(255) NOT NULL," +
"`contents` MEDIUMTEXT NOT NULL," +
"`count` INT NOT NULL," +
"`duration` INT NOT NULL," +
"PRIMARY KEY (`user`, `name`))" +
"CHARACTER SET utf8";
const TBL_ALIASES = "" +
"CREATE TABLE IF NOT EXISTS `aliases` (" +
"`visit_id` INT NOT NULL AUTO_INCREMENT," +
"`ip` VARCHAR(39) NOT NULL," +
"`name` VARCHAR(20) NOT NULL," +
"`time` BIGINT NOT NULL," +
"PRIMARY KEY (`visit_id`), INDEX (`ip`)" +
")";
const TBL_STATS = "" +
"CREATE TABLE IF NOT EXISTS `stats` (" +
"`time` BIGINT NOT NULL," +
"`usercount` INT NOT NULL," +
"`chancount` INT NOT NULL," +
"`mem` INT NOT NULL," +
"PRIMARY KEY (`time`))" +
"CHARACTER SET utf8";
const TBL_META = "" +
"CREATE TABLE IF NOT EXISTS `meta` (" +
"`key` VARCHAR(255) NOT NULL," +
"`value` TEXT NOT NULL," +
"PRIMARY KEY (`key`))" +
"CHARACTER SET utf8";
const TBL_LIBRARIES = "" +
"CREATE TABLE IF NOT EXISTS `channel_libraries` (" +
"`id` VARCHAR(255) NOT NULL," +
"`title` VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL," +
"`seconds` INT NOT NULL," +
"`type` VARCHAR(2) NOT NULL," +
"`meta` TEXT NOT NULL," +
"`channel` VARCHAR(30) NOT NULL," +
"PRIMARY KEY(`id`, `channel`), INDEX(`channel`, `title`)" +
") CHARACTER SET utf8";
const TBL_RANKS = "" +
"CREATE TABLE IF NOT EXISTS `channel_ranks` (" +
"`name` VARCHAR(20) NOT NULL," +
"`rank` INT NOT NULL," +
"`channel` VARCHAR(30) NOT NULL," +
"PRIMARY KEY(`name`, `channel`)" +
") CHARACTER SET utf8";
const TBL_BANS = "" +
"CREATE TABLE IF NOT EXISTS `channel_bans` (" +
"`id` INT NOT NULL AUTO_INCREMENT," +
"`ip` VARCHAR(39) NOT NULL," +
"`name` VARCHAR(20) NOT NULL," +
"`bannedby` VARCHAR(20) NOT NULL," +
"`reason` VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL," +
"`channel` VARCHAR(30) NOT NULL," +
"PRIMARY KEY (`id`, `channel`), UNIQUE (`name`, `ip`, `channel`), " +
"INDEX (`ip`, `channel`), INDEX (`name`, `channel`)" +
") CHARACTER SET utf8";
module.exports.init = function (queryfn, cb) {
var tables = {
users: TBL_USERS,
channels: TBL_CHANNELS,
channel_libraries: TBL_LIBRARIES,
channel_ranks: TBL_RANKS,
channel_bans: TBL_BANS,
global_bans: TBL_GLOBAL_BANS,
password_reset: TBL_PASSWORD_RESET,
user_playlists: TBL_USER_PLAYLISTS,
aliases: TBL_ALIASES,
stats: TBL_STATS,
meta: TBL_META
};
var AsyncQueue = require("../asyncqueue");
var aq = new AsyncQueue();
var hasError = false;
Object.keys(tables).forEach(function (tbl) {
aq.queue(function (lock) {
queryfn(tables[tbl], function (err) {
if (err) {
console.log(err);
hasError = true;
}
lock.release();
});
});
});
aq.queue(function (lock) {
lock.release();
cb(hasError);
});
};

332
src/database/update.js Normal file
View file

@ -0,0 +1,332 @@
var db = require("../database");
var Logger = require("../logger");
var Q = require("q");
const DB_VERSION = 7;
var hasUpdates = [];
module.exports.checkVersion = function () {
db.query("SELECT `key`,`value` FROM `meta` WHERE `key`=?", ["db_version"], function (err, rows) {
if (err) {
return;
}
if (rows.length === 0) {
Logger.errlog.log("[Warning] db_version key missing from database. Setting " +
"db_version=" + DB_VERSION);
db.query("INSERT INTO `meta` (`key`, `value`) VALUES ('db_version', ?)",
[DB_VERSION],
function (err) {
});
} else {
var v = parseInt(rows[0].value);
if (v >= DB_VERSION) {
return;
}
var next = function () {
hasUpdates.push(v);
Logger.syslog.log("Updated database to version " + v);
if (v < DB_VERSION) {
update(v++, next);
} else {
db.query("UPDATE `meta` SET `value`=? WHERE `key`='db_version'",
[DB_VERSION]);
}
};
update(v++, next);
}
});
};
function update(version, cb) {
if (version < 3 && hasUpdates.indexOf(2) < 0) {
addMetaColumnToLibraries(cb);
} else if (version < 4) {
Q.allSettled([
Q.nfcall(mergeChannelLibraries),
Q.nfcall(mergeChannelRanks),
Q.nfcall(mergeChannelBans)
]).done(function () {
Logger.syslog.log("Merged channel tables. Please verify that everything " +
"is working correctly, and then type '/delete_old_tables'" +
" into the CyTube process to remove the unused tables.");
cb();
})
} else if (version < 5) {
fixUtf8mb4(cb);
} else if (version < 6) {
fixCustomEmbeds(cb);
} else if (version < 7) {
fixCustomEmbedsInUserPlaylists(cb);
}
}
function addMetaColumnToLibraries(cb) {
Logger.syslog.log("[database] db version indicates channel libraries don't have " +
"meta column. Updating...");
Q.nfcall(db.query, "SHOW TABLES")
.then(function (rows) {
rows = rows.map(function (r) {
return r[Object.keys(r)[0]];
}).filter(function (r) {
return r.match(/_library$/);
});
var queue = [];
rows.forEach(function (table) {
queue.push(Q.nfcall(db.query, "ALTER TABLE `" + table + "` ADD meta TEXT")
.then(function () {
Logger.syslog.log("Added meta column to " + table);
})
);
});
return Q.all(queue);
}).catch(function (err) {
Logger.errlog.log("Adding meta column to library tables failed: " + err);
}).done(cb);
}
function mergeChannelLibraries(cb) {
Q.nfcall(db.query, "SHOW TABLES")
.then(function (rows) {
rows = rows.map(function (r) {
return r[Object.keys(r)[0]];
}).filter(function (r) {
return r.match(/chan_(.*)?_library$/);
});
var queue = [];
rows.forEach(function (table) {
var name = table.match(/chan_(.*?)_library$/)[1];
queue.push(Q.nfcall(db.query,
"INSERT INTO `channel_libraries` SELECT id, title, seconds, type, meta, ?" +
" AS channel FROM `" + table + "`", [name])
.then(function () {
Logger.syslog.log("Copied " + table + " to channel_libraries");
}).catch(function (err) {
Logger.errlog.log("Copying " + table + " to channel_libraries failed: " +
err);
if (err.stack) {
Logger.errlog.log(err.stack);
}
})
);
});
return Q.all(queue);
}).catch(function (err) {
Logger.errlog.log("Copying libraries to channel_libraries failed: " + err);
if (err.stack) {
Logger.errlog.log(err.stack);
}
}).done(function () { cb(null); });
}
function mergeChannelRanks(cb) {
Q.nfcall(db.query, "SHOW TABLES")
.then(function (rows) {
rows = rows.map(function (r) {
return r[Object.keys(r)[0]];
}).filter(function (r) {
return r.match(/chan_(.*?)_ranks$/);
});
var queue = [];
rows.forEach(function (table) {
var name = table.match(/chan_(.*?)_ranks$/)[1];
queue.push(Q.nfcall(db.query,
"INSERT INTO `channel_ranks` SELECT name, rank, ?" +
" AS channel FROM `" + table + "`", [name])
.then(function () {
Logger.syslog.log("Copied " + table + " to channel_ranks");
}).catch(function (err) {
Logger.errlog.log("Copying " + table + " to channel_ranks failed: " +
err);
if (err.stack) {
Logger.errlog.log(err.stack);
}
})
);
});
return Q.all(queue);
}).catch(function (err) {
Logger.errlog.log("Copying ranks to channel_ranks failed: " + err);
if (err.stack) {
Logger.errlog.log(err.stack);
}
}).done(function () { cb(null); });
}
function mergeChannelBans(cb) {
Q.nfcall(db.query, "SHOW TABLES")
.then(function (rows) {
rows = rows.map(function (r) {
return r[Object.keys(r)[0]];
}).filter(function (r) {
return r.match(/chan_(.*?)_bans$/);
});
var queue = [];
rows.forEach(function (table) {
var name = table.match(/chan_(.*?)_bans$/)[1];
queue.push(Q.nfcall(db.query,
"INSERT INTO `channel_bans` SELECT id, ip, name, bannedby, reason, ?" +
" AS channel FROM `" + table + "`", [name])
.then(function () {
Logger.syslog.log("Copied " + table + " to channel_bans");
}).catch(function (err) {
Logger.errlog.log("Copying " + table + " to channel_bans failed: " +
err);
if (err.stack) {
Logger.errlog.log(err.stack);
}
})
);
});
return Q.all(queue);
}).catch(function (err) {
Logger.errlog.log("Copying ranks to channel_bans failed: " + err);
if (err.stack) {
Logger.errlog.log(err.stack);
}
}).done(function () { cb(null); });
}
module.exports.deleteOldChannelTables = function (cb) {
Q.nfcall(db.query, "SHOW TABLES")
.then(function (rows) {
rows = rows.map(function (r) {
return r[Object.keys(r)[0]];
}).filter(function (r) {
return r.match(/chan_(.*?)_(library|ranks|bans)$/);
});
var queue = [];
rows.forEach(function (table) {
queue.push(Q.nfcall(db.query, "DROP TABLE `" + table + "`")
.then(function () {
Logger.syslog.log("Deleted " + table);
}).catch(function (err) {
Logger.errlog.log("Deleting " + table + " failed: " + err);
if (err.stack) {
Logger.errlog.log(err.stack);
}
})
);
});
return Q.all(queue);
}).catch(function (err) {
Logger.errlog.log("Deleting old tables failed: " + err);
if (err.stack) {
Logger.errlog.log(err.stack);
}
}).done(cb);
};
function fixUtf8mb4(cb) {
var queries = [
"ALTER TABLE `users` MODIFY `profile` TEXT CHARACTER SET utf8mb4 NOT NULL",
"ALTER TABLE `global_bans` MODIFY `reason` VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL",
"ALTER TABLE `channel_libraries` MODIFY `title` VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL",
"ALTER TABLE `channel_bans` MODIFY `reason` VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL"
];
Q.allSettled(queries.map(function (query) {
return Q.nfcall(db.query, query);
})).then(function () {
Logger.syslog.log("Fixed utf8mb4");
cb();
}).catch(function (e) {
Logger.errlog.log("Failed to fix utf8mb4: " + e);
});
};
function fixCustomEmbeds(cb) {
var CustomEmbedFilter = require("../customembed").filter;
Q.nfcall(db.query, "SELECT * FROM `channel_libraries` WHERE type='cu'")
.then(function (rows) {
var all = [];
rows.forEach(function (row) {
if (row.id.indexOf("cu:") === 0) return;
all.push(Q.nfcall(db.query, "DELETE FROM `channel_libraries` WHERE `id`=? AND `channel`=?",
[row.id, row.channel]));
try {
var media = CustomEmbedFilter(row.id);
all.push(Q.nfcall(db.channels.addToLibrary, row.channel, media));
} catch(e) {
console.error("WARNING: Unable to convert " + row.id);
}
});
Q.allSettled(all).then(function () {
Logger.syslog.log("Converted custom embeds.");
cb();
});
});
}
function fixCustomEmbedsInUserPlaylists(cb) {
var CustomEmbedFilter = require("../customembed").filter;
Q.nfcall(db.query, "SELECT * FROM `user_playlists` WHERE `contents` LIKE '%\"type\":\"cu\"%'")
.then(function (rows) {
var all = [];
rows.forEach(function (row) {
var data;
try {
data = JSON.parse(row.contents);
} catch (e) {
return;
}
var updated = [];
var item;
while ((item = data.shift()) !== undefined) {
if (item.type !== "cu") {
updated.push(item);
continue;
}
if (/^cu:/.test(item.id)) {
updated.push(item);
continue;
}
var media;
try {
media = CustomEmbedFilter(item.id);
} catch (e) {
Logger.syslog.log("WARNING: Unable to convert " + item.id);
continue;
}
updated.push({
id: media.id,
title: item.title,
seconds: media.seconds,
type: media.type,
meta: {
embed: media.meta.embed
}
});
all.push(Q.nfcall(db.query, "UPDATE `user_playlists` SET `contents`=?, `count`=? WHERE `user`=? AND `name`=?",
[JSON.stringify(updated), updated.length, row.user, row.name]));
}
});
Q.allSettled(all).then(function () {
Logger.syslog.log('Fixed custom embeds in user_playlists');
cb();
});
}).catch(function (err) {
Logger.errlog.log(err.stack);
});
}