mirror of https://github.com/calzoneman/sync.git
Merge refactoring into 3.0
This commit is contained in:
parent
91bf6a5062
commit
9ea48f58cf
|
@ -0,0 +1,155 @@
|
|||
var db = require("./database");
|
||||
var Q = require("q");
|
||||
|
||||
function Account(opts) {
|
||||
var defaults = {
|
||||
name: "",
|
||||
ip: "",
|
||||
aliases: [],
|
||||
globalRank: -1,
|
||||
channelRank: -1,
|
||||
guest: true,
|
||||
profile: {
|
||||
image: "",
|
||||
text: ""
|
||||
}
|
||||
};
|
||||
|
||||
this.name = opts.name || defaults.name;
|
||||
this.lowername = this.name.toLowerCase();
|
||||
this.ip = opts.ip || defaults.ip;
|
||||
this.aliases = opts.aliases || defaults.aliases;
|
||||
this.globalRank = "globalRank" in opts ? opts.globalRank : defaults.globalRank;
|
||||
this.channelRank = "channelRank" in opts ? opts.channelRank : defaults.channelRank;
|
||||
this.effectiveRank = Math.max(this.globalRank, this.channelRank);
|
||||
this.guest = this.globalRank === 0;
|
||||
this.profile = opts.profile || defaults.profile;
|
||||
}
|
||||
|
||||
module.exports.default = function (ip) {
|
||||
return new Account({ ip: ip });
|
||||
};
|
||||
|
||||
module.exports.getAccount = function (name, ip, opts, cb) {
|
||||
if (!cb) {
|
||||
cb = opts;
|
||||
opts = {};
|
||||
}
|
||||
opts.channel = opts.channel || false;
|
||||
|
||||
var data = {};
|
||||
Q.nfcall(db.getAliases, ip)
|
||||
.then(function (aliases) {
|
||||
data.aliases = aliases;
|
||||
if (name && opts.registered) {
|
||||
return Q.nfcall(db.users.getGlobalRank, name);
|
||||
} else if (name) {
|
||||
return 0;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}).then(function (globalRank) {
|
||||
data.globalRank = globalRank;
|
||||
if (opts.channel && opts.registered) {
|
||||
return Q.nfcall(db.channels.getRank, opts.channel, name);
|
||||
} else {
|
||||
if (opts.registered) {
|
||||
return 1;
|
||||
} else if (name) {
|
||||
return 0;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}).then(function (chanRank) {
|
||||
data.channelRank = chanRank;
|
||||
/* Look up profile for registered user */
|
||||
if (data.globalRank >= 1) {
|
||||
return Q.nfcall(db.users.getProfile, name);
|
||||
} else {
|
||||
return { text: "", image: "" };
|
||||
}
|
||||
}).then(function (profile) {
|
||||
setImmediate(function () {
|
||||
cb(null, new Account({
|
||||
name: name,
|
||||
ip: ip,
|
||||
aliases: data.aliases,
|
||||
globalRank: data.globalRank,
|
||||
channelRank: data.channelRank,
|
||||
profile: profile
|
||||
}));
|
||||
});
|
||||
}).catch(function (err) {
|
||||
cb(err, null);
|
||||
}).done();
|
||||
};
|
||||
|
||||
module.exports.rankForName = function (name, opts, cb) {
|
||||
if (!cb) {
|
||||
cb = opts;
|
||||
opts = {};
|
||||
}
|
||||
|
||||
var rank = 0;
|
||||
Q.fcall(function () {
|
||||
return Q.nfcall(db.users.getGlobalRank, name);
|
||||
}).then(function (globalRank) {
|
||||
rank = globalRank;
|
||||
if (opts.channel) {
|
||||
return Q.nfcall(db.channels.getRank, opts.channel, name);
|
||||
} else {
|
||||
return globalRank > 0 ? 1 : 0;
|
||||
}
|
||||
}).then(function (chanRank) {
|
||||
setImmediate(function () {
|
||||
cb(null, Math.max(rank, chanRank));
|
||||
});
|
||||
}).catch(function (err) {
|
||||
cb(err, 0);
|
||||
}).done();
|
||||
};
|
||||
|
||||
module.exports.rankForIP = function (ip, opts, cb) {
|
||||
if (!cb) {
|
||||
cb = opts;
|
||||
opts = {};
|
||||
}
|
||||
|
||||
var globalRank, rank, names;
|
||||
|
||||
var promise = Q.nfcall(db.getAliases, ip)
|
||||
.then(function (_names) {
|
||||
names = _names;
|
||||
return Q.nfcall(db.users.getGlobalRanks, names);
|
||||
}).then(function (ranks) {
|
||||
ranks.push(0);
|
||||
globalRank = Math.max.apply(Math, ranks);
|
||||
rank = globalRank;
|
||||
});
|
||||
|
||||
if (!opts.channel) {
|
||||
promise.then(function () {
|
||||
setImmediate(function () {
|
||||
cb(null, globalRank);
|
||||
});
|
||||
}).catch(function (err) {
|
||||
cb(err, null);
|
||||
}).done();
|
||||
} else {
|
||||
promise.then(function () {
|
||||
return Q.nfcall(db.channels.getRanks, opts.channel, names);
|
||||
}).then(function (ranks) {
|
||||
ranks.push(globalRank);
|
||||
rank = Math.max.apply(Math, ranks);
|
||||
}).then(function () {
|
||||
setImmediate(function () {
|
||||
cb(null, rank);
|
||||
});
|
||||
}).catch(function (err) {
|
||||
setImmediate(function () {
|
||||
cb(err, null);
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
};
|
|
@ -17,7 +17,7 @@ var Config = require("./config");
|
|||
var Server = require("./server");
|
||||
|
||||
function eventUsername(user) {
|
||||
return user.name + "@" + user.ip;
|
||||
return user.getName() + "@" + user.ip;
|
||||
}
|
||||
|
||||
function handleAnnounce(user, data) {
|
||||
|
@ -26,7 +26,7 @@ function handleAnnounce(user, data) {
|
|||
sv.announce({
|
||||
title: data.title,
|
||||
text: data.content,
|
||||
from: user.name
|
||||
from: user.getName()
|
||||
});
|
||||
|
||||
Logger.eventlog.log("[acp] " + eventUsername(user) + " opened announcement `" +
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
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 Server = require("./server");
|
||||
|
||||
module.exports = {
|
||||
record: function (ip, name, action, args) {
|
||||
var db = Server.getServer().db;
|
||||
if(!args)
|
||||
args = "";
|
||||
else {
|
||||
try {
|
||||
args = JSON.stringify(args);
|
||||
} catch(e) {
|
||||
args = "";
|
||||
}
|
||||
}
|
||||
|
||||
//db.recordAction(ip, name, action, args);
|
||||
},
|
||||
|
||||
clear: function (actions) {
|
||||
var db = Server.getServer().db;
|
||||
//db.clearActions(actions);
|
||||
},
|
||||
|
||||
clearOne: function (item) {
|
||||
var db = Server.getServer().db;
|
||||
//db.clearSingleAction(item);
|
||||
},
|
||||
|
||||
throttleRegistrations: function (ip, callback) {
|
||||
var db = Server.getServer().db;
|
||||
/*
|
||||
db.recentRegistrationCount(ip, function (err, count) {
|
||||
if(err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, count > 4);
|
||||
});
|
||||
*/
|
||||
},
|
||||
|
||||
listActionTypes: function (callback) {
|
||||
var db = Server.getServer().db;
|
||||
//db.listActionTypes(callback);
|
||||
},
|
||||
|
||||
listActions: function (types, callback) {
|
||||
var db = Server.getServer().db;
|
||||
//db.listActions(types, callback);
|
||||
}
|
||||
};
|
848
lib/api.js
848
lib/api.js
|
@ -1,848 +0,0 @@
|
|||
/*
|
||||
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");
|
||||
var ActionLog = require("./actionlog");
|
||||
|
||||
module.exports = function (Server) {
|
||||
function getIP(req) {
|
||||
var raw = req.connection.remoteAddress;
|
||||
var forward = req.header("x-forwarded-for");
|
||||
if((Server.cfg["trust-x-forward"] || raw === "127.0.0.1") && 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.voteskip_eligible = channel.calcVoteskipMax();
|
||||
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.express;
|
||||
var db = Server.db;
|
||||
|
||||
/* <https://en.wikipedia.org/wiki/Hyper_Text_Coffee_Pot_Control_Protocol> */
|
||||
app.get("/api/coffee", function (req, res) {
|
||||
res.send(418); // 418 I'm a teapot
|
||||
});
|
||||
|
||||
/* REGION channels */
|
||||
|
||||
/* data about a specific channel */
|
||||
app.get("/api/channels/:channel", function (req, res) {
|
||||
var name = req.params.channel;
|
||||
if(!$util.isValidChannelName(name)) {
|
||||
res.send(404);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {
|
||||
name: name,
|
||||
loaded: false
|
||||
};
|
||||
|
||||
var needPassword = false;
|
||||
var chan = null;
|
||||
if (Server.isChannelLoaded(name)) {
|
||||
chan = Server.getChannel(name);
|
||||
data = getChannelData(chan);
|
||||
needPassword = chan.opts.password;
|
||||
}
|
||||
|
||||
if (needPassword !== false) {
|
||||
var pw = req.query.password;
|
||||
if (pw !== needPassword) {
|
||||
var uname = req.cookies.cytube_uname;
|
||||
var session = req.cookies.cytube_session;
|
||||
Server.db.users.verifyAuth(uname + ":" + session, function (err, row) {
|
||||
if (err) {
|
||||
res.status(403);
|
||||
res.type("application/json");
|
||||
res.jsonp({
|
||||
error: "Password required to view this channel"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (chan !== null) {
|
||||
chan.getRank(uname, function (err, rank) {
|
||||
if (err || rank < 2) {
|
||||
res.status(403);
|
||||
res.type("application/json");
|
||||
res.jsonp({
|
||||
error: "Password required to view this channel"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.type("application/json");
|
||||
res.jsonp(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.type("application/json");
|
||||
res.jsonp(data);
|
||||
});
|
||||
|
||||
/* data about all channels (filter= public or all) */
|
||||
app.get("/api/allchannels/:filter", function (req, res) {
|
||||
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.users.verifyAuth(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 && channel.opts.password === false)
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
var callback = function (err, row) {
|
||||
if(err) {
|
||||
if(err !== "Session expired")
|
||||
ActionLog.record(getIP(req), name, "login-failure", err);
|
||||
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.hash
|
||||
});
|
||||
};
|
||||
|
||||
if (session) {
|
||||
db.users.verifyAuth(name + ":" + session, callback);
|
||||
} else {
|
||||
db.users.verifyLogin(name, pw, callback);
|
||||
}
|
||||
});
|
||||
|
||||
/* register an account */
|
||||
app.post("/api/register", function (req, res) {
|
||||
res.type("application/jsonp");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
var name = req.body.name;
|
||||
var pw = req.body.pw;
|
||||
if (typeof name !== "string" ||
|
||||
typeof pw !== "string") {
|
||||
res.status(400);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid request"
|
||||
});
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// db.registerUser checks if the name is taken already
|
||||
db.users.register(name, pw, "", req.ip, 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) {
|
||||
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 (typeof name !== "string" ||
|
||||
typeof oldpw !== "string" ||
|
||||
typeof newpw !== "string") {
|
||||
res.status(400);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid request"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!oldpw || !newpw) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Password cannot be empty"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.verifyLogin(name, oldpw, function (err, row) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.setPassword(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) {
|
||||
res.type("application/jsonp");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
var name = req.body.name;
|
||||
var email = req.body.email;
|
||||
if (typeof name !== "string" ||
|
||||
typeof email !== "string") {
|
||||
res.status(400);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid request"
|
||||
});
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
res.type("application/jsonp");
|
||||
var hash = req.query.hash;
|
||||
if (typeof hash !== "string") {
|
||||
res.status(400);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid request"
|
||||
});
|
||||
return;
|
||||
}
|
||||
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, auth.name, "password-recover-success");
|
||||
res.jsonp({
|
||||
success: true,
|
||||
name: auth.name,
|
||||
pw: auth.pw
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* profile retrieval */
|
||||
app.get("/api/users/:user/profile", function (req, res) {
|
||||
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) {
|
||||
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;
|
||||
if (typeof name !== "string" ||
|
||||
typeof session !== "string" ||
|
||||
typeof img !== "string" ||
|
||||
typeof text !== "string") {
|
||||
res.status(400);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid request"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (img.length > 255) {
|
||||
img = img.substring(0, 255);
|
||||
}
|
||||
|
||||
if (text.length > 255) {
|
||||
text = text.substring(0, 255);
|
||||
}
|
||||
|
||||
db.verifyAuth(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.sendAll("setUserProfile", {
|
||||
name: user.name,
|
||||
profile: user.profile
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* set email */
|
||||
app.post("/api/account/email", function (req, res) {
|
||||
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 (typeof name !== "string" ||
|
||||
typeof pw !== "string" ||
|
||||
typeof email !== "string") {
|
||||
res.status(400);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid request"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
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.users.verifyLogin(name, pw, function (err, row) {
|
||||
if(err) {
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.setEmail(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.hash
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* my channels */
|
||||
app.get("/api/account/mychannels", function (req, res) {
|
||||
res.type("application/jsonp");
|
||||
var name = req.query.name;
|
||||
var session = req.query.session;
|
||||
|
||||
if (typeof name !== "string" ||
|
||||
typeof session !== "string") {
|
||||
res.status(400);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid request"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.verifyAuth(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) {
|
||||
res.type("application/jsonp");
|
||||
var name = req.query.name;
|
||||
var session = req.query.session;
|
||||
var types = req.query.actions;
|
||||
|
||||
if (typeof name !== "string" ||
|
||||
typeof session !== "string" ||
|
||||
typeof types !== "string") {
|
||||
res.status(400);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid request"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.verifyAuth(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) {
|
||||
res.type("text/plain");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
var name = req.query.name;
|
||||
var session = req.query.session;
|
||||
|
||||
if (typeof name !== "string" ||
|
||||
typeof session !== "string") {
|
||||
res.status(400);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid request"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.verifyAuth(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) {
|
||||
res.type("text/plain");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
var name = req.query.name;
|
||||
var session = req.query.session;
|
||||
|
||||
if (typeof name !== "string" ||
|
||||
typeof session !== "string") {
|
||||
res.status(400);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid request"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.verifyAuth(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) {
|
||||
res.type("text/plain");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
var name = req.query.name;
|
||||
var session = req.query.session;
|
||||
|
||||
if (typeof name !== "string" ||
|
||||
typeof session !== "string") {
|
||||
res.status(400);
|
||||
res.jsonp({
|
||||
success: false,
|
||||
error: "Invalid request"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.users.verifyAuth(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;
|
||||
}
|
3405
lib/channel.js
3405
lib/channel.js
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,70 @@
|
|||
var Account = require("../account");
|
||||
var ChannelModule = require("./module");
|
||||
var Flags = require("../flags");
|
||||
|
||||
function AccessControlModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
}
|
||||
|
||||
AccessControlModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
var pending = 0;
|
||||
AccessControlModule.prototype.onUserPreJoin = function (user, data, cb) {
|
||||
var chan = this.channel,
|
||||
opts = this.channel.modules.options;
|
||||
var self = this;
|
||||
if (user.socket.disconnected) {
|
||||
return cb("User disconnected", ChannelModule.DENY);
|
||||
}
|
||||
|
||||
if (opts.get("password") !== false && data.pw !== opts.get("password")) {
|
||||
user.socket.on("disconnect", function () {
|
||||
if (!user.is(Flags.U_IN_CHANNEL)) {
|
||||
cb("User disconnected", ChannelModule.DENY);
|
||||
}
|
||||
});
|
||||
|
||||
if (user.is(Flags.U_LOGGED_IN) && user.account.effectiveRank >= 2) {
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
user.socket.emit("cancelNeedPassword");
|
||||
} else {
|
||||
user.socket.emit("needPassword", typeof data.pw !== "undefined");
|
||||
/* Option 1: log in as a moderator */
|
||||
user.waitFlag(Flags.U_LOGGED_IN, function () {
|
||||
user.refreshAccount({ channel: self.channel.name }, function (err, account) {
|
||||
|
||||
/* Already joined the channel by some other condition */
|
||||
if (user.is(Flags.U_IN_CHANNEL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (account.effectiveRank >= 2) {
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
user.socket.emit("cancelNeedPassword");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* Option 2: Enter correct password */
|
||||
var pwListener = function (pw) {
|
||||
if (chan.dead || user.is(Flags.U_IN_CHANNEL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pw !== opts.get("password")) {
|
||||
user.socket.emit("needPassword", true);
|
||||
return;
|
||||
}
|
||||
|
||||
user.socket.emit("cancelNeedPassword");
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
};
|
||||
|
||||
user.socket.on("channelPassword", pwListener);
|
||||
}
|
||||
} else {
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AccessControlModule;
|
|
@ -0,0 +1,623 @@
|
|||
var MakeEmitter = require("../emitter");
|
||||
var Logger = require("../logger");
|
||||
var ChannelModule = require("./module");
|
||||
var Flags = require("../flags");
|
||||
var Account = require("../account");
|
||||
var util = require("../utilities");
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var sio = require("socket.io");
|
||||
var db = require("../database");
|
||||
|
||||
/**
|
||||
* Previously, async channel functions were riddled with race conditions due to
|
||||
* an event causing the channel to be unloaded while a pending callback still
|
||||
* needed to reference it.
|
||||
*
|
||||
* This solution should be better than constantly checking whether the channel
|
||||
* has been unloaded in nested callbacks. The channel won't be unloaded until
|
||||
* nothing needs it anymore. Conceptually similar to a reference count.
|
||||
*/
|
||||
function ActiveLock(channel) {
|
||||
this.channel = channel;
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
ActiveLock.prototype = {
|
||||
lock: function () {
|
||||
this.count++;
|
||||
//console.log('dbg: lock/count: ', this.count);
|
||||
//console.trace();
|
||||
},
|
||||
|
||||
release: function () {
|
||||
this.count--;
|
||||
//console.log('dbg: release/count: ', this.count);
|
||||
//console.trace();
|
||||
if (this.count === 0) {
|
||||
/* sanity check */
|
||||
if (this.channel.users.length > 0) {
|
||||
Logger.errlog.log("Warning: ActiveLock count=0 but users.length > 0 (" +
|
||||
"channel: " + this.channel.name + ")");
|
||||
this.count = this.channel.users.length;
|
||||
} else {
|
||||
this.channel.emit("empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function Channel(name) {
|
||||
MakeEmitter(this);
|
||||
this.name = name;
|
||||
this.uniqueName = name.toLowerCase();
|
||||
this.modules = {};
|
||||
this.logger = new Logger.Logger(path.join(__dirname, "..", "..", "chanlogs",
|
||||
this.uniqueName));
|
||||
this.users = [];
|
||||
this.activeLock = new ActiveLock(this);
|
||||
this.flags = 0;
|
||||
var self = this;
|
||||
db.channels.load(this, function (err) {
|
||||
if (err && err !== "Channel is not registered") {
|
||||
return;
|
||||
} else {
|
||||
self.initModules();
|
||||
self.loadState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Channel.prototype.is = function (flag) {
|
||||
return Boolean(this.flags & flag);
|
||||
};
|
||||
|
||||
Channel.prototype.setFlag = function (flag) {
|
||||
this.flags |= flag;
|
||||
this.emit("setFlag", flag);
|
||||
};
|
||||
|
||||
Channel.prototype.clearFlag = function (flag) {
|
||||
this.flags &= ~flag;
|
||||
this.emit("clearFlag", flag);
|
||||
};
|
||||
|
||||
Channel.prototype.waitFlag = function (flag, cb) {
|
||||
var self = this;
|
||||
if (self.is(flag)) {
|
||||
cb();
|
||||
} else {
|
||||
var wait = function () {
|
||||
if (self.is(flag)) {
|
||||
self.unbind("setFlag", wait);
|
||||
cb();
|
||||
}
|
||||
};
|
||||
self.on("setFlag", wait);
|
||||
}
|
||||
};
|
||||
|
||||
Channel.prototype.moderators = function () {
|
||||
return this.users.filter(function (u) {
|
||||
return u.account.effectiveRank >= 2;
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.initModules = function () {
|
||||
const modules = {
|
||||
"./permissions" : "permissions",
|
||||
"./chat" : "chat",
|
||||
"./filters" : "filters",
|
||||
"./emotes" : "emotes",
|
||||
"./customization" : "customization",
|
||||
"./opts" : "options",
|
||||
"./library" : "library",
|
||||
"./playlist" : "playlist",
|
||||
"./voteskip" : "voteskip",
|
||||
"./poll" : "poll",
|
||||
"./kickban" : "kickban",
|
||||
"./ranks" : "rank",
|
||||
"./accesscontrol" : "password"
|
||||
};
|
||||
|
||||
var self = this;
|
||||
var inited = [];
|
||||
Object.keys(modules).forEach(function (m) {
|
||||
var ctor = require(m);
|
||||
var module = new ctor(self);
|
||||
self.modules[modules[m]] = module;
|
||||
inited.push(modules[m]);
|
||||
});
|
||||
|
||||
self.logger.log("[init] Loaded modules: " + inited.join(", "));
|
||||
};
|
||||
|
||||
Channel.prototype.loadState = function () {
|
||||
var self = this;
|
||||
var file = path.join(__dirname, "..", "..", "chandump", self.uniqueName);
|
||||
|
||||
/* Don't load from disk if not registered */
|
||||
if (!self.is(Flags.C_REGISTERED)) {
|
||||
self.modules.permissions.loadUnregistered();
|
||||
self.setFlag(Flags.C_READY);
|
||||
return;
|
||||
}
|
||||
|
||||
var errorLoad = function (msg) {
|
||||
if (self.modules.customization) {
|
||||
self.modules.customization.load({
|
||||
motd: {
|
||||
motd: msg,
|
||||
html: msg
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.setFlag(Flags.C_ERROR);
|
||||
};
|
||||
|
||||
fs.stat(file, function (err, stats) {
|
||||
if (!err) {
|
||||
var mb = stats.size / 1048576;
|
||||
mb = Math.floor(mb * 100) / 100;
|
||||
if (mb > 1) {
|
||||
Logger.errlog.log("Large chandump detected: " + self.uniqueName +
|
||||
" (" + mb + " MiB)");
|
||||
var msg = "This channel's state size has exceeded the memory limit " +
|
||||
"enforced by this server. Please contact an administrator " +
|
||||
"for assistance.";
|
||||
errorLoad(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
continueLoad();
|
||||
});
|
||||
|
||||
var continueLoad = function () {
|
||||
fs.readFile(file, function (err, data) {
|
||||
if (err) {
|
||||
/* ENOENT means the file didn't exist. This is normal for new channels */
|
||||
if (err.code === "ENOENT") {
|
||||
self.setFlag(Flags.C_READY);
|
||||
Object.keys(self.modules).forEach(function (m) {
|
||||
self.modules[m].load({});
|
||||
});
|
||||
} else {
|
||||
Logger.errlog.log("Failed to open channel dump " + self.uniqueName);
|
||||
Logger.errlog.log(err);
|
||||
errorLoad("Unknown error occurred when loading channel state. " +
|
||||
"Contact an administrator for assistance.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self.logger.log("[init] Loading channel state from disk");
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
Object.keys(self.modules).forEach(function (m) {
|
||||
self.modules[m].load(data);
|
||||
});
|
||||
self.setFlag(Flags.C_READY);
|
||||
} catch (e) {
|
||||
Logger.errlog.log("Channel dump for " + self.uniqueName + " is not " +
|
||||
"valid");
|
||||
Logger.errlog.log(e);
|
||||
errorLoad("Unknown error occurred when loading channel state. Contact " +
|
||||
"an administrator for assistance.");
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
Channel.prototype.saveState = function () {
|
||||
var self = this;
|
||||
var file = path.join(__dirname, "..", "..", "chandump", self.uniqueName);
|
||||
|
||||
/**
|
||||
* Don't overwrite saved state data if the current state is dirty,
|
||||
* or if this channel is unregistered
|
||||
*/
|
||||
if (self.is(Flags.C_ERROR) || !self.is(Flags.C_REGISTERED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.logger.log("[init] Saving channel state to disk");
|
||||
var data = {};
|
||||
Object.keys(this.modules).forEach(function (m) {
|
||||
self.modules[m].save(data);
|
||||
});
|
||||
|
||||
var json = JSON.stringify(data);
|
||||
/**
|
||||
* Synchronous on purpose.
|
||||
* When the server is shutting down, saveState() is called on all channels and
|
||||
* then the process terminates. Async writeFile causes a race condition that wipes
|
||||
* channels.
|
||||
*/
|
||||
var err = fs.writeFileSync(file, json);
|
||||
};
|
||||
|
||||
Channel.prototype.checkModules = function (fn, args, cb) {
|
||||
var self = this;
|
||||
this.waitFlag(Flags.C_READY, function () {
|
||||
self.activeLock.lock();
|
||||
var keys = Object.keys(self.modules);
|
||||
var next = function (err, result) {
|
||||
if (result !== ChannelModule.PASSTHROUGH) {
|
||||
/* Either an error occured, or the module denied the user access */
|
||||
cb(err, result);
|
||||
self.activeLock.release();
|
||||
return;
|
||||
}
|
||||
|
||||
var m = keys.shift();
|
||||
if (m === undefined) {
|
||||
/* No more modules to check */
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
self.activeLock.release();
|
||||
return;
|
||||
}
|
||||
|
||||
var module = self.modules[m];
|
||||
module[fn].apply(module, args);
|
||||
};
|
||||
|
||||
args.push(next);
|
||||
next(null, ChannelModule.PASSTHROUGH);
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.notifyModules = function (fn, args) {
|
||||
var self = this;
|
||||
this.waitFlag(Flags.C_READY, function () {
|
||||
var keys = Object.keys(self.modules);
|
||||
keys.forEach(function (k) {
|
||||
self.modules[k][fn].apply(self.modules[k], args);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.joinUser = function (user, data) {
|
||||
var self = this;
|
||||
|
||||
self.waitFlag(Flags.C_READY, function () {
|
||||
/* User closed the connection before the channel finished loading */
|
||||
if (user.socket.disconnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.is(Flags.C_REGISTERED)) {
|
||||
user.refreshAccount({ channel: self.name }, function (err, account) {
|
||||
if (err) {
|
||||
Logger.errlog.log("user.refreshAccount failed at Channel.joinUser");
|
||||
Logger.errlog.log(err.stack);
|
||||
return;
|
||||
}
|
||||
|
||||
afterAccount();
|
||||
});
|
||||
} else {
|
||||
afterAccount();
|
||||
}
|
||||
|
||||
function afterAccount() {
|
||||
if (self.dead || user.socket.disconnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.checkModules("onUserPreJoin", [user, data], function (err, result) {
|
||||
if (result === ChannelModule.PASSTHROUGH) {
|
||||
if (user.account.channelRank !== user.account.globalRank) {
|
||||
user.socket.emit("rank", user.account.effectiveRank);
|
||||
}
|
||||
self.activeLock.lock();
|
||||
self.acceptUser(user);
|
||||
} else {
|
||||
user.account.channelRank = 0;
|
||||
user.account.effectiveRank = user.account.globalRank;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.acceptUser = function (user) {
|
||||
user.channel = this;
|
||||
user.setFlag(Flags.U_IN_CHANNEL);
|
||||
user.socket.join(this.name);
|
||||
user.autoAFK();
|
||||
user.socket.on("readChanLog", this.handleReadLog.bind(this, user));
|
||||
|
||||
Logger.syslog.log(user.ip + " joined " + this.name);
|
||||
this.logger.log("[login] Accepted connection from " + user.longip);
|
||||
if (user.is(Flags.U_LOGGED_IN)) {
|
||||
this.logger.log("[login] " + user.longip + " authenticated as " + user.getName());
|
||||
}
|
||||
|
||||
var self = this;
|
||||
user.waitFlag(Flags.U_LOGGED_IN, function () {
|
||||
for (var i = 0; i < self.users.length; i++) {
|
||||
if (self.users[i] !== user &&
|
||||
self.users[i].getLowerName() === user.getLowerName()) {
|
||||
self.users[i].kick("Duplicate login");
|
||||
}
|
||||
}
|
||||
self.sendUserJoin(self.users, user);
|
||||
});
|
||||
|
||||
this.users.push(user);
|
||||
|
||||
user.socket.on("disconnect", this.partUser.bind(this, user));
|
||||
Object.keys(this.modules).forEach(function (m) {
|
||||
self.modules[m].onUserPostJoin(user);
|
||||
});
|
||||
|
||||
this.sendUserlist([user]);
|
||||
this.sendUsercount(this.users);
|
||||
if (!this.is(Flags.C_REGISTERED)) {
|
||||
user.socket.emit("channelNotRegistered");
|
||||
}
|
||||
};
|
||||
|
||||
Channel.prototype.partUser = function (user) {
|
||||
this.logger.log("[login] " + user.longip + " (" + user.getName() + ") " +
|
||||
"disconnected.");
|
||||
user.channel = null;
|
||||
/* Should be unnecessary because partUser only occurs if the socket dies */
|
||||
user.clearFlag(Flags.U_IN_CHANNEL);
|
||||
|
||||
if (user.is(Flags.U_LOGGED_IN)) {
|
||||
this.users.forEach(function (u) {
|
||||
u.socket.emit("userLeave", { name: user.getName() });
|
||||
});
|
||||
}
|
||||
|
||||
var idx = this.users.indexOf(user);
|
||||
if (idx >= 0) {
|
||||
this.users.splice(idx, 1);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
Object.keys(this.modules).forEach(function (m) {
|
||||
self.modules[m].onUserPart(user);
|
||||
});
|
||||
this.sendUserLeave(this.users, user);
|
||||
this.sendUsercount(this.users);
|
||||
|
||||
this.activeLock.release();
|
||||
user.die();
|
||||
};
|
||||
|
||||
Channel.prototype.packUserData = function (user) {
|
||||
var base = {
|
||||
name: user.getName(),
|
||||
rank: user.account.effectiveRank,
|
||||
profile: user.account.profile,
|
||||
meta: {
|
||||
afk: user.is(Flags.U_AFK),
|
||||
muted: user.is(Flags.U_MUTED) && !user.is(Flags.U_SMUTED)
|
||||
}
|
||||
};
|
||||
|
||||
var mod = {
|
||||
name: user.getName(),
|
||||
rank: user.account.effectiveRank,
|
||||
profile: user.account.profile,
|
||||
meta: {
|
||||
afk: user.is(Flags.U_AFK),
|
||||
muted: user.is(Flags.U_MUTED),
|
||||
smuted: user.is(Flags.U_SMUTED),
|
||||
aliases: user.account.aliases,
|
||||
ip: util.maskIP(user.longip)
|
||||
}
|
||||
};
|
||||
|
||||
var sadmin = {
|
||||
name: user.getName(),
|
||||
rank: user.account.effectiveRank,
|
||||
profile: user.account.profile,
|
||||
meta: {
|
||||
afk: user.is(Flags.U_AFK),
|
||||
muted: user.is(Flags.U_MUTED),
|
||||
smuted: user.is(Flags.U_SMUTED),
|
||||
aliases: user.account.aliases,
|
||||
ip: user.ip
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
base: base,
|
||||
mod: mod,
|
||||
sadmin: sadmin
|
||||
};
|
||||
};
|
||||
|
||||
Channel.prototype.sendUserMeta = function (users, user, minrank) {
|
||||
var self = this;
|
||||
var userdata = self.packUserData(user);
|
||||
users.filter(function (u) {
|
||||
return typeof minrank !== "number" || u.account.effectiveRank > minrank
|
||||
}).forEach(function (u) {
|
||||
if (u.account.globalRank >= 255) {
|
||||
u.socket.emit("setUserMeta", {
|
||||
name: user.getName(),
|
||||
meta: userdata.sadmin.meta
|
||||
});
|
||||
} else if (u.account.effectiveRank >= 2) {
|
||||
u.socket.emit("setUserMeta", {
|
||||
name: user.getName(),
|
||||
meta: userdata.mod.meta
|
||||
});
|
||||
} else {
|
||||
u.socket.emit("setUserMeta", {
|
||||
name: user.getName(),
|
||||
meta: userdata.base.meta
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.sendUserProfile = function (users, user) {
|
||||
var packet = {
|
||||
name: user.getName(),
|
||||
profile: user.account.profile
|
||||
};
|
||||
|
||||
users.forEach(function (u) {
|
||||
u.socket.emit("setUserProfile", packet);
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.sendUserlist = function (toUsers) {
|
||||
var self = this;
|
||||
var base = [];
|
||||
var mod = [];
|
||||
var sadmin = [];
|
||||
|
||||
for (var i = 0; i < self.users.length; i++) {
|
||||
var u = self.users[i];
|
||||
if (u.getName() === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
var data = self.packUserData(self.users[i]);
|
||||
base.push(data.base);
|
||||
mod.push(data.mod);
|
||||
sadmin.push(data.sadmin);
|
||||
}
|
||||
|
||||
toUsers.forEach(function (u) {
|
||||
if (u.account.globalRank >= 255) {
|
||||
u.socket.emit("userlist", sadmin);
|
||||
} else if (u.account.effectiveRank >= 2) {
|
||||
u.socket.emit("userlist", mod);
|
||||
} else {
|
||||
u.socket.emit("userlist", base);
|
||||
}
|
||||
|
||||
if (self.leader != null) {
|
||||
u.socket.emit("setLeader", self.leader.name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.sendUsercount = function (users) {
|
||||
var self = this;
|
||||
users.forEach(function (u) {
|
||||
u.socket.emit("usercount", self.users.length);
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.sendUserJoin = function (users, user) {
|
||||
var self = this;
|
||||
if (user.account.aliases.length === 0) {
|
||||
user.account.aliases.push(user.getName());
|
||||
}
|
||||
|
||||
var data = self.packUserData(user);
|
||||
|
||||
users.forEach(function (u) {
|
||||
if (u.account.globalRank >= 255) {
|
||||
u.socket.emit("addUser", data.sadmin);
|
||||
} else if (u.account.effectiveRank >= 2) {
|
||||
u.socket.emit("addUser", data.mod);
|
||||
} else {
|
||||
u.socket.emit("addUser", data.base);
|
||||
}
|
||||
});
|
||||
|
||||
self.modules.chat.sendModMessage(user.getName() + " joined (aliases: " +
|
||||
user.account.aliases.join(",") + ")", 2);
|
||||
};
|
||||
|
||||
Channel.prototype.sendUserLeave = function (users, user) {
|
||||
var data = {
|
||||
name: user.getName()
|
||||
};
|
||||
|
||||
users.forEach(function (u) {
|
||||
u.socket.emit("userLeave", data);
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.readLog = function (shouldMaskIP, cb) {
|
||||
var maxLen = 102400;
|
||||
var file = this.logger.filename;
|
||||
this.activeLock.lock();
|
||||
var self = this;
|
||||
fs.stat(file, function (err, data) {
|
||||
if (err) {
|
||||
self.activeLock.release();
|
||||
return cb(err, null);
|
||||
}
|
||||
|
||||
var start = Math.max(data.size - maxLen, 0);
|
||||
var end = data.size - 1;
|
||||
|
||||
var read = fs.createReadStream(file, {
|
||||
start: start,
|
||||
end: end
|
||||
});
|
||||
|
||||
var buffer = "";
|
||||
read.on("data", function (data) {
|
||||
buffer += data;
|
||||
});
|
||||
read.on("end", function () {
|
||||
if (shouldMaskIP) {
|
||||
buffer = buffer.replace(
|
||||
/^(\d+\.\d+\.\d+)\.\d+/g,
|
||||
"$1.x"
|
||||
).replace(
|
||||
/^((?:[0-9a-f]+:){3}[0-9a-f]+):(?:[0-9a-f]+:){3}[0-9a-f]+$/,
|
||||
"$1:x:x:x:x"
|
||||
);
|
||||
}
|
||||
|
||||
cb(null, buffer);
|
||||
self.activeLock.release();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.handleReadLog = function (user) {
|
||||
if (user.account.effectiveRank < 3) {
|
||||
user.kick("Attempted readChanLog with insufficient permission");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.is(Flags.C_REGISTERED)) {
|
||||
user.socket.emit("readChanLog", {
|
||||
success: false,
|
||||
data: "Channel log is only available to registered channels."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var shouldMaskIP = user.account.globalRank < 255;
|
||||
this.readLog(shouldMaskIP, function (err, data) {
|
||||
if (err) {
|
||||
user.socket.emit("readChanLog", {
|
||||
success: false,
|
||||
data: "Error reading channel log"
|
||||
});
|
||||
} else {
|
||||
user.socket.emit("readChanLog", {
|
||||
success: true,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype._broadcast = function (msg, data, ns) {
|
||||
sio.ioServers.forEach(function (io) {
|
||||
io.sockets.in(ns).emit(msg, data);
|
||||
});
|
||||
};
|
||||
|
||||
Channel.prototype.broadcastAll = function (msg, data) {
|
||||
this._broadcast(msg, data, this.name);
|
||||
};
|
||||
|
||||
module.exports = Channel;
|
|
@ -0,0 +1,527 @@
|
|||
var User = require("../user");
|
||||
var XSS = require("../xss");
|
||||
var ChannelModule = require("./module");
|
||||
var util = require("../utilities");
|
||||
var Flags = require("../flags");
|
||||
var url = require("url");
|
||||
|
||||
const SHADOW_TAG = "[shadow]";
|
||||
const LINK = /(\w+:\/\/(?:[^:\/\[\]\s]+|\[[0-9a-f:]+\])(?::\d+)?(?:\/[^\/\s]*)*)/ig;
|
||||
const TYPE_CHAT = {
|
||||
msg: "string",
|
||||
meta: "object,optional"
|
||||
};
|
||||
|
||||
const TYPE_PM = {
|
||||
msg: "string",
|
||||
to: "string",
|
||||
meta: "object,optional"
|
||||
};
|
||||
|
||||
function ChatModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
this.buffer = [];
|
||||
this.muted = new util.Set();
|
||||
this.commandHandlers = {};
|
||||
|
||||
/* Default commands */
|
||||
this.registerCommand("/me", this.handleCmdMe.bind(this));
|
||||
this.registerCommand("/sp", this.handleCmdSp.bind(this));
|
||||
this.registerCommand("/say", this.handleCmdSay.bind(this));
|
||||
this.registerCommand("/shout", this.handleCmdSay.bind(this));
|
||||
this.registerCommand("/clear", this.handleCmdClear.bind(this));
|
||||
this.registerCommand("/a", this.handleCmdAdminflair.bind(this));
|
||||
this.registerCommand("/afk", this.handleCmdAfk.bind(this));
|
||||
this.registerCommand("/mute", this.handleCmdMute.bind(this));
|
||||
this.registerCommand("/smute", this.handleCmdSMute.bind(this));
|
||||
this.registerCommand("/unmute", this.handleCmdUnmute.bind(this));
|
||||
this.registerCommand("/unsmute", this.handleCmdUnmute.bind(this));
|
||||
}
|
||||
|
||||
ChatModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
ChatModule.prototype.load = function (data) {
|
||||
this.buffer = [];
|
||||
this.muted = new util.Set();
|
||||
|
||||
if ("chatbuffer" in data) {
|
||||
for (var i = 0; i < data.chatbuffer.length; i++) {
|
||||
this.buffer.push(data.chatbuffer[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if ("chatmuted" in data) {
|
||||
for (var i = 0; i < data.chatmuted.length; i++) {
|
||||
this.muted.add(data.chatmuted[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ChatModule.prototype.save = function (data) {
|
||||
data.chatbuffer = this.buffer;
|
||||
data.chatmuted = Array.prototype.slice.call(this.muted);
|
||||
};
|
||||
|
||||
ChatModule.prototype.onUserPostJoin = function (user) {
|
||||
var self = this;
|
||||
user.waitFlag(Flags.U_LOGGED_IN, function () {
|
||||
var muteperm = self.channel.modules.permissions.permissions.mute;
|
||||
if (self.isShadowMuted(user.getName())) {
|
||||
user.setFlag(Flags.U_SMUTED | Flags.U_MUTED);
|
||||
self.channel.sendUserMeta(self.channel.users, user, muteperm);
|
||||
} else if (self.isMuted(user.getName())) {
|
||||
user.setFlag(Flags.U_MUTED);
|
||||
self.channel.sendUserMeta(self.channel.users, user, muteperm);
|
||||
}
|
||||
});
|
||||
|
||||
user.socket.typecheckedOn("chatMsg", TYPE_CHAT, this.handleChatMsg.bind(this, user));
|
||||
user.socket.typecheckedOn("pm", TYPE_PM, this.handlePm.bind(this, user));
|
||||
this.buffer.forEach(function (msg) {
|
||||
user.socket.emit("chatMsg", msg);
|
||||
});
|
||||
};
|
||||
|
||||
ChatModule.prototype.isMuted = function (name) {
|
||||
return this.muted.contains(name.toLowerCase()) ||
|
||||
this.muted.contains(SHADOW_TAG + name.toLowerCase());
|
||||
};
|
||||
|
||||
ChatModule.prototype.mutedUsers = function () {
|
||||
var self = this;
|
||||
return self.channel.users.filter(function (u) {
|
||||
return self.isMuted(u.getName());
|
||||
});
|
||||
};
|
||||
|
||||
ChatModule.prototype.isShadowMuted = function (name) {
|
||||
return this.muted.contains(SHADOW_TAG + name.toLowerCase());
|
||||
};
|
||||
|
||||
ChatModule.prototype.shadowMutedUsers = function () {
|
||||
var self = this;
|
||||
return self.channel.users.filter(function (u) {
|
||||
return self.isShadowMuted(u.getName());
|
||||
});
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleChatMsg = function (user, data) {
|
||||
var self = this;
|
||||
|
||||
if (!this.channel.modules.permissions.canChat(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.msg = data.msg.substring(0, 240);
|
||||
|
||||
if (!user.is(Flags.U_LOGGED_IN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var meta = {};
|
||||
data.meta = data.meta || {};
|
||||
if (user.account.effectiveRank >= 2) {
|
||||
if ("modflair" in data.meta && data.meta.modflair === user.account.effectiveRank) {
|
||||
meta.modflair = data.meta.modflair;
|
||||
}
|
||||
}
|
||||
data.meta = meta;
|
||||
|
||||
this.channel.checkModules("onUserChat", [user, data], function (err, result) {
|
||||
if (result === ChannelModule.PASSTHROUGH) {
|
||||
self.processChatMsg(user, data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ChatModule.prototype.handlePm = function (user, data) {
|
||||
var reallyTo = data.to;
|
||||
data.to = data.to.toLowerCase();
|
||||
|
||||
if (data.to === user.getLowerName()) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "You can't PM yourself!"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!util.isValidUserName(data.to)) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "PM failed: " + data.to + " isn't a valid username."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = data.msg.substring(0, 240);
|
||||
var to = null;
|
||||
for (var i = 0; i < this.channel.users.length; i++) {
|
||||
if (this.channel.users[i].getLowerName() === data.to) {
|
||||
to = this.channel.users[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!to) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "PM failed: " + data.to + " isn't connected to this channel."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var meta = {};
|
||||
data.meta = data.meta || {};
|
||||
if (user.rank >= 2) {
|
||||
if ("modflair" in data.meta && data.meta.modflair === user.rank) {
|
||||
meta.modflair = data.meta.modflair;
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.indexOf(">") === 0) {
|
||||
meta.addClass = "greentext";
|
||||
}
|
||||
|
||||
data.meta = meta;
|
||||
var msgobj = this.formatMessage(user.getName(), data);
|
||||
msgobj.to = to.getName();
|
||||
|
||||
to.socket.emit("pm", msgobj);
|
||||
user.socket.emit("pm", msgobj);
|
||||
};
|
||||
|
||||
ChatModule.prototype.processChatMsg = function (user, data) {
|
||||
if (data.msg.indexOf("/afk") !== 0) {
|
||||
user.setAFK(false);
|
||||
}
|
||||
|
||||
var msgobj = this.formatMessage(user.getName(), data);
|
||||
if (this.channel.modules.options &&
|
||||
this.channel.modules.options.get("chat_antiflood") &&
|
||||
user.account.effectiveRank < 2) {
|
||||
|
||||
var antiflood = this.channel.modules.options.get("chat_antiflood_params");
|
||||
if (user.chatLimiter.throttle(antiflood)) {
|
||||
user.socket.emit("cooldown", 1000 / antiflood.sustained);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (user.is(Flags.U_SMUTED)) {
|
||||
this.shadowMutedUsers().forEach(function (u) {
|
||||
u.socket.emit("chatMsg", msgobj);
|
||||
});
|
||||
msgobj.meta.shadow = true;
|
||||
this.channel.moderators().forEach(function (u) {
|
||||
u.socket.emit("chatMsg", msgobj);
|
||||
});
|
||||
return;
|
||||
} else if (user.is(Flags.U_MUTED)) {
|
||||
user.socket.emit("noflood", {
|
||||
action: "chat",
|
||||
msg: "You have been muted on this channel."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.msg.indexOf("/") === 0) {
|
||||
var space = data.msg.indexOf(" ");
|
||||
var cmd;
|
||||
if (space < 0) {
|
||||
cmd = data.msg.substring(1);
|
||||
} else {
|
||||
cmd = data.msg.substring(1, space);
|
||||
}
|
||||
|
||||
if (cmd in this.commandHandlers) {
|
||||
this.commandHandlers[cmd](user, data.msg, data.meta);
|
||||
} else {
|
||||
this.sendMessage(msgobj);
|
||||
}
|
||||
} else {
|
||||
if (data.msg.indexOf(">") === 0) {
|
||||
msgobj.meta.addClass = "greentext";
|
||||
}
|
||||
this.sendMessage(msgobj);
|
||||
}
|
||||
};
|
||||
|
||||
ChatModule.prototype.formatMessage = function (username, data) {
|
||||
var msg = XSS.sanitizeText(data.msg);
|
||||
if (this.channel.modules.filters) {
|
||||
msg = this.filterMessage(msg);
|
||||
}
|
||||
var obj = {
|
||||
username: username,
|
||||
msg: msg,
|
||||
meta: data.meta,
|
||||
time: Date.now()
|
||||
};
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
const link = /(\w+:\/\/(?:[^:\/\[\]\s]+|\[[0-9a-f:]+\])(?::\d+)?(?:\/[^\/\s]*)*)/ig;
|
||||
ChatModule.prototype.filterMessage = function (msg) {
|
||||
var filters = this.channel.modules.filters.filters;
|
||||
var chan = this.channel;
|
||||
var parts = msg.split(link);
|
||||
var convertLinks = this.channel.modules.options.get("enable_link_regex");
|
||||
|
||||
for (var j = 0; j < parts.length; j++) {
|
||||
/* substring is a URL */
|
||||
if (convertLinks && parts[j].match(link)) {
|
||||
var original = parts[j];
|
||||
parts[j] = filters.exec(parts[j], { filterlinks: true });
|
||||
|
||||
/* no filters changed the URL, apply link filter */
|
||||
if (parts[j] === original) {
|
||||
parts[j] = url.format(url.parse(parts[j]));
|
||||
parts[j] = parts[j].replace(link, "<a href=\"$1\" target=\"_blank\">$1</a>");
|
||||
}
|
||||
|
||||
} else {
|
||||
/* substring is not a URL */
|
||||
parts[j] = filters.exec(parts[j], { filterlinks: false });
|
||||
}
|
||||
}
|
||||
|
||||
msg = parts.join("");
|
||||
/* Anti-XSS */
|
||||
return XSS.sanitizeHTML(msg);
|
||||
};
|
||||
|
||||
ChatModule.prototype.sendModMessage = function (msg, minrank) {
|
||||
if (isNaN(minrank)) {
|
||||
minrank = 2;
|
||||
}
|
||||
|
||||
var msgobj = {
|
||||
username: "[server]",
|
||||
msg: msg,
|
||||
meta: {
|
||||
addClass: "server-whisper",
|
||||
addClassToNameAndTimestamp: true
|
||||
},
|
||||
time: Date.now()
|
||||
};
|
||||
|
||||
this.channel.users.forEach(function (u) {
|
||||
if (u.account.effectiveRank >= minrank) {
|
||||
u.socket.emit("chatMsg", msgobj);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ChatModule.prototype.sendMessage = function (msgobj) {
|
||||
this.channel.broadcastAll("chatMsg", msgobj);
|
||||
|
||||
this.buffer.push(msgobj);
|
||||
if (this.buffer.length > 15) {
|
||||
this.buffer.shift();
|
||||
}
|
||||
|
||||
this.channel.logger.log("<" + msgobj.username + (msgobj.meta.addClass ?
|
||||
"." + msgobj.meta.addClass : "") +
|
||||
"> " + XSS.decodeText(msgobj.msg));
|
||||
};
|
||||
|
||||
ChatModule.prototype.registerCommand = function (cmd, cb) {
|
||||
cmd = cmd.replace(/^\//, "");
|
||||
this.commandHandlers[cmd] = cb;
|
||||
};
|
||||
|
||||
/**
|
||||
* == Default commands ==
|
||||
*/
|
||||
|
||||
ChatModule.prototype.handleCmdMe = function (user, msg, meta) {
|
||||
meta.addClass = "action";
|
||||
meta.action = true;
|
||||
var args = msg.split(" ");
|
||||
args.shift();
|
||||
this.processChatMsg(user, { msg: args.join(" "), meta: meta });
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleCmdSp = function (user, msg, meta) {
|
||||
meta.addClass = "spoiler";
|
||||
var args = msg.split(" ");
|
||||
args.shift();
|
||||
this.processChatMsg(user, { msg: args.join(" "), meta: meta });
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleCmdSay = function (user, msg, meta) {
|
||||
if (user.account.effectiveRank < 1.5) {
|
||||
return;
|
||||
}
|
||||
meta.addClass = "shout";
|
||||
meta.addClassToNameAndTimestamp = true;
|
||||
meta.forceShowName = true;
|
||||
var args = msg.split(" ");
|
||||
args.shift();
|
||||
this.processChatMsg(user, { msg: args.join(" "), meta: meta });
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleCmdClear = function (user, msg, meta) {
|
||||
if (user.account.effectiveRank < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buffer = [];
|
||||
this.channel.broadcastAll("clearchat");
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleCmdAdminflair = function (user, msg, meta) {
|
||||
if (user.account.globalRank < 255) {
|
||||
return;
|
||||
}
|
||||
var args = msg.split(" ");
|
||||
args.shift();
|
||||
|
||||
var superadminflair = {
|
||||
labelclass: "label-danger",
|
||||
icon: "glyphicon-globe"
|
||||
};
|
||||
|
||||
var cargs = [];
|
||||
args.forEach(function (a) {
|
||||
if (a.indexOf("!icon-") === 0) {
|
||||
superadminflair.icon = "glyph" + a.substring(1);
|
||||
} else if (a.indexOf("!label-") === 0) {
|
||||
superadminflair.labelclass = a.substring(1);
|
||||
} else {
|
||||
cargs.push(a);
|
||||
}
|
||||
});
|
||||
|
||||
meta.superadminflair = superadminflair;
|
||||
meta.forceShowName = true;
|
||||
|
||||
this.processChatMsg(user, { msg: cargs.join(" "), meta: meta });
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleCmdAfk = function (user, msg, meta) {
|
||||
user.setAFK(!user.is(Flags.U_AFK));
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleCmdMute = function (user, msg, meta) {
|
||||
if (!this.channel.modules.permissions.canMute(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var muteperm = this.channel.modules.permissions.permissions.mute;
|
||||
var args = msg.split(" ");
|
||||
args.shift(); /* shift off /mute */
|
||||
|
||||
var name = args.shift().toLowerCase();
|
||||
var target;
|
||||
|
||||
for (var i = 0; i < this.channel.users.length; i++) {
|
||||
if (this.channel.users[i].getLowerName() === name) {
|
||||
target = this.channel.users[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "/mute target " + name + " not present in channel."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.account.effectiveRank >= user.account.effectiveRank) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "/mute failed - " + target.getName() + " has equal or higher rank " +
|
||||
"than you."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
target.setFlag(Flags.U_MUTED);
|
||||
this.muted.add(name);
|
||||
this.channel.sendUserMeta(this.channel.users, target, -1);
|
||||
this.channel.logger.log("[mod] " + user.getName() + " muted " + target.getName());
|
||||
this.sendModMessage(user.getName() + " muted " + target.getName(), muteperm);
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleCmdSMute = function (user, msg, meta) {
|
||||
if (!this.channel.modules.permissions.canMute(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var muteperm = this.channel.modules.permissions.permissions.mute;
|
||||
var args = msg.split(" ");
|
||||
args.shift(); /* shift off /smute */
|
||||
|
||||
var name = args.shift().toLowerCase();
|
||||
var target;
|
||||
|
||||
for (var i = 0; i < this.channel.users.length; i++) {
|
||||
if (this.channel.users[i].getLowerName() === name) {
|
||||
target = this.channel.users[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "/smute target " + name + " not present in channel."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.account.effectiveRank >= user.account.effectiveRank) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "/smute failed - " + target.getName() + " has equal or higher rank " +
|
||||
"than you."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
target.setFlag(Flags.U_MUTED | Flags.U_SMUTED);
|
||||
this.muted.add(name);
|
||||
this.muted.add(SHADOW_TAG + name);
|
||||
this.channel.sendUserMeta(this.channel.users, target, muteperm);
|
||||
this.channel.logger.log("[mod] " + user.getName() + " shadowmuted " + target.getName());
|
||||
this.sendModMessage(user.getName() + " shadowmuted " + target.getName(), muteperm);
|
||||
};
|
||||
|
||||
ChatModule.prototype.handleCmdUnmute = function (user, msg, meta) {
|
||||
if (!this.channel.modules.permissions.canMute(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var muteperm = this.channel.modules.permissions.permissions.mute;
|
||||
var args = msg.split(" ");
|
||||
args.shift(); /* shift off /mute */
|
||||
|
||||
var name = args.shift().toLowerCase();
|
||||
|
||||
if (!this.isMuted(name)) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: name + " is not muted."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.muted.remove(name);
|
||||
this.muted.remove(SHADOW_TAG + name);
|
||||
|
||||
var target;
|
||||
for (var i = 0; i < this.channel.users.length; i++) {
|
||||
if (this.channel.users[i].getLowerName() === name) {
|
||||
target = this.channel.users[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.clearFlag(Flags.U_MUTED | Flags.U_SMUTED);
|
||||
this.channel.sendUserMeta(this.channel.users, target, -1);
|
||||
this.channel.logger.log("[mod] " + user.getName() + " unmuted " + target.getName());
|
||||
this.sendModMessage(user.getName() + " unmuted " + target.getName(), muteperm);
|
||||
};
|
||||
|
||||
module.exports = ChatModule;
|
|
@ -0,0 +1,122 @@
|
|||
var ChannelModule = require("./module");
|
||||
var XSS = require("../xss");
|
||||
|
||||
const TYPE_SETCSS = {
|
||||
css: "string"
|
||||
};
|
||||
|
||||
const TYPE_SETJS = {
|
||||
js: "string"
|
||||
};
|
||||
|
||||
const TYPE_SETMOTD = {
|
||||
motd: "string"
|
||||
};
|
||||
|
||||
function CustomizationModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
this.css = "";
|
||||
this.js = "";
|
||||
this.motd = {
|
||||
motd: "",
|
||||
html: ""
|
||||
};
|
||||
}
|
||||
|
||||
CustomizationModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
CustomizationModule.prototype.load = function (data) {
|
||||
if ("css" in data) {
|
||||
this.css = data.css;
|
||||
}
|
||||
|
||||
if ("js" in data) {
|
||||
this.js = data.js;
|
||||
}
|
||||
|
||||
if ("motd" in data) {
|
||||
this.motd = {
|
||||
motd: data.motd.motd || "",
|
||||
html: data.motd.html || ""
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
CustomizationModule.prototype.save = function (data) {
|
||||
data.css = this.css;
|
||||
data.js = this.js;
|
||||
data.motd = this.motd;
|
||||
};
|
||||
|
||||
CustomizationModule.prototype.setMotd = function (motd) {
|
||||
motd = XSS.sanitizeHTML(motd);
|
||||
var html = motd.replace(/\n/g, "<br>");
|
||||
this.motd = {
|
||||
motd: motd,
|
||||
html: html
|
||||
};
|
||||
this.sendMotd(this.channel.users);
|
||||
};
|
||||
|
||||
CustomizationModule.prototype.onUserPostJoin = function (user) {
|
||||
this.sendCSSJS([user]);
|
||||
this.sendMotd([user]);
|
||||
user.socket.typecheckedOn("setChannelCSS", TYPE_SETCSS, this.handleSetCSS.bind(this, user));
|
||||
user.socket.typecheckedOn("setChannelJS", TYPE_SETJS, this.handleSetJS.bind(this, user));
|
||||
user.socket.typecheckedOn("setMotd", TYPE_SETMOTD, this.handleSetMotd.bind(this, user));
|
||||
};
|
||||
|
||||
CustomizationModule.prototype.sendCSSJS = function (users) {
|
||||
var data = {
|
||||
css: this.css,
|
||||
js: this.js
|
||||
};
|
||||
users.forEach(function (u) {
|
||||
u.socket.emit("channelCSSJS", data);
|
||||
});
|
||||
};
|
||||
|
||||
CustomizationModule.prototype.sendMotd = function (users) {
|
||||
var data = this.motd;
|
||||
users.forEach(function (u) {
|
||||
u.socket.emit("setMotd", data);
|
||||
});
|
||||
};
|
||||
|
||||
CustomizationModule.prototype.handleSetCSS = function (user, data) {
|
||||
if (!this.channel.modules.permissions.canSetCSS(user)) {
|
||||
user.kick("Attempted setChannelCSS as non-admin");
|
||||
return;
|
||||
}
|
||||
|
||||
this.css = data.css.substring(0, 20000);
|
||||
this.sendCSSJS(this.channel.users);
|
||||
|
||||
this.channel.logger.log("[mod] " + user.name + " updated the channel CSS");
|
||||
};
|
||||
|
||||
CustomizationModule.prototype.handleSetJS = function (user, data) {
|
||||
if (!this.channel.modules.permissions.canSetJS(user)) {
|
||||
user.kick("Attempted setChannelJS as non-admin");
|
||||
return;
|
||||
}
|
||||
|
||||
this.js = data.js.substring(0, 20000);
|
||||
this.sendCSSJS(this.channel.users);
|
||||
|
||||
this.channel.logger.log("[mod] " + user.name + " updated the channel JS");
|
||||
};
|
||||
|
||||
CustomizationModule.prototype.handleSetMotd = function (user, data) {
|
||||
if (!this.channel.modules.permissions.canEditMotd(user)) {
|
||||
user.kick("Attempted setMotd with insufficient permission");
|
||||
return;
|
||||
}
|
||||
|
||||
var motd = data.motd.substring(0, 20000);
|
||||
|
||||
this.setMotd(motd);
|
||||
this.channel.logger.log("[mod] " + user.name + " updated the MOTD");
|
||||
};
|
||||
|
||||
module.exports = CustomizationModule;
|
|
@ -0,0 +1,199 @@
|
|||
var ChannelModule = require("./module");
|
||||
var XSS = require("../xss");
|
||||
|
||||
function EmoteList(defaults) {
|
||||
if (!defaults) {
|
||||
defaults = [];
|
||||
}
|
||||
|
||||
this.emotes = defaults.map(validateEmote).filter(function (f) {
|
||||
return f !== false;
|
||||
});
|
||||
}
|
||||
|
||||
EmoteList.prototype = {
|
||||
pack: function () {
|
||||
return Array.prototype.slice.call(this.emotes);
|
||||
},
|
||||
|
||||
importList: function (emotes) {
|
||||
this.emotes = Array.prototype.slice.call(emotes);
|
||||
},
|
||||
|
||||
updateEmote: function (emote) {
|
||||
var found = false;
|
||||
for (var i = 0; i < this.emotes.length; i++) {
|
||||
if (this.emotes[i].name === emote.name) {
|
||||
found = true;
|
||||
this.emotes[i] = emote;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* If no emote was updated, add a new one */
|
||||
if (!found) {
|
||||
this.emotes.push(emote);
|
||||
}
|
||||
},
|
||||
|
||||
removeEmote: function (emote) {
|
||||
var found = false;
|
||||
for (var i = 0; i < this.emotes.length; i++) {
|
||||
if (this.emotes[i].name === emote.name) {
|
||||
this.emotes.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
moveEmote: function (from, to) {
|
||||
if (from < 0 || to < 0 ||
|
||||
from >= this.emotes.length || to >= this.emotes.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var f = this.emotes[from];
|
||||
/* Offset from/to indexes to account for the fact that removing
|
||||
an element changes the position of one of them.
|
||||
|
||||
I could have just done a swap, but it's already implemented this way
|
||||
and it works. */
|
||||
to = to > from ? to + 1 : to;
|
||||
from = to > from ? from : from + 1;
|
||||
|
||||
this.emotes.splice(to, 0, f);
|
||||
this.emotes.splice(from, 1);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
function validateEmote(f) {
|
||||
if (typeof f.name !== "string" || typeof f.image !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
f.image = f.image.substring(0, 1000);
|
||||
f.image = XSS.sanitizeText(f.image);
|
||||
|
||||
var s = XSS.sanitizeText(f.name).replace(/([\\\.\?\+\*\$\^\|\(\)\[\]\{\}])/g, "\\$1");
|
||||
s = "(^|\\s)" + s + "(?!\\S)";
|
||||
f.source = s;
|
||||
|
||||
try {
|
||||
new RegExp(f.source, "gi");
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return f;
|
||||
};
|
||||
|
||||
function EmoteModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
this.emotes = new EmoteList();
|
||||
}
|
||||
|
||||
EmoteModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
EmoteModule.prototype.load = function (data) {
|
||||
if ("emotes" in data) {
|
||||
for (var i = 0; i < data.emotes.length; i++) {
|
||||
this.emotes.updateEmote(data.emotes[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
EmoteModule.prototype.save = function (data) {
|
||||
data.emotes = this.emotes.pack();
|
||||
};
|
||||
|
||||
EmoteModule.prototype.onUserPostJoin = function (user) {
|
||||
user.socket.on("updateEmote", this.handleUpdateEmote.bind(this, user));
|
||||
user.socket.on("importEmotes", this.handleImportEmotes.bind(this, user));
|
||||
user.socket.on("moveEmote", this.handleMoveEmote.bind(this, user));
|
||||
user.socket.on("removeEmote", this.handleRemoveEmote.bind(this, user));
|
||||
this.sendEmotes([user]);
|
||||
};
|
||||
|
||||
EmoteModule.prototype.sendEmotes = function (users) {
|
||||
var f = this.emotes.pack();
|
||||
var chan = this.channel;
|
||||
users.forEach(function (u) {
|
||||
u.socket.emit("emoteList", f);
|
||||
});
|
||||
};
|
||||
|
||||
EmoteModule.prototype.handleUpdateEmote = function (user, data) {
|
||||
if (typeof data !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.modules.permissions.canEditEmotes(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var f = validateEmote(data);
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emotes.updateEmote(f);
|
||||
var chan = this.channel;
|
||||
chan.broadcastAll("updateEmote", f);
|
||||
|
||||
chan.logger.log("[mod] " + user.getName() + " updated emote: " + f.name + " -> " +
|
||||
f.image);
|
||||
};
|
||||
|
||||
EmoteModule.prototype.handleImportEmotes = function (user, data) {
|
||||
if (!(data instanceof Array)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Note: importing requires a different permission node than simply
|
||||
updating/removing */
|
||||
if (!this.channel.modules.permissions.canImportEmotes(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emotes.importList(data.map(validateEmote).filter(function (f) {
|
||||
return f !== false;
|
||||
}));
|
||||
this.sendEmotes(this.channel.users);
|
||||
};
|
||||
|
||||
EmoteModule.prototype.handleRemoveEmote = function (user, data) {
|
||||
if (typeof data !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.modules.permissions.canEditEmotes(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.name !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emotes.removeEmote(data);
|
||||
this.channel.logger.log("[mod] " + user.getName() + " removed emote: " + data.name);
|
||||
this.channel.broadcastAll("removeEmote", data);
|
||||
};
|
||||
|
||||
EmoteModule.prototype.handleMoveEmote = function (user, data) {
|
||||
if (typeof data !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.modules.permissions.canEditEmotes(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.to !== "number" || typeof data.from !== "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emotes.moveEmote(data.from, data.to);
|
||||
};
|
||||
|
||||
module.exports = EmoteModule;
|
|
@ -0,0 +1,276 @@
|
|||
var ChannelModule = require("./module");
|
||||
var XSS = require("../xss");
|
||||
|
||||
function ChatFilter(name, regex, flags, replace, active, filterlinks) {
|
||||
this.name = name;
|
||||
this.source = regex;
|
||||
this.flags = flags;
|
||||
this.regex = new RegExp(this.source, flags);
|
||||
this.replace = replace;
|
||||
this.active = active === false ? false : true;
|
||||
this.filterlinks = filterlinks || false;
|
||||
}
|
||||
|
||||
ChatFilter.prototype = {
|
||||
pack: function () {
|
||||
return {
|
||||
name: this.name,
|
||||
source: this.source,
|
||||
flags: this.flags,
|
||||
replace: this.replace,
|
||||
active: this.active,
|
||||
filterlinks: this.filterlinks
|
||||
};
|
||||
},
|
||||
|
||||
exec: function (str) {
|
||||
return str.replace(this.regex, this.replace);
|
||||
}
|
||||
};
|
||||
|
||||
function FilterList(defaults) {
|
||||
if (!defaults) {
|
||||
defaults = [];
|
||||
}
|
||||
|
||||
this.filters = defaults.map(function (f) {
|
||||
return new ChatFilter(f.name, f.source, f.flags, f.replace, f.active, f.filterlinks);
|
||||
});
|
||||
}
|
||||
|
||||
FilterList.prototype = {
|
||||
pack: function () {
|
||||
return this.filters.map(function (f) { return f.pack(); });
|
||||
},
|
||||
|
||||
importList: function (filters) {
|
||||
this.filters = Array.prototype.slice.call(filters);
|
||||
},
|
||||
|
||||
updateFilter: function (filter) {
|
||||
if (!filter.name) {
|
||||
filter.name = filter.source;
|
||||
}
|
||||
|
||||
var found = false;
|
||||
for (var i = 0; i < this.filters.length; i++) {
|
||||
if (this.filters[i].name === filter.name) {
|
||||
found = true;
|
||||
this.filters[i] = filter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* If no filter was updated, add a new one */
|
||||
if (!found) {
|
||||
this.filters.push(filter);
|
||||
}
|
||||
},
|
||||
|
||||
removeFilter: function (filter) {
|
||||
var found = false;
|
||||
for (var i = 0; i < this.filters.length; i++) {
|
||||
if (this.filters[i].name === filter.name) {
|
||||
this.filters.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
moveFilter: function (from, to) {
|
||||
if (from < 0 || to < 0 ||
|
||||
from >= this.filters.length || to >= this.filters.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var f = this.filters[from];
|
||||
/* Offset from/to indexes to account for the fact that removing
|
||||
an element changes the position of one of them.
|
||||
|
||||
I could have just done a swap, but it's already implemented this way
|
||||
and it works. */
|
||||
to = to > from ? to + 1 : to;
|
||||
from = to > from ? from : from + 1;
|
||||
|
||||
this.filters.splice(to, 0, f);
|
||||
this.filters.splice(from, 1);
|
||||
return true;
|
||||
},
|
||||
|
||||
exec: function (str, opts) {
|
||||
if (!opts) {
|
||||
opts = {};
|
||||
}
|
||||
|
||||
this.filters.forEach(function (f) {
|
||||
if (opts.filterlinks && !f.filterlinks) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (f.active) {
|
||||
str = f.exec(str);
|
||||
}
|
||||
});
|
||||
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
function validateFilter(f) {
|
||||
if (typeof f.source !== "string" || typeof f.flags !== "string" ||
|
||||
typeof f.replace !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof f.name !== "string") {
|
||||
f.name = f.source;
|
||||
}
|
||||
|
||||
f.replace = f.replace.substring(0, 1000);
|
||||
f.replace = XSS.sanitizeHTML(f.replace);
|
||||
f.flags = f.flags.substring(0, 4);
|
||||
|
||||
try {
|
||||
new RegExp(f.source, f.flags);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var filter = new ChatFilter(f.name, f.source, f.flags, f.replace,
|
||||
Boolean(f.active), Boolean(f.filterlinks));
|
||||
return filter;
|
||||
}
|
||||
|
||||
const DEFAULT_FILTERS = [
|
||||
new ChatFilter("monospace", "`(.+?)`", "g", "<code>$1</code>"),
|
||||
new ChatFilter("bold", "\\*(.+?)\\*", "g", "<strong>$1</strong>"),
|
||||
new ChatFilter("italic", "_(.+?)_", "g", "<em>$1</em>"),
|
||||
new ChatFilter("strike", "~~(.+?)~~", "g", "<s>$1</s>"),
|
||||
new ChatFilter("inline spoiler", "\\[sp\\](.*?)\\[\\/sp\\]", "ig", "<span class=\"spoiler\">$1</span>")
|
||||
];
|
||||
|
||||
function ChatFilterModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
this.filters = new FilterList(DEFAULT_FILTERS);
|
||||
}
|
||||
|
||||
ChatFilterModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
ChatFilterModule.prototype.load = function (data) {
|
||||
if ("filters" in data) {
|
||||
for (var i = 0; i < data.filters.length; i++) {
|
||||
var f = validateFilter(data.filters[i]);
|
||||
if (f) {
|
||||
this.filters.updateFilter(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.save = function (data) {
|
||||
data.filters = this.filters.pack();
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.onUserPostJoin = function (user) {
|
||||
user.socket.on("updateFilter", this.handleUpdateFilter.bind(this, user));
|
||||
user.socket.on("importFilters", this.handleImportFilters.bind(this, user));
|
||||
user.socket.on("moveFilter", this.handleMoveFilter.bind(this, user));
|
||||
user.socket.on("removeFilter", this.handleRemoveFilter.bind(this, user));
|
||||
user.socket.on("requestChatFilters", this.handleRequestChatFilters.bind(this, user));
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.sendChatFilters = function (users) {
|
||||
var f = this.filters.pack();
|
||||
var chan = this.channel;
|
||||
users.forEach(function (u) {
|
||||
if (chan.modules.permissions.canEditFilters(u)) {
|
||||
u.socket.emit("chatFilters", f);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.handleUpdateFilter = function (user, data) {
|
||||
if (typeof data !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.modules.permissions.canEditFilters(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var f = validateFilter(data);
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
data = f.pack();
|
||||
|
||||
this.filters.updateFilter(f);
|
||||
var chan = this.channel;
|
||||
chan.users.forEach(function (u) {
|
||||
if (chan.modules.permissions.canEditFilters(u)) {
|
||||
u.socket.emit("updateChatFilter", data);
|
||||
}
|
||||
});
|
||||
|
||||
chan.logger.log("[mod] " + user.getName() + " updated filter: " + f.name + " -> " +
|
||||
"s/" + f.source + "/" + f.replace + "/" + f.flags + " active: " +
|
||||
f.active + ", filterlinks: " + f.filterlinks);
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.handleImportFilters = function (user, data) {
|
||||
if (!(data instanceof Array)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Note: importing requires a different permission node than simply
|
||||
updating/removing */
|
||||
if (!this.channel.modules.permissions.canImportFilters(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.filters.importList(data.map(validateFilter).filter(function (f) {
|
||||
return f !== false;
|
||||
}));
|
||||
|
||||
this.channel.logger.log("[mod] " + user.getName() + " imported the filter list");
|
||||
this.sendChatFilters(this.channel.users);
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.handleRemoveFilter = function (user, data) {
|
||||
if (typeof data !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.modules.permissions.canEditFilters(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.name !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.filters.removeFilter(data);
|
||||
this.channel.logger.log("[mod] " + user.getName() + " removed filter: " + data.name);
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.handleMoveFilter = function (user, data) {
|
||||
if (typeof data !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.modules.permissions.canEditFilters(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.to !== "number" || typeof data.from !== "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.filters.moveFilter(data.from, data.to);
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.handleRequestChatFilters = function (user) {
|
||||
this.sendChatFilters([user]);
|
||||
};
|
||||
|
||||
module.exports = ChatFilterModule;
|
|
@ -0,0 +1,379 @@
|
|||
var ChannelModule = require("./module");
|
||||
var db = require("../database");
|
||||
var Flags = require("../flags");
|
||||
var util = require("../utilities");
|
||||
var Account = require("../account");
|
||||
var Q = require("q");
|
||||
|
||||
const TYPE_UNBAN = {
|
||||
id: "number",
|
||||
name: "string"
|
||||
};
|
||||
|
||||
function KickBanModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
|
||||
if (this.channel.modules.chat) {
|
||||
this.channel.modules.chat.registerCommand("/kick", this.handleCmdKick.bind(this));
|
||||
this.channel.modules.chat.registerCommand("/ban", this.handleCmdBan.bind(this));
|
||||
this.channel.modules.chat.registerCommand("/ipban", this.handleCmdIPBan.bind(this));
|
||||
this.channel.modules.chat.registerCommand("/banip", this.handleCmdIPBan.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
KickBanModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
KickBanModule.prototype.onUserPreJoin = function (user, data, cb) {
|
||||
if (!this.channel.is(Flags.C_REGISTERED)) {
|
||||
return cb(null, ChannelModule.PASSTHROUGH);
|
||||
}
|
||||
|
||||
var cname = this.channel.name;
|
||||
db.channels.isIPBanned(cname, user.longip, function (err, banned) {
|
||||
if (err) {
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
} else if (!banned) {
|
||||
if (user.is(Flags.U_LOGGED_IN)) {
|
||||
checkNameBan();
|
||||
} else {
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
}
|
||||
} else {
|
||||
cb(null, ChannelModule.DENY);
|
||||
user.kick("Your IP address is banned from this channel.");
|
||||
}
|
||||
});
|
||||
|
||||
function checkNameBan() {
|
||||
db.channels.isNameBanned(cname, user.getName(), function (err, banned) {
|
||||
if (err) {
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
} else {
|
||||
cb(null, banned ? ChannelModule.DENY : ChannelModule.PASSTHROUGH);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
KickBanModule.prototype.onUserPostJoin = function (user) {
|
||||
if (!this.channel.is(Flags.C_REGISTERED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var chan = this.channel;
|
||||
user.waitFlag(Flags.U_LOGGED_IN, function () {
|
||||
chan.activeLock.lock();
|
||||
db.channels.isNameBanned(chan.name, user.getName(), function (err, banned) {
|
||||
if (!err && banned) {
|
||||
user.kick("You are banned from this channel.");
|
||||
if (chan.modules.chat) {
|
||||
chan.modules.chat.sendModMessage(user.getName() + " was kicked (" +
|
||||
"name is banned)");
|
||||
}
|
||||
}
|
||||
chan.activeLock.release();
|
||||
});
|
||||
});
|
||||
|
||||
var self = this;
|
||||
user.socket.on("requestBanlist", function () { self.sendBanlist([user]); });
|
||||
user.socket.typecheckedOn("unban", TYPE_UNBAN, this.handleUnban.bind(this, user));
|
||||
};
|
||||
|
||||
KickBanModule.prototype.sendBanlist = function (users) {
|
||||
if (!this.channel.is(Flags.C_REGISTERED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var perms = this.channel.modules.permissions;
|
||||
|
||||
var bans = [];
|
||||
var unmaskedbans = [];
|
||||
db.channels.listBans(this.channel.name, function (err, banlist) {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < banlist.length; i++) {
|
||||
bans.push({
|
||||
id: banlist[i].id,
|
||||
ip: banlist[i].ip === "*" ? "*" : util.maskIP(banlist[i].ip),
|
||||
name: banlist[i].name,
|
||||
reason: banlist[i].reason,
|
||||
bannedby: banlist[i].bannedby
|
||||
});
|
||||
unmaskedbans.push({
|
||||
id: banlist[i].id,
|
||||
ip: banlist[i].ip,
|
||||
name: banlist[i].name,
|
||||
reason: banlist[i].reason,
|
||||
bannedby: banlist[i].bannedby
|
||||
});
|
||||
}
|
||||
|
||||
users.forEach(function (u) {
|
||||
if (!perms.canBan(u)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (u.account.effectiveRank >= 255) {
|
||||
u.socket.emit("banlist", unmaskedbans);
|
||||
} else {
|
||||
u.socket.emit("banlist", bans);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
KickBanModule.prototype.sendUnban = function (users, data) {
|
||||
var perms = this.channel.modules.permissions;
|
||||
users.forEach(function (u) {
|
||||
if (perms.canBan(u)) {
|
||||
u.socket.emit("banlistRemove", data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
KickBanModule.prototype.handleCmdKick = function (user, msg, meta) {
|
||||
if (!this.channel.modules.permissions.canKick(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var args = msg.split(" ");
|
||||
args.shift(); /* shift off /kick */
|
||||
var name = args.shift().toLowerCase();
|
||||
var reason = args.join(" ");
|
||||
var target = null;
|
||||
|
||||
for (var i = 0; i < this.channel.users.length; i++) {
|
||||
if (this.channel.users[i].getLowerName() === name) {
|
||||
target = this.channel.users[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (target === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.account.effectiveRank >= user.account.effectiveRank) {
|
||||
return user.socket.emit("errorMsg", {
|
||||
msg: "You do not have permission to kick " + target.getName()
|
||||
});
|
||||
}
|
||||
|
||||
target.kick(reason);
|
||||
this.channel.logger.log("[mod] " + user.getName() + " kicked " + target.getName() +
|
||||
" (" + reason + ")");
|
||||
if (this.channel.modules.chat) {
|
||||
this.channel.modules.chat.sendModMessage(user.getName() + " kicked " +
|
||||
target.getName());
|
||||
}
|
||||
};
|
||||
|
||||
/* /ban - name bans */
|
||||
KickBanModule.prototype.handleCmdBan = function (user, msg, meta) {
|
||||
var args = msg.split(" ");
|
||||
args.shift(); /* shift off /ban */
|
||||
var name = args.shift();
|
||||
var reason = args.join(" ");
|
||||
|
||||
var chan = this.channel;
|
||||
chan.activeLock.lock();
|
||||
this.banName(user, name, reason, function (err) {
|
||||
chan.activeLock.release();
|
||||
});
|
||||
};
|
||||
|
||||
/* /ipban - bans name and IP addresses associated with it */
|
||||
KickBanModule.prototype.handleCmdIPBan = function (user, msg, meta) {
|
||||
var args = msg.split(" ");
|
||||
args.shift(); /* shift off /ipban */
|
||||
var name = args.shift();
|
||||
var range = false;
|
||||
if (args[0] === "range") {
|
||||
range = "range";
|
||||
args.shift();
|
||||
} else if (args[0] === "wrange") {
|
||||
range = "wrange";
|
||||
args.shift();
|
||||
}
|
||||
var reason = args.join(" ");
|
||||
|
||||
var chan = this.channel;
|
||||
chan.activeLock.lock();
|
||||
this.banAll(user, name, range, reason, function (err) {
|
||||
chan.activeLock.release();
|
||||
});
|
||||
};
|
||||
|
||||
KickBanModule.prototype.banName = function (actor, name, reason, cb) {
|
||||
var self = this;
|
||||
reason = reason.substring(0, 255);
|
||||
|
||||
var chan = this.channel;
|
||||
var error = function (what) {
|
||||
actor.socket.emit("errorMsg", { msg: what });
|
||||
cb(what);
|
||||
};
|
||||
|
||||
if (!chan.modules.permissions.canBan(actor)) {
|
||||
return error("You do not have ban permissions on this channel");
|
||||
}
|
||||
|
||||
name = name.toLowerCase();
|
||||
if (name === actor.getLowerName()) {
|
||||
actor.socket.emit("costanza", {
|
||||
msg: "You can't ban yourself"
|
||||
});
|
||||
return cb("Attempted to ban self");
|
||||
}
|
||||
|
||||
Q.nfcall(Account.rankForName, name, { channel: chan.name })
|
||||
.then(function (rank) {
|
||||
if (rank >= actor.account.effectiveRank) {
|
||||
throw "You don't have permission to ban " + name;
|
||||
}
|
||||
|
||||
return Q.nfcall(db.channels.isNameBanned, chan.name, name);
|
||||
}).then(function (banned) {
|
||||
if (banned) {
|
||||
throw name + " is already banned";
|
||||
}
|
||||
|
||||
if (chan.dead) { throw null; }
|
||||
|
||||
return Q.nfcall(db.channels.ban, chan.name, "*", name, reason, actor.getName());
|
||||
}).then(function () {
|
||||
chan.logger.log("[mod] " + actor.getName() + " namebanned " + name);
|
||||
if (chan.modules.chat) {
|
||||
chan.modules.chat.sendModMessage(actor.getName() + " namebanned " + name,
|
||||
chan.modules.permissions.permissions.ban);
|
||||
}
|
||||
return true;
|
||||
}).then(function () {
|
||||
self.kickBanTarget(name, null);
|
||||
setImmediate(function () {
|
||||
cb(null);
|
||||
});
|
||||
}).catch(error).done();
|
||||
};
|
||||
|
||||
KickBanModule.prototype.banIP = function (actor, ip, name, reason, cb) {
|
||||
var self = this;
|
||||
reason = reason.substring(0, 255);
|
||||
var masked = util.maskIP(ip);
|
||||
|
||||
var chan = this.channel;
|
||||
var error = function (what) {
|
||||
actor.socket.emit("errorMsg", { msg: what });
|
||||
cb(what);
|
||||
};
|
||||
|
||||
if (!chan.modules.permissions.canBan(actor)) {
|
||||
return error("You do not have ban permissions on this channel");
|
||||
}
|
||||
|
||||
Q.nfcall(Account.rankForIP, ip).then(function (rank) {
|
||||
if (rank >= actor.account.effectiveRank) {
|
||||
throw "You don't have permission to ban IP " + masked;
|
||||
}
|
||||
|
||||
return Q.nfcall(db.channels.isIPBanned, chan.name, ip);
|
||||
}).then(function (banned) {
|
||||
if (banned) {
|
||||
throw masked + " is already banned";
|
||||
}
|
||||
|
||||
if (chan.dead) { throw null; }
|
||||
|
||||
return Q.nfcall(db.channels.ban, chan.name, ip, name, reason, actor.getName());
|
||||
}).then(function () {
|
||||
chan.logger.log("[mod] " + actor.getName() + " banned " + ip + " (" + name + ")");
|
||||
if (chan.modules.chat) {
|
||||
chan.modules.chat.sendModMessage(actor.getName() + " banned " +
|
||||
util.maskIP(ip) + " (" + name + ")",
|
||||
chan.modules.permissions.permissions.ban);
|
||||
}
|
||||
}).then(function () {
|
||||
self.kickBanTarget(name, ip);
|
||||
setImmediate(function () {
|
||||
cb(null);
|
||||
});
|
||||
}).catch(error).done();
|
||||
};
|
||||
|
||||
KickBanModule.prototype.banAll = function (actor, name, range, reason, cb) {
|
||||
var self = this;
|
||||
reason = reason.substring(0, 255);
|
||||
|
||||
var chan = self.channel;
|
||||
var error = function (what) {
|
||||
cb(what);
|
||||
};
|
||||
|
||||
if (!chan.modules.permissions.canBan(actor)) {
|
||||
return error("You do not have ban permissions on this channel");
|
||||
}
|
||||
|
||||
self.banName(actor, name, reason, function (err) {
|
||||
if (err && err.indexOf("is already banned") === -1) {
|
||||
cb(err);
|
||||
} else {
|
||||
db.getIPs(name, function (err, ips) {
|
||||
if (err) {
|
||||
return error(err);
|
||||
}
|
||||
var all = ips.map(function (ip) {
|
||||
if (range === "range") {
|
||||
ip = util.getIPRange(ip);
|
||||
} else if (range === "wrange") {
|
||||
ip = util.getWideIPRange(ip);
|
||||
}
|
||||
return Q.nfcall(self.banIP.bind(self), actor, ip, name, reason);
|
||||
});
|
||||
|
||||
Q.all(all).then(function () {
|
||||
setImmediate(cb);
|
||||
}).catch(error).done();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
KickBanModule.prototype.kickBanTarget = function (name, ip) {
|
||||
name = name.toLowerCase();
|
||||
for (var i = 0; i < this.channel.users.length; i++) {
|
||||
if (this.channel.users[i].getLowerName() === name ||
|
||||
this.channel.users[i].longip === ip) {
|
||||
this.channel.users[i].kick("You're banned!");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
KickBanModule.prototype.handleUnban = function (user, data) {
|
||||
if (!this.channel.modules.permissions.canBan(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.channel.activeLock.lock();
|
||||
db.channels.unbanId(this.channel.name, data.id, function (err) {
|
||||
if (err) {
|
||||
return user.socket.emit("errorMsg", {
|
||||
msg: err
|
||||
});
|
||||
}
|
||||
|
||||
self.sendUnban(self.channel.users, data);
|
||||
self.channel.logger.log("[mod] " + user.getName() + " unbanned " + data.name);
|
||||
if (self.channel.modules.chat) {
|
||||
var banperm = self.channel.modules.permissions.permissions.ban;
|
||||
self.channel.modules.chat.sendModMessage(user.getName() + " unbanned " +
|
||||
data.name, banperm);
|
||||
}
|
||||
self.channel.activeLock.release();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = KickBanModule;
|
|
@ -0,0 +1,109 @@
|
|||
var ChannelModule = require("./module");
|
||||
var Flags = require("../flags");
|
||||
var util = require("../utilities");
|
||||
var InfoGetter = require("../get-info");
|
||||
var db = require("../database");
|
||||
var Media = require("../media");
|
||||
|
||||
const TYPE_UNCACHE = {
|
||||
id: "string"
|
||||
};
|
||||
|
||||
const TYPE_SEARCH_MEDIA = {
|
||||
source: "string,optional",
|
||||
query: "string"
|
||||
};
|
||||
|
||||
function LibraryModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
}
|
||||
|
||||
LibraryModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
LibraryModule.prototype.onUserPostJoin = function (user) {
|
||||
user.socket.typecheckedOn("uncache", TYPE_UNCACHE, this.handleUncache.bind(this, user));
|
||||
user.socket.typecheckedOn("searchMedia", TYPE_SEARCH_MEDIA, this.handleSearchMedia.bind(this, user));
|
||||
};
|
||||
|
||||
LibraryModule.prototype.cacheMedia = function (media) {
|
||||
if (this.channel.is(Flags.C_REGISTERED) && !util.isLive(media.type)) {
|
||||
db.channels.addToLibrary(this.channel.name, media);
|
||||
}
|
||||
};
|
||||
|
||||
LibraryModule.prototype.getItem = function (id, cb) {
|
||||
db.channels.getLibraryItem(this.channel.name, id, function (err, row) {
|
||||
if (err) {
|
||||
cb(err, null);
|
||||
} else {
|
||||
cb(null, new Media(row.id, row.title, row.seconds, row.type, {}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
LibraryModule.prototype.handleUncache = function (user, data) {
|
||||
if (!this.channel.is(Flags.C_REGISTERED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.modules.permissions.canUncache(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var chan = this.channel;
|
||||
chan.activeLock.lock();
|
||||
db.channels.deleteFromLibrary(chan.name, data.id, function (err, res) {
|
||||
if (chan.dead || err) {
|
||||
return;
|
||||
}
|
||||
|
||||
chan.logger.log("[library] " + user.getName() + " deleted " + data.id +
|
||||
"from the library");
|
||||
chan.activeLock.release();
|
||||
});
|
||||
};
|
||||
|
||||
LibraryModule.prototype.handleSearchMedia = function (user, data) {
|
||||
var query = data.query.substring(0, 100);
|
||||
var searchYT = function () {
|
||||
InfoGetter.Getters.ytSearch(query.split(" "), function (e, vids) {
|
||||
if (!e) {
|
||||
user.socket.emit("searchResults", {
|
||||
source: "yt",
|
||||
results: vids
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (data.source === "yt" || !this.channel.is(Flags.C_REGISTERED)) {
|
||||
searchYT();
|
||||
} else {
|
||||
db.channels.searchLibrary(this.channel.name, query, function (err, res) {
|
||||
if (err) {
|
||||
res = [];
|
||||
}
|
||||
|
||||
if (res.length === 0) {
|
||||
return searchYT();
|
||||
}
|
||||
|
||||
res.sort(function (a, b) {
|
||||
var x = a.title.toLowerCase();
|
||||
var y = b.title.toLowerCase();
|
||||
return (x === y) ? 0 : (x < y ? -1 : 1);
|
||||
});
|
||||
|
||||
res.forEach(function (r) {
|
||||
r.duration = util.formatTime(r.seconds);
|
||||
});
|
||||
|
||||
user.socket.emit("searchResults", {
|
||||
source: "library",
|
||||
results: res
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = LibraryModule;
|
|
@ -0,0 +1,67 @@
|
|||
function ChannelModule(channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
ChannelModule.prototype = {
|
||||
/**
|
||||
* Called when the channel is loading its data from a JSON object.
|
||||
*/
|
||||
load: function (data) {
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the channel is saving its state to a JSON object.
|
||||
*/
|
||||
save: function (data) {
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the channel is being unloaded
|
||||
*/
|
||||
unload: function () {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a user is attempting to join a channel.
|
||||
*
|
||||
* data is the data sent by the client with the joinChannel
|
||||
* packet.
|
||||
*/
|
||||
onUserPreJoin: function (user, data, cb) {
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called after a user has been accepted to the channel.
|
||||
*/
|
||||
onUserPostJoin: function (user) {
|
||||
},
|
||||
|
||||
/**
|
||||
* Called after a user has been disconnected from the channel.
|
||||
*/
|
||||
onUserPart: function (user) {
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a chatMsg event is received
|
||||
*/
|
||||
onUserChat: function (user, data, cb) {
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a new video begins playing
|
||||
*/
|
||||
onMediaChange: function (data) {
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
/* Channel module callback return codes */
|
||||
ChannelModule.ERROR = -1;
|
||||
ChannelModule.PASSTHROUGH = 0;
|
||||
ChannelModule.DENY = 1;
|
||||
|
||||
module.exports = ChannelModule;
|
|
@ -0,0 +1,190 @@
|
|||
var ChannelModule = require("./module");
|
||||
var Config = require("../config");
|
||||
|
||||
function OptionsModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
this.opts = {
|
||||
allow_voteskip: true, // Allow users to voteskip
|
||||
voteskip_ratio: 0.5, // Ratio of skip votes:non-afk users needed to skip the video
|
||||
afk_timeout: 600, // Number of seconds before a user is automatically marked afk
|
||||
pagetitle: this.channel.name, // Title of the browser tab
|
||||
maxlength: 0, // Maximum length (in seconds) of a video queued
|
||||
externalcss: "", // Link to external stylesheet
|
||||
externaljs: "", // Link to external script
|
||||
chat_antiflood: false, // Throttle chat messages
|
||||
chat_antiflood_params: {
|
||||
burst: 4, // Number of messages to allow with no throttling
|
||||
sustained: 1, // Throttle rate (messages/second)
|
||||
cooldown: 4 // Number of seconds with no messages before burst is reset
|
||||
},
|
||||
show_public: false, // List the channel on the index page
|
||||
enable_link_regex: true, // Use the built-in link filter
|
||||
password: false, // Channel password (false -> no password required for entry)
|
||||
allow_dupes: false // Allow duplicate videos on the playlist
|
||||
};
|
||||
}
|
||||
|
||||
OptionsModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
OptionsModule.prototype.load = function (data) {
|
||||
if ("opts" in data) {
|
||||
for (var key in this.opts) {
|
||||
if (key in data.opts) {
|
||||
this.opts[key] = data.opts[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
OptionsModule.prototype.save = function (data) {
|
||||
data.opts = this.opts;
|
||||
};
|
||||
|
||||
OptionsModule.prototype.get = function (key) {
|
||||
return this.opts[key];
|
||||
};
|
||||
|
||||
OptionsModule.prototype.set = function (key, value) {
|
||||
this.opts[key] = value;
|
||||
};
|
||||
|
||||
OptionsModule.prototype.onUserPostJoin = function (user) {
|
||||
user.socket.on("setOptions", this.handleSetOptions.bind(this, user));
|
||||
|
||||
this.sendOpts([user]);
|
||||
};
|
||||
|
||||
OptionsModule.prototype.sendOpts = function (users) {
|
||||
var opts = this.opts;
|
||||
|
||||
if (users === this.channel.users) {
|
||||
this.channel.broadcastAll("channelOpts", opts);
|
||||
} else {
|
||||
users.forEach(function (user) {
|
||||
user.socket.emit("channelOpts", opts);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
OptionsModule.prototype.getPermissions = function () {
|
||||
return this.channel.modules.permissions;
|
||||
};
|
||||
|
||||
OptionsModule.prototype.handleSetOptions = function (user, data) {
|
||||
if (typeof data !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.getPermissions().canSetOptions(user)) {
|
||||
user.kick("Attempted setOptions as a non-moderator");
|
||||
return;
|
||||
}
|
||||
|
||||
if ("allow_voteskip" in data) {
|
||||
this.opts.allow_voteskip = Boolean(data.allow_voteskip);
|
||||
}
|
||||
|
||||
if ("voteskip_ratio" in data) {
|
||||
var ratio = parseFloat(data.voteskip_ratio);
|
||||
if (isNaN(ratio) || ratio < 0) {
|
||||
ratio = 0;
|
||||
}
|
||||
this.opts.voteskip_ratio = ratio;
|
||||
}
|
||||
|
||||
if ("afk_timeout" in data) {
|
||||
var tm = parseInt(data.afk_timeout);
|
||||
if (isNaN(tm) || tm < 0) {
|
||||
tm = 0;
|
||||
}
|
||||
|
||||
var same = tm === this.opts.afk_timeout;
|
||||
this.opts.afk_timeout = tm;
|
||||
if (!same) {
|
||||
this.channel.users.forEach(function (u) {
|
||||
u.autoAFK();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ("pagetitle" in data && user.account.effectiveRank >= 3) {
|
||||
var title = (""+data.pagetitle).substring(0, 100);
|
||||
if (!title.trim().match(Config.get("reserved-names.pagetitles"))) {
|
||||
this.opts.pagetitle = (""+data.pagetitle).substring(0, 100);
|
||||
} else {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "That pagetitle is reserved",
|
||||
alert: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ("maxlength" in data) {
|
||||
var ml = parseInt(data.maxlength);
|
||||
if (isNaN(ml) || ml < 0) {
|
||||
ml = 0;
|
||||
}
|
||||
this.opts.maxlength = ml;
|
||||
}
|
||||
|
||||
if ("externalcss" in data && user.account.effectiveRank >= 3) {
|
||||
this.opts.externalcss = (""+data.externalcss).substring(0, 255);
|
||||
}
|
||||
|
||||
if ("externaljs" in data && user.account.effectiveRank >= 3) {
|
||||
this.opts.externaljs = (""+data.externaljs).substring(0, 255);
|
||||
}
|
||||
|
||||
if ("chat_antiflood" in data) {
|
||||
this.opts.chat_antiflood = Boolean(data.chat_antiflood);
|
||||
}
|
||||
|
||||
if ("chat_antiflood_params" in data) {
|
||||
if (typeof data.chat_antiflood_params !== "object") {
|
||||
data.chat_antiflood_params = {
|
||||
burst: 4,
|
||||
sustained: 1
|
||||
};
|
||||
}
|
||||
|
||||
var b = parseInt(data.chat_antiflood_params.burst);
|
||||
if (isNaN(b) || b < 0) {
|
||||
b = 1;
|
||||
}
|
||||
|
||||
var s = parseInt(data.chat_antiflood_params.sustained);
|
||||
if (isNaN(s) || s <= 0) {
|
||||
s = 1;
|
||||
}
|
||||
|
||||
var c = b / s;
|
||||
this.opts.chat_antiflood_params = {
|
||||
burst: b,
|
||||
sustained: s,
|
||||
cooldown: c
|
||||
};
|
||||
}
|
||||
|
||||
if ("show_public" in data && user.account.effectiveRank >= 3) {
|
||||
this.opts.show_public = Boolean(data.show_public);
|
||||
}
|
||||
|
||||
if ("enable_link_regex" in data) {
|
||||
this.opts.enable_link_regex = Boolean(data.enable_link_regex);
|
||||
}
|
||||
|
||||
if ("password" in data && user.account.effectiveRank >= 3) {
|
||||
var pw = data.password + "";
|
||||
pw = pw === "" ? false : pw.substring(0, 100);
|
||||
this.opts.password = pw;
|
||||
}
|
||||
|
||||
if ("allow_dupes" in data) {
|
||||
this.opts.allow_dupes = Boolean(data.allow_dupes);
|
||||
}
|
||||
|
||||
this.channel.logger.log("[mod] " + user.getName() + " updated channel options");
|
||||
this.sendOpts(this.channel.users);
|
||||
};
|
||||
|
||||
module.exports = OptionsModule;
|
|
@ -0,0 +1,369 @@
|
|||
var ChannelModule = require("./module");
|
||||
var User = require("../user");
|
||||
|
||||
const DEFAULT_PERMISSIONS = {
|
||||
seeplaylist: -1, // See the playlist
|
||||
playlistadd: 1.5, // Add video to the playlist
|
||||
playlistnext: 1.5, // Add a video next on the playlist
|
||||
playlistmove: 1.5, // Move a video on the playlist
|
||||
playlistdelete: 2, // Delete a video from the playlist
|
||||
playlistjump: 1.5, // Start a different video on the playlist
|
||||
playlistaddlist: 1.5, // Add a list of videos to the playlist
|
||||
oplaylistadd: -1, // Same as above, but for open (unlocked) playlist
|
||||
oplaylistnext: 1.5,
|
||||
oplaylistmove: 1.5,
|
||||
oplaylistdelete: 2,
|
||||
oplaylistjump: 1.5,
|
||||
oplaylistaddlist: 1.5,
|
||||
playlistaddcustom: 3, // Add custom embed to the playlist
|
||||
playlistaddlive: 1.5, // Add a livestream to the playlist
|
||||
exceedmaxlength: 2, // Add a video longer than the maximum length set
|
||||
addnontemp: 2, // Add a permanent video to the playlist
|
||||
settemp: 2, // Toggle temporary status of a playlist item
|
||||
playlistshuffle: 2, // Shuffle the playlist
|
||||
playlistclear: 2, // Clear the playlist
|
||||
pollctl: 1.5, // Open/close polls
|
||||
pollvote: -1, // Vote in polls
|
||||
viewhiddenpoll: 1.5, // View results of hidden polls
|
||||
voteskip: -1, // Vote to skip the current video
|
||||
mute: 1.5, // Mute other users
|
||||
kick: 1.5, // Kick other users
|
||||
ban: 2, // Ban other users
|
||||
motdedit: 3, // Edit the MOTD
|
||||
filteredit: 3, // Control chat filters
|
||||
filterimport: 3, // Import chat filter list
|
||||
emoteedit: 3, // Control emotes
|
||||
emoteimport: 3, // Import emote list
|
||||
playlistlock: 2, // Lock/unlock the playlist
|
||||
leaderctl: 2, // Give/take leader
|
||||
drink: 1.5, // Use the /d command
|
||||
chat: 0 // Send chat messages
|
||||
};
|
||||
|
||||
function PermissionsModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
this.permissions = {};
|
||||
this.openPlaylist = false;
|
||||
}
|
||||
|
||||
PermissionsModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
PermissionsModule.prototype.load = function (data) {
|
||||
this.permissions = {};
|
||||
var preset = "permissions" in data ? data.permissions : {};
|
||||
for (var key in DEFAULT_PERMISSIONS) {
|
||||
if (key in preset) {
|
||||
this.permissions[key] = preset[key];
|
||||
} else {
|
||||
this.permissions[key] = DEFAULT_PERMISSIONS[key];
|
||||
}
|
||||
}
|
||||
|
||||
if ("openPlaylist" in data) {
|
||||
this.openPlaylist = data.openPlaylist;
|
||||
} else if ("playlistLock" in data) {
|
||||
this.openPlaylist = !data.playlistLock;
|
||||
}
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.save = function (data) {
|
||||
data.permissions = this.permissions;
|
||||
data.openPlaylist = this.openPlaylist;
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.hasPermission = function (account, node) {
|
||||
if (account instanceof User) {
|
||||
account = account.account;
|
||||
}
|
||||
|
||||
if (node.indexOf("playlist") === 0 && this.openPlaylist &&
|
||||
account.effectiveRank >= this.permissions["o"+node]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return account.effectiveRank >= this.permissions[node];
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.sendPermissions = function (users) {
|
||||
var perms = this.permissions;
|
||||
if (users === this.channel.users) {
|
||||
this.channel.broadcastAll("setPermissions", perms);
|
||||
} else {
|
||||
users.forEach(function (u) {
|
||||
u.socket.emit("setPermissions", perms);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.sendPlaylistLock = function (users) {
|
||||
if (users === this.channel.users) {
|
||||
this.channel.broadcastAll("setPlaylistLocked", !this.openPlaylist);
|
||||
} else {
|
||||
var locked = !this.openPlaylist;
|
||||
users.forEach(function (u) {
|
||||
u.socket.emit("setPlaylistLocked", locked);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.onUserPostJoin = function (user) {
|
||||
user.socket.on("setPermissions", this.handleSetPermissions.bind(this, user));
|
||||
user.socket.on("togglePlaylistLock", this.handleTogglePlaylistLock.bind(this, user));
|
||||
this.sendPermissions([user]);
|
||||
this.sendPlaylistLock([user]);
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.handleTogglePlaylistLock = function (user) {
|
||||
if (!this.hasPermission(user, "playlistlock")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.openPlaylist = !this.openPlaylist;
|
||||
if (this.openPlaylist) {
|
||||
this.channel.logger.log("[playlist] " + user.getName() + " unlocked the playlist");
|
||||
} else {
|
||||
this.channel.logger.log("[playlist] " + user.getName() + " locked the playlist");
|
||||
}
|
||||
|
||||
this.sendPlaylistLock(this.channel.users);
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.handleSetPermissions = function (user, perms) {
|
||||
if (typeof perms !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.canSetPermissions(user)) {
|
||||
user.kick("Attempted setPermissions as a non-admin");
|
||||
return;
|
||||
}
|
||||
|
||||
for (var key in perms) {
|
||||
if (typeof perms[key] !== "number") {
|
||||
perms[key] = parseFloat(perms[key]);
|
||||
if (isNaN(perms[key])) {
|
||||
delete perms[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in perms) {
|
||||
if (key in this.permissions) {
|
||||
this.permissions[key] = perms[key];
|
||||
}
|
||||
}
|
||||
|
||||
if ("seeplaylist" in perms) {
|
||||
if (this.channel.modules.playlist) {
|
||||
this.channel.modules.playlist.sendPlaylist(this.channel.users);
|
||||
}
|
||||
}
|
||||
|
||||
this.channel.logger.log("[mod] " + user.getName() + " updated permissions");
|
||||
this.sendPermissions(this.channel.users);
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canAddVideo = function (account) {
|
||||
return this.hasPermission(account, "playlistadd");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canSetTemp = function (account) {
|
||||
return this.hasPermission(account, "settemp");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canSeePlaylist = function (account) {
|
||||
return this.hasPermission(account, "seeplaylist");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canAddList = function (account) {
|
||||
return this.hasPermission(account, "playlistaddlist");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canAddNonTemp = function (account) {
|
||||
return this.hasPermission(account, "addnontemp");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canAddNext = function (account) {
|
||||
return this.hasPermission(account, "playlistnext");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canAddLive = function (account) {
|
||||
return this.hasPermission(account, "playlistaddlive");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canAddCustom = function (account) {
|
||||
return this.hasPermission(account, "playlistaddcustom");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canMoveVideo = function (account) {
|
||||
return this.hasPermission(account, "playlistmove");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canDeleteVideo = function (account) {
|
||||
return this.hasPermission(account, "playlistdelete")
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canSkipVideo = function (account) {
|
||||
return this.hasPermission(account, "playlistjump");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canToggleTemporary = function (account) {
|
||||
return this.hasPermission(account, "settemp");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canExceedMaxLength = function (account) {
|
||||
return this.hasPermission(account, "exceedmaxlength");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canShufflePlaylist = function (account) {
|
||||
return this.hasPermission(account, "playlistshuffle");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canClearPlaylist = function (account) {
|
||||
return this.hasPermission(account, "playlistclear");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canLockPlaylist = function (account) {
|
||||
return this.hasPermission(account, "playlistlock");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canAssignLeader = function (account) {
|
||||
return this.hasPermission(account, "leaderctl");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canControlPoll = function (account) {
|
||||
return this.hasPermission(account, "pollctl");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canVote = function (account) {
|
||||
return this.hasPermission(account, "pollvote");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canViewHiddenPoll = function (account) {
|
||||
return this.hasPermission(account, "viewhiddenpoll");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canVoteskip = function (account) {
|
||||
return this.hasPermission(account, "voteskip");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canMute = function (actor) {
|
||||
return this.hasPermission(actor, "mute");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canKick = function (actor) {
|
||||
return this.hasPermission(actor, "kick");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canBan = function (actor) {
|
||||
return this.hasPermission(actor, "ban");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canEditMotd = function (actor) {
|
||||
return this.hasPermission(actor, "motdedit");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canEditFilters = function (actor) {
|
||||
return this.hasPermission(actor, "filteredit");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canImportFilters = function (actor) {
|
||||
return this.hasPermission(actor, "filterimport");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canEditEmotes = function (actor) {
|
||||
return this.hasPermission(actor, "emoteedit");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canImportEmotes = function (actor) {
|
||||
return this.hasPermission(actor, "emoteimport");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canCallDrink = function (actor) {
|
||||
return this.hasPermission(actor, "drink");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canChat = function (actor) {
|
||||
return this.hasPermission(actor, "chat");
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canSetOptions = function (actor) {
|
||||
if (actor instanceof User) {
|
||||
actor = actor.account;
|
||||
}
|
||||
|
||||
return actor.effectiveRank >= 2;
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canSetCSS = function (actor) {
|
||||
if (actor instanceof User) {
|
||||
actor = actor.account;
|
||||
}
|
||||
|
||||
return actor.effectiveRank >= 3;
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canSetJS = function (actor) {
|
||||
if (actor instanceof User) {
|
||||
actor = actor.account;
|
||||
}
|
||||
|
||||
return actor.effectiveRank >= 3;
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canSetPermissions = function (actor) {
|
||||
if (actor instanceof User) {
|
||||
actor = actor.account;
|
||||
}
|
||||
|
||||
return actor.effectiveRank >= 3;
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.canUncache = function (actor) {
|
||||
if (actor instanceof User) {
|
||||
actor = actor.account;
|
||||
}
|
||||
|
||||
return actor.effectiveRank >= 2;
|
||||
};
|
||||
|
||||
PermissionsModule.prototype.loadUnregistered = function () {
|
||||
var perms = {
|
||||
seeplaylist: -1,
|
||||
playlistadd: -1, // Add video to the playlist
|
||||
playlistnext: 0,
|
||||
playlistmove: 0, // Move a video on the playlist
|
||||
playlistdelete: 0, // Delete a video from the playlist
|
||||
playlistjump: 0, // Start a different video on the playlist
|
||||
playlistaddlist: 0, // Add a list of videos to the playlist
|
||||
oplaylistadd: -1, // Same as above, but for open (unlocked) playlist
|
||||
oplaylistnext: 0,
|
||||
oplaylistmove: 0,
|
||||
oplaylistdelete: 0,
|
||||
oplaylistjump: 0,
|
||||
oplaylistaddlist: 0,
|
||||
playlistaddcustom: 0, // Add custom embed to the playlist
|
||||
playlistaddlive: 0, // Add a livestream to the playlist
|
||||
exceedmaxlength: 0, // Add a video longer than the maximum length set
|
||||
addnontemp: 0, // Add a permanent video to the playlist
|
||||
settemp: 0, // Toggle temporary status of a playlist item
|
||||
playlistshuffle: 0, // Shuffle the playlist
|
||||
playlistclear: 0, // Clear the playlist
|
||||
pollctl: 0, // Open/close polls
|
||||
pollvote: -1, // Vote in polls
|
||||
viewhiddenpoll: 1.5, // View results of hidden polls
|
||||
voteskip: -1, // Vote to skip the current video
|
||||
playlistlock: 2, // Lock/unlock the playlist
|
||||
leaderctl: 0, // Give/take leader
|
||||
drink: 0, // Use the /d command
|
||||
chat: 0 // Send chat messages
|
||||
};
|
||||
|
||||
for (var key in perms) {
|
||||
this.permissions[key] = perms[key];
|
||||
}
|
||||
|
||||
this.openPlaylist = true;
|
||||
};
|
||||
|
||||
module.exports = PermissionsModule;
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,173 @@
|
|||
var ChannelModule = require("./module");
|
||||
var Poll = require("../poll").Poll;
|
||||
|
||||
const TYPE_NEW_POLL = {
|
||||
title: "string",
|
||||
timeout: "number,optional",
|
||||
obscured: "boolean",
|
||||
opts: "array"
|
||||
};
|
||||
|
||||
const TYPE_VOTE = {
|
||||
option: "number"
|
||||
};
|
||||
|
||||
function PollModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
|
||||
this.poll = null;
|
||||
if (this.channel.modules.chat) {
|
||||
this.channel.modules.chat.registerCommand("poll", this.handlePollCmd.bind(this, false));
|
||||
this.channel.modules.chat.registerCommand("hpoll", this.handlePollCmd.bind(this, true));
|
||||
}
|
||||
}
|
||||
|
||||
PollModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
PollModule.prototype.load = function (data) {
|
||||
if ("poll" in data) {
|
||||
if (data.poll !== null) {
|
||||
this.poll = new Poll(data.poll.initiator, "", [], data.poll.obscured);
|
||||
this.poll.title = data.poll.title;
|
||||
this.poll.options = data.poll.options;
|
||||
this.poll.counts = data.poll.counts;
|
||||
this.poll.votes = data.poll.votes;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PollModule.prototype.save = function (data) {
|
||||
if (this.poll === null) {
|
||||
data.poll = null;
|
||||
return;
|
||||
}
|
||||
|
||||
data.poll = {
|
||||
title: this.poll.title,
|
||||
initiator: this.poll.initiator,
|
||||
options: this.poll.options,
|
||||
counts: this.poll.counts,
|
||||
votes: this.poll.votes,
|
||||
obscured: this.poll.obscured
|
||||
};
|
||||
};
|
||||
|
||||
PollModule.prototype.onUserPostJoin = function (user) {
|
||||
this.sendPoll([user]);
|
||||
user.socket.typecheckedOn("newPoll", TYPE_NEW_POLL, this.handleNewPoll.bind(this, user));
|
||||
user.socket.typecheckedOn("vote", TYPE_VOTE, this.handleVote.bind(this, user));
|
||||
user.socket.on("closePoll", this.handleClosePoll.bind(this, user));
|
||||
};
|
||||
|
||||
PollModule.prototype.sendPoll = function (users) {
|
||||
if (!this.poll) {
|
||||
return;
|
||||
}
|
||||
|
||||
var obscured = this.poll.packUpdate(false);
|
||||
var unobscured = this.poll.packUpdate(true);
|
||||
var perms = this.channel.modules.permissions;
|
||||
|
||||
users.forEach(function (u) {
|
||||
u.socket.emit("closePoll");
|
||||
if (perms.canViewHiddenPoll(u)) {
|
||||
u.socket.emit("newPoll", unobscured);
|
||||
} else {
|
||||
u.socket.emit("newPoll", obscured);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
PollModule.prototype.sendPollUpdate = function (users) {
|
||||
if (!this.poll) {
|
||||
return;
|
||||
}
|
||||
|
||||
var obscured = this.poll.packUpdate(false);
|
||||
var unobscured = this.poll.packUpdate(true);
|
||||
var perms = this.channel.modules.permissions;
|
||||
|
||||
users.forEach(function (u) {
|
||||
if (perms.canViewHiddenPoll(u)) {
|
||||
u.socket.emit("updatePoll", unobscured);
|
||||
} else {
|
||||
u.socket.emit("updatePoll", obscured);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
PollModule.prototype.handleNewPoll = function (user, data) {
|
||||
if (!this.channel.modules.permissions.canControlPoll(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var title = data.title.substring(0, 255);
|
||||
var opts = data.opts.map(function (x) { return (""+x).substring(0, 255); });
|
||||
var obscured = data.obscured;
|
||||
|
||||
var poll = new Poll(user.getName(), title, opts, obscured);
|
||||
var self = this;
|
||||
if (data.hasOwnProperty("timeout") && !isNaN(data.timeout) && data.timeout > 0) {
|
||||
poll.timer = setTimeout(function () {
|
||||
if (self.poll === poll) {
|
||||
self.handleClosePoll({
|
||||
getName: function () { return "[poll timer]" },
|
||||
account: { effectiveRank: 255 }
|
||||
});
|
||||
}
|
||||
}, data.timeout * 1000);
|
||||
}
|
||||
|
||||
this.poll = poll;
|
||||
this.sendPoll(this.channel.users);
|
||||
this.channel.logger.log("[poll] " + user.getName() + " opened poll: '" + poll.title + "'");
|
||||
};
|
||||
|
||||
PollModule.prototype.handleVote = function (user, data) {
|
||||
if (!this.channel.modules.permissions.canVote(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.poll) {
|
||||
this.poll.vote(user.ip, data.option);
|
||||
this.sendPollUpdate(this.channel.users);
|
||||
}
|
||||
};
|
||||
|
||||
PollModule.prototype.handleClosePoll = function (user) {
|
||||
if (!this.channel.modules.permissions.canControlPoll(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.poll) {
|
||||
if (this.poll.obscured) {
|
||||
this.poll.obscured = false;
|
||||
this.channel.broadcastAll("updatePoll", this.poll.packUpdate(true));
|
||||
}
|
||||
|
||||
if (this.poll.timer) {
|
||||
clearTimeout(this.poll.timer);
|
||||
}
|
||||
|
||||
this.channel.broadcastAll("closePoll");
|
||||
this.channel.logger.log("[poll] " + user.getName() + " closed the active poll");
|
||||
this.poll = null;
|
||||
}
|
||||
};
|
||||
|
||||
PollModule.prototype.handlePollCmd = function (obscured, user, msg, meta) {
|
||||
if (!this.channel.modules.permissions.canControlPoll(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
msg = msg.replace(/^\/h?poll/, "");
|
||||
|
||||
var args = msg.split(",");
|
||||
var title = args.shift();
|
||||
var poll = new Poll(user.getName(), title, args, obscured);
|
||||
this.poll = poll;
|
||||
this.sendPoll(this.channel.users);
|
||||
this.channel.logger.log("[poll] " + user.getName() + " opened poll: '" + poll.title + "'");
|
||||
};
|
||||
|
||||
module.exports = PollModule;
|
|
@ -0,0 +1,184 @@
|
|||
var ChannelModule = require("./module");
|
||||
var Flags = require("../flags");
|
||||
var Account = require("../account");
|
||||
var db = require("../database");
|
||||
|
||||
const TYPE_SET_CHANNEL_RANK = {
|
||||
name: "string",
|
||||
rank: "number"
|
||||
};
|
||||
|
||||
function RankModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
|
||||
if (this.channel.modules.chat) {
|
||||
this.channel.modules.chat.registerCommand("/rank", this.handleCmdRank.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
RankModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
RankModule.prototype.onUserPostJoin = function (user) {
|
||||
user.socket.typecheckedOn("setChannelRank", TYPE_SET_CHANNEL_RANK, this.handleRankChange.bind(this, user));
|
||||
var self = this;
|
||||
user.socket.on("requestChannelRanks", function () {
|
||||
self.sendChannelRanks([user]);
|
||||
});
|
||||
};
|
||||
|
||||
RankModule.prototype.sendChannelRanks = function (users) {
|
||||
if (!this.channel.is(Flags.C_REGISTERED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
db.channels.allRanks(this.channel.name, function (err, ranks) {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
users.forEach(function (u) {
|
||||
if (u.account.effectiveRank >= 3) {
|
||||
u.socket.emit("channelRanks", ranks);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
RankModule.prototype.handleCmdRank = function (user, msg, meta) {
|
||||
var args = msg.split(" ");
|
||||
args.shift(); /* shift off /rank */
|
||||
var name = args.shift();
|
||||
var rank = parseInt(args.shift());
|
||||
|
||||
if (!name || isNaN(rank)) {
|
||||
user.socket.emit("noflood", {
|
||||
action: "/rank",
|
||||
msg: "Syntax: /rank <username> <rank>. <rank> must be a positive integer > 1"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleRankChange(user, { name: name, rank: rank });
|
||||
};
|
||||
|
||||
RankModule.prototype.handleRankChange = function (user, data) {
|
||||
if (user.account.effectiveRank < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rank = data.rank;
|
||||
var userrank = user.account.effectiveRank;
|
||||
var name = data.name.substring(0, 20).toLowerCase();
|
||||
|
||||
if (isNaN(rank) || rank < 1 || (rank >= userrank && !(userrank === 4 && rank === 4))) {
|
||||
user.socket.emit("channelRankFail", {
|
||||
msg: "Updating user rank failed: You can't promote someone to a rank equal " +
|
||||
"or higher than yourself, or demote them to below rank 1."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var receiver;
|
||||
var lowerName = name.toLowerCase();
|
||||
for (var i = 0; i < this.channel.users.length; i++) {
|
||||
if (this.channel.users[i].getLowerName() === lowerName) {
|
||||
receiver = this.channel.users[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (name === user.getLowerName()) {
|
||||
user.socket.emit("channelRankFail", {
|
||||
msg: "Updating user rank failed: You can't promote or demote yourself."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.is(Flags.C_REGISTERED)) {
|
||||
user.socket.emit("channelRankFail", {
|
||||
msg: "Updating user rank failed: in an unregistered channel, a user must " +
|
||||
"be online in the channel in order to have their rank changed."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (receiver) {
|
||||
var current = Math.max(receiver.account.globalRank, receiver.account.channelRank);
|
||||
if (current >= userrank && !(userrank === 4 && current === 4)) {
|
||||
user.socket.emit("channelRankFail", {
|
||||
msg: "Updating user rank failed: You can't promote or demote "+
|
||||
"someone who has equal or higher rank than yourself"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
receiver.account.channelRank = rank;
|
||||
receiver.account.effectiveRank = rank;
|
||||
this.channel.logger.log("[mod] " + user.getName() + " set " + name + "'s rank " +
|
||||
"to " + rank);
|
||||
this.channel.broadcastAll("setUserRank", data);
|
||||
|
||||
if (!this.channel.is(Flags.C_REGISTERED)) {
|
||||
user.socket.emit("channelRankFail", {
|
||||
msg: "This channel is not registered. Any rank changes are temporary " +
|
||||
"and not stored in the database."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!receiver.is(Flags.U_REGISTERED)) {
|
||||
user.socket.emit("channelRankFail", {
|
||||
msg: "The user you promoted is not a registered account. " +
|
||||
"Any rank changes are temporary and not stored in the database."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
data.userrank = userrank;
|
||||
|
||||
this.updateDatabase(data, function (err) {
|
||||
if (err) {
|
||||
user.socket.emit("channelRankFail", {
|
||||
msg: "Database failure when updating rank"
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
data.userrank = userrank;
|
||||
var self = this;
|
||||
this.updateDatabase(data, function (err) {
|
||||
if (err) {
|
||||
user.socket.emit("channelRankFail", {
|
||||
msg: "Updating user rank failed: " + err
|
||||
});
|
||||
}
|
||||
self.channel.logger.log("[mod] " + user.getName() + " set " + data.name +
|
||||
"'s rank to " + rank);
|
||||
self.channel.broadcastAll("setUserRank", data);
|
||||
if (self.channel.modules.chat) {
|
||||
self.channel.modules.chat.sendModMessage(
|
||||
user.getName() + " set " + data.name + "'s rank to " + rank,
|
||||
3
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
RankModule.prototype.updateDatabase = function (data, cb) {
|
||||
var chan = this.channel;
|
||||
Account.rankForName(data.name, { channel: this.channel.name }, function (err, rank) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if (rank >= data.userrank && !(rank === 4 && data.userrank === 4)) {
|
||||
cb("You can't promote or demote someone with equal or higher rank than you.");
|
||||
return;
|
||||
}
|
||||
|
||||
db.channels.setRank(chan.name, data.name, data.rank, cb);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = RankModule;
|
|
@ -0,0 +1,103 @@
|
|||
var ChannelModule = require("./module");
|
||||
var Flags = require("../flags");
|
||||
var Poll = require("../poll").Poll;
|
||||
|
||||
function VoteskipModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
|
||||
this.poll = false;
|
||||
}
|
||||
|
||||
VoteskipModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
VoteskipModule.prototype.onUserPostJoin = function (user) {
|
||||
user.socket.on("voteskip", this.handleVoteskip.bind(this, user));
|
||||
};
|
||||
|
||||
VoteskipModule.prototype.handleVoteskip = function (user) {
|
||||
if (!this.channel.modules.options.get("allow_voteskip")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.modules.playlist) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.modules.permissions.canVoteskip(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.poll) {
|
||||
this.poll = new Poll("[server]", "voteskip", ["skip"], false);
|
||||
}
|
||||
|
||||
this.poll.vote(user.ip, 0);
|
||||
|
||||
var title = "";
|
||||
if (this.channel.modules.playlist.current) {
|
||||
title = " " + this.channel.modules.playlist.current;
|
||||
}
|
||||
|
||||
var name = user.getName() || "(anonymous)"
|
||||
|
||||
this.channel.logger.log("[playlist] " + name + " voteskipped " + title);
|
||||
this.update();
|
||||
};
|
||||
|
||||
VoteskipModule.prototype.update = function () {
|
||||
if (!this.channel.modules.options.get("allow_voteskip")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.poll) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.channel.modules.playlist.meta.count === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var max = this.calcVoteskipMax();
|
||||
var need = Math.ceil(max * this.channel.modules.options.get("voteskip_ratio"));
|
||||
if (this.poll.counts[0] >= need) {
|
||||
this.channel.logger.log("[playlist] Voteskip passed.");
|
||||
this.channel.modules.playlist._playNext();
|
||||
}
|
||||
|
||||
this.sendVoteskipData(this.channel.users);
|
||||
};
|
||||
|
||||
VoteskipModule.prototype.sendVoteskipData = function (users) {
|
||||
var max = this.calcVoteskipMax();
|
||||
var data = {
|
||||
count: this.poll ? this.poll.counts[0] : 0,
|
||||
need: this.poll ? Math.ceil(max * this.channel.modules.options.get("voteskip_ratio"))
|
||||
: 0
|
||||
};
|
||||
|
||||
users.forEach(function (u) {
|
||||
if (u.account.effectiveRank >= 1.5) {
|
||||
u.socket.emit("voteskip", data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
VoteskipModule.prototype.calcVoteskipMax = function () {
|
||||
var perms = this.channel.modules.permissions;
|
||||
return this.channel.users.map(function (u) {
|
||||
if (!perms.canVoteskip(u)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return u.is(Flags.U_AFK) ? 0 : 1;
|
||||
}).reduce(function (a, b) {
|
||||
return a + b;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
VoteskipModule.prototype.onMediaChange = function (data) {
|
||||
this.poll = false;
|
||||
this.sendVoteskipData(this.channel.users);
|
||||
};
|
||||
|
||||
module.exports = VoteskipModule;
|
|
@ -1,387 +0,0 @@
|
|||
/*
|
||||
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.js");
|
||||
var Poll = require("./poll").Poll;
|
||||
|
||||
var handlers = {
|
||||
/* commands that send chat messages */
|
||||
"me": function (chan, user, msg, meta) {
|
||||
meta.addClass = "action";
|
||||
meta.action = true;
|
||||
chan.sendMessage(user, msg, meta);
|
||||
return true;
|
||||
},
|
||||
"sp": function (chan, user, msg, meta) {
|
||||
meta.addClass = "spoiler";
|
||||
chan.sendMessage(user, msg, meta);
|
||||
return true;
|
||||
},
|
||||
"say": function (chan, user, msg, meta) {
|
||||
if (user.rank >= 1.5) {
|
||||
meta.addClass = "shout";
|
||||
meta.addClassToNameAndTimestamp = true;
|
||||
meta.forceShowName = true;
|
||||
chan.sendMessage(user, msg, meta);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
"a": function (chan, user, msg, meta) {
|
||||
if (user.global_rank < 255) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var superadminflair = {
|
||||
labelclass: "label-danger",
|
||||
icon: "glyphicon-globe"
|
||||
};
|
||||
|
||||
var args = msg.split(" ");
|
||||
var cargs = [];
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var a = args[i];
|
||||
if (a.indexOf("!icon-") === 0) {
|
||||
superadminflair.icon = "glyph" + a.substring(1);
|
||||
} else if (a.indexOf("!label-") === 0) {
|
||||
superadminflair.labelclass = a.substring(1);
|
||||
} else {
|
||||
cargs.push(a);
|
||||
}
|
||||
}
|
||||
|
||||
meta.superadminflair = superadminflair;
|
||||
meta.forceShowName = true;
|
||||
chan.sendMessage(user, cargs.join(" "), meta);
|
||||
return true;
|
||||
},
|
||||
"poll": function (chan, user, msg, meta) {
|
||||
handlePoll(chan, user, msg, false);
|
||||
return true;
|
||||
},
|
||||
"hpoll": function (chan, user, msg, meta) {
|
||||
handlePoll(chan, user, msg, true);
|
||||
return true;
|
||||
},
|
||||
|
||||
/* commands that do not send chat messages */
|
||||
"afk": function (chan, user, msg, meta) {
|
||||
user.setAFK(!user.meta.afk);
|
||||
return true;
|
||||
},
|
||||
"mute": function (chan, user, msg, meta) {
|
||||
handleMute(chan, user, msg.split(" "));
|
||||
return true;
|
||||
},
|
||||
"smute": function (chan, user, msg, meta) {
|
||||
handleShadowMute(chan, user, msg.split(" "));
|
||||
return true;
|
||||
},
|
||||
"unmute": function (chan, user, msg, meta) {
|
||||
handleUnmute(chan, user, msg.split(" "));
|
||||
return true;
|
||||
},
|
||||
"kick": function (chan, user, msg, meta) {
|
||||
handleKick(chan, user, msg.split(" "));
|
||||
return true;
|
||||
},
|
||||
"ban": function (chan, user, msg, meta) {
|
||||
handleBan(chan, user, msg.split(" "));
|
||||
return true;
|
||||
},
|
||||
"ipban": function (chan, user, msg, meta) {
|
||||
handleIPBan(chan, user, msg.split(" "));
|
||||
return true;
|
||||
},
|
||||
"clear": function (chan, user, msg, meta) {
|
||||
handleClear(chan, user);
|
||||
return true;
|
||||
},
|
||||
"clean": function (chan, user, msg, meta) {
|
||||
handleClean(chan, user, msg);
|
||||
return true;
|
||||
},
|
||||
"cleantitle": function (chan, user, msg, meta) {
|
||||
handleCleanTitle(chan, user, msg);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
var handlerList = [];
|
||||
for (var key in handlers) {
|
||||
handlerList.push({
|
||||
// match /command followed by a space or end of string
|
||||
re: new RegExp("^\\/" + key + "(?:\\s|$)"),
|
||||
fn: handlers[key]
|
||||
});
|
||||
}
|
||||
|
||||
function handle(chan, user, msg, meta) {
|
||||
// Special case because the drink command can vary
|
||||
var m = msg.match(/^\/d(-?[0-9]*)(?:\s|$)(.*)/);
|
||||
if (m) {
|
||||
handleDrink(chan, user, m[1], m[2], meta);
|
||||
return true;
|
||||
}
|
||||
for (var i = 0; i < handlerList.length; i++) {
|
||||
var h = handlerList[i];
|
||||
if (msg.match(h.re)) {
|
||||
var rest;
|
||||
if (msg.indexOf(" ") >= 0) {
|
||||
rest = msg.substring(msg.indexOf(" ") + 1);
|
||||
} else {
|
||||
rest = "";
|
||||
}
|
||||
return h.fn(chan, user, rest, meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrink(chan, user, count, msg, meta) {
|
||||
if (!chan.hasPermission(user, "drink")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count === "") {
|
||||
count = 1;
|
||||
}
|
||||
count = parseInt(count);
|
||||
if (isNaN(count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
meta.drink = true;
|
||||
meta.forceShowName = true;
|
||||
meta.addClass = "drink";
|
||||
chan.drinks += count;
|
||||
chan.sendDrinks(chan.users);
|
||||
if (count < 0 && msg.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
msg = msg + " drink!";
|
||||
if (count !== 1) {
|
||||
msg += " (x" + count + ")";
|
||||
}
|
||||
chan.sendMessage(user, msg, meta);
|
||||
}
|
||||
|
||||
function handleShadowMute(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;
|
||||
}
|
||||
|
||||
/* Reset a previous regular mute */
|
||||
if (chan.mutedUsers.contains(person.name.toLowerCase())) {
|
||||
chan.mutedUsers.remove(person.name.toLowerCase());
|
||||
}
|
||||
|
||||
person.meta.smuted = person.meta.muted = true;
|
||||
chan.sendUserMeta(chan.users, person, 2);
|
||||
chan.mutedUsers.add("[shadow]" + person.name.toLowerCase());
|
||||
chan.logger.log("[mod] " + user.name + " shadow muted " + args[0]);
|
||||
chan.sendModMessage(user.name + " shadow muted " + args[0], 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.muted = true;
|
||||
chan.sendUserMeta(chan.users, person);
|
||||
chan.mutedUsers.add(person.name.toLowerCase());
|
||||
chan.logger.log("[mod] " + user.name + " muted " + args[0]);
|
||||
chan.sendModMessage(user.name + " muted " + args[0], 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.muted = false;
|
||||
var wasSmuted = person.meta.smuted;
|
||||
person.meta.smuted = false;
|
||||
chan.sendUserMeta(chan.users, person, wasSmuted ? 2 : false);
|
||||
chan.mutedUsers.remove(person.name.toLowerCase());
|
||||
chan.mutedUsers.remove("[shadow]" + person.name.toLowerCase());
|
||||
chan.logger.log("[mod] " + user.name + " unmuted " + args[0]);
|
||||
chan.sendModMessage(user.name + " unmuted " + args[0], 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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("[mod] " + user.name + " kicked " + args[0]);
|
||||
args[0] = "";
|
||||
var reason = args.join(" ");
|
||||
kickee.kick(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleIPBan(chan, user, args) {
|
||||
var name = args.shift();
|
||||
var range = args.shift();
|
||||
var reason;
|
||||
if (range !== "range") {
|
||||
reason = range + " " + args.join(" ");
|
||||
range = false;
|
||||
} else {
|
||||
reason = args.join(" ");
|
||||
range = true;
|
||||
}
|
||||
chan.handleBanAllIP(user, name, reason, range);
|
||||
// Ban the name too for good measure
|
||||
chan.handleNameBan(user, name, reason);
|
||||
}
|
||||
|
||||
function handleBan(chan, user, args) {
|
||||
var name = args.shift();
|
||||
var reason = args.join(" ");
|
||||
chan.handleNameBan(user, name, reason);
|
||||
}
|
||||
|
||||
function handleUnban(chan, user, args) {
|
||||
if (chan.hasPermission(user, "ban") && args.length > 0) {
|
||||
chan.logger.log("[mod] " + 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, hidden) {
|
||||
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, hidden === true);
|
||||
chan.poll = poll;
|
||||
chan.sendPoll(chan.users);
|
||||
chan.logger.log("[poll] " + user.name + " Opened Poll: '" + poll.title + "'");
|
||||
}
|
||||
}
|
||||
|
||||
function handleClear(chan, user) {
|
||||
if (user.rank < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
chan.chatbuffer = [];
|
||||
chan.sendAll("clearchat");
|
||||
}
|
||||
|
||||
/*
|
||||
/clean and /cleantitle contributed by http://github.com/unbibium.
|
||||
Modifications by Calvin Montgomery
|
||||
*/
|
||||
|
||||
function generateTargetRegex(target) {
|
||||
const flagsre = /^(-[img]+\s+)/i
|
||||
var m = target.match(flagsre);
|
||||
var flags = "";
|
||||
if (m) {
|
||||
flags = m[0].slice(1,-1);
|
||||
target = target.replace(flagsre, "");
|
||||
}
|
||||
return new RegExp(target, flags);
|
||||
}
|
||||
|
||||
function handleClean(chan, user, target) {
|
||||
if (!chan.hasPermission(user, "playlistdelete"))
|
||||
return;
|
||||
target = generateTargetRegex(target);
|
||||
chan.playlist.clean(function (item) {
|
||||
return target.test(item.queueby);
|
||||
});
|
||||
}
|
||||
|
||||
function handleCleanTitle(chan, user, target) {
|
||||
if (!chan.hasPermission(user, "playlistdelete"))
|
||||
return;
|
||||
target = generateTargetRegex(target);
|
||||
chan.playlist.clean(function (item) {
|
||||
return target.exec(item.media.title) !== null;
|
||||
});
|
||||
}
|
||||
|
||||
exports.handle = handle;
|
||||
|
|
@ -13,6 +13,7 @@ var fs = require("fs");
|
|||
var path = require("path");
|
||||
var Logger = require("./logger");
|
||||
var nodemailer = require("nodemailer");
|
||||
var net = require("net");
|
||||
var YAML = require("yamljs");
|
||||
|
||||
var defaults = {
|
||||
|
@ -98,7 +99,11 @@ var defaults = {
|
|||
email: "cyzon@cytu.be"
|
||||
}
|
||||
],
|
||||
"aggressive-gc": false
|
||||
"aggressive-gc": false,
|
||||
playlist: {
|
||||
"max-items": 4000,
|
||||
"update-interval": 5
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -239,6 +244,77 @@ function preprocessConfig(cfg) {
|
|||
cfg.https["full-address"] = httpsfa;
|
||||
}
|
||||
|
||||
|
||||
// Socket.IO URLs
|
||||
cfg.io["ipv4-nossl"] = "";
|
||||
cfg.io["ipv4-ssl"] = "";
|
||||
cfg.io["ipv6-nossl"] = "";
|
||||
cfg.io["ipv6-ssl"] = "";
|
||||
for (var i = 0; i < cfg.listen.length; i++) {
|
||||
var srv = cfg.listen[i];
|
||||
if (!srv.io) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (srv.ip === "") {
|
||||
if (srv.port === cfg.io["default-port"]) {
|
||||
cfg.io["ipv4-nossl"] = cfg.io["domain"] + ":" + cfg.io["default-port"];
|
||||
} else if (srv.port === cfg.https["default-port"]) {
|
||||
cfg.io["ipv4-ssl"] = cfg.https["domain"] + ":" + cfg.https["default-port"];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (net.isIPv4(srv.ip) || srv.ip === "::") {
|
||||
if (srv.https && !cfg.io["ipv4-ssl"]) {
|
||||
if (srv.url) {
|
||||
cfg.io["ipv4-ssl"] = srv.url;
|
||||
} else {
|
||||
cfg.io["ipv4-ssl"] = "https://" + srv.ip + ":" + srv.port;
|
||||
}
|
||||
} else if (!cfg.io["ipv4-nossl"]) {
|
||||
if (srv.url) {
|
||||
cfg.io["ipv4-nossl"] = srv.url;
|
||||
} else {
|
||||
cfg.io["ipv4-nossl"] = "http://" + srv.ip + ":" + srv.port;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (net.isIPv6(srv.ip) || srv.ip === "::") {
|
||||
if (srv.https && !cfg.io["ipv6-ssl"]) {
|
||||
if (!srv.url) {
|
||||
Logger.errlog.log("Config Error: no URL defined for IPv6 " +
|
||||
"Socket.IO listener! Ignoring this listener " +
|
||||
"because the Socket.IO client cannot connect to " +
|
||||
"a raw IPv6 address.");
|
||||
Logger.errlog.log("(Listener was: " + JSON.stringify(srv) + ")");
|
||||
} else {
|
||||
cfg.io["ipv6-ssl"] = srv.url;
|
||||
}
|
||||
} else if (!cfg.io["ipv6-nossl"]) {
|
||||
if (!srv.url) {
|
||||
Logger.errlog.log("Config Error: no URL defined for IPv6 " +
|
||||
"Socket.IO listener! Ignoring this listener " +
|
||||
"because the Socket.IO client cannot connect to " +
|
||||
"a raw IPv6 address.");
|
||||
Logger.errlog.log("(Listener was: " + JSON.stringify(srv) + ")");
|
||||
} else {
|
||||
cfg.io["ipv6-nossl"] = srv.url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg.io["ipv4-default"] = cfg.io["ipv4-ssl"] || cfg.io["ipv4-nossl"];
|
||||
cfg.io["ipv6-default"] = cfg.io["ipv6-ssl"] || cfg.io["ipv6-nossl"];
|
||||
|
||||
// sioconfig
|
||||
var sioconfig = "var IO_URLS={'ipv4-nossl':'" + cfg.io["ipv4-nossl"] + "'," +
|
||||
"'ipv4-ssl':'" + cfg.io["ipv4-ssl"] + "'," +
|
||||
"'ipv6-nossl':'" + cfg.io["ipv6-nossl"] + "'," +
|
||||
"'ipv6-ssl':'" + cfg.io["ipv6-ssl"] + "'};";
|
||||
cfg.sioconfig = sioconfig;
|
||||
|
||||
// Generate RegExps for reserved names
|
||||
var reserved = cfg["reserved-names"];
|
||||
for (var key in reserved) {
|
||||
|
|
124
lib/database.js
124
lib/database.js
|
@ -5,6 +5,8 @@ var Logger = require("./logger");
|
|||
var Config = require("./config");
|
||||
var Server = require("./server");
|
||||
var tables = require("./database/tables");
|
||||
var net = require("net");
|
||||
var util = require("./utilities");
|
||||
|
||||
var pool = null;
|
||||
var global_ipbans = {};
|
||||
|
@ -97,23 +99,16 @@ function blackHole() {
|
|||
* Check if an IP address is globally banned
|
||||
*/
|
||||
module.exports.isGlobalIPBanned = function (ip, callback) {
|
||||
if (typeof callback !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO account for IPv6
|
||||
// Also possibly just change this to allow arbitrary
|
||||
// ranges instead of only /32, /24, /16
|
||||
const re = /(\d+)\.(\d+)\.(\d+)\.(\d+)/;
|
||||
// Account for range banning
|
||||
var s16 = ip.replace(re, "$1.$2");
|
||||
var s24 = ip.replace(re, "$1.$2.$3");
|
||||
|
||||
var range = util.getIPRange(ip);
|
||||
var wrange = util.getWideIPRange(ip);
|
||||
var banned = ip in global_ipbans ||
|
||||
s16 in global_ipbans ||
|
||||
s24 in global_ipbans;
|
||||
range in global_ipbans ||
|
||||
wrange in global_ipbans;
|
||||
|
||||
callback(null, banned);
|
||||
if (callback) {
|
||||
callback(null, banned);
|
||||
}
|
||||
return banned;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -478,9 +473,13 @@ module.exports.getAliases = function (ip, callback) {
|
|||
|
||||
var query = "SELECT name,time FROM aliases WHERE ip";
|
||||
// if the ip parameter is a /24 range, we want to match accordingly
|
||||
if(ip.match(/^\d+\.\d+\.\d+$/)) {
|
||||
if (ip.match(/^\d+\.\d+\.\d+$/) || ip.match(/^\d+\.\d+$/)) {
|
||||
query += " LIKE ?";
|
||||
ip += ".%";
|
||||
} else if (ip.match(/^(?:[0-9a-f]{4}:){3}[0-9a-f]{4}$/) ||
|
||||
ip.match(/^(?:[0-9a-f]{4}:){2}[0-9a-f]{4}$/)) {
|
||||
query += " LIKE ?";
|
||||
ip += ":%";
|
||||
} else {
|
||||
query += "=?";
|
||||
}
|
||||
|
@ -493,6 +492,8 @@ module.exports.getAliases = function (ip, callback) {
|
|||
names = res.map(function (row) { return row.name; });
|
||||
}
|
||||
|
||||
console.log(query, names);
|
||||
|
||||
callback(err, names);
|
||||
});
|
||||
};
|
||||
|
@ -511,103 +512,12 @@ module.exports.getIPs = function (name, callback) {
|
|||
if(!err) {
|
||||
ips = res.map(function (row) { return row.ip; });
|
||||
}
|
||||
|
||||
callback(err, ips);
|
||||
});
|
||||
};
|
||||
|
||||
/* END REGION */
|
||||
|
||||
/* REGION action log */
|
||||
|
||||
/*
|
||||
module.exports.recordAction = function (ip, name, action, args,
|
||||
callback) {
|
||||
if(typeof callback !== "function")
|
||||
callback = blackHole;
|
||||
|
||||
var query = "INSERT INTO actionlog (ip, name, action, args, time) " +
|
||||
"VALUES (?, ?, ?, ?, ?)";
|
||||
|
||||
module.exports.query(query, [ip, name, action, args, Date.now()], callback);
|
||||
};
|
||||
|
||||
module.exports.clearActions = function (actions, callback) {
|
||||
if(typeof callback !== "function")
|
||||
callback = blackHole;
|
||||
|
||||
var list = [];
|
||||
for(var i in actions)
|
||||
list.push("?");
|
||||
|
||||
var actionlist = "(" + list.join(",") + ")";
|
||||
|
||||
var query = "DELETE FROM actionlog WHERE action IN " + actionlist;
|
||||
module.exports.query(query, actions, callback);
|
||||
};
|
||||
|
||||
module.exports.clearSingleAction = function (item, callback) {
|
||||
if(typeof callback !== "function")
|
||||
callback = blackHole;
|
||||
|
||||
var query = "DELETE FROM actionlog WHERE ip=? AND time=?";
|
||||
module.exports.query(query, [item.ip, item.time], callback);
|
||||
};
|
||||
|
||||
|
||||
module.exports.recentRegistrationCount = function (ip, callback) {
|
||||
if(typeof callback !== "function")
|
||||
return;
|
||||
|
||||
var query = "SELECT * FROM actionlog WHERE ip=? " +
|
||||
"AND action='register-success' AND time > ?";
|
||||
|
||||
module.exports.query(query, [ip, Date.now() - 48 * 3600 * 1000],
|
||||
function (err, res) {
|
||||
if(err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, res.length);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.listActionTypes = function (callback) {
|
||||
if(typeof callback !== "function")
|
||||
return;
|
||||
|
||||
var query = "SELECT DISTINCT action FROM actionlog";
|
||||
module.exports.query(query, function (err, res) {
|
||||
if(err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var types = [];
|
||||
res.forEach(function (row) {
|
||||
types.push(row.action);
|
||||
});
|
||||
callback(null, types);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.listActions = function (types, callback) {
|
||||
if(typeof callback !== "function")
|
||||
return;
|
||||
|
||||
var list = [];
|
||||
for(var i in types)
|
||||
list.push("?");
|
||||
|
||||
var actionlist = "(" + list.join(",") + ")";
|
||||
var query = "SELECT * FROM actionlog WHERE action IN " + actionlist;
|
||||
module.exports.query(query, types, callback);
|
||||
};
|
||||
*/
|
||||
|
||||
/* END REGION */
|
||||
|
||||
/* REGION stats */
|
||||
|
||||
module.exports.addStatPoint = function (time, ucount, ccount, mem, callback) {
|
||||
|
|
|
@ -295,12 +295,17 @@ module.exports = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
callback(null, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
db.query("SELECT global_rank FROM `users` WHERE name=?", [name],
|
||||
function (err, rows) {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
} else if (rows.length === 0) {
|
||||
callback("User does not exist", null);
|
||||
callback(null, 0);
|
||||
} else {
|
||||
callback(null, rows[0].global_rank);
|
||||
}
|
||||
|
@ -344,6 +349,10 @@ module.exports = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (names.length === 0) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
var list = "(" + names.map(function () { return "?";}).join(",") + ")";
|
||||
|
||||
db.query("SELECT global_rank FROM `users` WHERE name IN " + list, names,
|
||||
|
|
|
@ -4,6 +4,8 @@ var fs = require("fs");
|
|||
var path = require("path");
|
||||
var Logger = require("../logger");
|
||||
var tables = require("./tables");
|
||||
var Flags = require("../flags");
|
||||
var util = require("../utilities");
|
||||
|
||||
var blackHole = function () { };
|
||||
|
||||
|
@ -292,7 +294,7 @@ module.exports = {
|
|||
// than the database has stored. Update accordingly.
|
||||
chan.name = res[0].name;
|
||||
chan.uniqueName = chan.name.toLowerCase();
|
||||
chan.registered = true;
|
||||
chan.setFlag(Flags.C_REGISTERED);
|
||||
chan.logger.log("[init] Loaded channel from database");
|
||||
callback(null, true);
|
||||
});
|
||||
|
@ -315,7 +317,7 @@ module.exports = {
|
|||
[name],
|
||||
function (err, rows) {
|
||||
if (err) {
|
||||
callback(err, 1);
|
||||
callback(err, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -529,9 +531,11 @@ module.exports = {
|
|||
return;
|
||||
}
|
||||
|
||||
var range = ip.replace(/^(\d+\.\d+\.\d+)\.\d+$/, "$1");
|
||||
var range = util.getIPRange(ip);
|
||||
var wrange = util.getWideIPRange(ip);
|
||||
|
||||
db.query("SELECT * FROM `chan_" + chan + "_bans` WHERE ip=? OR ip=?", [ip, range],
|
||||
db.query("SELECT * FROM `chan_" + chan + "_bans` WHERE ip IN (?, ?, ?)",
|
||||
[ip, range, wrange],
|
||||
function (err, rows) {
|
||||
callback(err, err ? false : rows.length > 0);
|
||||
});
|
||||
|
|
|
@ -46,6 +46,27 @@ function MakeEmitter(obj) {
|
|||
}
|
||||
});
|
||||
};
|
||||
|
||||
obj.unbind = function (ev, fn) {
|
||||
var self = this;
|
||||
if (ev in self.__evHandlers) {
|
||||
if (!fn) {
|
||||
self.__evHandlers[ev] = [];
|
||||
} else {
|
||||
var j = -1;
|
||||
for (var i = 0; i < self.__evHandlers[ev].length; i++) {
|
||||
if (self.__evHandlers[ev][i].fn === fn) {
|
||||
j = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (j >= 0) {
|
||||
self.__evHandlers[ev].splice(j, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = MakeEmitter;
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
module.exports = {
|
||||
C_READY : 1 << 0,
|
||||
C_ERROR : 1 << 1,
|
||||
C_REGISTERED : 1 << 2,
|
||||
|
||||
U_READY : 1 << 0,
|
||||
U_LOGGING_IN : 1 << 1,
|
||||
U_LOGGED_IN : 1 << 2,
|
||||
U_REGISTERED : 1 << 3,
|
||||
U_AFK : 1 << 4,
|
||||
U_MUTED : 1 << 5,
|
||||
U_SMUTED : 1 << 6,
|
||||
U_IN_CHANNEL : 1 << 7
|
||||
};
|
365
lib/get-info.js
365
lib/get-info.js
|
@ -13,7 +13,7 @@ var http = require("http");
|
|||
var https = require("https");
|
||||
var domain = require("domain");
|
||||
var Logger = require("./logger.js");
|
||||
var Media = require("./media.js").Media;
|
||||
var Media = require("./media");
|
||||
var CustomEmbedFilter = require("./customembed").filter;
|
||||
var Server = require("./server");
|
||||
var Config = require("./config");
|
||||
|
@ -49,14 +49,8 @@ var Getters = {
|
|||
/* youtube.com */
|
||||
yt: function (id, callback) {
|
||||
var sv = Server.getServer();
|
||||
/*
|
||||
if (sv.cfg["enable-ytv3"] && sv.cfg["ytv3apikey"]) {
|
||||
Getters["ytv3"](id, callback);
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
var m = id.match(/([\w-]+)/);
|
||||
var m = id.match(/([\w-]{11})/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
|
@ -73,33 +67,33 @@ var Getters = {
|
|||
timeout: 1000
|
||||
};
|
||||
|
||||
if(Config.get("youtube-v2-key")) {
|
||||
if (Config.get("youtube-v2-key")) {
|
||||
options.headers = {
|
||||
"X-Gdata-Key": "key=" + Config.get("youtube-v2-key")
|
||||
};
|
||||
}
|
||||
|
||||
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 === 400) {
|
||||
callback("Invalid video", null);
|
||||
return;
|
||||
} else if(status === 503) {
|
||||
callback("API failure", null);
|
||||
return;
|
||||
} else if(status !== 200) {
|
||||
callback("HTTP " + status, null);
|
||||
return;
|
||||
switch (status) {
|
||||
case 200:
|
||||
break; /* Request is OK, skip to handling data */
|
||||
case 400:
|
||||
return callback("Invalid request", null);
|
||||
case 403:
|
||||
return callback("Private video", null);
|
||||
case 404:
|
||||
return callback("Video not found", null);
|
||||
case 500:
|
||||
case 503:
|
||||
return callback("Service unavailable", null);
|
||||
default:
|
||||
return callback("HTTP " + status, null);
|
||||
}
|
||||
|
||||
var buffer = data;
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
/* Check for embedding restrictions */
|
||||
if (data.entry.yt$accessControl) {
|
||||
var ac = data.entry.yt$accessControl;
|
||||
for (var i = 0; i < ac.length; i++) {
|
||||
|
@ -115,26 +109,28 @@ var Getters = {
|
|||
|
||||
var seconds = data.entry.media$group.yt$duration.seconds;
|
||||
var title = data.entry.title.$t;
|
||||
var media = new Media(id, title, seconds, "yt");
|
||||
var meta = {};
|
||||
/* Check for country restrictions */
|
||||
if (data.entry.media$group.media$restriction) {
|
||||
var rest = data.entry.media$group.media$restriction;
|
||||
if (rest.length > 0) {
|
||||
if (rest[0].relationship === "deny") {
|
||||
media.restricted = rest[0].$t;
|
||||
meta.restricted = rest[0].$t;
|
||||
}
|
||||
}
|
||||
}
|
||||
var media = new Media(id, title, seconds, "yt", meta);
|
||||
callback(false, media);
|
||||
} catch(e) {
|
||||
} 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)
|
||||
if (m === null)
|
||||
m = buffer.match(/<code>([^<]+)<\/code>/);
|
||||
|
||||
var err = e;
|
||||
if(m) {
|
||||
if (m) {
|
||||
if(m[1] === "too_many_recent_calls") {
|
||||
err = "YouTube is throttling the server right "+
|
||||
"now for making too many requests. "+
|
||||
|
@ -149,65 +145,12 @@ var Getters = {
|
|||
});
|
||||
},
|
||||
|
||||
/* youtube.com API v3 (requires API key) */
|
||||
// DEPRECATED
|
||||
ytv3: function (id, callback) {
|
||||
var sv = Server.getServer();
|
||||
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=" + sv.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("YTv3: HTTP " + status, 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("Video not found", 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(e, null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* youtube.com playlists */
|
||||
yp: function (id, callback, url) {
|
||||
var sv = Server.getServer();
|
||||
/**
|
||||
* NOTE: callback may be called multiple times, once for each <= 25 video
|
||||
* batch of videos in the list. It will be called in order.
|
||||
*/
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
|
@ -216,11 +159,15 @@ var Getters = {
|
|||
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) {
|
||||
/**
|
||||
* NOTE: the third parameter, url, is used to chain this retriever
|
||||
* multiple times to get all the videos from a playlist, as each
|
||||
* request only returns 25 videos.
|
||||
*/
|
||||
if (url !== undefined) {
|
||||
path = "/" + url.split("gdata.youtube.com")[1];
|
||||
}
|
||||
|
||||
var options = {
|
||||
host: "gdata.youtube.com",
|
||||
port: 443,
|
||||
|
@ -230,24 +177,27 @@ var Getters = {
|
|||
timeout: 1000
|
||||
};
|
||||
|
||||
if(Config.get("youtube-v2-key")) {
|
||||
if (Config.get("youtube-v2-key")) {
|
||||
options.headers = {
|
||||
"X-Gdata-Key": "key=" + Config.get("youtube-v2-key")
|
||||
};
|
||||
}
|
||||
|
||||
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("YTPlaylist HTTP " + status, null);
|
||||
switch (status) {
|
||||
case 200:
|
||||
break; /* Request is OK, skip to handling data */
|
||||
case 400:
|
||||
return callback("Invalid request", null);
|
||||
case 403:
|
||||
return callback("Private playlist", null);
|
||||
case 404:
|
||||
return callback("Playlist not found", null);
|
||||
case 500:
|
||||
case 503:
|
||||
return callback("Service unavailable", null);
|
||||
default:
|
||||
return callback("HTTP " + status, null);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -255,6 +205,10 @@ var Getters = {
|
|||
var vids = [];
|
||||
for(var i in data.feed.entry) {
|
||||
try {
|
||||
/**
|
||||
* FIXME: This should probably check for embed restrictions
|
||||
* and country restrictions on each video in the list
|
||||
*/
|
||||
var item = data.feed.entry[i];
|
||||
var id = item.media$group.yt$videoid.$t;
|
||||
var title = item.title.$t;
|
||||
|
@ -268,11 +222,13 @@ var Getters = {
|
|||
callback(false, vids);
|
||||
|
||||
var links = data.feed.link;
|
||||
for(var i in links) {
|
||||
if(links[i].rel === "next")
|
||||
for (var i in links) {
|
||||
if (links[i].rel === "next") {
|
||||
/* Look up the next batch of videos from the list */
|
||||
Getters["yp"](id, callback, links[i].href);
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
callback(e, null);
|
||||
}
|
||||
|
||||
|
@ -281,9 +237,13 @@ var Getters = {
|
|||
|
||||
/* youtube.com search */
|
||||
ytSearch: function (terms, callback) {
|
||||
var sv = Server.getServer();
|
||||
for(var i in terms)
|
||||
/**
|
||||
* terms is a list of words from the search query. Each word must be
|
||||
* encoded properly for use in the request URI
|
||||
*/
|
||||
for (var i in terms) {
|
||||
terms[i] = encodeURIComponent(terms[i]);
|
||||
}
|
||||
var query = terms.join("+");
|
||||
|
||||
var options = {
|
||||
|
@ -295,15 +255,15 @@ var Getters = {
|
|||
timeout: 1000
|
||||
};
|
||||
|
||||
if(Config.get("youtube-v2-key")) {
|
||||
if (Config.get("youtube-v2-key")) {
|
||||
options.headers = {
|
||||
"X-Gdata-Key": "key=" + Config.get("youtube-v2-key")
|
||||
};
|
||||
}
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status !== 200) {
|
||||
callback("YTSearch HTTP " + status, null);
|
||||
if (status !== 200) {
|
||||
callback("YouTube search: HTTP " + status, null);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -312,6 +272,10 @@ var Getters = {
|
|||
var vids = [];
|
||||
for(var i in data.feed.entry) {
|
||||
try {
|
||||
/**
|
||||
* FIXME: This should probably check for embed restrictions
|
||||
* and country restrictions on each video in the list
|
||||
*/
|
||||
var item = data.feed.entry[i];
|
||||
var id = item.media$group.yt$videoid.$t;
|
||||
var title = item.title.$t;
|
||||
|
@ -354,18 +318,20 @@ var Getters = {
|
|||
};
|
||||
|
||||
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("YTv2 HTTP " + status, null);
|
||||
return;
|
||||
switch (status) {
|
||||
case 200:
|
||||
break; /* Request is OK, skip to handling data */
|
||||
case 400:
|
||||
return callback("Invalid request", null);
|
||||
case 403:
|
||||
return callback("Private video", null);
|
||||
case 404:
|
||||
return callback("Video not found", null);
|
||||
case 500:
|
||||
case 503:
|
||||
return callback("Service unavailable", null);
|
||||
default:
|
||||
return callback("HTTP " + status, null);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -377,7 +343,11 @@ var Getters = {
|
|||
callback(false, media);
|
||||
} catch(e) {
|
||||
var err = e;
|
||||
if(buffer.match(/not found/))
|
||||
/**
|
||||
* This should no longer be necessary as the outer handler
|
||||
* checks for HTTP 404
|
||||
*/
|
||||
if (buffer.match(/not found/))
|
||||
err = "Video not found";
|
||||
|
||||
callback(err, null);
|
||||
|
@ -431,12 +401,6 @@ var Getters = {
|
|||
|
||||
/* 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];
|
||||
|
@ -454,19 +418,31 @@ var Getters = {
|
|||
};
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if (status === 404) {
|
||||
callback("Video not found", null);
|
||||
return;
|
||||
} else if (status !== 200) {
|
||||
callback("DM HTTP " + status, null);
|
||||
return;
|
||||
switch (status) {
|
||||
case 200:
|
||||
break; /* Request is OK, skip to handling data */
|
||||
case 400:
|
||||
return callback("Invalid request", null);
|
||||
case 403:
|
||||
return callback("Private video", null);
|
||||
case 404:
|
||||
return callback("Video not found", null);
|
||||
case 500:
|
||||
case 503:
|
||||
return callback("Service unavailable", null);
|
||||
default:
|
||||
return callback("HTTP " + status, null);
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
var title = data.title;
|
||||
var seconds = data.duration;
|
||||
if(title === "Deleted video" && seconds === 10) {
|
||||
/**
|
||||
* This is a rather hacky way to indicate that a video has
|
||||
* been deleted...
|
||||
*/
|
||||
if (title === "Deleted video" && seconds === 10) {
|
||||
callback("Video not found", null);
|
||||
return;
|
||||
}
|
||||
|
@ -480,12 +456,7 @@ var Getters = {
|
|||
|
||||
/* 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
|
||||
|
||||
/* TODO: require server owners to register their own API key, put in config */
|
||||
const SC_CLIENT = "2e0c82ab5a020f3a7509318146128abd";
|
||||
|
||||
var m = id.match(/([\w-\/\.:]+)/);
|
||||
|
@ -506,15 +477,20 @@ var Getters = {
|
|||
};
|
||||
|
||||
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("SC HTTP " + status, null);
|
||||
return;
|
||||
switch (status) {
|
||||
case 200:
|
||||
break; /* Request is OK, skip to handling data */
|
||||
case 400:
|
||||
return callback("Invalid request", null);
|
||||
case 403:
|
||||
return callback("Private sound", null);
|
||||
case 404:
|
||||
return callback("Sound not found", null);
|
||||
case 500:
|
||||
case 503:
|
||||
return callback("Service unavailable", null);
|
||||
default:
|
||||
return callback("HTTP " + status, null);
|
||||
}
|
||||
|
||||
var track = null;
|
||||
|
@ -535,16 +511,30 @@ var Getters = {
|
|||
timeout: 1000
|
||||
};
|
||||
|
||||
// I want to get off async's wild ride
|
||||
/**
|
||||
* There has got to be a way to directly get the data I want without
|
||||
* making two requests to Soundcloud...right?
|
||||
* ...right?
|
||||
*/
|
||||
urlRetrieve(https, options2, function (status, data) {
|
||||
if(status !== 200) {
|
||||
callback("SC HTTP " + status, null);
|
||||
return;
|
||||
switch (status) {
|
||||
case 200:
|
||||
break; /* Request is OK, skip to handling data */
|
||||
case 400:
|
||||
return callback("Invalid request", null);
|
||||
case 403:
|
||||
return callback("Private sound", null);
|
||||
case 404:
|
||||
return callback("Sound not found", null);
|
||||
case 500:
|
||||
case 503:
|
||||
return callback("Service unavailable", null);
|
||||
default:
|
||||
return callback("HTTP " + status, null);
|
||||
}
|
||||
|
||||
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");
|
||||
|
@ -601,11 +591,13 @@ var Getters = {
|
|||
|
||||
/* ustream.tv */
|
||||
us: function (id, callback) {
|
||||
// 2013-09-17
|
||||
// They couldn't fucking decide whether channels should
|
||||
// be at http://www.ustream.tv/channel/foo or just
|
||||
// http://www.ustream.tv/foo so they do both.
|
||||
// [](/cleese)
|
||||
/**
|
||||
*2013-09-17
|
||||
* They couldn't fucking decide whether channels should
|
||||
* be at http://www.ustream.tv/channel/foo or just
|
||||
* http://www.ustream.tv/foo so they do both.
|
||||
* [](/cleese)
|
||||
*/
|
||||
var m = id.match(/([^\?&#]+)|(channel\/[^\?&#]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
|
@ -613,6 +605,7 @@ var Getters = {
|
|||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
|
||||
var options = {
|
||||
host: "www.ustream.tv",
|
||||
port: 80,
|
||||
|
@ -627,12 +620,14 @@ var Getters = {
|
|||
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)
|
||||
/**
|
||||
* 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;
|
||||
|
@ -660,6 +655,9 @@ var Getters = {
|
|||
|
||||
/* imgur.com albums */
|
||||
im: function (id, callback) {
|
||||
/**
|
||||
* TODO: Consider deprecating this in favor of custom embeds
|
||||
*/
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
|
@ -681,6 +679,7 @@ var Getters = {
|
|||
|
||||
/* google docs */
|
||||
gd: function (id, callback) {
|
||||
/* WARNING: hacks inbound */
|
||||
var options = {
|
||||
host: "docs.google.com",
|
||||
path: "/file/d/" + id + "/edit",
|
||||
|
@ -688,9 +687,20 @@ var Getters = {
|
|||
};
|
||||
|
||||
urlRetrieve(https, options, function (status, res) {
|
||||
if (status !== 200) {
|
||||
callback("Google Docs rejected: HTTP " + status, false);
|
||||
return;
|
||||
switch (status) {
|
||||
case 200:
|
||||
break; /* Request is OK, skip to handling data */
|
||||
case 400:
|
||||
return callback("Invalid request", null);
|
||||
case 403:
|
||||
return callback("Private video", null);
|
||||
case 404:
|
||||
return callback("Video not found", null);
|
||||
case 500:
|
||||
case 503:
|
||||
return callback("Service unavailable", null);
|
||||
default:
|
||||
return callback("HTTP " + status, null);
|
||||
}
|
||||
|
||||
var m = res.match(/main\((.*?)\);<\/script>/);
|
||||
|
@ -699,6 +709,7 @@ var Getters = {
|
|||
var data = m[1];
|
||||
data = data.substring(data.indexOf(",") + 1);
|
||||
data = data.replace(/'(.*?)'([:\,\}\]])/g, "\"$1\"$2");
|
||||
/* Fixes an issue with certain characters represented as \xkk */
|
||||
data = data.replace(/\\x(\d*)/g, function (sub, s1) {
|
||||
return String.fromCharCode(parseInt(s1, 16));
|
||||
});
|
||||
|
@ -706,8 +717,7 @@ var Getters = {
|
|||
var js = JSON.parse(data);
|
||||
var title = js[0].title;
|
||||
var seconds = js[1].videodetails.duration / 1000;
|
||||
var med = new Media(id, title, seconds, "gd");
|
||||
|
||||
var meta = {};
|
||||
var fv = js[1].videoplay.flashVars;
|
||||
var fvstr = "";
|
||||
for (var k in fv) {
|
||||
|
@ -718,7 +728,7 @@ var Getters = {
|
|||
fvstr = fvstr.substring(1);
|
||||
|
||||
var url = js[1].videoplay.swfUrl + "&enablejsapi=1";
|
||||
med.object = {
|
||||
meta.object = {
|
||||
type: "application/x-shockwave-flash",
|
||||
allowscriptaccess: "always",
|
||||
allowfullscreen: "true",
|
||||
|
@ -726,7 +736,7 @@ var Getters = {
|
|||
data: url
|
||||
};
|
||||
|
||||
med.params = [
|
||||
meta.params = [
|
||||
{
|
||||
name: "allowFullScreen",
|
||||
value: "true"
|
||||
|
@ -745,6 +755,8 @@ var Getters = {
|
|||
}
|
||||
];
|
||||
|
||||
var med = new Media(id, title, seconds, "gd", meta);
|
||||
|
||||
callback(false, med);
|
||||
} catch (e) {
|
||||
callback("Parsing of Google Docs output failed", null);
|
||||
|
@ -780,7 +792,8 @@ function vimeoWorkaround(id, cb) {
|
|||
var parse = function (data) {
|
||||
var i = data.indexOf("{\"cdn_url\"");
|
||||
if (i === -1) {
|
||||
Logger.errlog.log("Vimeo workaround failed (i=-1): http://vimeo.com/" + id);
|
||||
/* TODO possibly send an error message? */
|
||||
//Logger.errlog.log("Vimeo workaround failed (i=-1): http://vimeo.com/" + id);
|
||||
setImmediate(function () {
|
||||
cb({});
|
||||
});
|
||||
|
@ -798,10 +811,10 @@ function vimeoWorkaround(id, cb) {
|
|||
} catch (e) {
|
||||
// This shouldn't happen due to the user-agent, but just in case
|
||||
if (data.indexOf("crawler") !== -1) {
|
||||
Logger.syslog.log("Warning: VimeoIsADoucheCopter got crawler response");
|
||||
Logger.syslog.log("Warning: vimdeoWorkaround got crawler response");
|
||||
failcount++;
|
||||
if (failcount > 4) {
|
||||
Logger.errlog.log("VimeoIsADoucheCopter got bad response 5 times!"+
|
||||
Logger.errlog.log("vimeoWorkaround got bad response 5 times!"+
|
||||
" Giving up.");
|
||||
setImmediate(function () {
|
||||
cb({});
|
||||
|
|
|
@ -6,6 +6,11 @@ var User = require("../user");
|
|||
var Server = require("../server");
|
||||
var Config = require("../config");
|
||||
var $util = require("../utilities");
|
||||
var Flags = require("../flags");
|
||||
var Account = require("../account");
|
||||
var typecheck = require("json-typecheck");
|
||||
var net = require("net");
|
||||
var util = require("../utilities");
|
||||
|
||||
var CONNECT_RATE = {
|
||||
burst: 5,
|
||||
|
@ -42,8 +47,13 @@ function handleAuth(data, accept) {
|
|||
* Called after a connection is accepted
|
||||
*/
|
||||
function handleConnection(sock) {
|
||||
sock._ip = sock.handshake.address.address;
|
||||
var ip = sock._ip;
|
||||
var ip = sock.handshake.address.address;
|
||||
var longip = ip;
|
||||
sock._ip = ip;
|
||||
if (net.isIPv6(ip)) {
|
||||
longip = util.expandIPv6(ip);
|
||||
}
|
||||
sock._longip = longip;
|
||||
var srv = Server.getServer();
|
||||
if (srv.torblocker && srv.torblocker.shouldBlockIP(ip)) {
|
||||
sock.emit("kick", {
|
||||
|
@ -69,13 +79,12 @@ function handleConnection(sock) {
|
|||
}
|
||||
|
||||
// Check for global ban on the IP
|
||||
db.isGlobalIPBanned(ip, function (err, banned) {
|
||||
if (banned) {
|
||||
Logger.syslog.log("Disconnecting " + ip + " - global banned");
|
||||
sock.emit("kick", { reason: "Your IP is globally banned." });
|
||||
sock.disconnect(true);
|
||||
}
|
||||
});
|
||||
if (db.isGlobalIPBanned(ip)) {
|
||||
Logger.syslog.log("Rejecting " + ip + " - global banned");
|
||||
sock.emit("kick", { reason: "Your IP is globally banned." });
|
||||
sock.disconnect(true);
|
||||
return;
|
||||
}
|
||||
|
||||
sock.on("disconnect", function () {
|
||||
ipCount[ip]--;
|
||||
|
@ -99,21 +108,61 @@ function handleConnection(sock) {
|
|||
}
|
||||
|
||||
Logger.syslog.log("Accepted socket from " + ip);
|
||||
var user = new User(sock);
|
||||
if (sock.handshake && sock.handshake.user) {
|
||||
user.name = sock.handshake.user.name;
|
||||
user.global_rank = sock.handshake.user.global_rank;
|
||||
user.loggedIn = true;
|
||||
user.emit("login");
|
||||
user.socket.emit("login", {
|
||||
success: true,
|
||||
name: user.name,
|
||||
guest: false
|
||||
|
||||
sock.typecheckedOn = function (msg, template, cb) {
|
||||
sock.on(msg, function (data) {
|
||||
typecheck(data, template, function (err, data) {
|
||||
if (err) {
|
||||
sock.emit("errorMsg", {
|
||||
msg: "Unexpected error for message " + msg + ": " + err.message
|
||||
});
|
||||
} else {
|
||||
cb(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
sock.typecheckedOnce = function (msg, template, cb) {
|
||||
sock.once(msg, function (data) {
|
||||
typecheck(data, template, function (err, data) {
|
||||
if (err) {
|
||||
sock.emit("errorMsg", {
|
||||
msg: "Unexpected error for message " + msg + ": " + err.message
|
||||
});
|
||||
} else {
|
||||
cb(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var user = new User(sock);
|
||||
if (sock.handshake.user) {
|
||||
user.setFlag(Flags.U_REGISTERED);
|
||||
user.clearFlag(Flags.U_READY);
|
||||
user.refreshAccount({ name: sock.handshake.user.name },
|
||||
function (err, account) {
|
||||
if (err) {
|
||||
user.clearFlag(Flags.U_REGISTERED);
|
||||
user.setFlag(Flags.U_READY);
|
||||
return;
|
||||
}
|
||||
user.socket.emit("login", {
|
||||
success: true,
|
||||
name: user.getName(),
|
||||
guest: false
|
||||
});
|
||||
db.recordVisit(ip, user.getName());
|
||||
user.socket.emit("rank", user.account.effectiveRank);
|
||||
user.setFlag(Flags.U_LOGGED_IN);
|
||||
user.emit("login", account);
|
||||
Logger.syslog.log(ip + " logged in as " + user.getName());
|
||||
user.setFlag(Flags.U_READY);
|
||||
});
|
||||
user.socket.emit("rank", user.global_rank);
|
||||
Logger.syslog.log(ip + " logged in as " + user.name);
|
||||
} else {
|
||||
user.socket.emit("rank", -1);
|
||||
user.setFlag(Flags.U_READY);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,15 +182,30 @@ module.exports = {
|
|||
if (id in srv.servers) {
|
||||
io = srv.ioServers[id] = sio.listen(srv.servers[id]);
|
||||
} else {
|
||||
io = srv.ioServers[id] = sio.listen(bind.port, bind.ip);
|
||||
if (net.isIPv6(bind.ip) || bind.ip === "::") {
|
||||
/**
|
||||
* Socket.IO won't bind to a v6 address natively.
|
||||
* Instead, we have to create a node HTTP server, bind it
|
||||
* to the desired address, then have socket.io listen on it
|
||||
*/
|
||||
io = srv.ioServers[id] = sio.listen(
|
||||
require("http").createServer().listen(bind.port, bind.ip)
|
||||
);
|
||||
} else {
|
||||
io = srv.ioServers[id] = sio.listen(bind.port, bind.ip);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (io) {
|
||||
io.set("log level", 1);
|
||||
io.set("authorization", handleAuth);
|
||||
io.on("connection", handleConnection);
|
||||
}
|
||||
});
|
||||
|
||||
sio.ioServers = Object.keys(srv.ioServers)
|
||||
.filter(Object.hasOwnProperty.bind(srv.ioServers))
|
||||
.map(function (k) { return srv.ioServers[k] });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
119
lib/media.js
119
lib/media.js
|
@ -1,90 +1,59 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013 Calvin Montgomery
|
||||
var util = require("./utilities");
|
||||
|
||||
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:
|
||||
function Media(id, title, seconds, type, meta) {
|
||||
if (!meta) {
|
||||
meta = {};
|
||||
}
|
||||
|
||||
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)
|
||||
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.seconds = seconds === "--:--" ? 0 : parseInt(seconds);
|
||||
this.duration = util.formatTime(seconds);
|
||||
this.type = type;
|
||||
this.meta = meta;
|
||||
this.currentTime = 0;
|
||||
this.paused = false;
|
||||
}
|
||||
|
||||
Media.prototype.dup = function() {
|
||||
var m = new Media(this.id, this.title, this.seconds, this.type);
|
||||
return m;
|
||||
}
|
||||
Media.prototype = {
|
||||
pack: function () {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
seconds: this.seconds,
|
||||
duration: this.duration,
|
||||
type: this.type,
|
||||
meta: {
|
||||
object: this.meta.object,
|
||||
params: this.meta.params,
|
||||
direct: this.meta.direct,
|
||||
restricted: this.meta.restricted
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// Returns an object containing the data in this Media but not the
|
||||
// prototype
|
||||
Media.prototype.pack = function() {
|
||||
var x = {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
seconds: this.seconds,
|
||||
duration: this.duration,
|
||||
type: this.type,
|
||||
};
|
||||
getTimeUpdate: function () {
|
||||
return {
|
||||
currentTime: this.currentTime,
|
||||
paused: this.paused
|
||||
};
|
||||
},
|
||||
|
||||
if (this.object) {
|
||||
x.object = this.object;
|
||||
}
|
||||
if (this.params) {
|
||||
x.params = this.params;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
getFullUpdate: function () {
|
||||
var packed = this.pack();
|
||||
packed.currentTime = this.currentTime;
|
||||
packed.paused = this.paused;
|
||||
return packed;
|
||||
},
|
||||
|
||||
// Same as pack() but includes the currentTime variable set by the channel
|
||||
// when the media is being synchronized
|
||||
Media.prototype.fullupdate = function() {
|
||||
var x = {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
seconds: this.seconds,
|
||||
duration: this.duration,
|
||||
type: this.type,
|
||||
currentTime: this.currentTime,
|
||||
paused: this.paused,
|
||||
};
|
||||
if (this.object) {
|
||||
x.object = this.object;
|
||||
reset: function () {
|
||||
this.currentTime = 0;
|
||||
this.paused = false;
|
||||
}
|
||||
if (this.params) {
|
||||
x.params = this.params;
|
||||
}
|
||||
if (this.direct) {
|
||||
x.direct = this.direct;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
Media.prototype.timeupdate = function() {
|
||||
//return this.fullupdate();
|
||||
return {
|
||||
currentTime: this.currentTime,
|
||||
paused: this.paused
|
||||
};
|
||||
}
|
||||
|
||||
Media.prototype.reset = function () {
|
||||
delete this.currentTime;
|
||||
delete this.direct;
|
||||
};
|
||||
|
||||
exports.Media = Media;
|
||||
module.exports = Media;
|
||||
|
|
|
@ -1,195 +0,0 @@
|
|||
/*
|
||||
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);
|
458
lib/playlist.js
458
lib/playlist.js
|
@ -1,458 +0,0 @@
|
|||
/*
|
||||
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 ULList = require("./ullist").ULList;
|
||||
var AsyncQueue = require("./asyncqueue");
|
||||
var Media = require("./media").Media;
|
||||
var util = require("./utilities");
|
||||
var vimeoWorkaround = require("./get-info").vimeoWorkaround;
|
||||
var Config = require("./config");
|
||||
|
||||
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.uniqueName;
|
||||
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.fnqueue = new AsyncQueue();
|
||||
|
||||
this.channel = chan;
|
||||
this.server = chan.server;
|
||||
var pl = this;
|
||||
this.on("mediaUpdate", function(m) {
|
||||
if (chan.dead) {
|
||||
pl.die();
|
||||
return;
|
||||
}
|
||||
chan.sendAll("mediaUpdate", m.timeupdate());
|
||||
});
|
||||
this.on("changeMedia", function(m) {
|
||||
if (chan.dead) {
|
||||
pl.die();
|
||||
return;
|
||||
}
|
||||
chan.resetVideo();
|
||||
chan.sendAll("setCurrent", pl.current.uid);
|
||||
chan.sendAll("changeMedia", m.fullupdate());
|
||||
chan.logger.log("[playlist] Now playing: " + m.title + " (" + m.type + ":" + m.id +
|
||||
")");
|
||||
});
|
||||
this.on("remove", function(item) {
|
||||
if (chan.dead) {
|
||||
pl.die();
|
||||
return;
|
||||
}
|
||||
chan.updatePlaylistMeta();
|
||||
chan.sendPlaylistMeta(chan.users);
|
||||
chan.sendAll("delete", {
|
||||
uid: item.uid
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
m.object = e.object;
|
||||
m.params = e.params;
|
||||
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);
|
||||
self.channel.sendAll("delete", {
|
||||
uid: it.uid
|
||||
});
|
||||
self.channel.updatePlaylistMeta();
|
||||
self.channel.sendPlaylistMeta(self.channel.users);
|
||||
}
|
||||
|
||||
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.addMedia = function (data) {
|
||||
var pos = data.pos;
|
||||
if (pos === "next") {
|
||||
if (this.current !== null)
|
||||
pos = this.current.uid;
|
||||
else
|
||||
pos = "append";
|
||||
}
|
||||
|
||||
var m = new Media(data.id, data.title, data.seconds, data.type);
|
||||
m.object = data.object;
|
||||
m.params = data.params;
|
||||
var item = this.makeItem(m);
|
||||
item.queueby = data.queueby;
|
||||
item.temp = data.temp;
|
||||
return {
|
||||
item: item,
|
||||
error: this.add(item, pos)
|
||||
};
|
||||
};
|
||||
|
||||
Playlist.prototype.remove = function (uid) {
|
||||
var self = this;
|
||||
var item = self.items.find(uid);
|
||||
if (item && self.items.remove(uid)) {
|
||||
if (item === self.current) {
|
||||
self._next();
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Playlist.prototype.move = function (from, after) {
|
||||
var it = this.items.find(from);
|
||||
if (!this.items.remove(from))
|
||||
return false;
|
||||
|
||||
if (after === "prepend") {
|
||||
if (!this.items.prepend(it))
|
||||
return false;
|
||||
} else if (after === "append") {
|
||||
if (!this.items.append(it))
|
||||
return false;
|
||||
} else if (!this.items.insertAfter(it, after)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Playlist.prototype.next = function() {
|
||||
if(!this.current)
|
||||
return;
|
||||
|
||||
var it = this.current;
|
||||
|
||||
if (it.temp) {
|
||||
if (this.remove(it.uid)) {
|
||||
this.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) {
|
||||
if (this.remove(it.uid)) {
|
||||
this.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.getFullUpdate = function () {
|
||||
if (!this.current) {
|
||||
return null;
|
||||
} else if (!this.current.media) {
|
||||
return null;
|
||||
} else {
|
||||
return this.current.media.fullupdate();
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
var self = this;
|
||||
|
||||
if(!self.current || !self.current.media)
|
||||
return false;
|
||||
|
||||
if (self.current.media.type === "vi" &&
|
||||
!self.current.media.direct &&
|
||||
Config.get("vimeo-workaround")) {
|
||||
vimeoWorkaround(self.current.media.id, function (direct) {
|
||||
if (self.current != null && self.current.media != null) {
|
||||
self.current.media.direct = direct;
|
||||
self.startPlayback(time);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.leading) {
|
||||
self.current.media.paused = false;
|
||||
self.current.media.currentTime = time || 0;
|
||||
self.on("changeMedia")(self.current.media);
|
||||
return;
|
||||
}
|
||||
|
||||
time = time || -3;
|
||||
self.current.media.paused = time < 0;
|
||||
self.current.media.currentTime = time;
|
||||
|
||||
if(self._leadInterval) {
|
||||
clearInterval(self._leadInterval);
|
||||
self._leadInterval = false;
|
||||
}
|
||||
self.on("changeMedia")(self.current.media);
|
||||
if(self.current.media.seconds > 0) {
|
||||
self._lastUpdate = Date.now();
|
||||
self._leadInterval = setInterval(function() {
|
||||
self._leadLoop();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
const UPDATE_INTERVAL = 5;
|
||||
|
||||
Playlist.prototype._leadLoop = function() {
|
||||
if(this.current == null)
|
||||
return;
|
||||
|
||||
if(this.channel.name == "") {
|
||||
this.die();
|
||||
return;
|
||||
}
|
||||
|
||||
var dt = (Date.now() - this._lastUpdate) / 1000.0;
|
||||
var t = this.current.media.currentTime;
|
||||
|
||||
// Transition from lead-in
|
||||
if (t < 0 && (t + dt) >= 0) {
|
||||
this.current.media.currentTime = 0;
|
||||
this.current.media.paused = false;
|
||||
this._counter = 0;
|
||||
this._lastUpdate = Date.now();
|
||||
this.on("mediaUpdate")(this.current.media);
|
||||
return;
|
||||
}
|
||||
|
||||
this.current.media.currentTime += dt;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Delete items from the playlist for which filter(item) returns
|
||||
a truthy value
|
||||
|
||||
based on code contributed by http://github.com/unbibium
|
||||
*/
|
||||
Playlist.prototype.clean = function (filter) {
|
||||
var self = this;
|
||||
var matches = self.items.findAll(filter);
|
||||
var count = 0;
|
||||
var deleteNext = function () {
|
||||
if (count < matches.length) {
|
||||
var uid = matches[count].uid;
|
||||
count++;
|
||||
if (self.remove(uid)) {
|
||||
self.channel.sendAll("delete", {
|
||||
uid: uid
|
||||
});
|
||||
}
|
||||
deleteNext();
|
||||
} else {
|
||||
// refresh meta only once, at the end
|
||||
self.channel.updatePlaylistMeta();
|
||||
self.channel.sendPlaylistMeta(self.channel.users);
|
||||
}
|
||||
};
|
||||
// start initial callback
|
||||
deleteNext();
|
||||
};
|
||||
|
||||
module.exports = Playlist;
|
|
@ -9,7 +9,7 @@ The above copyright notice and this permission notice shall be included in all c
|
|||
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.
|
||||
*/
|
||||
|
||||
const VERSION = "3.0.3";
|
||||
const VERSION = "3.1.0";
|
||||
var singleton = null;
|
||||
var Config = require("./config");
|
||||
|
||||
|
@ -40,10 +40,11 @@ var http = require("http");
|
|||
var https = require("https");
|
||||
var express = require("express");
|
||||
var Logger = require("./logger");
|
||||
var Channel = require("./channel");
|
||||
var Channel = require("./channel/channel");
|
||||
var User = require("./user");
|
||||
var $util = require("./utilities");
|
||||
var db = require("./database");
|
||||
var Flags = require("./flags");
|
||||
|
||||
var Server = function () {
|
||||
var self = this;
|
||||
|
@ -167,14 +168,16 @@ Server.prototype.unloadChannel = function (chan) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (chan.registered) {
|
||||
chan.saveState();
|
||||
}
|
||||
chan.saveState();
|
||||
|
||||
chan.logger.log("[init] Channel shutting down");
|
||||
|
||||
chan.playlist.die();
|
||||
chan.logger.close();
|
||||
|
||||
chan.notifyModules("unload", []);
|
||||
Object.keys(chan.modules).forEach(function (k) {
|
||||
chan.modules[k].dead = true;
|
||||
});
|
||||
|
||||
for (var i = 0; i < this.channels.length; i++) {
|
||||
if (this.channels[i].uniqueName === chan.uniqueName) {
|
||||
this.channels.splice(i, 1);
|
||||
|
@ -197,29 +200,31 @@ Server.prototype.packChannelList = function (publicOnly) {
|
|||
return true;
|
||||
}
|
||||
|
||||
return c.opts.show_public;
|
||||
return c.modules.options && c.modules.options.get("show_public");
|
||||
});
|
||||
|
||||
return channels.map(this.packChannel.bind(this));
|
||||
};
|
||||
|
||||
Server.prototype.packChannel = function (c) {
|
||||
var opts = c.modules.options;
|
||||
var pl = c.modules.playlist;
|
||||
var chat = c.modules.chat;
|
||||
var data = {
|
||||
name: c.name,
|
||||
pagetitle: c.opts.pagetitle,
|
||||
mediatitle: c.playlist.current ? c.playlist.current.media.title : "-",
|
||||
pagetitle: opts.pagetitle ? opts.pagetitle : c.name,
|
||||
mediatitle: pl && pl.current ? pl.current.media.title : "-",
|
||||
usercount: c.users.length,
|
||||
voteskip_eligible: c.calcVoteskipMax(),
|
||||
users: [],
|
||||
chat: Array.prototype.slice.call(c.chatbuffer),
|
||||
registered: c.registered,
|
||||
public: c.opts.show_public
|
||||
chat: chat ? Array.prototype.slice.call(chat.buffer) : [],
|
||||
registered: c.is(Flags.C_REGISTERED),
|
||||
public: opts && opts.get("show_public")
|
||||
};
|
||||
|
||||
for (var i = 0; i < c.users.length; i++) {
|
||||
if (c.users[i].name !== "") {
|
||||
var name = c.users[i].name;
|
||||
var rank = c.users[i].rank;
|
||||
var name = c.users[i].getName();
|
||||
var rank = c.users[i].account.effectiveRank;
|
||||
if (rank >= 255) {
|
||||
name = "!" + name;
|
||||
} else if (rank >= 4) {
|
||||
|
@ -252,7 +257,7 @@ Server.prototype.announce = function (data) {
|
|||
Server.prototype.shutdown = function () {
|
||||
Logger.syslog.log("Unloading channels");
|
||||
for (var i = 0; i < this.channels.length; i++) {
|
||||
if (this.channels[i].registered) {
|
||||
if (this.channels[i].is(Flags.C_REGISTERED)) {
|
||||
Logger.syslog.log("Saving /r/" + this.channels[i].name);
|
||||
this.channels[i].saveState();
|
||||
}
|
||||
|
|
|
@ -191,4 +191,4 @@ ULList.prototype.findAll = function(fn) {
|
|||
return result;
|
||||
}
|
||||
|
||||
exports.ULList = ULList;
|
||||
module.exports = ULList;
|
||||
|
|
604
lib/user.js
604
lib/user.js
|
@ -6,51 +6,61 @@ var db = require("./database");
|
|||
var InfoGetter = require("./get-info");
|
||||
var Config = require("./config");
|
||||
var ACP = require("./acp");
|
||||
var Account = require("./account");
|
||||
var Flags = require("./flags");
|
||||
|
||||
function User(socket) {
|
||||
var self = this;
|
||||
MakeEmitter(self);
|
||||
self.flags = 0;
|
||||
self.socket = socket;
|
||||
self.ip = socket._ip;
|
||||
self.loggedIn = false;
|
||||
self.loggingIn = false;
|
||||
self.rank = -1;
|
||||
self.global_rank = -1;
|
||||
self.hasChannelRank = false;
|
||||
self.longip = socket._longip;
|
||||
self.account = Account.default(self.longip);
|
||||
self.channel = null;
|
||||
self.name = "";
|
||||
self.canonicalName = "";
|
||||
self.profile = {
|
||||
image: "",
|
||||
text: ""
|
||||
};
|
||||
self.meta = {
|
||||
afk: false,
|
||||
muted: false,
|
||||
smuted: false,
|
||||
aliases: []
|
||||
};
|
||||
self.queueLimiter = util.newRateLimiter();
|
||||
self.chatLimiter = util.newRateLimiter();
|
||||
self.awaytimer = false;
|
||||
|
||||
self.socket.once("initChannelCallbacks", function () {
|
||||
self.initChannelCallbacks();
|
||||
});
|
||||
var announcement = Server.getServer().announcement;
|
||||
if (announcement != null) {
|
||||
self.socket.emit("announcement", announcement);
|
||||
}
|
||||
|
||||
self.socket.once("initUserPLCallbacks", function () {
|
||||
self.initUserPLCallbacks();
|
||||
self.socket.once("joinChannel", function (data) {
|
||||
if (typeof data !== "object" || typeof data.name !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.inChannel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!util.isValidChannelName(data.name)) {
|
||||
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.kick("Invalid channel name");
|
||||
return;
|
||||
}
|
||||
|
||||
data.name = data.name.toLowerCase();
|
||||
self.waitFlag(Flags.U_READY, function () {
|
||||
var chan = Server.getServer().getChannel(data.name);
|
||||
chan.joinUser(self, data);
|
||||
});
|
||||
});
|
||||
|
||||
self.socket.once("initACP", function () {
|
||||
self.whenLoggedIn(function () {
|
||||
if (self.global_rank >= 255) {
|
||||
self.waitFlag(Flags.U_LOGGED_IN, function () {
|
||||
if (self.account.globalRank >= 255) {
|
||||
ACP.init(self);
|
||||
} else {
|
||||
self.kick("Attempted initACP from non privileged user. This incident " +
|
||||
"will be reported.");
|
||||
Logger.eventlog.log("[acp] Attempted initACP from socket client " +
|
||||
self.name + "@" + self.ip);
|
||||
self.getName() + "@" + self.ip);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -68,87 +78,129 @@ function User(socket) {
|
|||
pw = "";
|
||||
}
|
||||
|
||||
if (!pw && !self.loggingIn && !self.loggedIn) {
|
||||
if (self.is(Flags.U_LOGGING_IN) || self.is(Flags.U_LOGGED_IN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pw) {
|
||||
self.guestLogin(name);
|
||||
} else if (pw && !self.loggingIn && !self.loggedIn) {
|
||||
} else {
|
||||
self.login(name, pw);
|
||||
}
|
||||
});
|
||||
|
||||
var announcement = Server.getServer().announcement;
|
||||
if (announcement != null) {
|
||||
self.socket.emit("announcement", announcement);
|
||||
}
|
||||
|
||||
self.on("login", function () {
|
||||
db.recordVisit(self.ip, self.name);
|
||||
db.users.getProfile(self.name, function (err, profile) {
|
||||
if (!err) {
|
||||
self.profile = profile;
|
||||
if (self.inChannel()) {
|
||||
self.channel.sendUserProfile(self.channel.users, self);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (self.global_rank >= 255) {
|
||||
self.on("login", function (account) {
|
||||
if (account.globalRank >= 255) {
|
||||
self.initAdminCallbacks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user is in a valid channel
|
||||
*/
|
||||
User.prototype.die = function () {
|
||||
for (var key in this.socket._events) {
|
||||
delete this.socket._events[key];
|
||||
}
|
||||
|
||||
delete this.socket.typecheckedOn;
|
||||
delete this.socket.typecheckedOnce;
|
||||
|
||||
for (var key in this.__evHandlers) {
|
||||
delete this.__evHandlers[key];
|
||||
}
|
||||
|
||||
if (this.awaytimer) {
|
||||
clearTimeout(this.awaytimer);
|
||||
}
|
||||
|
||||
this.dead = true;
|
||||
};
|
||||
|
||||
User.prototype.is = function (flag) {
|
||||
return Boolean(this.flags & flag);
|
||||
};
|
||||
|
||||
User.prototype.setFlag = function (flag) {
|
||||
this.flags |= flag;
|
||||
this.emit("setFlag", flag);
|
||||
};
|
||||
|
||||
User.prototype.clearFlag = function (flag) {
|
||||
this.flags &= ~flag;
|
||||
this.emit("clearFlag", flag);
|
||||
};
|
||||
|
||||
User.prototype.waitFlag = function (flag, cb) {
|
||||
var self = this;
|
||||
if (self.is(flag)) {
|
||||
cb();
|
||||
} else {
|
||||
var wait = function (f) {
|
||||
if ((f & flag) === flag) {
|
||||
self.unbind("setFlag", wait);
|
||||
cb();
|
||||
}
|
||||
};
|
||||
self.on("setFlag", wait);
|
||||
}
|
||||
};
|
||||
|
||||
User.prototype.getName = function () {
|
||||
return this.account.name;
|
||||
};
|
||||
|
||||
User.prototype.getLowerName = function () {
|
||||
return this.account.lowername;
|
||||
};
|
||||
|
||||
User.prototype.inChannel = function () {
|
||||
return this.channel != null && !this.channel.dead;
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes a user's AFK status, updating the channel voteskip if necessary
|
||||
*/
|
||||
/* Called when a user's AFK status changes */
|
||||
User.prototype.setAFK = function (afk) {
|
||||
if (!this.inChannel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.meta.afk === afk) {
|
||||
/* No change in AFK status, don't need to change anything */
|
||||
if (this.is(Flags.U_AFK) === afk) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.meta.afk = afk;
|
||||
|
||||
var chan = this.channel;
|
||||
if (afk) {
|
||||
if (chan.voteskip) {
|
||||
chan.voteskip.unvote(this.ip);
|
||||
this.setFlag(Flags.U_AFK);
|
||||
if (this.channel.voteskip) {
|
||||
this.channel.voteskip.unvote(this.ip);
|
||||
}
|
||||
} else {
|
||||
this.clearFlag(Flags.U_AFK);
|
||||
this.autoAFK();
|
||||
}
|
||||
|
||||
chan.checkVoteskipPass();
|
||||
chan.sendAll("setAFK", {
|
||||
name: this.name,
|
||||
/* Number of AFK users changed, voteskip state changes */
|
||||
if (this.channel.modules.voteskip) {
|
||||
this.channel.modules.voteskip.update();
|
||||
}
|
||||
|
||||
this.channel.broadcastAll("setAFK", {
|
||||
name: this.getName(),
|
||||
afk: afk
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets a timer to automatically mark the user as AFK after
|
||||
* a period of inactivity
|
||||
*/
|
||||
/* Automatically tag a user as AFK after a period of inactivity */
|
||||
User.prototype.autoAFK = function () {
|
||||
var self = this;
|
||||
if (self.awaytimer) {
|
||||
clearTimeout(self.awaytimer);
|
||||
}
|
||||
|
||||
if (!self.inChannel()) {
|
||||
if (!self.inChannel() || !self.channel.modules.options) {
|
||||
return;
|
||||
}
|
||||
|
||||
var timeout = parseFloat(self.channel.opts.afk_timeout);
|
||||
/* Don't set a timer if the duration is invalid */
|
||||
var timeout = parseFloat(self.channel.modules.options.get("afk_timeout"));
|
||||
if (isNaN(timeout) || timeout <= 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -158,289 +210,11 @@ User.prototype.autoAFK = function () {
|
|||
}, timeout * 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a kick message and disconnects the user
|
||||
*/
|
||||
User.prototype.kick = function (reason) {
|
||||
this.socket.emit("kick", { reason: reason });
|
||||
this.socket.disconnect(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes socket message callbacks for a channel user
|
||||
*/
|
||||
User.prototype.initChannelCallbacks = function () {
|
||||
var self = this;
|
||||
|
||||
// Verifies datatype before calling a function
|
||||
// Passes a default value if the typecheck fails
|
||||
var typecheck = function (type, def, fn) {
|
||||
return function (data) {
|
||||
if (typeof data !== type) {
|
||||
fn(def);
|
||||
} else {
|
||||
fn(data);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Verify that the user is in a channel, and that the passed data is an Object
|
||||
var wrapTypecheck = function (msg, fn) {
|
||||
self.socket.on(msg, typecheck("object", {}, function (data) {
|
||||
if (self.inChannel()) {
|
||||
fn(data);
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
// Verify that the user is in a channel, but don't typecheck the data
|
||||
var wrap = function (msg, fn) {
|
||||
self.socket.on(msg, function (data) {
|
||||
if (self.inChannel()) {
|
||||
fn(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
self.socket.on("disconnect", function () {
|
||||
if (self.awaytimer) {
|
||||
clearTimeout(self.awaytimer);
|
||||
}
|
||||
|
||||
if (self.inChannel()) {
|
||||
self.channel.part(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.once("joinChannel", typecheck("object", {}, function (data) {
|
||||
if (self.inChannel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.name !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!util.isValidChannelName(data.name)) {
|
||||
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.kick("Invalid channel name");
|
||||
return;
|
||||
}
|
||||
|
||||
data.name = data.name.toLowerCase();
|
||||
var chan = Server.getServer().getChannel(data.name);
|
||||
chan.preJoin(self, data.pw);
|
||||
}));
|
||||
|
||||
wrapTypecheck("assignLeader", function (data) {
|
||||
self.channel.handleChangeLeader(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("setChannelRank", function (data) {
|
||||
self.channel.handleSetRank(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("unban", function (data) {
|
||||
self.channel.handleUnban(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("chatMsg", function (data) {
|
||||
if (typeof data.msg !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.msg.indexOf("/afk") !== 0) {
|
||||
self.setAFK(false);
|
||||
self.autoAFK();
|
||||
}
|
||||
|
||||
self.channel.handleChat(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("pm", function (data) {
|
||||
if (typeof data.msg !== "string" || typeof data.to !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
self.channel.handlePm(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("newPoll", function (data) {
|
||||
self.channel.handleOpenPoll(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("vote", function (data) {
|
||||
self.channel.handlePollVote(self, data);
|
||||
});
|
||||
|
||||
wrap("closePoll", function () {
|
||||
self.channel.handleClosePoll(self);
|
||||
});
|
||||
|
||||
wrap("playerReady", function () {
|
||||
self.channel.sendMediaUpdate([self]);
|
||||
});
|
||||
|
||||
wrap("requestPlaylist", function () {
|
||||
self.channel.sendPlaylist([self]);
|
||||
});
|
||||
|
||||
wrapTypecheck("queue", function (data) {
|
||||
self.channel.handleQueue(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("queuePlaylist", function (data) {
|
||||
self.channel.handleQueuePlaylist(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("setTemp", function (data) {
|
||||
self.channel.handleSetTemp(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("moveMedia", function (data) {
|
||||
self.channel.handleMove(self, data);
|
||||
});
|
||||
|
||||
wrap("delete", function (data) {
|
||||
self.channel.handleDelete(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("uncache", function (data) {
|
||||
self.channel.handleUncache(self, data);
|
||||
});
|
||||
|
||||
wrap("jumpTo", function (data) {
|
||||
self.channel.handleJumpTo(self, data);
|
||||
});
|
||||
|
||||
wrap("playNext", function () {
|
||||
self.channel.handlePlayNext(self);
|
||||
});
|
||||
|
||||
wrap("clearPlaylist", function () {
|
||||
self.channel.handleClear(self);
|
||||
});
|
||||
|
||||
wrap("shufflePlaylist", function () {
|
||||
self.channel.handleShuffle(self);
|
||||
});
|
||||
|
||||
wrap("togglePlaylistLock", function () {
|
||||
self.channel.handleToggleLock(self);
|
||||
});
|
||||
|
||||
wrapTypecheck("mediaUpdate", function (data) {
|
||||
self.channel.handleUpdate(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("searchMedia", function (data) {
|
||||
if (typeof data.query !== "string") {
|
||||
return;
|
||||
}
|
||||
data.query = data.query.substring(0, 255);
|
||||
|
||||
var searchYT = function () {
|
||||
InfoGetter.Getters.ytSearch(data.query.split(" "), function (e, vids) {
|
||||
if (!e) {
|
||||
self.socket.emit("searchResults", {
|
||||
source: "yt",
|
||||
results: vids
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (data.source === "yt") {
|
||||
searchYT();
|
||||
} else {
|
||||
self.channel.search(data.query, function (vids) {
|
||||
if (vids.length === 0) {
|
||||
searchYT();
|
||||
} else {
|
||||
self.socket.emit("searchResults", {
|
||||
source: "library",
|
||||
results: vids
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
wrapTypecheck("setOptions", function (data) {
|
||||
self.channel.handleUpdateOptions(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("setPermissions", function (data) {
|
||||
self.channel.handleSetPermissions(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("setChannelCSS", function (data) {
|
||||
self.channel.handleSetCSS(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("setChannelJS", function (data) {
|
||||
self.channel.handleSetJS(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("setMotd", function (data) {
|
||||
self.channel.handleSetMotd(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("updateFilter", function (data) {
|
||||
self.channel.handleUpdateFilter(self, data);
|
||||
});
|
||||
|
||||
wrap("importFilters", function (data) {
|
||||
self.channel.handleImportFilters(self, data);
|
||||
});
|
||||
|
||||
// REMOVE FILTER
|
||||
// https://www.youtube.com/watch?v=SxUU3zncVmI
|
||||
wrapTypecheck("removeFilter", function (data) {
|
||||
self.channel.handleRemoveFilter(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("moveFilter", function (data) {
|
||||
self.channel.handleMoveFilter(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("updateEmote", function (data) {
|
||||
self.channel.handleUpdateEmote(self, data);
|
||||
});
|
||||
|
||||
wrap("importEmotes", function (data) {
|
||||
self.channel.handleImportEmotes(self, data);
|
||||
});
|
||||
|
||||
wrapTypecheck("removeEmote", function (data) {
|
||||
self.channel.handleRemoveEmote(self, data);
|
||||
});
|
||||
|
||||
wrap("requestBanlist", function () {
|
||||
self.channel.sendBanlist([self]);
|
||||
});
|
||||
|
||||
wrap("requestChannelRanks", function () {
|
||||
self.channel.sendChannelRanks([self]);
|
||||
});
|
||||
|
||||
wrap("requestChatFilters", function () {
|
||||
self.channel.sendChatFilters([self]);
|
||||
});
|
||||
|
||||
wrap("voteskip", function () {
|
||||
self.channel.handleVoteskip(self);
|
||||
});
|
||||
|
||||
wrap("readChanLog", function () {
|
||||
self.channel.handleReadLog(self);
|
||||
});
|
||||
};
|
||||
|
||||
User.prototype.initAdminCallbacks = function () {
|
||||
var self = this;
|
||||
self.socket.on("borrow-rank", function (rank) {
|
||||
|
@ -449,46 +223,30 @@ User.prototype.initAdminCallbacks = function () {
|
|||
return;
|
||||
}
|
||||
|
||||
if (rank > self.global_rank) {
|
||||
if (rank > self.account.globalRank) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rank === 255 && self.global_rank > 255) {
|
||||
rank = self.global_rank;
|
||||
if (rank === 255 && self.account.globalRank > 255) {
|
||||
rank = self.account.globalRank;
|
||||
}
|
||||
|
||||
self.rank = rank;
|
||||
self.account.channelRank = rank;
|
||||
self.socket.emit("rank", rank);
|
||||
self.channel.sendAll("setUserRank", {
|
||||
name: self.name,
|
||||
self.channel.broadcastAll("setUserRank", {
|
||||
name: self.getName(),
|
||||
rank: rank
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
User.prototype.whenLoggedIn = function (fn) {
|
||||
if (this.loggedIn) {
|
||||
fn();
|
||||
} else {
|
||||
this.once("login", fn);
|
||||
}
|
||||
};
|
||||
|
||||
User.prototype.whenChannelRank = function (fn) {
|
||||
if (this.hasChannelRank) {
|
||||
fn();
|
||||
} else {
|
||||
this.once("channelRank", fn);
|
||||
}
|
||||
};
|
||||
|
||||
User.prototype.login = function (name, pw) {
|
||||
var self = this;
|
||||
self.loggingIn = true;
|
||||
self.setFlag(Flags.U_LOGGING_IN);
|
||||
|
||||
db.users.verifyLogin(name, pw, function (err, user) {
|
||||
self.loggingIn = false;
|
||||
self.clearFlag(Flags.U_LOGGING_IN);
|
||||
if (err) {
|
||||
if (err === "Invalid username/password combination") {
|
||||
Logger.eventlog.log("[loginfail] Login failed (bad password): " + name
|
||||
|
@ -502,19 +260,31 @@ User.prototype.login = function (name, pw) {
|
|||
return;
|
||||
}
|
||||
|
||||
self.rank = self.global_rank = user.global_rank;
|
||||
self.name = user.name;
|
||||
self.loggedIn = true;
|
||||
self.socket.emit("login", {
|
||||
success: true,
|
||||
name: user.name
|
||||
});
|
||||
self.socket.emit("rank", self.rank);
|
||||
Logger.syslog.log(self.ip + " logged in as " + name);
|
||||
var opts = { name: user.name };
|
||||
if (self.inChannel()) {
|
||||
self.channel.logger.log(self.ip + " logged in as " + name);
|
||||
opts.channel = self.channel.name;
|
||||
}
|
||||
self.emit("login");
|
||||
self.setFlag(Flags.U_REGISTERED);
|
||||
self.refreshAccount(opts, function (err, account) {
|
||||
if (err) {
|
||||
Logger.errlog.log("[SEVERE] getAccount failed for user " + user.name);
|
||||
Logger.errlog.log(err);
|
||||
user.clearFlag(Flags.U_REGISTERED);
|
||||
return;
|
||||
}
|
||||
self.socket.emit("login", {
|
||||
success: true,
|
||||
name: user.name
|
||||
});
|
||||
db.recordVisit(self.longip, self.getName());
|
||||
self.socket.emit("rank", self.account.effectiveRank);
|
||||
Logger.syslog.log(self.ip + " logged in as " + user.name);
|
||||
if (self.inChannel()) {
|
||||
self.channel.logger.log(self.longip + " logged in as " + user.name);
|
||||
}
|
||||
self.setFlag(Flags.U_LOGGED_IN);
|
||||
self.emit("login", self.account);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -544,9 +314,9 @@ User.prototype.guestLogin = function (name) {
|
|||
}
|
||||
|
||||
// Prevent duplicate logins
|
||||
self.loggingIn = true;
|
||||
self.setFlag(Flags.U_LOGGING_IN);
|
||||
db.users.isUsernameTaken(name, function (err, taken) {
|
||||
self.loggingIn = false;
|
||||
self.clearFlag(Flags.U_LOGGING_IN);
|
||||
if (err) {
|
||||
self.socket.emit("login", {
|
||||
success: false,
|
||||
|
@ -566,7 +336,7 @@ User.prototype.guestLogin = function (name) {
|
|||
if (self.inChannel()) {
|
||||
var nameLower = name.toLowerCase();
|
||||
for (var i = 0; i < self.channel.users.length; i++) {
|
||||
if (self.channel.users[i].name.toLowerCase() === nameLower) {
|
||||
if (self.channel.users[i].getLowerName() === nameLower) {
|
||||
self.socket.emit("login", {
|
||||
success: false,
|
||||
error: "That name is already in use on this channel."
|
||||
|
@ -578,24 +348,32 @@ User.prototype.guestLogin = function (name) {
|
|||
|
||||
// Login succeeded
|
||||
lastguestlogin[self.ip] = Date.now();
|
||||
self.rank = 0;
|
||||
self.global_rank = 0;
|
||||
self.socket.emit("rank", 0);
|
||||
self.name = name;
|
||||
self.loggedIn = true;
|
||||
self.socket.emit("login", {
|
||||
success: true,
|
||||
name: name,
|
||||
guest: true
|
||||
});
|
||||
|
||||
// TODO you shouldn't be able to guest login without being in a channel
|
||||
Logger.syslog.log(self.ip + " signed in as " + name);
|
||||
var opts = { name: name };
|
||||
if (self.inChannel()) {
|
||||
self.channel.logger.log(self.ip + " signed in as " + name);
|
||||
opts.channel = self.channel.name;
|
||||
}
|
||||
self.refreshAccount(opts, function (err, account) {
|
||||
if (err) {
|
||||
Logger.errlog.log("[SEVERE] getAccount failed for guest login " + name);
|
||||
Logger.errlog.log(err);
|
||||
return;
|
||||
}
|
||||
|
||||
self.emit("login");
|
||||
self.socket.emit("login", {
|
||||
success: true,
|
||||
name: name,
|
||||
guest: true
|
||||
});
|
||||
db.recordVisit(self.longip, self.getName());
|
||||
self.socket.emit("rank", 0);
|
||||
Logger.syslog.log(self.ip + " signed in as " + name);
|
||||
if (self.inChannel()) {
|
||||
self.channel.logger.log(self.longip + " signed in as " + name);
|
||||
}
|
||||
self.setFlag(Flags.U_LOGGED_IN);
|
||||
self.emit("login", self.account);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -614,8 +392,40 @@ setInterval(function () {
|
|||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
User.prototype.initUserPLCallbacks = function () {
|
||||
require("./userplaylists").init(this);
|
||||
User.prototype.refreshAccount = function (opts, cb) {
|
||||
if (!cb) {
|
||||
cb = opts;
|
||||
opts = {};
|
||||
}
|
||||
|
||||
var different = false;
|
||||
for (var key in opts) {
|
||||
if (opts[key] !== this.account[key]) {
|
||||
different = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!different) {
|
||||
return;
|
||||
}
|
||||
|
||||
var name = ("name" in opts) ? opts.name : this.account.name;
|
||||
opts.registered = this.is(Flags.U_REGISTERED);
|
||||
var self = this;
|
||||
var old = this.account;
|
||||
Account.getAccount(name, this.longip, opts, function (err, account) {
|
||||
if (!err) {
|
||||
/* Update account if anything changed in the meantime */
|
||||
for (var key in old) {
|
||||
if (self.account[key] !== old[key]) {
|
||||
account[key] = self.account[key];
|
||||
}
|
||||
}
|
||||
self.account = account;
|
||||
}
|
||||
cb(err, account);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = User;
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
var db = require("./database");
|
||||
|
||||
function listPlaylists(user) {
|
||||
db.listUserPlaylists(user.name, function (err, rows) {
|
||||
if (err) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Database error when attempting to fetch list of playlists"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
user.socket.emit("listPlaylists", rows);
|
||||
});
|
||||
}
|
||||
|
||||
function clonePlaylist(user, data) {
|
||||
if (!user.inChannel()) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "You must be in a channel in order to clone its playlist"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.name !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
var pl = user.channel.playlist.items.toArray();
|
||||
db.saveUserPlaylist(pl, user.name, data.name, function (err, res) {
|
||||
if (err) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Database error when saving playlist"
|
||||
});
|
||||
} else {
|
||||
listPlaylists(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deletePlaylist(user, data) {
|
||||
if (typeof data.name !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
db.deleteUserPlaylist(user.name, data.name, function (err) {
|
||||
if (err) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setImmediate(function () {
|
||||
listPlaylists(user);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.init = function (user) {
|
||||
if (user.userPlInited) {
|
||||
return;
|
||||
}
|
||||
|
||||
var s = user.socket;
|
||||
var wrap = function (cb) {
|
||||
return function (data) {
|
||||
if (!user.loggedIn || user.global_rank < 1) {
|
||||
s.emit("errorMsg", {
|
||||
msg: "You must be logged in to manage playlists"
|
||||
});
|
||||
return;
|
||||
}
|
||||
cb(user, data);
|
||||
};
|
||||
};
|
||||
|
||||
s.on("listPlaylists", wrap(listPlaylists));
|
||||
s.on("clonePlaylist", wrap(clonePlaylist));
|
||||
s.on("deletePlaylist", wrap(deletePlaylist));
|
||||
user.userPlInited = true;
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
(function () {
|
||||
var root, crypto = false;
|
||||
var root, crypto, net = false;
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
root = module.exports;
|
||||
|
@ -9,6 +9,7 @@
|
|||
|
||||
if (typeof require === "function") {
|
||||
crypto = require("crypto");
|
||||
net = require("net");
|
||||
}
|
||||
|
||||
var Set = function (items) {
|
||||
|
@ -78,15 +79,74 @@
|
|||
},
|
||||
|
||||
root.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.*");
|
||||
if (net.isIPv4(ip)) {
|
||||
/* Full /32 IPv4 address */
|
||||
return ip.replace(/^(\d+\.\d+\.\d+)\.\d+/, "$1.x");
|
||||
} else if (net.isIPv4(ip + ".0")) {
|
||||
/* /24 IPv4 range */
|
||||
return ip + ".0/24";
|
||||
} else if (net.isIPv4(ip + ".0.0")) {
|
||||
/* /16 IPv4 widerange */
|
||||
return ip + ".0.0/16";
|
||||
} else if (net.isIPv6(ip)) {
|
||||
/* /128 IPv6 address */
|
||||
return ip.replace(/^((?:[0-9a-f]+:){3}[0-9a-f]+):(?:[0-9a-f]+:){3}[0-9a-f]+$/,
|
||||
"$1:x:x:x:x");
|
||||
} else if (net.isIPv6(ip + ":0:0:0:0")) {
|
||||
/* /64 IPv6 range */
|
||||
return ip + "::0/64";
|
||||
} else if (net.isIPv6(ip + ":0:0:0:0:0")) {
|
||||
/* /48 IPv6 widerange */
|
||||
return ip + "::0/48";
|
||||
} else {
|
||||
return ip;
|
||||
}
|
||||
},
|
||||
|
||||
root.getIPRange = function (ip) {
|
||||
if (net.isIPv6(ip)) {
|
||||
return root.expandIPv6(ip)
|
||||
.replace(/((?:[0-9a-f]{4}:){3}[0-9a-f]{4}):(?:[0-9a-f]{4}:){3}[0-9a-f]{4}/, "$1");
|
||||
} else {
|
||||
return ip.replace(/((?:[0-9]+\.){2}[0-9]+)\.[0-9]+/, "$1");
|
||||
}
|
||||
},
|
||||
|
||||
root.getWideIPRange = function (ip) {
|
||||
if (net.isIPv6(ip)) {
|
||||
return root.expandIPv6(ip)
|
||||
.replace(/((?:[0-9a-f]{4}:){2}[0-9a-f]{4}):(?:[0-9a-f]{4}:){4}[0-9a-f]{4}/, "$1");
|
||||
} else {
|
||||
return ip.replace(/([0-9]+\.[0-9]+)\.[0-9]+\.[0-9]+/, "$1");
|
||||
}
|
||||
},
|
||||
|
||||
root.expandIPv6 = function (ip) {
|
||||
var result = "0000:0000:0000:0000:0000:0000:0000:0000".split(":");
|
||||
var parts = ip.split("::");
|
||||
var left = parts[0].split(":");
|
||||
var i = 0;
|
||||
left.forEach(function (block) {
|
||||
while (block.length < 4) {
|
||||
block = "0" + block;
|
||||
}
|
||||
result[i++] = block;
|
||||
});
|
||||
|
||||
if (parts.length > 1) {
|
||||
var right = parts[1].split(":");
|
||||
i = 7;
|
||||
right.forEach(function (block) {
|
||||
while (block.length < 4) {
|
||||
block = "0" + block;
|
||||
}
|
||||
result[i--] = block;
|
||||
});
|
||||
}
|
||||
|
||||
return result.join(":");
|
||||
},
|
||||
|
||||
root.formatTime = function (sec) {
|
||||
if(sec === "--:--")
|
||||
return sec;
|
||||
|
|
|
@ -116,11 +116,14 @@ function handleChannel(req, res) {
|
|||
}
|
||||
|
||||
var sio;
|
||||
if (req.secure) {
|
||||
sio = Config.get("https.full-address");
|
||||
} else {
|
||||
sio = Config.get("io.domain") + ":" + Config.get("io.default-port");
|
||||
if (net.isIPv6(ipForRequest(req))) {
|
||||
sio = Config.get("io.ipv6-default");
|
||||
}
|
||||
|
||||
if (!sio) {
|
||||
sio = Config.get("io.ipv4-default");
|
||||
}
|
||||
|
||||
sio += "/socket.io/socket.io.js";
|
||||
|
||||
sendJade(res, "channel", {
|
||||
|
@ -166,14 +169,19 @@ function handleSocketConfig(req, res) {
|
|||
|
||||
res.type("application/javascript");
|
||||
|
||||
var io_url = Config.get("io.domain") + ":" + Config.get("io.default-port");
|
||||
var web_url = Config.get("http.domain") + ":" + Config.get("http.default-port");
|
||||
var ssl_url = Config.get("https.domain") + ":" + Config.get("https.default-port");
|
||||
res.send("var IO_URL='"+io_url+"',WEB_URL='"+web_url+"',SSL_URL='" + ssl_url +
|
||||
"',ALLOW_SSL="+Config.get("https.enabled")+";" +
|
||||
(Config.get("https.enabled") ?
|
||||
"if(location.protocol=='https:'||USEROPTS.secure_connection){" +
|
||||
"IO_URL=WEB_URL=SSL_URL;}" : ""));
|
||||
var sioconfig = Config.get("sioconfig");
|
||||
var iourl;
|
||||
var ip = ipForRequest(req);
|
||||
|
||||
if (net.isIPv6(ip)) {
|
||||
iourl = Config.get("io.ipv6-default");
|
||||
}
|
||||
|
||||
if (!iourl) {
|
||||
iourl = Config.get("io.ipv4-default");
|
||||
}
|
||||
sioconfig += "var IO_URL='" + iourl + "';";
|
||||
res.send(sioconfig);
|
||||
}
|
||||
|
||||
function handleUserAgreement(req, res) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"author": "Calvin Montgomery",
|
||||
"name": "CyTube",
|
||||
"description": "Online media synchronizer and chat",
|
||||
"version": "3.0.3",
|
||||
"version": "3.1.0",
|
||||
"repository": {
|
||||
"url": "http://github.com/calzoneman/sync"
|
||||
},
|
||||
|
@ -16,6 +16,8 @@
|
|||
"cookie": "~0.1.0",
|
||||
"yamljs": "~0.1.4",
|
||||
"express-minify": "0.0.7",
|
||||
"q": "^1.0.0",
|
||||
"json-typecheck": "^0.1.0",
|
||||
"oauth": "^0.9.11"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue