Merge refactoring into 3.0
This commit is contained in:
parent
91bf6a5062
commit
9ea48f58cf
39 changed files with 5555 additions and 6262 deletions
70
lib/channel/accesscontrol.js
Normal file
70
lib/channel/accesscontrol.js
Normal file
|
|
@ -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;
|
||||
623
lib/channel/channel.js
Normal file
623
lib/channel/channel.js
Normal file
|
|
@ -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;
|
||||
527
lib/channel/chat.js
Normal file
527
lib/channel/chat.js
Normal file
|
|
@ -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;
|
||||
122
lib/channel/customization.js
Normal file
122
lib/channel/customization.js
Normal file
|
|
@ -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;
|
||||
199
lib/channel/emotes.js
Normal file
199
lib/channel/emotes.js
Normal file
|
|
@ -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;
|
||||
276
lib/channel/filters.js
Normal file
276
lib/channel/filters.js
Normal file
|
|
@ -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;
|
||||
379
lib/channel/kickban.js
Normal file
379
lib/channel/kickban.js
Normal file
|
|
@ -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;
|
||||
109
lib/channel/library.js
Normal file
109
lib/channel/library.js
Normal file
|
|
@ -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;
|
||||
67
lib/channel/module.js
Normal file
67
lib/channel/module.js
Normal file
|
|
@ -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;
|
||||
190
lib/channel/opts.js
Normal file
190
lib/channel/opts.js
Normal file
|
|
@ -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;
|
||||
369
lib/channel/permissions.js
Normal file
369
lib/channel/permissions.js
Normal file
|
|
@ -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;
|
||||
1220
lib/channel/playlist.js
Normal file
1220
lib/channel/playlist.js
Normal file
File diff suppressed because it is too large
Load diff
173
lib/channel/poll.js
Normal file
173
lib/channel/poll.js
Normal file
|
|
@ -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;
|
||||
184
lib/channel/ranks.js
Normal file
184
lib/channel/ranks.js
Normal file
|
|
@ -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;
|
||||
103
lib/channel/voteskip.js
Normal file
103
lib/channel/voteskip.js
Normal file
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue