Move server files to lib/ to clean up root directory
This commit is contained in:
parent
7b54b2fcc0
commit
7840fa35e8
26 changed files with 83 additions and 51 deletions
BIN
lib/.logger.js.swp
Normal file
BIN
lib/.logger.js.swp
Normal file
Binary file not shown.
186
lib/acp.js
Normal file
186
lib/acp.js
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var Logger = require("./logger");
|
||||
|
||||
module.exports = function (Server) {
|
||||
var db = Server.db;
|
||||
var ActionLog = Server.actionlog;
|
||||
return {
|
||||
init: function(user) {
|
||||
ActionLog.record(user.ip, user.name, "acp-init");
|
||||
user.socket.on("acp-announce", function(data) {
|
||||
ActionLog.record(user.ip, user.name, "acp-announce", data);
|
||||
Server.announcement = data;
|
||||
Server.io.sockets.emit("announcement", data);
|
||||
});
|
||||
|
||||
user.socket.on("acp-announce-clear", function() {
|
||||
ActionLog.record(user.ip, user.name, "acp-announce-clear");
|
||||
Server.announcement = null;
|
||||
});
|
||||
|
||||
user.socket.on("acp-global-ban", function(data) {
|
||||
ActionLog.record(user.ip, user.name, "acp-global-ban", data.ip);
|
||||
db.setGlobalIPBan(data.ip, data.note, function (err, res) {
|
||||
db.listGlobalIPBans(function (err, res) {
|
||||
res = res || [];
|
||||
user.socket.emit("acp-global-banlist", res);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
user.socket.on("acp-global-unban", function(ip) {
|
||||
ActionLog.record(user.ip, user.name, "acp-global-unban", ip);
|
||||
db.clearGlobalIPBan(ip, function (err, res) {
|
||||
db.listGlobalIPBans(function (err, res) {
|
||||
res = res || [];
|
||||
user.socket.emit("acp-global-banlist", res);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
db.listGlobalIPBans(function (err, res) {
|
||||
res = res || [];
|
||||
user.socket.emit("acp-global-banlist", res);
|
||||
});
|
||||
|
||||
user.socket.on("acp-lookup-user", function(name) {
|
||||
db.searchUser(name, function (err, res) {
|
||||
res = res || [];
|
||||
user.socket.emit("acp-userdata", res);
|
||||
});
|
||||
});
|
||||
|
||||
user.socket.on("acp-lookup-channel", function (data) {
|
||||
db.searchChannel(data.field, data.value, function (e, res) {
|
||||
res = res || [];
|
||||
user.socket.emit("acp-channeldata", res);
|
||||
});
|
||||
});
|
||||
|
||||
user.socket.on("acp-reset-password", function(data) {
|
||||
db.getGlobalRank(data.name, function (err, rank) {
|
||||
if(err || rank >= user.global_rank)
|
||||
return;
|
||||
|
||||
db.genPasswordReset(user.ip, data.name, data.email,
|
||||
function (err, hash) {
|
||||
var pkt = {
|
||||
success: !err
|
||||
};
|
||||
|
||||
if(err) {
|
||||
pkt.error = err;
|
||||
} else {
|
||||
pkt.hash = hash;
|
||||
}
|
||||
|
||||
user.socket.emit("acp-reset-password", pkt);
|
||||
ActionLog.record(user.ip, user.name,
|
||||
"acp-reset-password", data.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
user.socket.on("acp-set-rank", function(data) {
|
||||
if(data.rank < 1 || data.rank >= user.global_rank)
|
||||
return;
|
||||
|
||||
db.getGlobalRank(data.name, function (err, rank) {
|
||||
if(err || rank >= user.global_rank)
|
||||
return;
|
||||
|
||||
db.setGlobalRank(data.name, data.rank,
|
||||
function (err, res) {
|
||||
ActionLog.record(user.ip, user.name, "acp-set-rank",
|
||||
data);
|
||||
if(!err)
|
||||
user.socket.emit("acp-set-rank", data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
user.socket.on("acp-list-loaded", function() {
|
||||
var chans = [];
|
||||
var all = Server.channels;
|
||||
for(var c in all) {
|
||||
var chan = all[c];
|
||||
|
||||
chans.push({
|
||||
name: chan.name,
|
||||
title: chan.opts.pagetitle,
|
||||
usercount: chan.users.length,
|
||||
mediatitle: chan.playlist.current ? chan.playlist.current.media.title : "-",
|
||||
is_public: chan.opts.show_public,
|
||||
registered: chan.registered
|
||||
});
|
||||
}
|
||||
|
||||
user.socket.emit("acp-list-loaded", chans);
|
||||
});
|
||||
|
||||
user.socket.on("acp-channel-unload", function(data) {
|
||||
if(Server.channelLoaded(data.name)) {
|
||||
var c = Server.getChannel(data.name);
|
||||
if(!c)
|
||||
return;
|
||||
ActionLog.record(user.ip, user.name, "acp-channel-unload");
|
||||
c.initialized = data.save;
|
||||
c.users.forEach(function(u) {
|
||||
c.kick(u, "Channel shutting down");
|
||||
});
|
||||
|
||||
// At this point c should be unloaded
|
||||
// if it's still loaded, kill it
|
||||
if(Server.channelLoaded(data.name))
|
||||
Server.unloadChannel(Server.getChannel(data.name));
|
||||
}
|
||||
});
|
||||
|
||||
user.socket.on("acp-actionlog-list", function () {
|
||||
ActionLog.listActionTypes(function (err, types) {
|
||||
if(!err)
|
||||
user.socket.emit("acp-actionlog-list", types);
|
||||
});
|
||||
});
|
||||
|
||||
user.socket.on("acp-actionlog-clear", function(data) {
|
||||
ActionLog.clear(data);
|
||||
ActionLog.record(user.ip, user.name, "acp-actionlog-clear", data);
|
||||
});
|
||||
|
||||
user.socket.on("acp-actionlog-clear-one", function(data) {
|
||||
ActionLog.clearOne(data);
|
||||
ActionLog.record(user.ip, user.name, "acp-actionlog-clear-one", data);
|
||||
});
|
||||
|
||||
user.socket.on("acp-view-stats", function () {
|
||||
db.listStats(function (err, res) {
|
||||
if(!err)
|
||||
user.socket.emit("acp-view-stats", res);
|
||||
});
|
||||
});
|
||||
|
||||
user.socket.on("acp-view-connstats", function () {
|
||||
var http = Server.stats.readAverages("http");
|
||||
var sio = Server.stats.readAverages("socketio");
|
||||
var api = Server.stats.readAverages("api");
|
||||
|
||||
user.socket.emit("acp-view-connstats", {
|
||||
http: http,
|
||||
sio: sio,
|
||||
api: api
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
58
lib/actionlog.js
Normal file
58
lib/actionlog.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var Logger = require("./logger");
|
||||
|
||||
module.exports = function (Server) {
|
||||
var db = Server.db;
|
||||
return {
|
||||
record: function (ip, name, action, args) {
|
||||
if(!args)
|
||||
args = "";
|
||||
else {
|
||||
try {
|
||||
args = JSON.stringify(args);
|
||||
} catch(e) {
|
||||
args = "";
|
||||
}
|
||||
}
|
||||
|
||||
db.recordAction(ip, name, action, args);
|
||||
},
|
||||
|
||||
clear: function (actions) {
|
||||
db.clearActions(actions);
|
||||
},
|
||||
|
||||
clearOne: function (item) {
|
||||
db.clearSingleAction(item);
|
||||
},
|
||||
|
||||
throttleRegistrations: function (ip, callback) {
|
||||
db.recentRegistrationCount(ip, function (err, count) {
|
||||
if(err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, count > 4);
|
||||
});
|
||||
},
|
||||
|
||||
listActionTypes: function (callback) {
|
||||
db.listActionTypes(callback);
|
||||
},
|
||||
|
||||
listActions: function (types, callback) {
|
||||
db.listActions(types, callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
709
lib/api.js
Normal file
709
lib/api.js
Normal file
|
|
@ -0,0 +1,709 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var Logger = require("./logger");
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var $util = require("./utilities");
|
||||
|
||||
module.exports = function (Server) {
|
||||
var ActionLog = Server.actionlog;
|
||||
function getIP(req) {
|
||||
var raw = req.connection.remoteAddress;
|
||||
var forward = req.header("x-forwarded-for");
|
||||
if(Server.cfg["trust-x-forward"] && forward) {
|
||||
var ip = forward.split(",")[0];
|
||||
Logger.syslog.log("REVPROXY " + raw + " => " + ip);
|
||||
return ip;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function getChannelData(channel) {
|
||||
var data = {
|
||||
name: channel.name,
|
||||
loaded: true
|
||||
};
|
||||
|
||||
data.pagetitle = channel.opts.pagetitle;
|
||||
data.media = channel.playlist.current ?
|
||||
channel.playlist.current.media.pack() :
|
||||
{};
|
||||
data.usercount = channel.users.length;
|
||||
data.afkcount = channel.afkers.length;
|
||||
data.users = [];
|
||||
for(var i in channel.users)
|
||||
if(channel.users[i].name !== "")
|
||||
data.users.push(channel.users[i].name);
|
||||
|
||||
data.chat = [];
|
||||
for(var i in channel.chatbuffer)
|
||||
data.chat.push(channel.chatbuffer[i]);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
var app = Server.app;
|
||||
var db = Server.db;
|
||||
|
||||
/* <https://en.wikipedia.org/wiki/Hyper_Text_Coffee_Pot_Control_Protocol> */
|
||||
app.get("/api/coffee", function (req, res) {
|
||||
Server.stats.record("api", "/api/coffee");
|
||||
res.send(418); // 418 I'm a teapot
|
||||
});
|
||||
|
||||
/* REGION channels */
|
||||
|
||||
/* data about a specific channel */
|
||||
app.get("/api/channels/:channel", function (req, res) {
|
||||
Server.stats.record("api", "/api/channels/:channel");
|
||||
var name = req.params.channel;
|
||||
if(!$util.isValidChannelName(name)) {
|
||||
res.send(404);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {
|
||||
name: name,
|
||||
loaded: false
|
||||
};
|
||||
|
||||
if(Server.channelLoaded(name))
|
||||
data = getChannelData(Server.getChannel(name));
|
||||
|
||||
res.type("application/json");
|
||||
res.jsonp(data);
|
||||
});
|
||||
|
||||
/* data about all channels (filter= public or all) */
|
||||
app.get("/api/allchannels/:filter", function (req, res) {
|
||||
Server.stats.record("api", "/api/allchannels/:filter");
|
||||
var filter = req.params.filter;
|
||||
if(filter !== "public" && filter !== "all") {
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
var query = req.query;
|
||||
|
||||
// Listing non-public channels requires authenticating as an admin
|
||||
if(filter !== "public") {
|
||||
var name = query.name || "";
|
||||
var session = query.session || "";
|
||||
db.userLoginSession(name, session, function (err, row) {
|
||||
if(err) {
|
||||
if(err !== "Invalid session" &&
|
||||
err !== "Session expired") {
|
||||
res.send(500);
|
||||
} else {
|
||||
res.send(403);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(row.global_rank < 255) {
|
||||
res.send(403);
|
||||
return;
|
||||
}
|
||||
|
||||
var channels = [];
|
||||
for(var key in Server.channels) {
|
||||
var channel = Server.channels[key];
|
||||
channels.push(getChannelData(channel));
|
||||
}
|
||||
|
||||
res.type("application/jsonp");
|
||||
res.jsonp(channels);
|
||||
});
|
||||
}
|
||||
|
||||
// If we get here, the filter is public channels
|
||||
|
||||
var channels = [];
|
||||
for(var key in Server.channels) {
|
||||
var channel = Server.channels[key];
|
||||
if(channel.opts.show_public)
|
||||
channels.push(getChannelData(channel));
|
||||
}
|
||||
|
||||
res.type("application/jsonp");
|
||||
res.jsonp(channels);
|
||||
});
|
||||
|
||||
/* ENDREGION channels */
|
||||
|
||||
/* REGION authentication, account management */
|
||||
|
||||
/* login */
|
||||
app.post("/api/login", function (req, res) {
|
||||
Server.stats.record("api", "/api/login");
|
||||
res.type("application/jsonp");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
var name = req.body.name;
|
||||
var pw = req.body.pw;
|
||||
var session = req.body.session;
|
||||
|
||||
// for some reason CyTube previously allowed guest logins
|
||||
// over the API...wat
|
||||
if(!pw && !session) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "You must provide a password"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.userLogin(name, pw, session, function (err, row) {
|
||||
if(err) {
|
||||
if(err !== "Session expired")
|
||||
ActionLog.record(getIP(req), name, "login-failure");
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Only record login-success for admins
|
||||
if(row.global_rank >= 255)
|
||||
ActionLog.record(getIP(req), name, "login-success");
|
||||
|
||||
res.jsonp({
|
||||
success: true,
|
||||
name: name,
|
||||
session: row.session_hash
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* register an account */
|
||||
app.post("/api/register", function (req, res) {
|
||||
Server.stats.record("api", "/api/register");
|
||||
res.type("application/jsonp");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
var name = req.body.name;
|
||||
var pw = req.body.pw;
|
||||
var ip = getIP(req);
|
||||
|
||||
// Limit registrations per IP within a certain time period
|
||||
ActionLog.throttleRegistrations(ip, function (err, toomany) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(toomany) {
|
||||
ActionLog.record(ip, name, "register-failure",
|
||||
"Too many recent registrations");
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Your IP address has registered too many " +
|
||||
"accounts in the past 48 hours. Please wait " +
|
||||
"a while before registering another."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!pw) {
|
||||
// costanza.jpg
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "You must provide a password"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if(!$util.isValidUserName(name)) {
|
||||
ActionLog.record(ip, name, "register-failure",
|
||||
"Invalid name");
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid username. Valid usernames must be " +
|
||||
"1-20 characters long and consist only of " +
|
||||
"alphanumeric characters and underscores (_)"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// db.registerUser checks if the name is taken already
|
||||
db.registerUser(name, pw, function (err, session) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ActionLog.record(ip, name, "register-success");
|
||||
res.jsonp({
|
||||
success: true,
|
||||
session: session
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* password change */
|
||||
app.post("/api/account/passwordchange", function (req, res) {
|
||||
Server.stats.record("api", "/api/account/passwordchange");
|
||||
res.type("application/jsonp");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
var name = req.body.name;
|
||||
var oldpw = req.body.oldpw;
|
||||
var newpw = req.body.newpw;
|
||||
|
||||
if(!oldpw || !newpw) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Password cannot be empty"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.userLoginPassword(name, oldpw, function (err, row) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.setUserPassword(name, newpw, function (err, row) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ActionLog.record(getIP(req), name, "password-change");
|
||||
res.jsonp({
|
||||
success: true
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* password reset */
|
||||
app.post("/api/account/passwordreset", function (req, res) {
|
||||
Server.stats.record("api", "/api/account/passwordreset");
|
||||
res.type("application/jsonp");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
var name = req.body.name;
|
||||
var email = req.body.email;
|
||||
var ip = getIP(req);
|
||||
var hash = false;
|
||||
|
||||
db.genPasswordReset(ip, name, email, function (err, hash) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
ActionLog.record(ip, name, "password-reset-generate", email);
|
||||
if(!Server.cfg["enable-mail"]) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "This server does not have email recovery " +
|
||||
"enabled. Contact an administrator for " +
|
||||
"assistance."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!email) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "You don't have a recovery email address set. "+
|
||||
"Contact an administrator for assistance."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = "A password reset request was issued for your " +
|
||||
"account '"+ name + "' on " + Server.cfg["domain"] +
|
||||
". This request is valid for 24 hours. If you did "+
|
||||
"not initiate this, there is no need to take action."+
|
||||
" To reset your password, copy and paste the " +
|
||||
"following link into your browser: " +
|
||||
Server.cfg["domain"] + "/reset.html?"+hash;
|
||||
|
||||
var mail = {
|
||||
from: "CyTube Services <" + Server.cfg["mail-from"] + ">",
|
||||
to: email,
|
||||
subject: "Password reset request",
|
||||
text: msg
|
||||
};
|
||||
|
||||
Server.cfg["nodemailer"].sendMail(mail, function (err, response) {
|
||||
if(err) {
|
||||
Logger.errlog.log("mail fail: " + err);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Email send failed. Contact an administrator "+
|
||||
"if this persists"
|
||||
});
|
||||
} else {
|
||||
res.jsonp({
|
||||
success: true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* password recovery */
|
||||
app.get("/api/account/passwordrecover", function (req, res) {
|
||||
Server.stats.record("api", "/api/account/passwordrecover");
|
||||
res.type("application/jsonp");
|
||||
var hash = req.query.hash;
|
||||
var ip = getIP(req);
|
||||
|
||||
db.recoverUserPassword(hash, function (err, auth) {
|
||||
if(err) {
|
||||
ActionLog.record(ip, "", "password-recover-failure", hash);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
ActionLog.record(ip, info[0], "password-recover-success");
|
||||
res.jsonp({
|
||||
success: true,
|
||||
name: auth.name,
|
||||
pw: auth.pw
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* profile retrieval */
|
||||
app.get("/api/users/:user/profile", function (req, res) {
|
||||
Server.stats.record("api", "/api/users/:user/profile");
|
||||
res.type("application/jsonp");
|
||||
var name = req.params.user;
|
||||
|
||||
db.getUserProfile(name, function (err, profile) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.jsonp({
|
||||
success: true,
|
||||
profile_image: profile.profile_image,
|
||||
profile_text: profile.profile_text
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* profile change */
|
||||
app.post("/api/account/profile", function (req, res) {
|
||||
Server.stats.record("api", "/api/account/profile");
|
||||
res.type("application/jsonp");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
var name = req.body.name;
|
||||
var session = req.body.session;
|
||||
var img = req.body.profile_image;
|
||||
var text = req.body.profile_text;
|
||||
|
||||
db.userLoginSession(name, session, function (err, row) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.setUserProfile(name, { image: img, text: text },
|
||||
function (err, dbres) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.jsonp({ success: true });
|
||||
name = name.toLowerCase();
|
||||
for(var i in Server.channels) {
|
||||
var chan = Server.channels[i];
|
||||
for(var j in chan.users) {
|
||||
var user = chan.users[j];
|
||||
if(user.name.toLowerCase() == name) {
|
||||
user.profile = {
|
||||
image: img,
|
||||
text: text
|
||||
};
|
||||
chan.broadcastUserUpdate(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* set email */
|
||||
app.post("/api/account/email", function (req, res) {
|
||||
Server.stats.record("api", "/api/account/email");
|
||||
res.type("application/jsonp");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
var name = req.body.name;
|
||||
var pw = req.body.pw;
|
||||
var email = req.body.email;
|
||||
|
||||
if(!email.match(/^[\w_\.]+@[\w_\.]+[a-z]+$/i)) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid email address"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(email.match(/.*@(localhost|127\.0\.0\.1)/i)) {
|
||||
res.jsonp({ success: false,
|
||||
error: "Nice try, but no"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.userLoginPassword(name, pw, function (err, row) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.setUserEmail(name, email, function (err, dbres) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ActionLog.record(getIP(req), name, "email-update", email);
|
||||
res.jsonp({
|
||||
success: true,
|
||||
session: row.session_hash
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* my channels */
|
||||
app.get("/api/account/mychannels", function (req, res) {
|
||||
Server.stats.record("/api/account/mychannels");
|
||||
res.type("application/jsonp");
|
||||
var name = req.query.name;
|
||||
var session = req.query.session;
|
||||
|
||||
db.userLoginSession(name, session, function (err, row) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.listUserChannels(name, function (err, dbres) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
channels: []
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.jsonp({
|
||||
success: true,
|
||||
channels: dbres
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/* END REGION */
|
||||
|
||||
/* REGION log reading */
|
||||
|
||||
/* action log */
|
||||
app.get("/api/logging/actionlog", function (req, res) {
|
||||
Server.stats.record("api", "/api/logging/actionlog");
|
||||
res.type("application/jsonp");
|
||||
var name = req.query.name;
|
||||
var session = req.query.session;
|
||||
var types = req.query.actions;
|
||||
|
||||
db.userLoginSession(name, session, function (err, row) {
|
||||
if(err) {
|
||||
if(err !== "Invalid session" &&
|
||||
err !== "Session expired") {
|
||||
res.send(500);
|
||||
} else {
|
||||
res.send(403);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(row.global_rank < 255) {
|
||||
res.send(403);
|
||||
return;
|
||||
}
|
||||
|
||||
types = types.split(",");
|
||||
ActionLog.listActions(types, function (err, actions) {
|
||||
if(err)
|
||||
actions = [];
|
||||
|
||||
res.jsonp(actions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* helper function to pipe the last N bytes of a file */
|
||||
function pipeLast(res, file, len) {
|
||||
fs.stat(file, function (err, data) {
|
||||
if(err) {
|
||||
res.send(500);
|
||||
return;
|
||||
}
|
||||
var start = data.size - len;
|
||||
if(start < 0)
|
||||
start = 0;
|
||||
var end = data.size - 1;
|
||||
if(end < 0)
|
||||
end = 0;
|
||||
fs.createReadStream(file, { start: start, end: end })
|
||||
.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
app.get("/api/logging/syslog", function (req, res) {
|
||||
Server.stats.record("api", "/api/logging/syslog");
|
||||
res.type("text/plain");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
var name = req.query.name;
|
||||
var session = req.query.session;
|
||||
|
||||
db.userLoginSession(name, session, function (err, row) {
|
||||
if(err) {
|
||||
if(err !== "Invalid session" &&
|
||||
err !== "Session expired") {
|
||||
res.send(500);
|
||||
} else {
|
||||
res.send(403);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(row.global_rank < 255) {
|
||||
res.send(403);
|
||||
return;
|
||||
}
|
||||
|
||||
pipeLast(res, path.join(__dirname, "../sys.log"), 1048576);
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/logging/errorlog", function (req, res) {
|
||||
Server.stats.record("api", "/api/logging/errorlog");
|
||||
res.type("text/plain");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
var name = req.query.name;
|
||||
var session = req.query.session;
|
||||
|
||||
db.userLoginSession(name, session, function (err, row) {
|
||||
if(err) {
|
||||
if(err !== "Invalid session" &&
|
||||
err !== "Session expired") {
|
||||
res.send(500);
|
||||
} else {
|
||||
res.send(403);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(row.global_rank < 255) {
|
||||
res.send(403);
|
||||
return;
|
||||
}
|
||||
|
||||
pipeLast(res, path.join(__dirname, "../error.log"), 1048576);
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/logging/channels/:channel", function (req, res) {
|
||||
Server.stats.record("api", "/api/logging/channels/:channel");
|
||||
res.type("text/plain");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
var name = req.query.name;
|
||||
var session = req.query.session;
|
||||
|
||||
db.userLoginSession(name, session, function (err, row) {
|
||||
if(err) {
|
||||
if(err !== "Invalid session" &&
|
||||
err !== "Session expired") {
|
||||
res.send(500);
|
||||
} else {
|
||||
res.send(403);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(row.global_rank < 255) {
|
||||
res.send(403);
|
||||
return;
|
||||
}
|
||||
|
||||
var chan = req.params.channel || "";
|
||||
if(!$util.isValidChannelName(chan)) {
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.exists(path.join(__dirname, "../chanlogs", chan + ".log"),
|
||||
function(exists) {
|
||||
if(exists) {
|
||||
pipeLast(res, path.join(__dirname, "../chanlogs",
|
||||
chan + ".log"), 1048576);
|
||||
} else {
|
||||
res.send(404);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
2145
lib/channel.js
Normal file
2145
lib/channel.js
Normal file
File diff suppressed because it is too large
Load diff
244
lib/chatcommand.js
Normal file
244
lib/chatcommand.js
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var Rank = require("./rank.js");
|
||||
var Poll = require("./poll.js").Poll;
|
||||
var Logger = require("./logger.js");
|
||||
|
||||
function handle(chan, user, msg, data) {
|
||||
if(msg.indexOf("/me ") == 0)
|
||||
chan.sendMessage(user.name, msg.substring(4), "action", data);
|
||||
else if(msg.indexOf("/sp ") == 0)
|
||||
chan.sendMessage(user.name, msg.substring(4), "spoiler", data);
|
||||
else if(msg.indexOf("/say ") == 0) {
|
||||
if(Rank.hasPermission(user, "shout") || chan.leader == user) {
|
||||
chan.sendMessage(user.name, msg.substring(5), "shout", data);
|
||||
}
|
||||
}
|
||||
else if(msg.indexOf("/afk") == 0) {
|
||||
user.setAFK(!user.meta.afk);
|
||||
}
|
||||
else if(msg.indexOf("/m ") == 0) {
|
||||
if(user.rank >= Rank.Moderator) {
|
||||
chan.chainMessage(user, msg.substring(3), {modflair: user.rank})
|
||||
}
|
||||
}
|
||||
else if(msg.indexOf("/a ") == 0) {
|
||||
if(user.rank >= Rank.Siteadmin) {
|
||||
var flair = {
|
||||
superadminflair: {
|
||||
labelclass: "label-important",
|
||||
icon: "icon-globe"
|
||||
}
|
||||
};
|
||||
var args = msg.substring(3).split(" ");
|
||||
var cargs = [];
|
||||
for(var i = 0; i < args.length; i++) {
|
||||
var a = args[i];
|
||||
if(a.indexOf("!icon-") == 0)
|
||||
flair.superadminflair.icon = a.substring(1);
|
||||
else if(a.indexOf("!label-") == 0)
|
||||
flair.superadminflair.labelclass = a.substring(1);
|
||||
else {
|
||||
cargs.push(a);
|
||||
}
|
||||
}
|
||||
chan.chainMessage(user, cargs.join(" "), flair);
|
||||
}
|
||||
}
|
||||
else if(msg.indexOf("/mute ") == 0) {
|
||||
handleMute(chan, user, msg.substring(6).split(" "));
|
||||
}
|
||||
else if(msg.indexOf("/unmute ") == 0) {
|
||||
handleUnmute(chan, user, msg.substring(8).split(" "));
|
||||
}
|
||||
else if(msg.indexOf("/kick ") == 0) {
|
||||
handleKick(chan, user, msg.substring(6).split(" "));
|
||||
}
|
||||
else if(msg.indexOf("/ban ") == 0) {
|
||||
handleBan(chan, user, msg.substring(5).split(" "));
|
||||
}
|
||||
else if(msg.indexOf("/ipban ") == 0) {
|
||||
handleIPBan(chan, user, msg.substring(7).split(" "));
|
||||
}
|
||||
else if(msg.indexOf("/unban ") == 0) {
|
||||
handleUnban(chan, user, msg.substring(7).split(" "));
|
||||
}
|
||||
else if(msg.indexOf("/poll ") == 0) {
|
||||
handlePoll(chan, user, msg.substring(6));
|
||||
}
|
||||
else if(msg.indexOf("/d") == 0 && msg.length > 2 &&
|
||||
msg[2].match(/[-0-9 ]/)) {
|
||||
if(msg[2] == "-") {
|
||||
if(msg.length == 3)
|
||||
return;
|
||||
if(!msg[3].match(/[0-9]/))
|
||||
return;
|
||||
}
|
||||
handleDrink(chan, user, msg.substring(2), data);
|
||||
}
|
||||
else if(msg.indexOf("/clear") == 0) {
|
||||
handleClear(chan, user);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMute(chan, user, args) {
|
||||
if(chan.hasPermission(user, "mute") && args.length > 0) {
|
||||
args[0] = args[0].toLowerCase();
|
||||
var person = false;
|
||||
for(var i = 0; i < chan.users.length; i++) {
|
||||
if(chan.users[i].name.toLowerCase() == args[0]) {
|
||||
person = chan.users[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(person) {
|
||||
if(person.rank >= user.rank) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "You don't have permission to mute that person."
|
||||
});
|
||||
return;
|
||||
}
|
||||
person.meta.icon = "icon-volume-off";
|
||||
person.muted = true;
|
||||
chan.broadcastUserUpdate(person);
|
||||
chan.logger.log("*** " + user.name + " muted " + args[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnmute(chan, user, args) {
|
||||
if(chan.hasPermission(user, "mute") && args.length > 0) {
|
||||
args[0] = args[0].toLowerCase();
|
||||
var person = false;
|
||||
for(var i = 0; i < chan.users.length; i++) {
|
||||
if(chan.users[i].name.toLowerCase() == args[0]) {
|
||||
person = chan.users[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(person) {
|
||||
if(person.rank >= user.rank) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "You don't have permission to unmute that person."
|
||||
});
|
||||
return;
|
||||
}
|
||||
person.meta.icon = false;
|
||||
person.muted = false;
|
||||
chan.broadcastUserUpdate(person);
|
||||
chan.logger.log("*** " + user.name + " unmuted " + args[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKick(chan, user, args) {
|
||||
if(chan.hasPermission(user, "kick") && args.length > 0) {
|
||||
args[0] = args[0].toLowerCase();
|
||||
if(args[0] == user.name.toLowerCase()) {
|
||||
user.socket.emit("costanza", {
|
||||
msg: "Kicking yourself?"
|
||||
});
|
||||
return;
|
||||
}
|
||||
var kickee;
|
||||
for(var i = 0; i < chan.users.length; i++) {
|
||||
if(chan.users[i].name.toLowerCase() == args[0]) {
|
||||
if(chan.users[i].rank >= user.rank) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "You don't have permission to kick " + args[0]
|
||||
});
|
||||
return;
|
||||
}
|
||||
kickee = chan.users[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(kickee) {
|
||||
chan.logger.log("*** " + user.name + " kicked " + args[0]);
|
||||
args[0] = "";
|
||||
var reason = args.join(" ");
|
||||
chan.kick(kickee, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleIPBan(chan, user, args) {
|
||||
chan.tryIPBan(user, args[0], args[1]);
|
||||
// Ban the name too for good measure
|
||||
chan.tryNameBan(user, args[0]);
|
||||
}
|
||||
|
||||
function handleBan(chan, user, args) {
|
||||
chan.tryNameBan(user, args[0]);
|
||||
}
|
||||
|
||||
function handleUnban(chan, user, args) {
|
||||
if(chan.hasPermission(user, "ban") && args.length > 0) {
|
||||
chan.logger.log("*** " + user.name + " unbanned " + args[0]);
|
||||
if(args[0].match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/)) {
|
||||
chan.unbanIP(user, args[0]);
|
||||
}
|
||||
else {
|
||||
chan.unbanName(user, args[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handlePoll(chan, user, msg) {
|
||||
if(chan.hasPermission(user, "pollctl")) {
|
||||
var args = msg.split(",");
|
||||
var title = args[0];
|
||||
args.splice(0, 1);
|
||||
var poll = new Poll(user.name, title, args);
|
||||
chan.poll = poll;
|
||||
chan.broadcastPoll();
|
||||
chan.logger.log("*** " + user.name + " Opened Poll: '" + poll.title + "'");
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrink(chan, user, msg, data) {
|
||||
if(!chan.hasPermission(user, "drink")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var count = msg.split(" ")[0];
|
||||
msg = msg.substring(count.length + 1);
|
||||
if(count == "")
|
||||
count = 1;
|
||||
else
|
||||
count = parseInt(count);
|
||||
|
||||
chan.drinks += count;
|
||||
chan.broadcastDrinks();
|
||||
if(count < 0 && msg.trim() == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
msg = msg + " drink!";
|
||||
if(count != 1)
|
||||
msg += " (x" + count + ")";
|
||||
chan.sendMessage(user.name, msg, "drink", data);
|
||||
}
|
||||
|
||||
function handleClear(chan, user) {
|
||||
if(user.rank < Rank.Moderator) {
|
||||
return;
|
||||
}
|
||||
|
||||
chan.chatbuffer = [];
|
||||
chan.sendAll("clearchat");
|
||||
}
|
||||
|
||||
exports.handle = handle;
|
||||
|
||||
98
lib/config.js
Normal file
98
lib/config.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var fs = require("fs");
|
||||
var Logger = require("./logger");
|
||||
var nodemailer = require("nodemailer");
|
||||
|
||||
var defaults = {
|
||||
"mysql-server" : "localhost",
|
||||
"mysql-db" : "cytube",
|
||||
"mysql-user" : "cytube",
|
||||
"mysql-pw" : "supersecretpass",
|
||||
"express-host" : "0.0.0.0",
|
||||
"asset-cache-ttl" : 0,
|
||||
"web-port" : 8080,
|
||||
"io-port" : 1337,
|
||||
"ip-connection-limit" : 10,
|
||||
"guest-login-delay" : 60,
|
||||
"trust-x-forward" : false,
|
||||
"enable-mail" : false,
|
||||
"mail-transport" : "SMTP",
|
||||
"mail-config" : {
|
||||
"service" : "Gmail",
|
||||
"auth" : {
|
||||
"user" : "some.user@gmail.com",
|
||||
"pass" : "supersecretpassword"
|
||||
}
|
||||
},
|
||||
"mail-from" : "some.user@gmail.com",
|
||||
"domain" : "http://localhost",
|
||||
"ytv3apikey" : "",
|
||||
"enable-ytv3" : false,
|
||||
"ytv2devkey" : ""
|
||||
}
|
||||
|
||||
function save(cfg, file) {
|
||||
if(!cfg.loaded)
|
||||
return;
|
||||
var x = {};
|
||||
for(var k in cfg) {
|
||||
if(k !== "nodemailer" && k !== "loaded")
|
||||
x[k] = cfg[k];
|
||||
}
|
||||
fs.writeFileSync(file, JSON.stringify(x, null, 4));
|
||||
}
|
||||
|
||||
exports.load = function (Server, file, callback) {
|
||||
var cfg = {};
|
||||
for(var k in defaults)
|
||||
cfg[k] = defaults[k];
|
||||
|
||||
fs.readFile(file, function (err, data) {
|
||||
if(err) {
|
||||
if(err.code == "ENOENT") {
|
||||
Logger.syslog.log("Config file not found, generating default");
|
||||
Logger.syslog.log("Edit cfg.json to configure");
|
||||
data = "{}";
|
||||
}
|
||||
else {
|
||||
Logger.errlog.log("Config load failed");
|
||||
Logger.errlog.log(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(data + "");
|
||||
} catch(e) {
|
||||
Logger.errlog.log("Config JSON is invalid: ");
|
||||
Logger.errlog.log(e);
|
||||
return;
|
||||
}
|
||||
|
||||
for(var k in data)
|
||||
cfg[k] = data[k];
|
||||
|
||||
if(cfg["enable-mail"]) {
|
||||
cfg["nodemailer"] = nodemailer.createTransport(
|
||||
cfg["mail-transport"],
|
||||
cfg["mail-config"]
|
||||
);
|
||||
}
|
||||
|
||||
cfg["loaded"] = true;
|
||||
|
||||
save(cfg, file);
|
||||
Server.cfg = cfg;
|
||||
callback();
|
||||
});
|
||||
}
|
||||
17
lib/customembed.js
Normal file
17
lib/customembed.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
const allowed = ["iframe", "object", "param", "embed"];
|
||||
const tag_re = /<\s*\/?\s*([a-z]+)(\s*([a-z]+)\s*=\s*('[^']*'|"[^"]*"|[^"'>]*))*\s*>/ig;
|
||||
|
||||
function filter(str) {
|
||||
str = str.replace(tag_re, function (match, tag) {
|
||||
if(!~allowed.indexOf(tag.toLowerCase())) {
|
||||
return match.replace("<", "<").replace(">", ">");
|
||||
}
|
||||
return match;
|
||||
});
|
||||
str = str.replace(/(\bon\w*\s*=\s*('[^']*'|"[^"]"|[^\s><]*))/ig, function () {
|
||||
return "";
|
||||
});
|
||||
return str;
|
||||
}
|
||||
|
||||
exports.filter = filter;
|
||||
1457
lib/database.js
Normal file
1457
lib/database.js
Normal file
File diff suppressed because it is too large
Load diff
37
lib/filter.js
Normal file
37
lib/filter.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var Filter = function(name, regex, flags, replace) {
|
||||
this.name = name;
|
||||
this.source = regex;
|
||||
this.flags = flags;
|
||||
this.regex = new RegExp(this.source, this.flags);
|
||||
this.replace = replace;
|
||||
this.active = true;
|
||||
this.filterlinks = false;
|
||||
}
|
||||
|
||||
Filter.prototype.pack = function() {
|
||||
return {
|
||||
name: this.name,
|
||||
source: this.source,
|
||||
flags: this.flags,
|
||||
replace: this.replace,
|
||||
active: this.active,
|
||||
filterlinks: this.filterlinks
|
||||
}
|
||||
}
|
||||
|
||||
Filter.prototype.filter = function(text) {
|
||||
return text.replace(this.regex, this.replace);
|
||||
}
|
||||
|
||||
exports.Filter = Filter;
|
||||
600
lib/get-info.js
Normal file
600
lib/get-info.js
Normal file
|
|
@ -0,0 +1,600 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var http = require("http");
|
||||
var https = require("https");
|
||||
var domain = require("domain");
|
||||
var Logger = require("./logger.js");
|
||||
var Media = require("./media.js").Media;
|
||||
var CustomEmbedFilter = require("./customembed").filter;
|
||||
|
||||
module.exports = function (Server) {
|
||||
var urlRetrieve = function (transport, options, callback) {
|
||||
// Catch any errors that crop up along the way of the request
|
||||
// in order to prevent them from reaching the global handler.
|
||||
// This should cut down on needing to restart the server
|
||||
var d = domain.create();
|
||||
d.on("error", function (err) {
|
||||
Logger.errlog.log("urlRetrieve failed: " + err);
|
||||
Logger.errlog.log("Request was: " + options.host + options.path);
|
||||
callback(503, err);
|
||||
});
|
||||
d.run(function () {
|
||||
var req = transport.request(options, function (res) {
|
||||
var buffer = "";
|
||||
res.setEncoding("utf-8");
|
||||
res.on("data", function (chunk) {
|
||||
buffer += chunk;
|
||||
});
|
||||
res.on("end", function () {
|
||||
callback(res.statusCode, buffer);
|
||||
});
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
|
||||
var Getters = {
|
||||
/* youtube.com */
|
||||
yt: function (id, callback) {
|
||||
if(Server.cfg["enable-ytv3"] && Server.cfg["ytv3apikey"]) {
|
||||
Getters["ytv3"](id, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
|
||||
var options = {
|
||||
host: "gdata.youtube.com",
|
||||
port: 443,
|
||||
path: "/feeds/api/videos/" + id + "?v=2&alt=json",
|
||||
method: "GET",
|
||||
dataType: "jsonp",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
if(Server.cfg["ytv2devkey"]) {
|
||||
options.headers = {
|
||||
"X-Gdata-Key": "key=" + Server.cfg["ytv2devkey"]
|
||||
};
|
||||
}
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status === 404) {
|
||||
callback("Video not found", null);
|
||||
return;
|
||||
} else if(status === 403) {
|
||||
callback("Private video", null);
|
||||
return;
|
||||
} else if(status === 503) {
|
||||
callback("API failure", null);
|
||||
return;
|
||||
} else if(status !== 200) {
|
||||
callback(true, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
var seconds = data.entry.media$group.yt$duration.seconds;
|
||||
var title = data.entry.title.$t;
|
||||
var media = new Media(id, title, seconds, "yt");
|
||||
callback(false, media);
|
||||
} catch(e) {
|
||||
// Gdata version 2 has the rather silly habit of
|
||||
// returning error codes in XML when I explicitly asked
|
||||
// for JSON
|
||||
var m = buffer.match(/<internalReason>([^<]+)<\/internalReason>/);
|
||||
if(m === null)
|
||||
m = buffer.match(/<code>([^<]+)<\/code>/);
|
||||
|
||||
var err = true;
|
||||
if(m) {
|
||||
if(m[1] === "too_many_recent_calls") {
|
||||
err = "YouTube is throttling the server right "+
|
||||
"now for making too many requests. "+
|
||||
"Please try again in a moment.";
|
||||
} else {
|
||||
err = m[1];
|
||||
}
|
||||
}
|
||||
|
||||
callback(err, null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* youtube.com API v3 (requires API key) */
|
||||
ytv3: function (id, callback) {
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var params = [
|
||||
"part=" + encodeURIComponent("id,snippet,contentDetails"),
|
||||
"id=" + id,
|
||||
"key=" + Server.cfg["ytapikey"]
|
||||
].join("&");
|
||||
var options = {
|
||||
host: "www.googleapis.com",
|
||||
port: 443,
|
||||
path: "/youtube/v3/videos?" + params,
|
||||
method: "GET",
|
||||
dataType: "jsonp",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status !== 200) {
|
||||
callback(true, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
// I am a bit disappointed that the API v3 just doesn't
|
||||
// return anything in any error case
|
||||
if(data.pageInfo.totalResults !== 1) {
|
||||
callback(true, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var vid = data.items[0];
|
||||
var title = vid.snippet.title;
|
||||
// No, it's not possible to get a number representing
|
||||
// the video length. Instead, I get a time of the format
|
||||
// PT#M#S which represents
|
||||
// "Period of Time" # Minutes, # Seconds
|
||||
var m = vid.contentDetails.duration.match(/PT(\d+)M(\d+)S/);
|
||||
var seconds = parseInt(m[1]) * 60 + parseInt(m[2]);
|
||||
var media = new Media(id, title, seconds, "yt");
|
||||
callback(false, media);
|
||||
} catch(e) {
|
||||
callback(true, media);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* youtube.com playlists */
|
||||
yp: function (id, callback, url) {
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var path = "/feeds/api/playlists/" + id + "?v=2&alt=json";
|
||||
// YouTube only returns 25 at a time, so I have to keep asking
|
||||
// for more with the URL they give me
|
||||
if(url !== undefined) {
|
||||
path = "/" + url.split("gdata.youtube.com")[1];
|
||||
}
|
||||
var options = {
|
||||
host: "gdata.youtube.com",
|
||||
port: 443,
|
||||
path: path,
|
||||
method: "GET",
|
||||
dataType: "jsonp",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
if(Server.cfg["ytv2devkey"]) {
|
||||
options.headers = {
|
||||
"X-Gdata-Key": "key=" + Server.cfg["ytv2devkey"]
|
||||
};
|
||||
}
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status === 404) {
|
||||
callback("Playlist not found", null);
|
||||
return;
|
||||
} else if(status === 403) {
|
||||
callback("Playlist is private", null);
|
||||
return;
|
||||
} else if(status === 503) {
|
||||
callback("API failure", null);
|
||||
return;
|
||||
} else if(status !== 200) {
|
||||
callback(true, null);
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
var vids = [];
|
||||
for(var i in data.feed.entry) {
|
||||
try {
|
||||
var item = data.feed.entry[i];
|
||||
var id = item.media$group.yt$videoid.$t;
|
||||
var title = item.title.$t;
|
||||
var seconds = item.media$group.yt$duration.seconds;
|
||||
var media = new Media(id, title, seconds, "yt");
|
||||
vids.push(media);
|
||||
} catch(e) {
|
||||
}
|
||||
}
|
||||
|
||||
callback(false, vids);
|
||||
|
||||
var links = data.feed.link;
|
||||
for(var i in links) {
|
||||
if(links[i].rel === "next")
|
||||
Getters["yp"](id, callback, links[i].href);
|
||||
}
|
||||
} catch(e) {
|
||||
callback(true, null);
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
/* youtube.com search */
|
||||
ytSearch: function (terms, callback) {
|
||||
for(var i in terms)
|
||||
terms[i] = encodeURIComponent(terms[i]);
|
||||
var query = terms.join("+");
|
||||
|
||||
var options = {
|
||||
host: "gdata.youtube.com",
|
||||
port: 443,
|
||||
path: "/feeds/api/videos/?q=" + query + "&v=2&alt=json",
|
||||
method: "GET",
|
||||
dataType: "jsonp",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
if(Server.cfg["ytv2devkey"]) {
|
||||
options.headers = {
|
||||
"X-Gdata-Key": "key=" + Server.cfg["ytv2devkey"]
|
||||
};
|
||||
}
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status !== 200) {
|
||||
callback(true, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
var vids = [];
|
||||
for(var i in data.feed.entry) {
|
||||
try {
|
||||
var item = data.feed.entry[i];
|
||||
var id = item.media$group.yt$videoid.$t;
|
||||
var title = item.title.$t;
|
||||
var seconds = item.media$group.yt$duration.seconds;
|
||||
var media = new Media(id, title, seconds, "yt");
|
||||
media.thumb = item.media$group.media$thumbnail[0];
|
||||
vids.push(media);
|
||||
} catch(e) {
|
||||
}
|
||||
}
|
||||
|
||||
callback(false, vids);
|
||||
} catch(e) {
|
||||
callback(true, null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* vimeo.com */
|
||||
vi: function (id, callback) {
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var options = {
|
||||
host: "vimeo.com",
|
||||
port: 443,
|
||||
path: "/api/v2/video/" + id + ".json",
|
||||
method: "GET",
|
||||
dataType: "jsonp",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status === 404) {
|
||||
callback("Video not found", null);
|
||||
return;
|
||||
} else if(status === 403) {
|
||||
callback("Private video", null);
|
||||
return;
|
||||
} else if(status === 503) {
|
||||
callback("API failure", null);
|
||||
return;
|
||||
} else if(status !== 200) {
|
||||
callback(true, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
data = data[0];
|
||||
var seconds = data.duration;
|
||||
var title = data.title;
|
||||
var media = new Media(id, title, seconds, "vi");
|
||||
callback(false, media);
|
||||
} catch(e) {
|
||||
var err = true;
|
||||
if(buffer.match(/not found/))
|
||||
err = "Video not found";
|
||||
|
||||
callback(err, null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* dailymotion.com */
|
||||
dm: function (id, callback) {
|
||||
// Dailymotion's API is an example of an API done right
|
||||
// - Supports SSL
|
||||
// - I can ask for exactly which fields I want
|
||||
// - URL is simple
|
||||
// - Field names are sensible
|
||||
// Other media providers take notes, please
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var options = {
|
||||
host: "api.dailymotion.com",
|
||||
port: 443,
|
||||
path: "/video/" + id + "?fields=duration,title",
|
||||
method: "GET",
|
||||
dataType: "jsonp",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status !== 200) {
|
||||
callback(true, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
var title = data.title;
|
||||
var seconds = data.duration;
|
||||
if(title === "Deleted video" && seconds === 10) {
|
||||
callback("Video not found", null);
|
||||
return;
|
||||
}
|
||||
var media = new Media(id, title, seconds, "dm");
|
||||
callback(false, media);
|
||||
} catch(e) {
|
||||
callback(err, null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* soundcloud.com */
|
||||
sc: function (id, callback) {
|
||||
// Soundcloud's API is badly designed and badly documented
|
||||
// In order to lookup track data from a URL, I have to first
|
||||
// make a call to /resolve to get the track id, then make a second
|
||||
// call to /tracks/{track.id} to actally get useful data
|
||||
// This is a waste of bandwidth and a pain in the ass
|
||||
|
||||
const SC_CLIENT = "2e0c82ab5a020f3a7509318146128abd";
|
||||
|
||||
var m = id.match(/([\w-\/\.:]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
|
||||
var options = {
|
||||
host: "api.soundcloud.com",
|
||||
port: 443,
|
||||
path: "/resolve.json?url=" + id + "&client_id=" + SC_CLIENT,
|
||||
method: "GET",
|
||||
dataType: "jsonp",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status === 404) {
|
||||
callback("Sound not found", null);
|
||||
return;
|
||||
} else if(status === 503) {
|
||||
callback("API failure", null);
|
||||
return;
|
||||
} else if(status !== 302) {
|
||||
callback(true, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var track = null;
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
track = data.location;
|
||||
} catch(e) {
|
||||
callback(true, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var options2 = {
|
||||
host: "api.soundcloud.com",
|
||||
port: 443,
|
||||
path: track,
|
||||
method: "GET",
|
||||
dataType: "jsonp",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
// I want to get off async's wild ride
|
||||
urlRetrieve(https, options2, function (status, data) {
|
||||
if(status !== 200) {
|
||||
callback(true, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
// Duration is in ms, but I want s
|
||||
var seconds = data.duration / 1000;
|
||||
var title = data.title;
|
||||
var media = new Media(id, title, seconds, "sc");
|
||||
callback(false, media);
|
||||
} catch(e) {
|
||||
callback(true, null);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
/* livestream.com */
|
||||
li: function (id, callback) {
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var title = "Livestream.com - " + id;
|
||||
var media = new Media(id, title, "--:--", "li");
|
||||
callback(false, media);
|
||||
},
|
||||
|
||||
/* twitch.tv */
|
||||
tw: function (id, callback) {
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var title = "Twitch.tv - " + id;
|
||||
var media = new Media(id, title, "--:--", "tw");
|
||||
callback(false, media);
|
||||
},
|
||||
|
||||
/* justin.tv */
|
||||
jt: function (id, callback) {
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var title = "Justin.tv - " + id;
|
||||
var media = new Media(id, title, "--:--", "jt");
|
||||
callback(false, media);
|
||||
},
|
||||
|
||||
/* ustream.tv */
|
||||
us: function (id, callback) {
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var options = {
|
||||
host: "www.ustream.tv",
|
||||
port: 80,
|
||||
path: "/" + id,
|
||||
method: "GET",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
urlRetrieve(http, options, function (status, data) {
|
||||
if(status !== 200) {
|
||||
callback(true, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Regexing the ID out of the HTML because
|
||||
// Ustream's API is so horribly documented
|
||||
// I literally could not figure out how to retrieve
|
||||
// this information.
|
||||
//
|
||||
// [](/eatadick)
|
||||
var m = data.match(/cid":([0-9]+)/);
|
||||
if(m) {
|
||||
var title = "Ustream.tv - " + id;
|
||||
var media = new Media(m[1], title, "--:--", "us");
|
||||
callback(false, media);
|
||||
} else {
|
||||
callback(true, null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* JWPlayer */
|
||||
jw: function (id, callback) {
|
||||
var title = "JWPlayer - " + id;
|
||||
var media = new Media(id, title, "--:--", "jw");
|
||||
callback(false, media);
|
||||
},
|
||||
|
||||
/* rtmp stream */
|
||||
rt: function (id, callback) {
|
||||
var title = "Livestream";
|
||||
var media = new Media(id, title, "--:--", "rt");
|
||||
callback(false, media);
|
||||
},
|
||||
|
||||
/* imgur.com albums */
|
||||
im: function (id, callback) {
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var title = "Imgur Album - " + id;
|
||||
var media = new Media(id, title, "--:--", "im");
|
||||
callback(false, media);
|
||||
},
|
||||
|
||||
/* custom embed */
|
||||
cu: function (id, callback) {
|
||||
id = CustomEmbedFilter(id);
|
||||
var media = new Media(id, "Custom Media", "--:--", "cu");
|
||||
callback(false, media);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
Getters: Getters,
|
||||
getMedia: function (id, type, callback) {
|
||||
if(type in this.Getters) {
|
||||
this.Getters[type](id, callback);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
64
lib/logger.js
Normal file
64
lib/logger.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
|
||||
function getTimeString() {
|
||||
var d = new Date();
|
||||
return d.toDateString() + " " + d.toTimeString().split(" ")[0];
|
||||
}
|
||||
|
||||
var Logger = function(filename) {
|
||||
this.filename = filename;
|
||||
this.writer = fs.createWriteStream(filename, {
|
||||
flags: "a",
|
||||
encoding: "utf-8"
|
||||
});
|
||||
}
|
||||
|
||||
Logger.prototype.log = function () {
|
||||
var msg = "";
|
||||
for(var i in arguments)
|
||||
msg += arguments[i];
|
||||
|
||||
if(this.dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
var str = "[" + getTimeString() + "] " + msg + "\n";
|
||||
try {
|
||||
this.writer.write(str);
|
||||
} catch(e) {
|
||||
errlog.log("WARNING: Attempted logwrite failed: " + this.filename);
|
||||
errlog.log("Message was: " + msg);
|
||||
errlog.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.prototype.close = function () {
|
||||
try {
|
||||
this.writer.end();
|
||||
} catch(e) {
|
||||
errlog.log("Log close failed: " + this.filename);
|
||||
}
|
||||
}
|
||||
|
||||
var errlog = new Logger(path.join(__dirname, "../error.log"));
|
||||
var syslog = new Logger(path.join(__dirname, "../sys.log"));
|
||||
errlog.actualLog = errlog.log;
|
||||
errlog.log = function(what) { console.log(what); this.actualLog(what); }
|
||||
syslog.actualLog = syslog.log;
|
||||
syslog.log = function(what) { console.log(what); this.actualLog(what); }
|
||||
|
||||
exports.Logger = Logger;
|
||||
exports.errlog = errlog;
|
||||
exports.syslog = syslog;
|
||||
67
lib/media.js
Normal file
67
lib/media.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var formatTime = require("./utilities").formatTime;
|
||||
|
||||
// Represents a media entry
|
||||
var Media = function(id, title, seconds, type) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
if(this.title.length > 100)
|
||||
this.title = this.title.substring(0, 97) + "...";
|
||||
this.seconds = seconds == "--:--" ? "--:--" : parseInt(seconds);
|
||||
this.duration = formatTime(this.seconds);
|
||||
if(seconds == "--:--") {
|
||||
this.seconds = 0;
|
||||
}
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
Media.prototype.dup = function() {
|
||||
var m = new Media(this.id, this.title, this.seconds, this.type);
|
||||
return m;
|
||||
}
|
||||
|
||||
// Returns an object containing the data in this Media but not the
|
||||
// prototype
|
||||
Media.prototype.pack = function() {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
seconds: this.seconds,
|
||||
duration: this.duration,
|
||||
type: this.type,
|
||||
};
|
||||
}
|
||||
|
||||
// Same as pack() but includes the currentTime variable set by the channel
|
||||
// when the media is being synchronized
|
||||
Media.prototype.fullupdate = function() {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
seconds: this.seconds,
|
||||
duration: this.duration,
|
||||
type: this.type,
|
||||
currentTime: this.currentTime,
|
||||
paused: this.paused,
|
||||
};
|
||||
}
|
||||
|
||||
Media.prototype.timeupdate = function() {
|
||||
//return this.fullupdate();
|
||||
return {
|
||||
currentTime: this.currentTime,
|
||||
paused: this.paused
|
||||
};
|
||||
}
|
||||
|
||||
exports.Media = Media;
|
||||
195
lib/notwebsocket.js
Normal file
195
lib/notwebsocket.js
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var Logger = require("./logger");
|
||||
|
||||
const chars = "abcdefghijklmnopqsrtuvwxyz" +
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||
"0123456789";
|
||||
|
||||
var NotWebsocket = function() {
|
||||
this.hash = "";
|
||||
for(var i = 0; i < 30; i++) {
|
||||
this.hash += chars[parseInt(Math.random() * (chars.length - 1))];
|
||||
}
|
||||
|
||||
this.pktqueue = [];
|
||||
this.handlers = {};
|
||||
this.room = "";
|
||||
this.lastpoll = Date.now();
|
||||
this.noflood = {};
|
||||
}
|
||||
|
||||
NotWebsocket.prototype.checkFlood = function(id, rate) {
|
||||
if(id in this.noflood) {
|
||||
this.noflood[id].push(Date.now());
|
||||
}
|
||||
else {
|
||||
this.noflood[id] = [Date.now()];
|
||||
}
|
||||
if(this.noflood[id].length > 10) {
|
||||
this.noflood[id].shift();
|
||||
var hz = 10000 / (this.noflood[id][9] - this.noflood[id][0]);
|
||||
if(hz > rate) {
|
||||
throw "Rate is too high: " + id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotWebsocket.prototype.emit = function(msg, data) {
|
||||
var pkt = [msg, data];
|
||||
this.pktqueue.push(pkt);
|
||||
}
|
||||
|
||||
NotWebsocket.prototype.poll = function() {
|
||||
this.checkFlood("poll", 100);
|
||||
this.lastpoll = Date.now();
|
||||
var q = [];
|
||||
for(var i = 0; i < this.pktqueue.length; i++) {
|
||||
q.push(this.pktqueue[i]);
|
||||
}
|
||||
this.pktqueue.length = 0;
|
||||
return q;
|
||||
}
|
||||
|
||||
NotWebsocket.prototype.on = function(msg, callback) {
|
||||
if(!(msg in this.handlers))
|
||||
this.handlers[msg] = [];
|
||||
this.handlers[msg].push(callback);
|
||||
}
|
||||
|
||||
NotWebsocket.prototype.recv = function(urlstr) {
|
||||
this.checkFlood("recv", 100);
|
||||
var msg, data;
|
||||
try {
|
||||
var js = JSON.parse(urlstr);
|
||||
msg = js[0];
|
||||
data = js[1];
|
||||
}
|
||||
catch(e) {
|
||||
Logger.errlog.log("Failed to parse NWS string");
|
||||
Logger.errlog.log(urlstr);
|
||||
}
|
||||
if(!msg)
|
||||
return;
|
||||
if(!(msg in this.handlers))
|
||||
return;
|
||||
for(var i = 0; i < this.handlers[msg].length; i++) {
|
||||
this.handlers[msg][i](data);
|
||||
}
|
||||
}
|
||||
|
||||
NotWebsocket.prototype.join = function(rm) {
|
||||
if(!(rm in rooms)) {
|
||||
rooms[rm] = [];
|
||||
}
|
||||
|
||||
rooms[rm].push(this);
|
||||
}
|
||||
|
||||
NotWebsocket.prototype.leave = function(rm) {
|
||||
if(rm in rooms) {
|
||||
var idx = rooms[rm].indexOf(this);
|
||||
if(idx >= 0) {
|
||||
rooms[rm].splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotWebsocket.prototype.disconnect = function() {
|
||||
for(var rm in rooms) {
|
||||
this.leave(rm);
|
||||
}
|
||||
|
||||
this.recv(JSON.stringify(["disconnect", undefined]));
|
||||
this.emit("disconnect");
|
||||
|
||||
clients[this.hash] = null;
|
||||
delete clients[this.hash];
|
||||
}
|
||||
|
||||
function sendJSON(res, obj) {
|
||||
var response = JSON.stringify(obj, null, 4);
|
||||
if(res.callback) {
|
||||
response = res.callback + "(" + response + ")";
|
||||
}
|
||||
var len = unescape(encodeURIComponent(response)).length;
|
||||
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.setHeader("Content-Length", len);
|
||||
res.end(response);
|
||||
}
|
||||
|
||||
var clients = {};
|
||||
var rooms = {};
|
||||
|
||||
function newConnection(req, res) {
|
||||
var nws = new NotWebsocket();
|
||||
clients[nws.hash] = nws;
|
||||
res.callback = req.query.callback;
|
||||
sendJSON(res, nws.hash);
|
||||
return nws;
|
||||
}
|
||||
exports.newConnection = newConnection;
|
||||
|
||||
function msgReceived(req, res) {
|
||||
res.callback = req.query.callback;
|
||||
var h = req.params.hash;
|
||||
if(h in clients && clients[h] != null) {
|
||||
var str = req.params.str;
|
||||
res.callback = req.query.callback;
|
||||
try {
|
||||
if(str == "poll") {
|
||||
sendJSON(res, clients[h].poll());
|
||||
}
|
||||
else {
|
||||
clients[h].recv(decodeURIComponent(str));
|
||||
sendJSON(res, "");
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
res.send(429); // 429 Too Many Requests
|
||||
}
|
||||
}
|
||||
else {
|
||||
res.send(404);
|
||||
}
|
||||
}
|
||||
exports.msgReceived = msgReceived;
|
||||
|
||||
function inRoom(rm) {
|
||||
var cl = [];
|
||||
|
||||
if(rm in rooms) {
|
||||
for(var i = 0; i < rooms[rm].length; i++) {
|
||||
cl.push(rooms[rm][i]);
|
||||
}
|
||||
}
|
||||
|
||||
cl.emit = function(msg, data) {
|
||||
for(var i = 0; i < this.length; i++) {
|
||||
this[i].emit(msg, data);
|
||||
}
|
||||
};
|
||||
|
||||
return cl;
|
||||
}
|
||||
exports.inRoom = inRoom;
|
||||
|
||||
function checkDeadSockets() {
|
||||
for(var h in clients) {
|
||||
if(Date.now() - clients[h].lastpoll >= 2000) {
|
||||
clients[h].disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(checkDeadSockets, 2000);
|
||||
554
lib/playlist.js
Normal file
554
lib/playlist.js
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
ULList = require("./ullist").ULList;
|
||||
var Media = require("./media").Media;
|
||||
var AllPlaylists = {};
|
||||
|
||||
function PlaylistItem(media, uid) {
|
||||
this.media = media;
|
||||
this.uid = uid;
|
||||
this.temp = false;
|
||||
this.queueby = "";
|
||||
this.prev = null;
|
||||
this.next = null;
|
||||
}
|
||||
|
||||
PlaylistItem.prototype.pack = function() {
|
||||
return {
|
||||
media: this.media.pack(),
|
||||
uid: this.uid,
|
||||
temp: this.temp,
|
||||
queueby: this.queueby
|
||||
};
|
||||
}
|
||||
|
||||
function Playlist(chan) {
|
||||
var name = chan.canonical_name;
|
||||
if(name in AllPlaylists && AllPlaylists[name]) {
|
||||
var pl = AllPlaylists[name];
|
||||
if(!pl.dead)
|
||||
pl.die();
|
||||
}
|
||||
this.items = new ULList();
|
||||
this.current = null;
|
||||
this.next_uid = 0;
|
||||
this._leadInterval = false;
|
||||
this._lastUpdate = 0;
|
||||
this._counter = 0;
|
||||
this.leading = true;
|
||||
this.callbacks = {
|
||||
"changeMedia": [],
|
||||
"mediaUpdate": [],
|
||||
"remove": [],
|
||||
};
|
||||
this.lock = false;
|
||||
this.action_queue = [];
|
||||
this._qaInterval = false;
|
||||
AllPlaylists[name] = this;
|
||||
|
||||
this.channel = chan;
|
||||
this.server = chan.server;
|
||||
var pl = this;
|
||||
this.on("mediaUpdate", function(m) {
|
||||
chan.sendAll("mediaUpdate", m.timeupdate());
|
||||
});
|
||||
this.on("changeMedia", function(m) {
|
||||
chan.onVideoChange();
|
||||
chan.sendAll("setCurrent", pl.current.uid);
|
||||
chan.sendAll("changeMedia", m.fullupdate());
|
||||
});
|
||||
this.on("remove", function(item) {
|
||||
chan.broadcastPlaylistMeta();
|
||||
chan.sendAll("delete", {
|
||||
uid: item.uid
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Playlist.prototype.queueAction = function(data) {
|
||||
this.action_queue.push(data);
|
||||
if(this._qaInterval)
|
||||
return;
|
||||
var pl = this;
|
||||
this._qaInterval = setInterval(function() {
|
||||
var data = pl.action_queue.shift();
|
||||
if(data.waiting) {
|
||||
if(!("expire" in data))
|
||||
data.expire = Date.now() + 10000;
|
||||
if(Date.now() < data.expire)
|
||||
pl.action_queue.unshift(data);
|
||||
}
|
||||
else
|
||||
data.fn();
|
||||
if(pl.action_queue.length == 0) {
|
||||
clearInterval(pl._qaInterval);
|
||||
pl._qaInterval = false;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
Playlist.prototype.dump = function() {
|
||||
var arr = this.items.toArray();
|
||||
var pos = 0;
|
||||
for(var i in arr) {
|
||||
if(this.current && arr[i].uid == this.current.uid) {
|
||||
pos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var time = 0;
|
||||
if(this.current)
|
||||
time = this.current.media.currentTime;
|
||||
|
||||
return {
|
||||
pl: arr,
|
||||
pos: pos,
|
||||
time: time
|
||||
};
|
||||
}
|
||||
|
||||
Playlist.prototype.die = function () {
|
||||
this.clear();
|
||||
if(this._leadInterval) {
|
||||
clearInterval(this._leadInterval);
|
||||
this._leadInterval = false;
|
||||
}
|
||||
if(this._qaInterval) {
|
||||
clearInterval(this._qaInterval);
|
||||
this._qaInterval = false;
|
||||
}
|
||||
//for(var key in this)
|
||||
// delete this[key];
|
||||
this.dead = true;
|
||||
}
|
||||
|
||||
Playlist.prototype.load = function(data, callback) {
|
||||
this.clear();
|
||||
for(var i in data.pl) {
|
||||
var e = data.pl[i].media;
|
||||
var m = new Media(e.id, e.title, e.seconds, e.type);
|
||||
var it = this.makeItem(m);
|
||||
it.temp = data.pl[i].temp;
|
||||
it.queueby = data.pl[i].queueby;
|
||||
this.items.append(it);
|
||||
if(i == parseInt(data.pos)) {
|
||||
this.current = it;
|
||||
}
|
||||
}
|
||||
|
||||
if(callback)
|
||||
callback();
|
||||
}
|
||||
|
||||
Playlist.prototype.on = function(ev, fn) {
|
||||
if(typeof fn === "undefined") {
|
||||
var pl = this;
|
||||
return function() {
|
||||
for(var i = 0; i < pl.callbacks[ev].length; i++) {
|
||||
pl.callbacks[ev][i].apply(this, arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(typeof fn === "function") {
|
||||
this.callbacks[ev].push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
Playlist.prototype.makeItem = function(media) {
|
||||
return new PlaylistItem(media, this.next_uid++);
|
||||
}
|
||||
|
||||
Playlist.prototype.add = function(item, pos) {
|
||||
var self = this;
|
||||
if(this.items.length >= 4000) {
|
||||
return "Playlist limit reached (4,000)";
|
||||
}
|
||||
|
||||
var it = this.items.findVideoId(item.media.id);
|
||||
if(it) {
|
||||
if(pos === "append" || it == this.current) {
|
||||
return "This item is already on the playlist";
|
||||
}
|
||||
|
||||
self.remove(it.uid, function () {
|
||||
self.channel.sendAll("delete", {
|
||||
uid: it.uid
|
||||
});
|
||||
self.channel.broadcastPlaylistMeta();
|
||||
});
|
||||
}
|
||||
|
||||
if(pos == "append") {
|
||||
if(!this.items.append(item)) {
|
||||
return "Playlist failure";
|
||||
}
|
||||
} else if(pos == "prepend") {
|
||||
if(!this.items.prepend(item)) {
|
||||
return "Playlist failure";
|
||||
}
|
||||
} else {
|
||||
if(!this.items.insertAfter(item, pos)) {
|
||||
return "Playlist failure";
|
||||
}
|
||||
}
|
||||
|
||||
if(this.items.length == 1) {
|
||||
this.current = item;
|
||||
this.startPlayback();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Playlist.prototype.addCachedMedia = function(data, callback) {
|
||||
var pos = "append";
|
||||
if(data.pos == "next") {
|
||||
if(!this.current)
|
||||
pos = "prepend";
|
||||
else
|
||||
pos = this.current.uid;
|
||||
}
|
||||
|
||||
var it = this.makeItem(data.media);
|
||||
it.temp = data.temp;
|
||||
it.queueby = data.queueby;
|
||||
|
||||
var pl = this;
|
||||
|
||||
var action = {
|
||||
fn: function() {
|
||||
var err = pl.add(it, pos);
|
||||
callback(err, err ? null : it);
|
||||
},
|
||||
waiting: false
|
||||
};
|
||||
this.queueAction(action);
|
||||
}
|
||||
|
||||
Playlist.prototype.addMedia = function(data, callback) {
|
||||
|
||||
if(data.type == "yp") {
|
||||
this.addYouTubePlaylist(data, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
var pos = "append";
|
||||
if(data.pos == "next") {
|
||||
if(!this.current)
|
||||
pos = "prepend";
|
||||
else
|
||||
pos = this.current.uid;
|
||||
}
|
||||
|
||||
var it = this.makeItem(null);
|
||||
var pl = this;
|
||||
var action = {
|
||||
fn: function() {
|
||||
var err = pl.add(it, pos);
|
||||
callback(err, err ? null : it);
|
||||
},
|
||||
waiting: true
|
||||
};
|
||||
this.queueAction(action);
|
||||
|
||||
// Pre-cached data
|
||||
if(typeof data.title === "string" &&
|
||||
typeof data.seconds === "number") {
|
||||
if(data.maxlength && data.seconds > data.maxlength) {
|
||||
action.expire = 0;
|
||||
callback("Media is too long!", null);
|
||||
return;
|
||||
}
|
||||
it.media = new Media(data.id, data.title, data.seconds, data.type);
|
||||
action.waiting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.server.infogetter.getMedia(data.id, data.type, function(err, media) {
|
||||
if(err) {
|
||||
action.expire = 0;
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.maxlength && media.seconds > data.maxlength) {
|
||||
action.expire = 0;
|
||||
callback("Media is too long!", null);
|
||||
return;
|
||||
}
|
||||
|
||||
it.media = media;
|
||||
it.temp = data.temp;
|
||||
it.queueby = data.queueby;
|
||||
action.waiting = false;
|
||||
});
|
||||
}
|
||||
|
||||
Playlist.prototype.addMediaList = function(data, callback) {
|
||||
var start = false;
|
||||
if(data.pos == "next") {
|
||||
data.list = data.list.reverse();
|
||||
start = data.list[data.list.length - 1];
|
||||
}
|
||||
|
||||
if(this.items.length != 0)
|
||||
start = false;
|
||||
|
||||
var pl = this;
|
||||
for(var i = 0; i < data.list.length; i++) {
|
||||
var x = data.list[i];
|
||||
x.pos = data.pos;
|
||||
if(start && x == start) {
|
||||
pl.addMedia(x, function (err, item) {
|
||||
if(err) {
|
||||
callback(err, item);
|
||||
}
|
||||
else {
|
||||
callback(err, item);
|
||||
pl.current = item;
|
||||
pl.startPlayback();
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
pl.addMedia(x, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Playlist.prototype.addYouTubePlaylist = function(data, callback) {
|
||||
var pos = "append";
|
||||
if(data.pos == "next") {
|
||||
if(!this.current)
|
||||
pos = "prepend";
|
||||
else
|
||||
pos = this.current.uid;
|
||||
}
|
||||
|
||||
var pl = this;
|
||||
this.server.infogetter.getMedia(data.id, data.type, function(err, vids) {
|
||||
if(err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.pos === "next")
|
||||
vids.reverse();
|
||||
|
||||
vids.forEach(function(media) {
|
||||
if(data.maxlength && media.seconds > data.maxlength) {
|
||||
callback("Media is too long!", null);
|
||||
return;
|
||||
}
|
||||
var it = pl.makeItem(media);
|
||||
it.temp = data.temp;
|
||||
it.queueby = data.queueby;
|
||||
pl.queueAction({
|
||||
fn: function() {
|
||||
var err = pl.add(it, pos);
|
||||
callback(err, err ? null : it);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Playlist.prototype.remove = function(uid, callback) {
|
||||
var pl = this;
|
||||
this.queueAction({
|
||||
fn: function() {
|
||||
var item = pl.items.find(uid);
|
||||
if(pl.items.remove(uid)) {
|
||||
if(callback)
|
||||
callback();
|
||||
if(item == pl.current)
|
||||
pl._next();
|
||||
}
|
||||
},
|
||||
waiting: false
|
||||
});
|
||||
}
|
||||
|
||||
Playlist.prototype.move = function(from, after, callback) {
|
||||
var pl = this;
|
||||
this.queueAction({
|
||||
fn: function() {
|
||||
pl._move(from, after, callback);
|
||||
},
|
||||
waiting: false
|
||||
});
|
||||
}
|
||||
|
||||
Playlist.prototype._move = function(from, after, callback) {
|
||||
var it = this.items.find(from);
|
||||
if(!this.items.remove(from))
|
||||
return;
|
||||
|
||||
if(after === "prepend") {
|
||||
if(!this.items.prepend(it))
|
||||
return;
|
||||
}
|
||||
|
||||
else if(after === "append") {
|
||||
if(!this.items.append(it))
|
||||
return;
|
||||
}
|
||||
|
||||
else if(!this.items.insertAfter(it, after))
|
||||
return;
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
Playlist.prototype.next = function() {
|
||||
if(!this.current)
|
||||
return;
|
||||
|
||||
var it = this.current;
|
||||
|
||||
if(it.temp) {
|
||||
var pl = this;
|
||||
this.remove(it.uid, function() {
|
||||
pl.on("remove")(it);
|
||||
});
|
||||
}
|
||||
else {
|
||||
this._next();
|
||||
}
|
||||
|
||||
return this.current;
|
||||
}
|
||||
|
||||
Playlist.prototype._next = function() {
|
||||
if(!this.current)
|
||||
return;
|
||||
this.current = this.current.next;
|
||||
if(this.current === null && this.items.first !== null)
|
||||
this.current = this.items.first;
|
||||
|
||||
if(this.current) {
|
||||
this.startPlayback();
|
||||
}
|
||||
}
|
||||
|
||||
Playlist.prototype.jump = function(uid) {
|
||||
if(!this.current)
|
||||
return false;
|
||||
|
||||
var jmp = this.items.find(uid);
|
||||
if(!jmp)
|
||||
return false;
|
||||
|
||||
var it = this.current;
|
||||
|
||||
this.current = jmp;
|
||||
|
||||
if(this.current) {
|
||||
this.startPlayback();
|
||||
}
|
||||
|
||||
if(it.temp) {
|
||||
var pl = this;
|
||||
this.remove(it.uid, function () {
|
||||
pl.on("remove")(it);
|
||||
});
|
||||
}
|
||||
|
||||
return this.current;
|
||||
}
|
||||
|
||||
Playlist.prototype.clear = function() {
|
||||
this.items.clear();
|
||||
this.next_uid = 0;
|
||||
this.current = null;
|
||||
clearInterval(this._leadInterval);
|
||||
}
|
||||
|
||||
Playlist.prototype.count = function (id) {
|
||||
var count = 0;
|
||||
this.items.forEach(function (i) {
|
||||
if(i.media.id === id)
|
||||
count++;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
Playlist.prototype.lead = function(lead) {
|
||||
this.leading = lead;
|
||||
var pl = this;
|
||||
if(!this.leading && this._leadInterval) {
|
||||
clearInterval(this._leadInterval);
|
||||
this._leadInterval = false;
|
||||
}
|
||||
else if(this.leading && !this._leadInterval) {
|
||||
this._lastUpdate = Date.now();
|
||||
this._leadInterval = setInterval(function() {
|
||||
pl._leadLoop();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
Playlist.prototype.startPlayback = function(time) {
|
||||
if(!this.current || !this.current.media)
|
||||
return false;
|
||||
this.current.media.paused = false;
|
||||
this.current.media.currentTime = time || -1;
|
||||
var pl = this;
|
||||
if(this._leadInterval) {
|
||||
clearInterval(this._leadInterval);
|
||||
this._leadInterval = false;
|
||||
}
|
||||
this.on("changeMedia")(this.current.media);
|
||||
if(this.leading && !isLive(this.current.media.type)) {
|
||||
this._lastUpdate = Date.now();
|
||||
this._leadInterval = setInterval(function() {
|
||||
pl._leadLoop();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function isLive(type) {
|
||||
return type == "li" // Livestream.com
|
||||
|| type == "tw" // Twitch.tv
|
||||
|| type == "jt" // Justin.tv
|
||||
|| type == "rt" // RTMP
|
||||
|| type == "jw" // JWPlayer
|
||||
|| type == "us" // Ustream.tv
|
||||
|| type == "im" // Imgur album
|
||||
|| type == "cu";// Custom embed
|
||||
}
|
||||
|
||||
const UPDATE_INTERVAL = 5;
|
||||
|
||||
Playlist.prototype._leadLoop = function() {
|
||||
if(this.current == null)
|
||||
return;
|
||||
|
||||
if(this.channel.name == "") {
|
||||
this.die();
|
||||
return;
|
||||
}
|
||||
|
||||
this.current.media.currentTime += (Date.now() - this._lastUpdate) / 1000.0;
|
||||
this._lastUpdate = Date.now();
|
||||
this._counter++;
|
||||
|
||||
if(this.current.media.currentTime >= this.current.media.seconds + 2) {
|
||||
this.next();
|
||||
}
|
||||
else if(this._counter % UPDATE_INTERVAL == 0) {
|
||||
this.on("mediaUpdate")(this.current.media);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Playlist;
|
||||
46
lib/poll.js
Normal file
46
lib/poll.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var Poll = function(initiator, title, options) {
|
||||
this.initiator = initiator;
|
||||
this.title = title;
|
||||
this.options = options;
|
||||
this.counts = new Array(options.length);
|
||||
for(var i = 0; i < this.counts.length; i++) {
|
||||
this.counts[i] = 0;
|
||||
}
|
||||
this.votes = {};
|
||||
}
|
||||
|
||||
Poll.prototype.vote = function(ip, option) {
|
||||
if(!(ip in this.votes) || this.votes[ip] == null) {
|
||||
this.votes[ip] = option;
|
||||
this.counts[option]++;
|
||||
}
|
||||
}
|
||||
|
||||
Poll.prototype.unvote = function(ip) {
|
||||
if(ip in this.votes && this.votes[ip] != null) {
|
||||
this.counts[this.votes[ip]]--;
|
||||
this.votes[ip] = null;
|
||||
}
|
||||
}
|
||||
|
||||
Poll.prototype.packUpdate = function() {
|
||||
return {
|
||||
title: this.title,
|
||||
options: this.options,
|
||||
counts: this.counts,
|
||||
initiator: this.initiator
|
||||
}
|
||||
}
|
||||
|
||||
exports.Poll = Poll;
|
||||
55
lib/rank.js
Normal file
55
lib/rank.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
exports.Anonymous = -1;
|
||||
exports.Guest = 0;
|
||||
exports.Member = 1;
|
||||
exports.Moderator = 2;
|
||||
exports.Owner = 3;
|
||||
exports.Siteadmin = 255;
|
||||
|
||||
var permissions = {
|
||||
acp : exports.Siteadmin,
|
||||
announce : exports.Siteadmin,
|
||||
registerChannel : exports.Owner,
|
||||
acl : exports.Owner,
|
||||
chatFilter : exports.Owner,
|
||||
setcss : exports.Owner,
|
||||
setjs : exports.Owner,
|
||||
channelperms : exports.Owner,
|
||||
queue : exports.Moderator,
|
||||
assignLeader : exports.Moderator,
|
||||
kick : exports.Moderator,
|
||||
ipban : exports.Moderator,
|
||||
ban : exports.Moderator,
|
||||
promote : exports.Moderator,
|
||||
qlock : exports.Moderator,
|
||||
poll : exports.Moderator,
|
||||
shout : exports.Moderator,
|
||||
channelOpts : exports.Moderator,
|
||||
jump : exports.Moderator,
|
||||
updateMotd : exports.Moderator,
|
||||
drink : exports.Moderator,
|
||||
seeVoteskip : exports.Moderator,
|
||||
uncache : exports.Moderator,
|
||||
seenlogins : exports.Moderator,
|
||||
settemp : exports.Moderator,
|
||||
search : exports.Guest,
|
||||
chat : exports.Guest,
|
||||
};
|
||||
|
||||
// Check if someone has permission to do shit
|
||||
exports.hasPermission = function(user, what) {
|
||||
if(what in permissions) {
|
||||
return user.rank >= permissions[what];
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
261
lib/server.js
Normal file
261
lib/server.js
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
var path = require("path");
|
||||
var fs = require("fs");
|
||||
var express = require("express");
|
||||
var Config = require("./config");
|
||||
var Logger = require("./logger");
|
||||
var Channel = require("./channel");
|
||||
var User = require("./user");
|
||||
|
||||
const VERSION = "2.4.2";
|
||||
|
||||
function getIP(req) {
|
||||
var raw = req.connection.remoteAddress;
|
||||
var forward = req.header("x-forwarded-for");
|
||||
if(Server.cfg["trust-x-forward"] && forward) {
|
||||
var ip = forward.split(",")[0];
|
||||
Logger.syslog.log("REVPROXY " + raw + " => " + ip);
|
||||
return ip;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function getSocketIP(socket) {
|
||||
var raw = socket.handshake.address.address;
|
||||
if(Server.cfg["trust-x-forward"]) {
|
||||
if(typeof socket.handshake.headers["x-forwarded-for"] == "string") {
|
||||
var ip = socket.handshake.headers["x-forwarded-for"]
|
||||
.split(",")[0];
|
||||
Logger.syslog.log("REVPROXY " + raw + " => " + ip);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
var Server = {
|
||||
channels: [],
|
||||
channelLoaded: function (name) {
|
||||
for(var i in this.channels) {
|
||||
if(this.channels[i].canonical_name == name.toLowerCase())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
getChannel: function (name) {
|
||||
for(var i in this.channels) {
|
||||
if(this.channels[i].canonical_name == name.toLowerCase())
|
||||
return this.channels[i];
|
||||
}
|
||||
|
||||
var c = new Channel(name, this);
|
||||
this.channels.push(c);
|
||||
return c;
|
||||
},
|
||||
unloadChannel: function(chan) {
|
||||
if(chan.registered)
|
||||
chan.saveDump();
|
||||
chan.playlist.die();
|
||||
chan.logger.close();
|
||||
for(var i in this.channels) {
|
||||
if(this.channels[i].canonical_name == chan.canonical_name) {
|
||||
this.channels.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
chan.name = "";
|
||||
chan.canonical_name = "";
|
||||
},
|
||||
stats: null,
|
||||
app: null,
|
||||
io: null,
|
||||
httpserv: null,
|
||||
ioserv: null,
|
||||
db: null,
|
||||
ips: {},
|
||||
acp: null,
|
||||
httpaccess: null,
|
||||
actionlog: null,
|
||||
logHTTP: function (req, status) {
|
||||
if(status === undefined)
|
||||
status = 200;
|
||||
var ip = req.connection.remoteAddress;
|
||||
var ip2 = false;
|
||||
if(this.cfg["trust-x-forward"])
|
||||
ip2 = req.header("x-forwarded-for") || req.header("cf-connecting-ip");
|
||||
var ipstr = !ip2 ? ip : ip + " (X-Forwarded-For " + ip2 + ")";
|
||||
var url = req.url;
|
||||
// Remove query
|
||||
if(url.indexOf("?") != -1)
|
||||
url = url.substring(0, url.lastIndexOf("?"));
|
||||
this.httpaccess.log([ipstr, req.method, url, status, req.headers["user-agent"]].join(" "));
|
||||
},
|
||||
init: function () {
|
||||
var self = this;
|
||||
// init database
|
||||
var Database = require("./database");
|
||||
this.db = new Database(self.cfg);
|
||||
this.db.init();
|
||||
this.actionlog = require("./actionlog")(self);
|
||||
this.httpaccess = new Logger.Logger(path.join(__dirname,
|
||||
"../httpaccess.log"));
|
||||
this.app = express();
|
||||
this.app.use(express.bodyParser());
|
||||
// channel path
|
||||
self.app.get("/r/:channel(*)", function (req, res, next) {
|
||||
var c = req.params.channel;
|
||||
if(!c.match(/^[\w-_]+$/)) {
|
||||
res.redirect("/" + c);
|
||||
}
|
||||
else {
|
||||
self.stats.record("http", "/r/" + c);
|
||||
self.logHTTP(req);
|
||||
res.sendfile("channel.html", {
|
||||
root: path.join(__dirname, "../www")
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// api path
|
||||
self.api = require("./api")(self);
|
||||
|
||||
self.app.get("/", function (req, res, next) {
|
||||
self.logHTTP(req);
|
||||
self.stats.record("http", "/");
|
||||
res.sendfile("index.html", {
|
||||
root: path.join(__dirname, "../www")
|
||||
});
|
||||
});
|
||||
|
||||
// default path
|
||||
self.app.get("/:thing(*)", function (req, res, next) {
|
||||
var opts = {
|
||||
root: path.join(__dirname, "../www"),
|
||||
maxAge: self.cfg["asset-cache-ttl"]
|
||||
}
|
||||
res.sendfile(req.params.thing, opts, function (err) {
|
||||
if(err) {
|
||||
self.logHTTP(req, err.status);
|
||||
// Damn path traversal attacks
|
||||
if(req.params.thing.indexOf("%2e") != -1) {
|
||||
res.send("Don't try that again, I'll ban you");
|
||||
Logger.syslog.log("WARNING: Attempted path "+
|
||||
"traversal from /" + getIP(req));
|
||||
Logger.syslog.log("URL: " + req.url);
|
||||
}
|
||||
// Something actually went wrong
|
||||
else {
|
||||
// Status codes over 500 are server errors
|
||||
if(err.status >= 500)
|
||||
Logger.errlog.log(err);
|
||||
res.send(err.status);
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.stats.record("http", req.params.thing);
|
||||
self.logHTTP(req);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// fallback
|
||||
self.app.use(function (err, req, res, next) {
|
||||
self.logHTTP(req, err.status);
|
||||
if(err.status == 404) {
|
||||
res.send(404);
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// bind servers
|
||||
self.httpserv = self.app.listen(Server.cfg["web-port"],
|
||||
Server.cfg["express-host"]);
|
||||
self.ioserv = express().listen(Server.cfg["io-port"],
|
||||
Server.cfg["express-host"]);
|
||||
|
||||
// init socket.io
|
||||
self.io = require("socket.io").listen(self.ioserv);
|
||||
self.io.set("log level", 1);
|
||||
self.io.sockets.on("connection", function (socket) {
|
||||
self.stats.record("socketio", "socket");
|
||||
var ip = getSocketIP(socket);
|
||||
socket._ip = ip;
|
||||
self.db.isGlobalIPBanned(ip, function (err, bant) {
|
||||
if(bant) {
|
||||
Logger.syslog.log("Disconnecting " + ip + " - gbanned");
|
||||
socket.emit("kick", {
|
||||
reason: "You're globally banned."
|
||||
});
|
||||
socket.disconnect(true);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("disconnect", function () {
|
||||
self.ips[ip]--;
|
||||
}.bind(self));
|
||||
|
||||
if(!(ip in self.ips))
|
||||
self.ips[ip] = 0;
|
||||
self.ips[ip]++;
|
||||
|
||||
if(self.ips[ip] > Server.cfg["ip-connection-limit"]) {
|
||||
socket.emit("kick", {
|
||||
reason: "Too many connections from your IP address"
|
||||
});
|
||||
socket.disconnect(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// finally a valid user
|
||||
Logger.syslog.log("Accepted socket from /" + socket._ip);
|
||||
new User(socket, self);
|
||||
}.bind(self));
|
||||
|
||||
|
||||
// init ACP
|
||||
self.acp = require("./acp")(self);
|
||||
|
||||
// init stats
|
||||
self.stats = require("./stats")(self);
|
||||
|
||||
// init media retriever
|
||||
self.infogetter = require("./get-info")(self);
|
||||
},
|
||||
shutdown: function () {
|
||||
Logger.syslog.log("Unloading channels");
|
||||
for(var i in this.channels) {
|
||||
if(this.channels[i].registered) {
|
||||
Logger.syslog.log("Saving /r/" + this.channels[i].name);
|
||||
this.channels[i].saveDump();
|
||||
}
|
||||
}
|
||||
Logger.syslog.log("Goodbye");
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
Logger.syslog.log("Starting CyTube v" + VERSION);
|
||||
|
||||
var chanlogpath = path.join(__dirname, "../chanlogs");
|
||||
fs.exists(chanlogpath, function (exists) {
|
||||
exists || fs.mkdir(chanlogpath);
|
||||
});
|
||||
|
||||
var chandumppath = path.join(__dirname, "../chandump");
|
||||
fs.exists(chandumppath, function (exists) {
|
||||
exists || fs.mkdir(chandumppath);
|
||||
});
|
||||
|
||||
Config.load(Server, path.join(__dirname, "../cfg.json"), function () {
|
||||
Server.init();
|
||||
if(!Server.cfg["debug"]) {
|
||||
process.on("uncaughtException", function (err) {
|
||||
Logger.errlog.log("[SEVERE] Uncaught Exception: " + err);
|
||||
Logger.errlog.log(err.stack);
|
||||
});
|
||||
|
||||
process.on("SIGINT", function () {
|
||||
Server.shutdown();
|
||||
});
|
||||
}
|
||||
});
|
||||
69
lib/stats.js
Normal file
69
lib/stats.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var Logger = require("./logger");
|
||||
|
||||
const STAT_INTERVAL = 60 * 60 * 1000;
|
||||
const STAT_EXPIRE = 24 * STAT_INTERVAL;
|
||||
|
||||
module.exports = function (Server) {
|
||||
var db = Server.db;
|
||||
|
||||
setInterval(function () {
|
||||
var chancount = Server.channels.length;
|
||||
var usercount = 0;
|
||||
Server.channels.forEach(function (chan) {
|
||||
usercount += chan.users.length;
|
||||
});
|
||||
|
||||
var mem = process.memoryUsage().rss;
|
||||
|
||||
db.addStatPoint(Date.now(), usercount, chancount, mem, function () {
|
||||
db.pruneStats(Date.now() - STAT_EXPIRE);
|
||||
});
|
||||
}, STAT_INTERVAL);
|
||||
|
||||
return {
|
||||
stores: {
|
||||
"http": {},
|
||||
"socketio": {},
|
||||
"api": {}
|
||||
},
|
||||
record: function (type, key) {
|
||||
var store;
|
||||
if(!(type in this.stores))
|
||||
return;
|
||||
|
||||
store = this.stores[type];
|
||||
|
||||
if(key in store) {
|
||||
store[key].push(Date.now());
|
||||
if(store[key].length > 100)
|
||||
store[key].shift();
|
||||
} else {
|
||||
store[key] = [Date.now()];
|
||||
}
|
||||
},
|
||||
readAverages: function (type) {
|
||||
if(!(type in this.stores))
|
||||
return;
|
||||
var avg = {};
|
||||
var store = this.stores[type];
|
||||
for(var k in store) {
|
||||
var time = Date.now() - store[k][0];
|
||||
avg[k] = store[k].length / time;
|
||||
avg[k] = parseInt(avg[k] * 1000);
|
||||
}
|
||||
return avg;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
184
lib/ullist.js
Normal file
184
lib/ullist.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
ullist.js
|
||||
|
||||
Description: Defines ULList, which represents a doubly linked list
|
||||
in which each item has a unique identifier stored in the `uid` field.
|
||||
|
||||
*/
|
||||
|
||||
function ULList() {
|
||||
this.first = null;
|
||||
this.last = null;
|
||||
this.length = 0;
|
||||
}
|
||||
|
||||
/* Add an item to the beginning of the list */
|
||||
ULList.prototype.prepend = function(item) {
|
||||
if(this.first !== null) {
|
||||
item.next = this.first;
|
||||
this.first.prev = item;
|
||||
}
|
||||
else {
|
||||
this.last = item;
|
||||
}
|
||||
this.first = item;
|
||||
this.first.prev = null;
|
||||
this.length++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Add an item to the end of the list */
|
||||
ULList.prototype.append = function(item) {
|
||||
if(this.last !== null) {
|
||||
item.prev = this.last;
|
||||
this.last.next = item;
|
||||
}
|
||||
else {
|
||||
this.first = item;
|
||||
}
|
||||
this.last = item;
|
||||
this.last.next = null;
|
||||
this.length++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Insert an item after one which has a specified UID */
|
||||
ULList.prototype.insertAfter = function(item, uid) {
|
||||
var after = this.find(uid);
|
||||
|
||||
if(!after)
|
||||
return false;
|
||||
|
||||
// Update links
|
||||
item.next = after.next;
|
||||
if(item.next)
|
||||
item.next.prev = item;
|
||||
item.prev = after;
|
||||
after.next = item;
|
||||
|
||||
// New end of list
|
||||
if(after == this.last)
|
||||
this.last = item;
|
||||
|
||||
this.length++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Insert an item before one that has a specified UID */
|
||||
ULList.prototype.insertBefore = function(item, uid) {
|
||||
var before = this.find(uid);
|
||||
|
||||
if(!before)
|
||||
return false;
|
||||
|
||||
// Update links
|
||||
item.next = before;
|
||||
item.prev = before.prev;
|
||||
if(item.prev)
|
||||
item.prev.next = item;
|
||||
before.prev = item;
|
||||
|
||||
// New beginning of list
|
||||
if(before == this.first)
|
||||
this.first = item;
|
||||
|
||||
this.length++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Remove an item from the list */
|
||||
ULList.prototype.remove = function(uid) {
|
||||
var item = this.find(uid);
|
||||
if(!item)
|
||||
return false;
|
||||
|
||||
// Boundary conditions
|
||||
if(item == this.first)
|
||||
this.first = item.next;
|
||||
if(item == this.last)
|
||||
this.last = item.prev;
|
||||
|
||||
// General case
|
||||
if(item.prev)
|
||||
item.prev.next = item.next;
|
||||
if(item.next)
|
||||
item.next.prev = item.prev;
|
||||
|
||||
this.length--;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Find an element in the list, return false if specified UID not found */
|
||||
ULList.prototype.find = function(uid) {
|
||||
// Can't possibly find it in an empty list
|
||||
if(this.first === null)
|
||||
return false;
|
||||
|
||||
var item = this.first;
|
||||
var iter = this.first;
|
||||
while(iter !== null && item.uid != uid) {
|
||||
item = iter;
|
||||
iter = iter.next;
|
||||
}
|
||||
|
||||
if(item && item.uid == uid)
|
||||
return item;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Clear all elements from the list */
|
||||
ULList.prototype.clear = function() {
|
||||
this.first = null;
|
||||
this.last = null;
|
||||
this.length = 0;
|
||||
}
|
||||
|
||||
/* Dump the contents of the list into an array */
|
||||
ULList.prototype.toArray = function(pack) {
|
||||
var arr = new Array(this.length);
|
||||
var item = this.first;
|
||||
var i = 0;
|
||||
while(item !== null) {
|
||||
if(pack !== false && typeof item.pack == "function")
|
||||
arr[i++] = item.pack();
|
||||
else
|
||||
arr[i++] = item;
|
||||
item = item.next;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/* iterate across the playlist */
|
||||
ULList.prototype.forEach = function (fn) {
|
||||
var item = this.first;
|
||||
while(item !== null) {
|
||||
fn(item);
|
||||
item = item.next;
|
||||
}
|
||||
};
|
||||
|
||||
/* find a media with the given video id */
|
||||
ULList.prototype.findVideoId = function (id) {
|
||||
var item = this.first;
|
||||
while(item !== null) {
|
||||
if(item.media && item.media.id === id)
|
||||
return item;
|
||||
item = item.next;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
exports.ULList = ULList;
|
||||
689
lib/user.js
Normal file
689
lib/user.js
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
var Rank = require("./rank.js");
|
||||
var Channel = require("./channel.js").Channel;
|
||||
var Logger = require("./logger.js");
|
||||
var $util = require("./utilities");
|
||||
|
||||
// Represents a client connected via socket.io
|
||||
var User = function(socket, Server) {
|
||||
this.ip = socket._ip;
|
||||
this.server = Server;
|
||||
this.socket = socket;
|
||||
this.loggedIn = false;
|
||||
this.saverank = false;
|
||||
this.rank = Rank.Anonymous;
|
||||
this.global_rank = Rank.Anonymous;
|
||||
this.channel = null;
|
||||
this.name = "";
|
||||
this.meta = {
|
||||
afk: false,
|
||||
icon: false
|
||||
};
|
||||
this.muted = false;
|
||||
this.throttle = {};
|
||||
this.flooded = {};
|
||||
this.queueLimiter = $util.newRateLimiter();
|
||||
this.profile = {
|
||||
image: "",
|
||||
text: ""
|
||||
};
|
||||
this.awaytimer = false;
|
||||
this.autoAFK();
|
||||
|
||||
this.initCallbacks();
|
||||
if(Server.announcement != null) {
|
||||
this.socket.emit("announcement", Server.announcement);
|
||||
}
|
||||
};
|
||||
|
||||
// Throttling/cooldown
|
||||
User.prototype.noflood = function(name, hz) {
|
||||
var time = new Date().getTime();
|
||||
if(!(name in this.throttle)) {
|
||||
this.throttle[name] = [time];
|
||||
return false;
|
||||
}
|
||||
else if(name in this.flooded && time < this.flooded[name]) {
|
||||
this.socket.emit("noflood", {
|
||||
action: name,
|
||||
msg: "You're still on cooldown!"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
this.throttle[name].push(time);
|
||||
var diff = (time - this.throttle[name][0]) / 1000.0;
|
||||
// Twice might be an accident, more than that is probably spam
|
||||
if(this.throttle[name].length > 2) {
|
||||
var rate = this.throttle[name].length / diff;
|
||||
this.throttle[name] = [time];
|
||||
if(rate > hz) {
|
||||
this.flooded[name] = time + 5000;
|
||||
this.socket.emit("noflood", {
|
||||
action: name,
|
||||
msg: "Stop doing that so fast! Cooldown: 5s"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
User.prototype.setAFK = function (afk) {
|
||||
if(this.channel === null)
|
||||
return;
|
||||
if(this.meta.afk === afk)
|
||||
return;
|
||||
var chan = this.channel;
|
||||
this.meta.afk = afk;
|
||||
if(afk) {
|
||||
if(chan.afkers.indexOf(this.name.toLowerCase()) == -1)
|
||||
chan.afkers.push(this.name.toLowerCase());
|
||||
if(chan.voteskip)
|
||||
chan.voteskip.unvote(this.ip);
|
||||
}
|
||||
else {
|
||||
if(chan.afkers.indexOf(this.name.toLowerCase()) != -1)
|
||||
chan.afkers.splice(chan.afkers.indexOf(this.name.toLowerCase()), 1);
|
||||
this.autoAFK();
|
||||
}
|
||||
chan.checkVoteskipPass();
|
||||
chan.sendAll("setAFK", {
|
||||
name: this.name,
|
||||
afk: afk
|
||||
});
|
||||
}
|
||||
|
||||
User.prototype.autoAFK = function () {
|
||||
if(this.awaytimer)
|
||||
clearTimeout(this.awaytimer);
|
||||
|
||||
if(this.channel === null || this.channel.opts.afk_timeout == 0)
|
||||
return;
|
||||
|
||||
this.awaytimer = setTimeout(function () {
|
||||
this.setAFK(true);
|
||||
}.bind(this), this.channel.opts.afk_timeout * 1000);
|
||||
}
|
||||
|
||||
User.prototype.initCallbacks = function() {
|
||||
var self = this;
|
||||
self.socket.on("disconnect", function() {
|
||||
self.awaytimer && clearTimeout(self.awaytimer);
|
||||
if(self.channel != null)
|
||||
self.channel.userLeave(self);
|
||||
});
|
||||
|
||||
self.socket.on("joinChannel", function(data) {
|
||||
if(self.channel != null)
|
||||
return;
|
||||
if(typeof data.name != "string")
|
||||
return;
|
||||
if(!data.name.match(/^[\w-_]{1,30}$/)) {
|
||||
self.socket.emit("errorMsg", {
|
||||
msg: "Invalid channel name. Channel names may consist of"+
|
||||
" 1-30 characters in the set a-z, A-Z, 0-9, -, and _"
|
||||
});
|
||||
self.socket.emit("kick", {
|
||||
reason: "Bad channel name"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
data.name = data.name.toLowerCase();
|
||||
self.channel = self.server.getChannel(data.name);
|
||||
if(self.loggedIn) {
|
||||
self.channel.getRank(self.name, function (err, rank) {
|
||||
if(!err && rank > self.rank)
|
||||
self.rank = rank;
|
||||
});
|
||||
}
|
||||
self.channel.userJoin(self);
|
||||
self.autoAFK();
|
||||
});
|
||||
|
||||
self.socket.on("login", function(data) {
|
||||
var name = data.name || "";
|
||||
var pw = data.pw || "";
|
||||
var session = data.session || "";
|
||||
if(pw.length > 100)
|
||||
pw = pw.substring(0, 100);
|
||||
if(self.name == "")
|
||||
self.login(name, pw, session);
|
||||
});
|
||||
|
||||
self.socket.on("assignLeader", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryChangeLeader(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("promote", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryPromoteUser(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("demote", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryDemoteUser(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setChannelRank", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.trySetRank(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("banName", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.banName(self, data.name || "");
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("banIP", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryIPBan(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("unban", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryUnban(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("chatMsg", function(data) {
|
||||
if(self.channel != null) {
|
||||
if(data.msg.indexOf("/afk") != 0) {
|
||||
self.setAFK(false);
|
||||
self.autoAFK();
|
||||
}
|
||||
self.channel.tryChat(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("newPoll", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryOpenPoll(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("playerReady", function() {
|
||||
if(self.channel != null) {
|
||||
self.channel.sendMediaUpdate(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("requestPlaylist", function() {
|
||||
if(self.channel != null) {
|
||||
self.channel.sendPlaylist(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("queue", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryQueue(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setTemp", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.trySetTemp(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("delete", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryDequeue(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("uncache", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryUncache(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("moveMedia", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryMove(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("jumpTo", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryJumpTo(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("playNext", function() {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryPlayNext(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("clearPlaylist", function() {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryClearqueue(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("shufflePlaylist", function() {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryShufflequeue(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("togglePlaylistLock", function() {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryToggleLock(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("mediaUpdate", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryUpdate(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("searchMedia", function(data) {
|
||||
if(self.channel != null) {
|
||||
if(data.source == "yt") {
|
||||
var searchfn = self.server.infogetter.Getters["ytSearch"];
|
||||
searchfn(data.query.split(" "), function (e, vids) {
|
||||
if(!e) {
|
||||
self.socket.emit("searchResults", {
|
||||
results: vids
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
self.channel.search(data.query, function (vids) {
|
||||
self.socket.emit("searchResults", {
|
||||
results: vids
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("closePoll", function() {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryClosePoll(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("vote", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryVote(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("registerChannel", function(data) {
|
||||
if(self.channel == null) {
|
||||
self.socket.emit("channelRegistration", {
|
||||
success: false,
|
||||
error: "You're not in any channel!"
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.channel.tryRegister(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("unregisterChannel", function() {
|
||||
if(self.channel == null) {
|
||||
return;
|
||||
}
|
||||
self.channel.unregister(self);
|
||||
});
|
||||
|
||||
self.socket.on("setOptions", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryUpdateOptions(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setPermissions", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryUpdatePermissions(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setChannelCSS", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.trySetCSS(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setChannelJS", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.trySetJS(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("updateFilter", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryUpdateFilter(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("removeFilter", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryRemoveFilter(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("moveFilter", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryMoveFilter(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setMotd", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryUpdateMotd(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("requestLoginHistory", function() {
|
||||
if(self.channel != null) {
|
||||
self.channel.sendLoginHistory(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("requestBanlist", function() {
|
||||
if(self.channel != null) {
|
||||
self.channel.sendBanlist(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("requestChatFilters", function() {
|
||||
if(self.channel != null) {
|
||||
self.channel.sendChatFilters(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("requestChannelRanks", function() {
|
||||
if(self.channel != null) {
|
||||
if(self.noflood("requestChannelRanks", 0.25))
|
||||
return;
|
||||
self.channel.sendChannelRanks(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("voteskip", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryVoteskip(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("listPlaylists", function(data) {
|
||||
if(self.name == "" || self.rank < 1) {
|
||||
self.socket.emit("listPlaylists", {
|
||||
pllist: [],
|
||||
error: "You must be logged in to manage playlists"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
self.server.db.listUserPlaylists(self.name, function (err, list) {
|
||||
if(err)
|
||||
list = [];
|
||||
for(var i = 0; i < list.length; i++) {
|
||||
list[i].time = $util.formatTime(list[i].time);
|
||||
}
|
||||
self.socket.emit("listPlaylists", {
|
||||
pllist: list,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
self.socket.on("savePlaylist", function(data) {
|
||||
if(self.rank < 1) {
|
||||
self.socket.emit("savePlaylist", {
|
||||
success: false,
|
||||
error: "You must be logged in to manage playlists"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(self.channel == null) {
|
||||
self.socket.emit("savePlaylist", {
|
||||
success: false,
|
||||
error: "Not in a channel"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof data.name != "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
var pl = self.channel.playlist.items.toArray();
|
||||
self.server.db.saveUserPlaylist(pl, self.name, data.name,
|
||||
function (err, res) {
|
||||
if(err) {
|
||||
self.socket.emit("savePlaylist", {
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
self.socket.emit("savePlaylist", {
|
||||
success: true
|
||||
});
|
||||
|
||||
self.server.db.listUserPlaylists(self.name,
|
||||
function (err, list) {
|
||||
if(err)
|
||||
list = [];
|
||||
for(var i = 0; i < list.length; i++) {
|
||||
list[i].time = $util.formatTime(list[i].time);
|
||||
}
|
||||
self.socket.emit("listPlaylists", {
|
||||
pllist: list,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
self.socket.on("queuePlaylist", function(data) {
|
||||
if(self.channel != null) {
|
||||
self.channel.tryQueuePlaylist(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("deletePlaylist", function(data) {
|
||||
if(typeof data.name != "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
self.server.db.deleteUserPlaylist(self.name, data.name,
|
||||
function () {
|
||||
self.server.db.listUserPlaylists(self.name,
|
||||
function (err, list) {
|
||||
if(err)
|
||||
list = [];
|
||||
for(var i = 0; i < list.length; i++) {
|
||||
list[i].time = $util.formatTime(list[i].time);
|
||||
}
|
||||
self.socket.emit("listPlaylists", {
|
||||
pllist: list,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
self.socket.on("readChanLog", function () {
|
||||
if(self.channel !== null) {
|
||||
self.channel.tryReadLog(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("acp-init", function() {
|
||||
if(self.global_rank >= Rank.Siteadmin)
|
||||
self.server.acp.init(self);
|
||||
});
|
||||
|
||||
self.socket.on("borrow-rank", function(rank) {
|
||||
if(self.global_rank < 255)
|
||||
return;
|
||||
if(rank > self.global_rank)
|
||||
return;
|
||||
|
||||
self.rank = rank;
|
||||
self.socket.emit("rank", rank);
|
||||
if(self.channel != null)
|
||||
self.channel.broadcastUserUpdate(self);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
var lastguestlogin = {};
|
||||
// Attempt to login
|
||||
User.prototype.login = function(name, pw, session) {
|
||||
var self = this;
|
||||
// No password => try guest login
|
||||
if(pw == "" && session == "") {
|
||||
if(self.ip in lastguestlogin) {
|
||||
var diff = (Date.now() - lastguestlogin[self.ip])/1000;
|
||||
if(diff < self.server.cfg["guest-login-delay"]) {
|
||||
self.socket.emit("login", {
|
||||
success: false,
|
||||
error: ["Guest logins are restricted to one per ",
|
||||
self.server.cfg["guest-login-delay"]
|
||||
+ " seconds per IP. ",
|
||||
"This restriction does not apply to registered users."
|
||||
].join("")
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(!$util.isValidUserName(name)) {
|
||||
self.socket.emit("login", {
|
||||
success: false,
|
||||
error: "Invalid username. Usernames must be 1-20 characters long and consist only of alphanumeric characters and underscores"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
self.server.db.isUsernameTaken(name, function (err, taken) {
|
||||
if(err) {
|
||||
self.socket.emit("login", {
|
||||
success: false,
|
||||
error: "Internal error: " + err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(taken) {
|
||||
self.socket.emit("login", {
|
||||
success: false,
|
||||
error: "That username is taken"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(self.channel != null) {
|
||||
for(var i = 0; i < self.channel.users.length; i++) {
|
||||
if(self.channel.users[i].name == name) {
|
||||
self.socket.emit("login", {
|
||||
success: false,
|
||||
error: "That name is already taken on self channel"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
lastguestlogin[self.ip] = Date.now();
|
||||
self.rank = Rank.Guest;
|
||||
Logger.syslog.log(self.ip + " signed in as " + name);
|
||||
self.server.db.recordVisit(self.ip, name);
|
||||
self.name = name;
|
||||
self.loggedIn = false;
|
||||
self.socket.emit("login", {
|
||||
success: true,
|
||||
name: name
|
||||
});
|
||||
self.socket.emit("rank", self.rank);
|
||||
if(self.channel != null) {
|
||||
self.channel.logger.log(self.ip + " signed in as " + name);
|
||||
self.channel.broadcastNewUser(self);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
self.server.db.userLogin(name, pw, session, function (err, row) {
|
||||
if(err) {
|
||||
self.server.actionlog.record(self.ip, name, "login-failure");
|
||||
self.socket.emit("login", {
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(self.channel != null) {
|
||||
for(var i = 0; i < self.channel.users.length; i++) {
|
||||
if(self.channel.users[i].name.toLowerCase() == name.toLowerCase()) {
|
||||
self.channel.kick(self.channel.users[i], "Duplicate login");
|
||||
}
|
||||
}
|
||||
}
|
||||
if(self.global_rank >= 255)
|
||||
self.server.actionlog.record(self.ip, name, "login-success");
|
||||
self.loggedIn = true;
|
||||
self.socket.emit("login", {
|
||||
success: true,
|
||||
session: row.session_hash,
|
||||
name: name
|
||||
});
|
||||
Logger.syslog.log(self.ip + " logged in as " + name);
|
||||
self.server.db.recordVisit(self.ip, name);
|
||||
self.profile = {
|
||||
image: row.profile_image,
|
||||
text: row.profile_text
|
||||
};
|
||||
self.global_rank = row.global_rank;
|
||||
var afterRankLookup = function () {
|
||||
self.socket.emit("rank", self.rank);
|
||||
self.name = name;
|
||||
if(self.channel != null) {
|
||||
self.channel.logger.log(self.ip + " logged in as " +
|
||||
name);
|
||||
self.channel.broadcastNewUser(self);
|
||||
}
|
||||
};
|
||||
if(self.channel !== null) {
|
||||
self.channel.getRank(name, function (err, rank) {
|
||||
if(!err) {
|
||||
self.saverank = true;
|
||||
self.rank = rank;
|
||||
} else {
|
||||
self.saverank = false;
|
||||
self.rank = self.global_rank;
|
||||
}
|
||||
afterRankLookup();
|
||||
});
|
||||
} else {
|
||||
self.rank = self.global_rank;
|
||||
afterRankLookup();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User;
|
||||
102
lib/utilities.js
Normal file
102
lib/utilities.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
module.exports = {
|
||||
isValidChannelName: function (name) {
|
||||
return name.match(/^[\w-_]{1,30}$/);
|
||||
},
|
||||
|
||||
isValidUserName: function (name) {
|
||||
return name.match(/^[\w-_]{1,20}$/);
|
||||
},
|
||||
|
||||
randomSalt: function (length) {
|
||||
var chars = "abcdefgihjklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
+ "0123456789!@#$%^&*_+=~";
|
||||
var salt = [];
|
||||
for(var i = 0; i < length; i++) {
|
||||
salt.push(chars[parseInt(Math.random()*chars.length)]);
|
||||
}
|
||||
return salt.join('');
|
||||
},
|
||||
|
||||
maskIP: function (ip) {
|
||||
if(ip.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||
// standard 32 bit IP
|
||||
return ip.replace(/\d+\.\d+\.(\d+\.\d+)/, "x.x.$1");
|
||||
} else if(ip.match(/^\d+\.\d+\.\d+/)) {
|
||||
// /24 range
|
||||
return ip.replace(/\d+\.\d+\.(\d+)/, "x.x.$1.*");
|
||||
}
|
||||
},
|
||||
|
||||
formatTime: function (sec) {
|
||||
if(sec === "--:--")
|
||||
return sec;
|
||||
|
||||
sec = Math.floor(+sec);
|
||||
var h = "", m = "", s = "";
|
||||
|
||||
if(sec >= 3600) {
|
||||
h = "" + Math.floor(sec / 3600);
|
||||
if(h.length < 2)
|
||||
h = "0" + h;
|
||||
sec %= 3600;
|
||||
}
|
||||
|
||||
m = "" + Math.floor(sec / 60);
|
||||
if(m.length < 2)
|
||||
m = "0" + m;
|
||||
|
||||
s = "" + (sec % 60);
|
||||
if(s.length < 2)
|
||||
s = "0" + s;
|
||||
|
||||
if(h === "")
|
||||
return [m, s].join(":");
|
||||
|
||||
return [h, m, s].join(":");
|
||||
},
|
||||
|
||||
newRateLimiter: function () {
|
||||
return {
|
||||
count: 0,
|
||||
lastTime: 0,
|
||||
throttle: function (opts) {
|
||||
if (typeof opts === "undefined")
|
||||
opts = {};
|
||||
|
||||
var burst = +opts.burst,
|
||||
sustained = +opts.sustained,
|
||||
cooldown = +opts.cooldown;
|
||||
|
||||
if (isNaN(burst))
|
||||
burst = 10;
|
||||
|
||||
if (isNaN(sustained))
|
||||
sustained = 2;
|
||||
|
||||
if (isNaN(cooldown))
|
||||
cooldown = burst / sustained;
|
||||
|
||||
// Haven't reached burst cap yet, allow
|
||||
if (this.count < burst) {
|
||||
this.count++;
|
||||
this.lastTime = Date.now();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cooled down, allow and clear buffer
|
||||
if (this.lastTime < Date.now() - cooldown*1000) {
|
||||
this.count = 0;
|
||||
this.lastTime = Date.now();
|
||||
return false;
|
||||
}
|
||||
|
||||
var diff = Date.now() - this.lastTime;
|
||||
if (diff < 1000/sustained)
|
||||
return true;
|
||||
|
||||
this.lastTime = Date.now();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue