package: build with babel for ES2015 support
* Rename lib/ -> src/ * Add `postinstall` npm target for compiling src files to lib * Add `build-watch` npm target for development with babel --watch * Add `lib/` to .gitignore * Add `source-map-support` module for babel-generated sourcemaps
This commit is contained in:
parent
d042619b21
commit
0109a87e55
55 changed files with 9 additions and 3 deletions
70
src/channel/accesscontrol.js
Normal file
70
src/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;
|
||||
689
src/channel/channel.js
Normal file
689
src/channel/channel.js
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
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("graceful-fs");
|
||||
var path = require("path");
|
||||
var sio = require("socket.io");
|
||||
var db = require("../database");
|
||||
|
||||
const SIZE_LIMIT = 1048576;
|
||||
|
||||
/**
|
||||
* 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++;
|
||||
},
|
||||
|
||||
release: function () {
|
||||
this.count--;
|
||||
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 + ".log"));
|
||||
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 (f) {
|
||||
if (f === 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",
|
||||
"./emotes" : "emotes",
|
||||
"./chat" : "chat",
|
||||
"./drink" : "drink",
|
||||
"./filters" : "filters",
|
||||
"./customization" : "customization",
|
||||
"./opts" : "options",
|
||||
"./library" : "library",
|
||||
"./playlist" : "playlist",
|
||||
"./mediarefresher": "mediarefresher",
|
||||
"./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.getDiskSize = function (cb) {
|
||||
if (this._getDiskSizeTimeout > Date.now()) {
|
||||
return cb(null, this._cachedDiskSize);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var file = path.join(__dirname, "..", "..", "chandump", self.uniqueName);
|
||||
fs.stat(file, function (err, stats) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
self._cachedDiskSize = stats.size;
|
||||
cb(null, self._cachedDiskSize);
|
||||
});
|
||||
};
|
||||
|
||||
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: msg
|
||||
});
|
||||
}
|
||||
|
||||
self.setFlag(Flags.C_READY | Flags.C_ERROR);
|
||||
};
|
||||
|
||||
fs.stat(file, function (err, stats) {
|
||||
if (!err) {
|
||||
var mb = stats.size / 1048576;
|
||||
mb = Math.floor(mb * 100) / 100;
|
||||
if (mb > SIZE_LIMIT / 1048576) {
|
||||
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);
|
||||
|
||||
// Check for large chandump and warn moderators/admins
|
||||
self.getDiskSize(function (err, size) {
|
||||
if (!err && size > SIZE_LIMIT && self.users) {
|
||||
self.users.forEach(function (u) {
|
||||
if (u.account.effectiveRank >= 2) {
|
||||
u.socket.emit("warnLargeChandump", {
|
||||
limit: SIZE_LIMIT,
|
||||
actual: size
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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.activeLock.lock();
|
||||
self.waitFlag(Flags.C_READY, function () {
|
||||
/* User closed the connection before the channel finished loading */
|
||||
if (user.socket.disconnected) {
|
||||
self.activeLock.release();
|
||||
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);
|
||||
self.activeLock.release();
|
||||
return;
|
||||
}
|
||||
|
||||
afterAccount();
|
||||
});
|
||||
} else {
|
||||
afterAccount();
|
||||
}
|
||||
|
||||
function afterAccount() {
|
||||
if (self.dead || user.socket.disconnected) {
|
||||
if (self.activeLock) self.activeLock.release();
|
||||
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.acceptUser(user);
|
||||
} else {
|
||||
user.account.channelRank = 0;
|
||||
user.account.effectiveRank = user.account.globalRank;
|
||||
self.activeLock.release();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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.realip + " joined " + this.name);
|
||||
if (user.socket._isUsingTor) {
|
||||
if (this.modules.options && this.modules.options.get("torbanned")) {
|
||||
user.kick("This channel has banned connections from Tor.");
|
||||
this.logger.log("[login] Blocked connection from Tor exit at " +
|
||||
user.displayip);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log("[login] Accepted connection from Tor exit at " +
|
||||
user.displayip);
|
||||
} else {
|
||||
this.logger.log("[login] Accepted connection from " + user.displayip);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
var loginStr = "[login] " + user.displayip + " logged in as " + user.getName();
|
||||
if (user.account.globalRank === 0) loginStr += " (guest)";
|
||||
loginStr += " (aliases: " + user.account.aliases.join(",") + ")";
|
||||
self.logger.log(loginStr);
|
||||
|
||||
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) {
|
||||
if (user.dead) return;
|
||||
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) {
|
||||
if (!this.logger) {
|
||||
Logger.errlog.log("partUser called on dead channel");
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log("[login] " + user.displayip + " (" + 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.broadcastAll("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.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: user.displayip
|
||||
}
|
||||
};
|
||||
|
||||
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.realip
|
||||
}
|
||||
};
|
||||
|
||||
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.readLog = function (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 () {
|
||||
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(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.instance.in(ns).emit(msg, data);
|
||||
};
|
||||
|
||||
Channel.prototype.broadcastAll = function (msg, data) {
|
||||
this._broadcast(msg, data, this.name);
|
||||
};
|
||||
|
||||
Channel.prototype.packInfo = function (isAdmin) {
|
||||
var data = {
|
||||
name: this.name,
|
||||
usercount: this.users.length,
|
||||
users: [],
|
||||
registered: this.is(Flags.C_REGISTERED)
|
||||
};
|
||||
|
||||
for (var i = 0; i < this.users.length; i++) {
|
||||
if (this.users[i].name !== "") {
|
||||
var name = this.users[i].getName();
|
||||
var rank = this.users[i].account.effectiveRank;
|
||||
if (rank >= 255) {
|
||||
name = "!" + name;
|
||||
} else if (rank >= 4) {
|
||||
name = "~" + name;
|
||||
} else if (rank >= 3) {
|
||||
name = "&" + name;
|
||||
} else if (rank >= 2) {
|
||||
name = "@" + name;
|
||||
}
|
||||
data.users.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
data.activeLockCount = this.activeLock.count;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var keys = Object.keys(this.modules);
|
||||
keys.forEach(function (k) {
|
||||
self.modules[k].packInfo(data, isAdmin);
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
module.exports = Channel;
|
||||
615
src/channel/chat.js
Normal file
615
src/channel/chat.js
Normal file
|
|
@ -0,0 +1,615 @@
|
|||
var Config = require("../config");
|
||||
var User = require("../user");
|
||||
var XSS = require("../xss");
|
||||
var ChannelModule = require("./module");
|
||||
var util = require("../utilities");
|
||||
var Flags = require("../flags");
|
||||
var url = require("url");
|
||||
var counters = require("../counters");
|
||||
|
||||
const SHADOW_TAG = "[shadow]";
|
||||
const LINK = /(\w+:\/\/(?:[^:\/\[\]\s]+|\[[0-9a-f:]+\])(?::\d+)?(?:\/[^\/\s]*)*)/ig;
|
||||
const LINK_PLACEHOLDER = '\ueeee';
|
||||
const LINK_PLACEHOLDER_RE = /\ueeee/g;
|
||||
|
||||
const TYPE_CHAT = {
|
||||
msg: "string",
|
||||
meta: "object,optional"
|
||||
};
|
||||
|
||||
const TYPE_PM = {
|
||||
msg: "string",
|
||||
to: "string",
|
||||
meta: "object,optional"
|
||||
};
|
||||
|
||||
// Limit to 10 messages/sec
|
||||
const MIN_ANTIFLOOD = {
|
||||
burst: 20,
|
||||
sustained: 10
|
||||
};
|
||||
|
||||
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("/rcv", 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.packInfo = function (data, isAdmin) {
|
||||
data.chat = Array.prototype.slice.call(this.buffer);
|
||||
};
|
||||
|
||||
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;
|
||||
counters.add("chat:incoming");
|
||||
|
||||
if (!this.channel || !this.channel.modules.permissions.canChat(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit to 240 characters
|
||||
data.msg = data.msg.substring(0, 240);
|
||||
// If channel doesn't permit them, strip ASCII control characters
|
||||
if (!this.channel.modules.options ||
|
||||
!this.channel.modules.options.get("allow_ascii_control")) {
|
||||
|
||||
data.msg = data.msg.replace(/[\x00-\x1f]+/g, " ");
|
||||
}
|
||||
|
||||
// Disallow blankposting
|
||||
if (!data.msg.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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("onUserPreChat", [user, data], function (err, result) {
|
||||
if (result === ChannelModule.PASSTHROUGH) {
|
||||
self.processChatMsg(user, data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ChatModule.prototype.handlePm = function (user, data) {
|
||||
if (!this.channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.is(Flags.U_LOGGED_IN)) {
|
||||
return user.socket.emit("errorMsg", {
|
||||
msg: "You must be signed in to send PMs"
|
||||
});
|
||||
}
|
||||
|
||||
if (data.msg.match(Config.get("link-domain-blacklist-regex"))) {
|
||||
this.channel.logger.log(user.displayip + " (" + user.getName() + ") was kicked for " +
|
||||
"blacklisted domain");
|
||||
user.kick();
|
||||
this.sendModMessage(user.getName() + " was kicked: blacklisted domain in " +
|
||||
"private message", 2);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (user.chatLimiter.throttle(MIN_ANTIFLOOD)) {
|
||||
user.socket.emit("cooldown", 1000 / MIN_ANTIFLOOD.sustained);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
data.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 (data.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.match(Config.get("link-domain-blacklist-regex"))) {
|
||||
this.channel.logger.log(user.displayip + " (" + user.getName() + ") was kicked for " +
|
||||
"blacklisted domain");
|
||||
user.kick();
|
||||
this.sendModMessage(user.getName() + " was kicked: blacklisted domain in " +
|
||||
"chat message", 2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.msg.indexOf("/afk") === -1) {
|
||||
user.setAFK(false);
|
||||
}
|
||||
|
||||
var msgobj = this.formatMessage(user.getName(), data);
|
||||
var antiflood = MIN_ANTIFLOOD;
|
||||
if (this.channel.modules.options &&
|
||||
this.channel.modules.options.get("chat_antiflood") &&
|
||||
user.account.effectiveRank < 2) {
|
||||
|
||||
antiflood = this.channel.modules.options.get("chat_antiflood_params");
|
||||
}
|
||||
|
||||
if (user.chatLimiter.throttle(antiflood)) {
|
||||
user.socket.emit("cooldown", 1000 / antiflood.sustained);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.msg.indexOf(">") === 0) {
|
||||
msgobj.meta.addClass = "greentext";
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
this.sendMessage(msgobj);
|
||||
counters.add("chat:sent");
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
ChatModule.prototype.filterMessage = function (msg) {
|
||||
var filters = this.channel.modules.filters.filters;
|
||||
var chan = this.channel;
|
||||
var convertLinks = this.channel.modules.options.get("enable_link_regex");
|
||||
var links = msg.match(LINK);
|
||||
var intermediate = msg.replace(LINK, LINK_PLACEHOLDER);
|
||||
|
||||
var result = filters.filter(intermediate, false);
|
||||
result = result.replace(LINK_PLACEHOLDER_RE, function () {
|
||||
var link = links.shift();
|
||||
if (!link) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var filtered = filters.filter(link, true);
|
||||
if (filtered !== link) {
|
||||
return filtered;
|
||||
} else if (convertLinks) {
|
||||
return "<a href=\"" + link + "\" target=\"_blank\">" + link + "</a>";
|
||||
} else {
|
||||
return link;
|
||||
}
|
||||
});
|
||||
|
||||
return XSS.sanitizeHTML(result);
|
||||
};
|
||||
|
||||
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 (!this.channel.modules.permissions.canClearChat(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buffer = [];
|
||||
this.channel.broadcastAll("clearchat");
|
||||
this.channel.logger.log("[mod] " + user.getName() + " used /clear");
|
||||
};
|
||||
|
||||
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();
|
||||
if (typeof name !== "string") {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "/mute requires a target name"
|
||||
});
|
||||
return;
|
||||
}
|
||||
name = name.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
|
||||
|| target.account.globalRank > user.account.globalRank) {
|
||||
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();
|
||||
if (typeof name !== "string") {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "/smute requires a target name"
|
||||
});
|
||||
return;
|
||||
}
|
||||
name = name.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
|
||||
|| target.account.globalRank > user.account.globalRank) {
|
||||
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();
|
||||
if (typeof name !== "string") {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "/unmute requires a target name"
|
||||
});
|
||||
return;
|
||||
}
|
||||
name = name.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;
|
||||
119
src/channel/customization.js
Normal file
119
src/channel/customization.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
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 = "";
|
||||
}
|
||||
|
||||
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) {
|
||||
if (typeof data.motd === "object" && data.motd.motd) {
|
||||
// Old style MOTD, convert to new
|
||||
this.motd = XSS.sanitizeHTML(data.motd.motd).replace(
|
||||
/\n/g, "<br>\n");
|
||||
} else if (typeof data.motd === "string") {
|
||||
// The MOTD is filtered before it is saved, however it is also
|
||||
// re-filtered on load in case the filtering rules change
|
||||
this.motd = XSS.sanitizeHTML(data.motd);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CustomizationModule.prototype.save = function (data) {
|
||||
data.css = this.css;
|
||||
data.js = this.js;
|
||||
data.motd = this.motd;
|
||||
};
|
||||
|
||||
CustomizationModule.prototype.setMotd = function (motd) {
|
||||
this.motd = XSS.sanitizeHTML(motd);
|
||||
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.getName() + " 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.getName() + " 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.getName() + " updated the MOTD");
|
||||
};
|
||||
|
||||
module.exports = CustomizationModule;
|
||||
56
src/channel/drink.js
Normal file
56
src/channel/drink.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
var ChannelModule = require("./module");
|
||||
|
||||
function DrinkModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
this.drinks = 0;
|
||||
}
|
||||
|
||||
DrinkModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
DrinkModule.prototype.onUserPostJoin = function (user) {
|
||||
user.socket.emit("drinkCount", this.drinks);
|
||||
};
|
||||
|
||||
DrinkModule.prototype.onUserPreChat = function (user, data, cb) {
|
||||
var msg = data.msg;
|
||||
var perms = this.channel.modules.permissions;
|
||||
if (msg.match(/^\/d-?[0-9]*/) && perms.canCallDrink(user)) {
|
||||
msg = msg.substring(2);
|
||||
var m = msg.match(/^(-?[0-9]+)/);
|
||||
var count;
|
||||
if (m) {
|
||||
count = parseInt(m[1]);
|
||||
if (isNaN(count) || count < -10000 || count > 10000) {
|
||||
return;
|
||||
}
|
||||
|
||||
msg = msg.replace(m[1], "").trim();
|
||||
if (msg || count > 0) {
|
||||
msg += " drink! (x" + count + ")";
|
||||
} else {
|
||||
this.drinks += count;
|
||||
this.channel.broadcastAll("drinkCount", this.drinks);
|
||||
return cb(null, ChannelModule.DENY);
|
||||
}
|
||||
} else {
|
||||
msg = msg.trim() + " drink!";
|
||||
count = 1;
|
||||
}
|
||||
|
||||
this.drinks += count;
|
||||
this.channel.broadcastAll("drinkCount", this.drinks);
|
||||
data.msg = msg;
|
||||
data.meta.addClass = "drink";
|
||||
data.meta.forceShowName = true;
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
} else {
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
}
|
||||
};
|
||||
|
||||
DrinkModule.prototype.onMediaChange = function () {
|
||||
this.drinks = 0;
|
||||
this.channel.broadcastAll("drinkCount", 0);
|
||||
};
|
||||
|
||||
module.exports = DrinkModule;
|
||||
205
src/channel/emotes.js
Normal file
205
src/channel/emotes.js
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
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.packInfo = function (data, isAdmin) {
|
||||
if (isAdmin) {
|
||||
data.emoteCount = this.emotes.emotes.length;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
300
src/channel/filters.js
Normal file
300
src/channel/filters.js
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
var FilterList = require("cytubefilters");
|
||||
var ChannelModule = require("./module");
|
||||
var XSS = require("../xss");
|
||||
var Logger = require("../logger");
|
||||
|
||||
/*
|
||||
* Converts JavaScript-style replacements ($1, $2, etc.) with
|
||||
* PCRE-style (\1, \2, etc.)
|
||||
*/
|
||||
function fixReplace(replace) {
|
||||
return replace.replace(/\$(\d)/g, "\\$1");
|
||||
}
|
||||
|
||||
function validateFilter(f) {
|
||||
if (typeof f.source !== "string" || typeof f.flags !== "string" ||
|
||||
typeof f.replace !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof f.name !== "string") {
|
||||
f.name = f.source;
|
||||
}
|
||||
|
||||
f.replace = fixReplace(f.replace.substring(0, 1000));
|
||||
f.replace = XSS.sanitizeHTML(f.replace);
|
||||
f.flags = f.flags.substring(0, 4);
|
||||
|
||||
try {
|
||||
FilterList.checkValidRegex(f.source);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var filter = {
|
||||
name: f.name,
|
||||
source: f.source,
|
||||
replace: fixReplace(f.replace),
|
||||
flags: f.flags,
|
||||
active: !!f.active,
|
||||
filterlinks: !!f.filterlinks
|
||||
};
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
function makeDefaultFilter(name, source, flags, replace) {
|
||||
return {
|
||||
name: name,
|
||||
source: source,
|
||||
flags: flags,
|
||||
replace: replace,
|
||||
active: true,
|
||||
filterlinks: false
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_FILTERS = [
|
||||
makeDefaultFilter("monospace", "`(.+?)`", "g", "<code>\\1</code>"),
|
||||
makeDefaultFilter("bold", "\\*(.+?)\\*", "g", "<strong>\\1</strong>"),
|
||||
makeDefaultFilter("italic", "_(.+?)_", "g", "<em>\\1</em>"),
|
||||
makeDefaultFilter("strike", "~~(.+?)~~", "g", "<s>\\1</s>"),
|
||||
makeDefaultFilter("inline spoiler", "\\[sp\\](.*?)\\[\\/sp\\]", "ig",
|
||||
"<span class=\"spoiler\">\\1</span>")
|
||||
];
|
||||
|
||||
function ChatFilterModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
this.filters = new FilterList();
|
||||
}
|
||||
|
||||
ChatFilterModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
ChatFilterModule.prototype.load = function (data) {
|
||||
if ("filters" in data) {
|
||||
var filters = data.filters.map(validateFilter).filter(function (f) {
|
||||
return f !== null;
|
||||
});
|
||||
try {
|
||||
this.filters = new FilterList(filters);
|
||||
} catch (e) {
|
||||
Logger.errlog.log("Filter load failed: " + e + " (channel:" +
|
||||
this.channel.name);
|
||||
this.channel.logger.log("Failed to load filters: " + e);
|
||||
}
|
||||
} else {
|
||||
this.filters = new FilterList(DEFAULT_FILTERS);
|
||||
}
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.save = function (data) {
|
||||
data.filters = this.filters.pack();
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.packInfo = function (data, isAdmin) {
|
||||
if (isAdmin) {
|
||||
data.chatFilterCount = this.filters.length;
|
||||
}
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.onUserPostJoin = function (user) {
|
||||
user.socket.on("addFilter", this.handleAddFilter.bind(this, 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.handleAddFilter = function (user, data) {
|
||||
if (typeof data !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.modules.permissions.canEditFilters(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
FilterList.checkValidRegex(data.source);
|
||||
} catch (e) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Invalid regex: " + e.message,
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
data = validateFilter(data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.filters.addFilter(data);
|
||||
} catch (e) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Filter add failed: " + e.message,
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
user.socket.emit("addFilterSuccess");
|
||||
|
||||
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() + " added filter: " + data.name + " -> " +
|
||||
"s/" + data.source + "/" + data.replace + "/" + data.flags +
|
||||
" active: " + data.active + ", filterlinks: " + data.filterlinks);
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.handleUpdateFilter = function (user, data) {
|
||||
if (typeof data !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.channel.modules.permissions.canEditFilters(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
FilterList.checkValidRegex(data.source);
|
||||
} catch (e) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Invalid regex: " + e.message,
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
data = validateFilter(data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.filters.updateFilter(data);
|
||||
} catch (e) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Filter update failed: " + e.message,
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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: " + data.name + " -> " +
|
||||
"s/" + data.source + "/" + data.replace + "/" + data.flags +
|
||||
" active: " + data.active + ", filterlinks: " + data.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;
|
||||
}
|
||||
|
||||
try {
|
||||
this.filters = new FilterList(data.map(validateFilter).filter(function (f) {
|
||||
return f !== null;
|
||||
}));
|
||||
} catch (e) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Filter import failed: " + e.message,
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
this.filters.removeFilter(data);
|
||||
} catch (e) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Filter removal failed: " + e.message,
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
var chan = this.channel;
|
||||
chan.users.forEach(function (u) {
|
||||
if (chan.modules.permissions.canEditFilters(u)) {
|
||||
u.socket.emit("deleteChatFilter", 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;
|
||||
}
|
||||
|
||||
try {
|
||||
this.filters.moveFilter(data.from, data.to);
|
||||
} catch (e) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Filter move failed: " + e.message,
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
ChatFilterModule.prototype.handleRequestChatFilters = function (user) {
|
||||
this.sendChatFilters([user]);
|
||||
};
|
||||
|
||||
module.exports = ChatFilterModule;
|
||||
429
src/channel/kickban.js
Normal file
429
src/channel/kickban.js
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
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("/kickanons", this.handleCmdKickAnons.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);
|
||||
|
||||
function checkIPBan(cname, ip, cb) {
|
||||
db.channels.isIPBanned(cname, ip, function (err, banned) {
|
||||
if (err) {
|
||||
cb(false);
|
||||
} else {
|
||||
cb(banned);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkNameBan(cname, name, cb) {
|
||||
db.channels.isNameBanned(cname, name, function (err, banned) {
|
||||
if (err) {
|
||||
cb(false);
|
||||
} else {
|
||||
cb(banned);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
KickBanModule.prototype.onUserPreJoin = function (user, data, cb) {
|
||||
if (!this.channel.is(Flags.C_REGISTERED)) {
|
||||
return cb(null, ChannelModule.PASSTHROUGH);
|
||||
}
|
||||
|
||||
var cname = this.channel.name;
|
||||
checkIPBan(cname, user.realip, function (banned) {
|
||||
if (banned) {
|
||||
cb(null, ChannelModule.DENY);
|
||||
user.kick("Your IP address is banned from this channel.");
|
||||
} else {
|
||||
checkNameBan(cname, user.getName(), function (banned) {
|
||||
if (banned) {
|
||||
cb(null, ChannelModule.DENY);
|
||||
user.kick("Your username is banned from this channel.");
|
||||
} else {
|
||||
cb(null, 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.cloakIP(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 */
|
||||
if (args.length === 0 || args[0].trim() === "") {
|
||||
return user.socket.emit("errorMsg", {
|
||||
msg: "No kick target specified. If you're trying to kick " +
|
||||
"anonymous users, use /kickanons"
|
||||
});
|
||||
}
|
||||
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
|
||||
|| target.account.globalRank > user.account.globalRank) {
|
||||
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());
|
||||
}
|
||||
};
|
||||
|
||||
KickBanModule.prototype.handleCmdKickAnons = function (user, msg, meta) {
|
||||
if (!this.channel.modules.permissions.canKick(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var users = Array.prototype.slice.call(this.channel.users);
|
||||
users.forEach(function (u) {
|
||||
if (!u.is(Flags.U_LOGGED_IN)) {
|
||||
u.kick("anonymous user");
|
||||
}
|
||||
});
|
||||
|
||||
this.channel.logger.log("[mod] " + user.getName() + " kicked anonymous users.");
|
||||
if (this.channel.modules.chat) {
|
||||
this.channel.modules.chat.sendModMessage(user.getName() + " kicked anonymous " +
|
||||
"users");
|
||||
}
|
||||
};
|
||||
|
||||
/* /ban - name bans */
|
||||
KickBanModule.prototype.handleCmdBan = function (user, msg, meta) {
|
||||
var args = msg.split(" ");
|
||||
args.shift(); /* shift off /ban */
|
||||
if (args.length === 0 || args[0].trim() === "") {
|
||||
return user.socket.emit("errorMsg", {
|
||||
msg: "No ban target specified."
|
||||
});
|
||||
}
|
||||
var name = args.shift().toLowerCase();
|
||||
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 */
|
||||
if (args.length === 0 || args[0].trim() === "") {
|
||||
return user.socket.emit("errorMsg", {
|
||||
msg: "No ban target specified."
|
||||
});
|
||||
}
|
||||
var name = args.shift().toLowerCase();
|
||||
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.cloakIP(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, { channel: chan.name }).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 () {
|
||||
var cloaked = util.cloakIP(ip);
|
||||
chan.logger.log("[mod] " + actor.getName() + " banned " + cloaked + " (" + name + ")");
|
||||
if (chan.modules.chat) {
|
||||
chan.modules.chat.sendModMessage(actor.getName() + " banned " +
|
||||
cloaked + " (" + 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].realip === 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;
|
||||
111
src/channel/library.js
Normal file
111
src/channel/library.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
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 {
|
||||
var meta = JSON.parse(row.meta || "{}");
|
||||
cb(null, new Media(row.id, row.title, row.seconds, row.type, meta));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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, function (e, vids) {
|
||||
if (!e) {
|
||||
user.socket.emit("searchResults", {
|
||||
source: "yt",
|
||||
results: vids
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (data.source === "yt" || !this.channel.is(Flags.C_REGISTERED) ||
|
||||
!this.channel.modules.permissions.canSeePlaylist(user)) {
|
||||
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;
|
||||
209
src/channel/mediarefresher.js
Normal file
209
src/channel/mediarefresher.js
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
var Vimeo = require("cytube-mediaquery/lib/provider/vimeo");
|
||||
var ChannelModule = require("./module");
|
||||
var Config = require("../config");
|
||||
var InfoGetter = require("../get-info");
|
||||
var Logger = require("../logger");
|
||||
|
||||
function MediaRefresherModule(channel) {
|
||||
ChannelModule.apply(this, arguments);
|
||||
this._interval = false;
|
||||
this._media = null;
|
||||
this._playlist = channel.modules.playlist;
|
||||
}
|
||||
|
||||
MediaRefresherModule.prototype = Object.create(ChannelModule.prototype);
|
||||
|
||||
MediaRefresherModule.prototype.onPreMediaChange = function (data, cb) {
|
||||
if (this._interval) clearInterval(this._interval);
|
||||
|
||||
this._media = data;
|
||||
var pl = this._playlist;
|
||||
|
||||
switch (data.type) {
|
||||
case "gd":
|
||||
pl._refreshing = true;
|
||||
return this.initGoogleDocs(data, function () {
|
||||
|
||||
pl._refreshing = false;
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
});
|
||||
case "gp":
|
||||
pl._refreshing = true;
|
||||
return this.initGooglePlus(data, function () {
|
||||
pl._refreshing = false;
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
});
|
||||
case "vi":
|
||||
pl._refreshing = true;
|
||||
return this.initVimeo(data, function () {
|
||||
pl._refreshing = false;
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
});
|
||||
default:
|
||||
return cb(null, ChannelModule.PASSTHROUGH);
|
||||
}
|
||||
};
|
||||
|
||||
MediaRefresherModule.prototype.initGoogleDocs = function (data, cb) {
|
||||
var self = this;
|
||||
self.refreshGoogleDocs(data, cb);
|
||||
|
||||
/*
|
||||
* Refresh every 55 minutes.
|
||||
* The expiration is 1 hour, but refresh 5 minutes early to be safe
|
||||
*/
|
||||
self._interval = setInterval(function () {
|
||||
self.refreshGoogleDocs(data);
|
||||
}, 55 * 60 * 1000);
|
||||
};
|
||||
|
||||
MediaRefresherModule.prototype.initVimeo = function (data, cb) {
|
||||
if (!Config.get("vimeo-workaround")) {
|
||||
if (cb) cb();
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
self.channel.activeLock.lock();
|
||||
Vimeo.extract(data.id).then(function (direct) {
|
||||
if (self.dead || self.channel.dead)
|
||||
return;
|
||||
|
||||
if (self._media === data) {
|
||||
data.meta.direct = direct;
|
||||
self.channel.logger.log("[mediarefresher] Refreshed vimeo video with ID " +
|
||||
data.id);
|
||||
}
|
||||
self.channel.activeLock.release();
|
||||
|
||||
if (cb) cb();
|
||||
}).catch(function (err) {
|
||||
Logger.errlog.log("Unexpected vimeo::extract() fail: " + err.stack);
|
||||
if (cb) cb();
|
||||
});
|
||||
};
|
||||
|
||||
MediaRefresherModule.prototype.refreshGoogleDocs = function (media, cb) {
|
||||
var self = this;
|
||||
|
||||
if (self.dead || self.channel.dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.channel.activeLock.lock();
|
||||
InfoGetter.getMedia(media.id, "gd", function (err, data) {
|
||||
if (self.dead || self.channel.dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof err === "string") {
|
||||
err = err.replace(/Google Drive lookup failed for [\w-]+: /, "");
|
||||
err = err.replace(/Forbidden/, "Access Denied");
|
||||
err = err.replace(/You don't have permission to access this video\./,
|
||||
"Access Denied");
|
||||
}
|
||||
|
||||
switch (err) {
|
||||
case "Moved Temporarily":
|
||||
self.channel.logger.log("[mediarefresher] Google Docs refresh failed " +
|
||||
"(likely redirect to login page-- make sure it is shared " +
|
||||
"correctly)");
|
||||
self.channel.activeLock.release();
|
||||
if (cb) cb();
|
||||
return;
|
||||
case "Access Denied":
|
||||
case "Not Found":
|
||||
case "Internal Server Error":
|
||||
case "Service Unavailable":
|
||||
case "Google Drive does not permit videos longer than 1 hour to be played":
|
||||
case "Google Drive videos must be shared publicly":
|
||||
self.channel.logger.log("[mediarefresher] Google Docs refresh failed: " +
|
||||
err);
|
||||
self.channel.activeLock.release();
|
||||
if (cb) cb();
|
||||
return;
|
||||
default:
|
||||
if (err) {
|
||||
self.channel.logger.log("[mediarefresher] Google Docs refresh failed: " +
|
||||
err);
|
||||
Logger.errlog.log("Google Docs refresh failed for ID " + media.id +
|
||||
": " + err);
|
||||
self.channel.activeLock.release();
|
||||
if (cb) cb();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (media !== self._media) {
|
||||
self.channel.activeLock.release();
|
||||
if (cb) cb();
|
||||
return;
|
||||
}
|
||||
|
||||
self.channel.logger.log("[mediarefresher] Refreshed Google Docs video with ID " +
|
||||
media.id);
|
||||
media.meta = data.meta;
|
||||
self.channel.activeLock.release();
|
||||
if (cb) cb();
|
||||
});
|
||||
};
|
||||
|
||||
MediaRefresherModule.prototype.initGooglePlus = function (media, cb) {
|
||||
var self = this;
|
||||
|
||||
if (self.dead || self.channel.dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.channel.activeLock.lock();
|
||||
InfoGetter.getMedia(media.id, "gp", function (err, data) {
|
||||
if (self.dead || self.channel.dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof err === "string") {
|
||||
err = err.replace(/Forbidden/, "Access Denied");
|
||||
}
|
||||
|
||||
switch (err) {
|
||||
case "Access Denied":
|
||||
case "Not Found":
|
||||
case "Internal Server Error":
|
||||
case "Service Unavailable":
|
||||
case "The video is still being processed":
|
||||
case "A processing error has occured":
|
||||
case "The video has been processed but is not yet accessible":
|
||||
case ("Unable to retreive video information. Check that the video exists " +
|
||||
"and is shared publicly"):
|
||||
self.channel.logger.log("[mediarefresher] Google+ refresh failed: " +
|
||||
err);
|
||||
self.channel.activeLock.release();
|
||||
if (cb) cb();
|
||||
return;
|
||||
default:
|
||||
if (err) {
|
||||
self.channel.logger.log("[mediarefresher] Google+ refresh failed: " +
|
||||
err);
|
||||
Logger.errlog.log("Google+ refresh failed for ID " + media.id +
|
||||
": " + err);
|
||||
self.channel.activeLock.release();
|
||||
if (cb) cb();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (media !== self._media) {
|
||||
self.channel.activeLock.release();
|
||||
if (cb) cb();
|
||||
return;
|
||||
}
|
||||
|
||||
self.channel.logger.log("[mediarefresher] Refreshed Google+ video with ID " +
|
||||
media.id);
|
||||
media.meta = data.meta;
|
||||
self.channel.activeLock.release();
|
||||
if (cb) cb();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = MediaRefresherModule;
|
||||
81
src/channel/module.js
Normal file
81
src/channel/module.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
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 to pack info, e.g. for channel detail view
|
||||
*/
|
||||
packInfo: function (data, isAdmin) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
onUserPreChat: function (user, data, cb) {
|
||||
cb(null, ChannelModule.PASSTHROUGH);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called before a new video begins playing
|
||||
*/
|
||||
onPreMediaChange: function (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;
|
||||
278
src/channel/opts.js
Normal file
278
src/channel/opts.js
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
var ChannelModule = require("./module");
|
||||
var Config = require("../config");
|
||||
var Utilities = require("../utilities");
|
||||
var url = require("url");
|
||||
|
||||
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
|
||||
torbanned: false, // Block connections from Tor exit nodes
|
||||
allow_ascii_control: false,// Allow ASCII control characters (\x00-\x1f)
|
||||
playlist_max_per_user: 0 // Maximum number of playlist items per user
|
||||
};
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.opts.chat_antiflood_params.burst = Math.min(20,
|
||||
this.opts.chat_antiflood_params.burst);
|
||||
this.opts.chat_antiflood_params.sustained = Math.min(10,
|
||||
this.opts.chat_antiflood_params.sustained);
|
||||
};
|
||||
|
||||
OptionsModule.prototype.save = function (data) {
|
||||
data.opts = this.opts;
|
||||
};
|
||||
|
||||
OptionsModule.prototype.packInfo = function (data, isAdmin) {
|
||||
data.pagetitle = this.opts.pagetitle;
|
||||
data.public = this.opts.show_public;
|
||||
if (isAdmin) {
|
||||
data.hasPassword = this.opts.password !== false;
|
||||
}
|
||||
};
|
||||
|
||||
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 = 0;
|
||||
if (typeof data.maxlength !== "number") {
|
||||
ml = Utilities.parseTime(data.maxlength);
|
||||
} else {
|
||||
ml = parseInt(data.maxlength);
|
||||
}
|
||||
|
||||
if (isNaN(ml) || ml < 0) {
|
||||
ml = 0;
|
||||
}
|
||||
this.opts.maxlength = ml;
|
||||
}
|
||||
|
||||
if ("externalcss" in data && user.account.effectiveRank >= 3) {
|
||||
var link = (""+data.externalcss).substring(0, 255);
|
||||
if (!link) {
|
||||
this.opts.externalcss = "";
|
||||
} else {
|
||||
try {
|
||||
var data = url.parse(link);
|
||||
if (!data.protocol || !data.protocol.match(/^(https?|ftp):/)) {
|
||||
throw "Unacceptable protocol " + data.protocol;
|
||||
} else if (!data.host) {
|
||||
throw "URL is missing host";
|
||||
} else {
|
||||
link = data.href;
|
||||
}
|
||||
} catch (e) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Invalid URL for external CSS: " + e,
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.opts.externalcss = link;
|
||||
}
|
||||
}
|
||||
|
||||
if ("externaljs" in data && user.account.effectiveRank >= 3) {
|
||||
var link = (""+data.externaljs).substring(0, 255);
|
||||
if (!link) {
|
||||
this.opts.externaljs = "";
|
||||
} else {
|
||||
|
||||
try {
|
||||
var data = url.parse(link);
|
||||
if (!data.protocol || !data.protocol.match(/^(https?|ftp):/)) {
|
||||
throw "Unacceptable protocol " + data.protocol;
|
||||
} else if (!data.host) {
|
||||
throw "URL is missing host";
|
||||
} else {
|
||||
link = data.href;
|
||||
}
|
||||
} catch (e) {
|
||||
user.socket.emit("errorMsg", {
|
||||
msg: "Invalid URL for external JS: " + e,
|
||||
alert: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.opts.externaljs = link;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
b = Math.min(20, b);
|
||||
|
||||
var s = parseFloat(data.chat_antiflood_params.sustained);
|
||||
if (isNaN(s) || s <= 0) {
|
||||
s = 1;
|
||||
}
|
||||
|
||||
s = Math.min(10, s);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if ("torbanned" in data && user.account.effectiveRank >= 3) {
|
||||
this.opts.torbanned = Boolean(data.torbanned);
|
||||
}
|
||||
|
||||
if ("allow_ascii_control" in data && user.account.effectiveRank >= 3) {
|
||||
this.opts.allow_ascii_control = Boolean(data.allow_ascii_control);
|
||||
}
|
||||
|
||||
if ("playlist_max_per_user" in data && user.account.effectiveRank >= 3) {
|
||||
var max = parseInt(data.playlist_max_per_user);
|
||||
if (!isNaN(max) && max >= 0) {
|
||||
this.opts.playlist_max_per_user = max;
|
||||
}
|
||||
}
|
||||
|
||||
this.channel.logger.log("[mod] " + user.getName() + " updated channel options");
|
||||
this.sendOpts(this.channel.users);
|
||||
};
|
||||
|
||||
module.exports = OptionsModule;
|
||||
392
src/channel/permissions.js
Normal file
392
src/channel/permissions.js
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
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
|
||||
playlistaddrawfile: 2, // Add raw file 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
|
||||
viewvoteskip: 1.5, // View voteskip results
|
||||
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
|
||||
chatclear: 2, // Use the /clear command
|
||||
exceedmaxitems: 2 // Exceed maximum items per user limit
|
||||
};
|
||||
|
||||
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.canAddRawFile = function (account) {
|
||||
return this.hasPermission(account, "playlistaddrawfile");
|
||||
};
|
||||
|
||||
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.canSeeVoteskipResults = function (actor) {
|
||||
return this.hasPermission(actor, "viewvoteskip");
|
||||
};
|
||||
|
||||
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.canClearChat = function (actor) {
|
||||
return this.hasPermission(actor, "chatclear");
|
||||
};
|
||||
|
||||
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.canExceedMaxItemsPerUser = function (actor) {
|
||||
return this.hasPermission(actor, "exceedmaxitems");
|
||||
};
|
||||
|
||||
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
|
||||
viewvoteskip: 1.5, // View voteskip results
|
||||
playlistlock: 2, // Lock/unlock the playlist
|
||||
leaderctl: 0, // Give/take leader
|
||||
drink: 0, // Use the /d command
|
||||
chat: 0, // Send chat messages
|
||||
chatclear: 2, // Use the /clear command
|
||||
exceedmaxitems: 2 // Exceed max items per user
|
||||
};
|
||||
|
||||
for (var key in perms) {
|
||||
this.permissions[key] = perms[key];
|
||||
}
|
||||
|
||||
this.openPlaylist = true;
|
||||
};
|
||||
|
||||
module.exports = PermissionsModule;
|
||||
1372
src/channel/playlist.js
Normal file
1372
src/channel/playlist.js
Normal file
File diff suppressed because it is too large
Load diff
186
src/channel/poll.js
Normal file
186
src/channel/poll.js
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
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.unload = function () {
|
||||
if (this.poll && this.poll.timer) {
|
||||
clearTimeout(this.poll.timer);
|
||||
}
|
||||
};
|
||||
|
||||
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.onUserPart = function(user) {
|
||||
if (this.poll) {
|
||||
this.poll.unvote(user.realip);
|
||||
this.sendPollUpdate(this.channel.users);
|
||||
}
|
||||
};
|
||||
|
||||
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]" },
|
||||
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.realip, 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;
|
||||
192
src/channel/ranks.js
Normal file
192
src/channel/ranks.js
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
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 (!name.match(/^[a-zA-Z0-9_-]{1,20}$/)) {
|
||||
user.socket.emit("channelRankFail", {
|
||||
msg: "Invalid target name " + data.name
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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 = Math.max(receiver.account.globalRank, rank);
|
||||
receiver.socket.emit("rank", receiver.account.effectiveRank);
|
||||
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;
|
||||
123
src/channel/voteskip.js
Normal file
123
src/channel/voteskip.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
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.onUserPart = function(user) {
|
||||
if (!this.poll) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unvote(user.realip);
|
||||
this.update();
|
||||
};
|
||||
|
||||
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.realip, 0);
|
||||
|
||||
var title = "";
|
||||
if (this.channel.modules.playlist.current) {
|
||||
title = " " + this.channel.modules.playlist.current.media.title;
|
||||
}
|
||||
|
||||
var name = user.getName() || "(anonymous)";
|
||||
|
||||
this.channel.logger.log("[playlist] " + name + " voteskipped " + title);
|
||||
user.setAFK(false);
|
||||
this.update();
|
||||
};
|
||||
|
||||
VoteskipModule.prototype.unvote = function(ip) {
|
||||
if (!this.poll) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.poll.unvote(ip);
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
var perms = this.channel.modules.permissions;
|
||||
|
||||
users.forEach(function (u) {
|
||||
if (perms.canSeeVoteskipResults(u)) {
|
||||
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