-spaced out usernames -added a few moar tokes -added more solotoke images -repeats !toke command used by user
2061 lines
65 KiB
JavaScript
2061 lines
65 KiB
JavaScript
"use strict";
|
|
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
const sio = require("socket.io-client");
|
|
const C = require("cli-color");
|
|
const fetch = require("node-fetch");
|
|
const ent = require("html-entities").AllHtmlEntities;
|
|
|
|
const EventHandlers = require("./eventhandlers.js");
|
|
const clicmd = require("./clicommands.js");
|
|
const utils = require("./utils.js");
|
|
const chatcmd = require("./chatcommands.js");
|
|
const strings = require("./strings.js");
|
|
const classes = require("./classes.js");
|
|
|
|
const PLATFORM = ((process && process.platform) ? process.platform : "win32");
|
|
|
|
/**
|
|
* class for Bot object.
|
|
* @constructor
|
|
* @param {Object} config Configuration object. Read from config.js
|
|
* @param {Object} readline Readline interface object
|
|
* @param {String} ROOT Root filepath (absolute) of the bot.
|
|
*/
|
|
function Bot(config, readline, ROOT) {
|
|
|
|
this.rl = readline;
|
|
this.readlineInitialized = false;
|
|
|
|
this.ROOTPATH = ROOT;
|
|
|
|
this.DiscordBot = null;
|
|
|
|
if (config.discord.use) {
|
|
if (config.discord.token.trim() !== "") {
|
|
this.DiscordBot = require("./discordbot.js").init(this, config.discord.token);
|
|
} else {
|
|
this.logger.error(strings.format(this, "DISCORD_ERR_INIT", ["no token given"]));
|
|
}
|
|
}
|
|
|
|
const ROOM = config.login.room;
|
|
this.settingsFile = config.advanced.useChannelSettingsFile ? "settings-" + ROOM + ".json" : "settings.json";
|
|
|
|
var streamOpts = {
|
|
flags: 'a',
|
|
encoding: 'utf8',
|
|
fd: null,
|
|
mode: 0o660,
|
|
autoClose: true
|
|
};
|
|
var logStream = fs.createWriteStream(path.join(ROOT, "logs", ROOM, "bot.log"), streamOpts),
|
|
errStream = fs.createWriteStream(path.join(ROOT, "logs", ROOM, "err.log"), streamOpts),
|
|
modStream = fs.createWriteStream(path.join(ROOT, "logs", ROOM, "mod.log"), streamOpts),
|
|
mediaStream = fs.createWriteStream(path.join(ROOT, "logs", ROOM, "media.log"), streamOpts),
|
|
serverStream = fs.createWriteStream(path.join(ROOT, "logs", ROOM, "server.log"), streamOpts),
|
|
debugStream = config.interface.logDebug ? fs.createWriteStream(path.join(ROOT, "logs", ROOM, "debug.log"), streamOpts) : null;
|
|
|
|
this.getLogStream = function() {return logStream;}
|
|
|
|
let commonLog = (label, message, stream, consolidate)=>{
|
|
if (!message) return;
|
|
this.write(C.blackBright(utils.getTimestamp(config.interface.useTwentyFourHourTime)) + " " + label + message, stream, consolidate);
|
|
}
|
|
|
|
this.logger = {
|
|
log: (message) => { if (!message) return;
|
|
commonLog("", message, logStream, false);
|
|
},
|
|
verbose: (message) => { if (!message) return;
|
|
if (config.interface.logVerbose)
|
|
commonLog(C.magentaBright("* "), C.blueBright(message), logStream, false);
|
|
},
|
|
debug: (message) => { if (!message) return;
|
|
if (config.interface.logDebug)
|
|
commonLog(C.magenta("[DEBUG] "), C.magentaBright(message), debugStream, config.interface.logConsolidation);
|
|
},
|
|
info: (message) => { if (!message) return;
|
|
commonLog(C.blue("[INFO] "), C.blueBright(message), logStream, false);
|
|
},
|
|
media: (message) => { if (!message) return;
|
|
commonLog(C.yellowBright("[MEDIA] "), message, mediaStream, config.interface.logConsolidation);
|
|
},
|
|
error: (message) => { if (!message) return;
|
|
commonLog(C.red("[ERROR] "), C.redBright(message), errStream, (config.interface.logConsolidation && !this.cfg.interface.excludeErrorsFromLog));
|
|
},
|
|
warn: (message) => { if (!message) return;
|
|
commonLog(C.yellow("[WARN] "), C.yellowBright(message), logStream, false);
|
|
},
|
|
mod: (message) => { if (!message) return;
|
|
commonLog(C.green("[MOD] "), C.greenBright(message), modStream, config.interface.logConsolidation);
|
|
},
|
|
cylog: (message) => { if (!message) return;
|
|
commonLog(C.cyan("[SERVER] "), C.cyanBright(message), serverStream, config.interface.logConsolidation);
|
|
}
|
|
}
|
|
|
|
this.killed = false;
|
|
this.botName = "ChozoBot";
|
|
this.version = "0.9961a";
|
|
|
|
this.RANKS = config.RANKS;
|
|
this.RANKS["SITEADMIN"] = 255;
|
|
|
|
this.actionQueue = new classes.AutoFnQueue(config.misc.queueInterval);
|
|
this.afk = false;
|
|
this.broadcastPMQueue = new classes.AutoFnQueue(config.misc.broadcastPMQueueInterval);
|
|
this.bumpStats = {
|
|
lastBumpedUIDs: [],
|
|
users:{},
|
|
bumpingUIDs:[]
|
|
};
|
|
this.changingPartition = false;
|
|
this.CHANNEL = {
|
|
badEmotes: [],
|
|
banlist: [],
|
|
currentMedia: null,
|
|
currentUID: -1,
|
|
emoteMap: {},
|
|
emotes: [],
|
|
leader: "",
|
|
opts: {},
|
|
perms: {},
|
|
playlist: [],
|
|
playlistIsLocked: false,
|
|
playlistMeta: { count: 0, rawTime: 0, time: 0 },
|
|
poll: {
|
|
active: false,
|
|
title: '',
|
|
options: [],
|
|
counts: [],
|
|
initiator: '',
|
|
timestamp: -1
|
|
},
|
|
rankList: [],
|
|
room: ROOM,
|
|
usercount: 0, //includes anons, updated with usercount event
|
|
users: [],
|
|
voteskip: { count: 0, need: 0 }
|
|
};
|
|
this.cfg = {
|
|
api: config.api,
|
|
connection: config.connection,
|
|
db: config.db,
|
|
discord: config.discord,
|
|
interface: config.interface,
|
|
chat: config.chat,
|
|
media: config.media,
|
|
misc: config.misc,
|
|
moderation: config.moderation,
|
|
advanced: config.advanced,
|
|
rankNames: config.rankNames
|
|
};
|
|
//set defaults on important cfg option(s) in case users have not updated their config:
|
|
if (!this.cfg.connection.hasOwnProperty("sameDomainSocketOnly")) {
|
|
this.cfg.connection["sameDomainSocketOnly"] = true;
|
|
}
|
|
this.currentVideoData = {
|
|
id: null,
|
|
comments: null,
|
|
commentsDisabled: false,
|
|
views: -1,
|
|
likes: 0,
|
|
dislikes: 0,
|
|
ratingsDisabled: false,
|
|
noStats: false
|
|
}
|
|
this.db = require("./db.js");
|
|
this.duels = [];
|
|
//This is for runonce things such as requesting the rank list.
|
|
this.first = {
|
|
grabbedChannelOpts: true,
|
|
grabbedChannelRanks: true,
|
|
grabbedPermissions: true,
|
|
motdChange: true,
|
|
playlistLock: true
|
|
};
|
|
this.largeDataReqQueue = new classes.AutoFnQueue(config.misc.largeDataQueueInterval);
|
|
this.leadFinishingMedia = false;
|
|
this.gettingBanList = false;
|
|
this.gettingComments = false;
|
|
this.gettingVideoMeta = false;
|
|
this.guest = false;
|
|
this.handlingChatCommands = false;
|
|
this.hasConnectedBefore = false;
|
|
this.KICKED = false;
|
|
this.lastLowPlaylistNotification = Date.now();
|
|
this.leader = false;
|
|
this.leadTimer = null;
|
|
this.logged_in = false;
|
|
//Used with minuteTick for half-hours and such to avoid multiple timers
|
|
this.minutesPassed = 0;
|
|
this.notifiedLowPlaylistTime = false;
|
|
this.pendingLanguageChange = null;
|
|
this.rank = 0;
|
|
this.seek = {
|
|
time: -1,
|
|
autoUnassign: true
|
|
};
|
|
this.socketConnErrors = 0;
|
|
this.started = Date.now(); //let's timestamp when the bot started because that's useful
|
|
this.userCooldowns = {};
|
|
this.username = "";
|
|
this.timeouts = {
|
|
changeMediaLead: null,
|
|
minuteTick: null,
|
|
playingNext: null,
|
|
},
|
|
this.trigger = this.validateTrigger(config.chat.trigger);
|
|
|
|
this.settings = {
|
|
muted: false,
|
|
disallow: [],
|
|
timeBans: {},
|
|
minRankOverrides: {},
|
|
rankMatchOverrides: {},
|
|
userCooldownOverrides: {},
|
|
cmdCooldownOverrides: {},
|
|
cmdStateOverrides: {},
|
|
userData: {},
|
|
mediaBlacklist:[],
|
|
userBlacklist:[],
|
|
lucky:{},
|
|
flatSkiprate: {
|
|
managing: false,
|
|
target: -1,
|
|
original_rate: -1
|
|
},
|
|
};
|
|
|
|
this.logger.info(strings.format(this, "INIT", [this.botName, this.version]));
|
|
this.logger.verbose(strings.format(this, "ACTIONQUEUE_INIT_INTERVAL", [config.misc.queueInterval]));
|
|
|
|
//read settings.json and if there was an error (file doesn't exist) then write a new one
|
|
//also, initializes chat commands
|
|
this.readSettings(hasErr => {
|
|
if (!hasErr) {
|
|
if (this.getOpt("muted", false)) {
|
|
this.logger.warn(strings.format(this, "BOT_MUTED_INIT"));
|
|
}
|
|
if (!this.getOpt("timeBans", {}).hasOwnProperty(ROOM)) {
|
|
this.settings.timeBans[ROOM] = [];
|
|
}
|
|
chatcmd.init(this);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Takes socket server config and attempts to establish a connection. Sets event handlers.
|
|
* @function
|
|
* @name connectToServer
|
|
* @param {Object} server Socket server config object
|
|
*/
|
|
let connectToServer = (server)=>{
|
|
this.logger.debug(strings.format(this, "DBG_FOUND_SOCKET", [server.url]));
|
|
if (!this.cfg.connection.sameDomainSocketOnly) {
|
|
this.logger.warn("sameDomainSocketOnly is disabled!");
|
|
}
|
|
var fn = (()=>{
|
|
/*if (this.cfg.connection.sameDomainSocketOnly && utils.getHostname(server.url).toLowerCase() !== config.connection.hostname.toLowerCase()) {
|
|
this.logger.error(strings.format(this, "SERVER_BAD_HOSTNAME", [config.connection.hostname]));
|
|
this.kill("bad server hostname");
|
|
} else {*/
|
|
this.socket = sio(server.url, {secure: server.secure});
|
|
EventHandlers.setHandlers(this, this.socket, config);
|
|
//}
|
|
});
|
|
this.actionQueue.enqueue([this, fn, []]);
|
|
}
|
|
|
|
|
|
this.getSocketConfig(this.CHANNEL.room, config.connection.secureSocket, connectToServer);
|
|
|
|
//for use with partitionChange
|
|
//NOT TESTED FULLY! could work though
|
|
this.hardReconnect = function(socketConfig) {
|
|
if (this.socket && this.socket.connected && !this.changingPartition) {
|
|
this.socket.disconnect();
|
|
}
|
|
this.hasConnectedBefore = false;
|
|
this.logger.log(strings.format(this, "CONNECTING"));
|
|
let i = 0,
|
|
server = null,
|
|
servers = socketConfig.servers;
|
|
for (;i < servers.length; i++) {
|
|
if (servers[i].secure === config.connection.secureSocket) {
|
|
server = servers[i];
|
|
break;
|
|
}
|
|
}
|
|
if (!server && servers.length >= 1) {
|
|
server = servers[0];
|
|
} else if (!server) {
|
|
this.kill("No socket config handed over during partition change", 2000, 0);
|
|
return;
|
|
}
|
|
this.changingPartition = false;
|
|
connectToServer(server);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tries to set leader to the given user.
|
|
*
|
|
* @param {string} name A username. Can be blank ("") to remove the current leader.
|
|
* @return {boolean} True if the given user is in the room; false if not, or no leaderctl permissions.
|
|
*/
|
|
Bot.prototype.assignLeader = function(name) {
|
|
if (!this.checkChannelPermission("leaderctl")) {
|
|
this.logger.error("Tried to assign a leader to " + name + ", but bot doesn't have leaderctl perms");
|
|
if (!this.leader && name.toLowerCase() === this.username.toLowerCase()) {
|
|
this.seek.time = -1;
|
|
}
|
|
return false;
|
|
}
|
|
let usr = (name === "" ? {name: ""} : this.getUser(name));
|
|
if (usr) {
|
|
let fn = ()=>{this.socket.emit("assignLeader", {name:usr.name})};
|
|
this.actionQueue.enqueue([this, fn, []]);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Removes a given user from the disallowed list.
|
|
*
|
|
* @param {string} username Username.
|
|
* @return {boolean} True if the user was previously disallowed. False otherwise.
|
|
*/
|
|
Bot.prototype.allowUser = function(username) {
|
|
if (!username || username.trim() === "") return;
|
|
username = username.toLowerCase().trim();
|
|
var da = this.getOpt("disallow", []);
|
|
let i = 0;
|
|
for (; i < da.length; i++) {
|
|
if (da[i] === username) {
|
|
this.logger.info(strings.format(this, "USER_ALLOWED", [username]));
|
|
utils.unsortedRemove(da, i);
|
|
this.writeSettings();
|
|
return true;
|
|
}
|
|
}
|
|
this.logger.info(strings.format(this, "USER_ALLOWED_FAIL", [username]));
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Sends a private message to multiple users at once.
|
|
*
|
|
* @param {string[]} users Users to send messages to.
|
|
* @param {string} message The message.
|
|
*/
|
|
Bot.prototype.broadcastPM = function(users, message) {
|
|
if (this.settings.muted) return;
|
|
var fn = ((username, message)=>{
|
|
this.socket.emit("pm", {
|
|
"to":C.strip(username),
|
|
"msg":C.strip(message)
|
|
});
|
|
});
|
|
let i = 0;
|
|
for(;i < users.length; i++) {
|
|
this.broadcastPMQueue.enqueue([this, fn, [users[i], message]])
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends a private message to all online mods.
|
|
*
|
|
* @param {string} message Message to send.
|
|
*/
|
|
Bot.prototype.broadcastModPM = function(message) {
|
|
this.broadcastPM(this.getOnlineMods(true, true), message);
|
|
}
|
|
|
|
/**
|
|
* Check if the bot has a given channel permission.
|
|
*
|
|
* @param {string} perm Permission to check against. See bottom of bot.js for a list
|
|
* @return {boolean} True if bot has permission, otherwise false.
|
|
*/
|
|
Bot.prototype.checkChannelPermission = function(perm) {
|
|
return this.userHasChannelPermission(perm, this.rank);
|
|
}
|
|
|
|
/**
|
|
* Check if bot has multiple channel permissions.
|
|
*
|
|
* @param {string[]} perms Array of permissions to check.
|
|
* @return {boolean} True if bot has every permission, otherwise false.
|
|
*/
|
|
Bot.prototype.checkChannelPermissions = function(perms) {
|
|
let i = 0;
|
|
for (;i < perms.length;i++) {
|
|
if (!this.userHasChannelPermission(perms[i], this.rank)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Unbans all timebanned users if their times have expired.
|
|
*
|
|
* @return {boolean} False if bot cannot ban or banlist is empty, otherwise true.
|
|
*/
|
|
Bot.prototype.checkTimeBans = function() {
|
|
let bl = this.CHANNEL.banlist,
|
|
tb = this.settings.timeBans[this.CHANNEL.room];
|
|
if (bl.length <= 0 || !this.checkChannelPermission("ban")) return false;
|
|
let i = 0;
|
|
for (;i < tb.length; i++) {
|
|
if (Date.now() >= tb[i].unbanTime) {
|
|
this.unbanUser(tb[i].name);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Purges blacklisted users, and deletes blacklisted media from the playlist.
|
|
*
|
|
* @return {boolean} False if bot cannot delete media, otherwise true.
|
|
*/
|
|
Bot.prototype.cleanBlacklistedMedia = function() {
|
|
if (!this.checkChannelPermission("playlistdelete")) return false;
|
|
let pl = this.CHANNEL.playlist,
|
|
bu = this.getOpt("userBlacklist", []),
|
|
bm = this.getOpt("mediaBlacklist", []),
|
|
i = 0,
|
|
purge = [],
|
|
del = [];
|
|
for (;i < pl.length;i++){
|
|
let usr = pl[i].queueby.toLowerCase();
|
|
let userIsBlacklisted = ~bu.indexOf(usr);
|
|
if (!~purge.indexOf(usr) && userIsBlacklisted) {
|
|
purge.push(pl[i].queueby);
|
|
} else if (!userIsBlacklisted && this.mediaIsBlacklisted(pl[i])) {
|
|
del.push(pl[i].uid);
|
|
}
|
|
}
|
|
for (i = 0;i < purge.length; i++) {
|
|
this.purgeUser(purge[i]);
|
|
}
|
|
for (i = 0;i < del.length; i++) {
|
|
this.deleteVideo(del[i]);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Cleans bot state. Dangerous: Only use if disconnected!
|
|
*/
|
|
Bot.prototype.cleanState = function() {
|
|
let room = this.CHANNEL.room;
|
|
this.CHANNEL = {
|
|
badEmotes: [],
|
|
banlist: [],
|
|
currentMedia: null,
|
|
currentUID: -1,
|
|
emoteMap: {},
|
|
emotes: [],
|
|
leader: "",
|
|
opts: {},
|
|
perms: {},
|
|
playlist: [],
|
|
playlistIsLocked: false,
|
|
playlistMeta: { count: 0, rawTime: 0, time: 0 },
|
|
poll: {
|
|
active: false,
|
|
title: '',
|
|
options: [],
|
|
counts: [],
|
|
initiator: '',
|
|
timestamp: -1
|
|
},
|
|
rankList: [],
|
|
room: room,
|
|
usercount: 0,
|
|
users: [],
|
|
voteskip: { count: 0, need: 0 }
|
|
};
|
|
this.afk = false;
|
|
this.first = {
|
|
grabbedChannelOpts: true,
|
|
grabbedChannelRanks: true,
|
|
grabbedPermissions: true,
|
|
loadedBlacklist: true,
|
|
motdChange: true,
|
|
playlistLock: true
|
|
};
|
|
this.leadFinishingMedia = false;
|
|
this.gettingBanList = false;
|
|
this.guest = false;
|
|
this.leader = false;
|
|
this.leadTimer = null;
|
|
this.logged_in = false;
|
|
this.rank = 0;
|
|
this.socketConnErrors = 0;
|
|
this.username = "";
|
|
}
|
|
|
|
/**
|
|
* Carries out a duel (the given object).
|
|
*
|
|
* @param {Object} duel Object containing duel info.
|
|
* @return {boolean} False if duel is null, otherwise true.
|
|
*/
|
|
Bot.prototype.commenceDuel = function(duel) {
|
|
if (!duel) return false;
|
|
let A = 0,
|
|
B = 0;
|
|
//Setting the min/max to the same number will cause an infinite loop
|
|
function roll() {
|
|
A = Math.floor((Math.random() * 100) + 1);
|
|
B = Math.floor((Math.random() * 100) + 1);
|
|
}
|
|
while (A === B) {
|
|
roll();
|
|
}
|
|
let strArgs = [duel[0], duel[1], A, B, "/tinyrekt"];
|
|
if (A > B) {
|
|
this.sendChatMsg(strings.format(this, "DUEL_RESULT_WIN", strArgs));
|
|
this.db.run("insertDuelRecord", [duel[0], duel[1]]);
|
|
} else {
|
|
this.sendChatMsg(strings.format(this, "DUEL_RESULT_LOSS", strArgs));
|
|
this.db.run("insertDuelRecord", [duel[1], duel[0]]);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Attempts to delete the video with the given UID.
|
|
*
|
|
* @param {number} uid UID of the desired video
|
|
* @return {boolean} True if UID is a number and bot can delete media, otherwise false.
|
|
*/
|
|
Bot.prototype.deleteVideo = function(uid) {
|
|
if (typeof uid === "number" && this.checkChannelPermission("playlistdelete")) {
|
|
let fn = ()=>{this.socket.emit("delete", uid)};
|
|
this.actionQueue.enqueue([this, fn, []]);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Takes a media object and deletes all occurrences of media with the same type and ID.
|
|
*
|
|
* @param {Object} media Media object.
|
|
* @return {boolean} False if bot cannot delete media or see the playlist, otherwise true.
|
|
*/
|
|
Bot.prototype.deleteVideoAndDupes = function(media) {
|
|
if (!this.checkChannelPermissions(["playlistdelete", "seeplaylist"])) return false;
|
|
let _media = media.hasOwnProperty("media") ? media.media : media;
|
|
if (_media && _media.id && _media.type) {
|
|
let vids = this.getMediaAll(_media.id, _media.type),
|
|
i = vids.length-1;
|
|
for (;i>=0;i--) {
|
|
this.deleteVideo(vids[i].uid);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if a user is disallowed.
|
|
*
|
|
* @param {string} username Username.
|
|
* @return {boolean} True if given user is disallowed, otherwise false.
|
|
*/
|
|
Bot.prototype.disallowed = function(username) {
|
|
return ~this.settings.disallow.indexOf(username.toLowerCase().trim());
|
|
}
|
|
|
|
/**
|
|
* Disallows a user.
|
|
*
|
|
* @param {string} username Username.
|
|
* @return {boolean} True if user is not already disallowed, false otherwise.
|
|
*/
|
|
Bot.prototype.disallowUser = function(username) {
|
|
if (!username || username.trim() === "") return false;
|
|
username = username.toLowerCase().trim();
|
|
if (!~this.getOpt("disallow", []).indexOf(username)) {
|
|
this.settings.disallow.push(username);
|
|
this.writeSettings();
|
|
this.logger.info(strings.format(this, "USER_DISALLOWED", [username]));
|
|
return true;
|
|
} else
|
|
this.logger.info(strings.format(this, "USER_DISALLOWED_FAIL", [username]));
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if an emote exists in the channel's emote list.
|
|
*
|
|
* @param {string} emote Name of emote as it would be used normally in chat.
|
|
* @return {boolean} True if emote exists, false if not.
|
|
*/
|
|
Bot.prototype.emoteExists = function(emote) {
|
|
return (this.CHANNEL.emoteMap.hasOwnProperty(emote) || ~this.CHANNEL.badEmotes.indexOf(emote));
|
|
}
|
|
|
|
/**
|
|
* Get number of AFK users in the room.
|
|
*
|
|
* @return {number} Number of AFK users.
|
|
*/
|
|
Bot.prototype.getAFKCount = function() {
|
|
var users = this.CHANNEL.users;
|
|
var afk = 0;
|
|
let i = 0;
|
|
for (; i < users.length; i++) {
|
|
if (users[i].meta.afk)
|
|
afk++;
|
|
}
|
|
return afk;
|
|
}
|
|
|
|
/**
|
|
* Send socket request to retrieve the ban list. Can be quite heavy, use with care.
|
|
*/
|
|
Bot.prototype.getBanList = function() {
|
|
if (this.checkChannelPermission("ban")) {
|
|
if (!this.gettingBanList) {
|
|
this.gettingBanList = true;
|
|
let fn = ()=>{this.socket.emit("requestBanlist")};
|
|
this.largeDataReqQueue.enqueue([this, fn, []]);
|
|
}
|
|
} else {
|
|
this.CHANNEL.banlist = [];
|
|
}
|
|
}
|
|
|
|
//WARNING! Only works if the bot is rank >= 3, so be careful
|
|
/**
|
|
* Get rank of user from the actual rank list. Must be rank >= 3 to retrieve the list.
|
|
*
|
|
* @param {string} name Username.
|
|
* @return {number} Rank of the user if they are moderator+. -1 if user is not a mod, or the bot cannot see the rank list.
|
|
*/
|
|
Bot.prototype.getChanRank = function(name) {
|
|
name = name.toLowerCase();
|
|
let i = 0,
|
|
rl = this.CHANNEL.rankList;
|
|
for (;i < rl.length; i++) {
|
|
if (rl[i].name === name) {
|
|
return rl[i].rank;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Gets the currently active poll as an object that can be used to create another poll. Does not include initiator or timestamp.
|
|
*
|
|
* @return {Object|null} Poll object, null if no poll active or blank poll
|
|
*/
|
|
Bot.prototype.getCurrentPollFrame = function() {
|
|
let poll = this.CHANNEL.poll;
|
|
if (!poll || !poll.active || (poll.options.length <= 0 && poll.title.trim() === "")) return null;
|
|
let obscured = (poll.counts.length >= 1 && (typeof poll.counts[0] === "string") && poll.counts[0].indexOf("?") >= 0);
|
|
return {
|
|
title: poll.title,
|
|
opts: poll.options,
|
|
obscured: obscured
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the full media object from a media's UID
|
|
*
|
|
* @param {number} uid Media UID
|
|
* @return {Object|null} Media object matching the given UID, or null if not found.
|
|
*/
|
|
Bot.prototype.getMedia = function(uid) {
|
|
var index = this.getMediaIndex(uid);
|
|
if (~index) return this.CHANNEL.playlist[index];
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get a list of all media objects matching an ID and Type. Good for duplicates if allowed.
|
|
*
|
|
* @param {string} id Media ID, such as Video ID of YouTube videos.
|
|
* @param {string} type Host type abbreviation (yt, vi, dm, etc.)
|
|
* @return {Object[]} An array containing any matching media objects.
|
|
*/
|
|
Bot.prototype.getMediaAll = function(id, type) {
|
|
if (!id || !type) return [];
|
|
let vids = [],
|
|
i = 0,
|
|
pl = this.CHANNEL.playlist;
|
|
for (;i < pl.length; i++) {
|
|
if (pl[i].media && pl[i].media.id === id && pl[i].media.type === type) {
|
|
vids.push(pl[i]);
|
|
}
|
|
}
|
|
return vids;
|
|
}
|
|
|
|
/**
|
|
* Gets the first index of a media object within the playlist matching the provided UID.
|
|
*
|
|
* @param {number} uid Media UID.
|
|
* @return {number} Index of media object within the playlist. -1 if not found.
|
|
*/
|
|
Bot.prototype.getMediaIndex = function(uid) {
|
|
var i = 0;
|
|
for (; i < this.CHANNEL.playlist.length; i++) {
|
|
if (this.CHANNEL.playlist[i]["uid"] === uid) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Gets a list of all mods (users with rank >= 2) online.
|
|
*
|
|
* @param {boolean} excludeSelf If true, excludes bot from the resulting array.
|
|
* @return {string[]} Array of usernames.
|
|
*/
|
|
Bot.prototype.getOnlineMods = function(excludeSelf, excludeBots) {
|
|
let mods = [],
|
|
users = this.CHANNEL.users,
|
|
i = 0;
|
|
for (;i<users.length;i++) {
|
|
if (!(excludeSelf && users[i].name === this.username) && !(excludeBots && ~this.cfg.misc.bots.indexOf(users[i].name)) && users[i].rank >= this.RANKS.MOD) {
|
|
mods.push(users[i].name);
|
|
}
|
|
}
|
|
return mods;
|
|
}
|
|
|
|
/**
|
|
* Gets an option from settings.json, setting a default value if not found.
|
|
*
|
|
* @param {string} opt Option name.
|
|
* @param {*} def Default value if the option was not found.
|
|
* @return {*} Value of the option, or the default value given.
|
|
*/
|
|
Bot.prototype.getOpt = function(opt, def) {
|
|
if (this.settings.hasOwnProperty(opt)) return this.settings[opt];
|
|
this.logger.warn("getOpt: could not find " + opt + ", setting default value");
|
|
this.settings[opt] = def;
|
|
this.writeSettings();
|
|
return def;
|
|
}
|
|
|
|
/**
|
|
* Gets saved user data from settings.json.
|
|
*
|
|
* @param {string} username A username.
|
|
* @return {Object} User data object. Empty if not found.
|
|
*/
|
|
Bot.prototype.getSavedUserData = function(username) {
|
|
username = username.toLowerCase();
|
|
if (this.settings.userData.hasOwnProperty(username))
|
|
return this.settings.userData[username];
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* Queries CyTube for room socket info.
|
|
*
|
|
* @param {string} room Room name.
|
|
* @param {boolean} secure If true, select a secure server.
|
|
* @param {connectToServer} callback Callback to send the socket server object to.
|
|
*/
|
|
Bot.prototype.getSocketConfig = function(room, secure, callback) {
|
|
if (typeof callback !== "function") {
|
|
return this.logger.error(strings.format(this, "CALLBACK_INVALID", ["getSocketConfig"]));
|
|
}
|
|
if (!secure) {
|
|
this.logger.warn(strings.format(this, "SERVER_INSECURE"))
|
|
}
|
|
var fn = (()=>{
|
|
fetch(""+this.cfg.connection.hostname+"/socketconfig/" + room + ".json")
|
|
.then((res)=>{
|
|
/*(if (!res.ok) {
|
|
this.logger.error(strings.format(this, "SERVER_ROOM_NOT_FOUND", [room]));
|
|
this.logger.warn("The server may be having issues, or the room was not found.");
|
|
this.logger.info("Retrying connection in 60 seconds. Use CTRL+C to exit.");
|
|
setTimeout(()=>{
|
|
this.getSocketConfig(room, false, callback);
|
|
}, 60000);
|
|
throw new Error(res.statusText);
|
|
} else {*/
|
|
return res.json();
|
|
//}
|
|
})
|
|
.then((json)=>{
|
|
var servers = json.servers;
|
|
let i = 0;
|
|
for (; i < servers.length; i++) {
|
|
if (servers[i]["secure"] === secure) {
|
|
return servers[i];
|
|
}
|
|
}
|
|
return "";
|
|
})
|
|
.then((server)=>{
|
|
if (server && server.url && server.url.trim() !== "") {
|
|
callback(server);
|
|
return true;
|
|
} else {
|
|
this.logger.error(strings.format(this, "SERVER_ROOM_NOT_FOUND", [room]));
|
|
return false;
|
|
}
|
|
})
|
|
.catch((error)=>{
|
|
this.logger.error(strings.format(this, "SERVER_REQUEST_ERROR", [error.stack]));
|
|
this.logger.warn("The server may be down. Check to see if you can connect to it using a browser. Use CTRL+C to exit.");
|
|
});
|
|
});
|
|
this.actionQueue.enqueue([this, fn, []]);
|
|
}
|
|
|
|
/**
|
|
* Get user object from a username.
|
|
*
|
|
* @param {string} username Username.
|
|
* @return {Object|null} User object if the given user is in the room, or null if not.
|
|
*/
|
|
Bot.prototype.getUser = function(username) {
|
|
var i = 0;
|
|
username = username.toLowerCase();
|
|
for (; i < this.CHANNEL.users.length; i++) {
|
|
if (this.CHANNEL.users[i].name.toLowerCase() === username) {
|
|
return this.CHANNEL.users[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get duel object that the given username is in.
|
|
*
|
|
* @param {string} username A username.
|
|
* @param {boolean=} remove Optional, but if true, removes the object from the duel pool.
|
|
* @return {Object|null} Duel object if found, null otherwise.
|
|
*/
|
|
Bot.prototype.getUserDuel = function(username, remove) {
|
|
let i = 0;
|
|
for (;i < this.duels.length; i++) {
|
|
if (~this.duels[i].indexOf(username)) {
|
|
let duel = this.duels[i];
|
|
if (remove) {
|
|
clearTimeout(duel[2]);
|
|
utils.unsortedRemove(this.duels, i);
|
|
}
|
|
return duel;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets the index of a user object in the userlist with the given username.
|
|
*
|
|
* @param {string} username A username.
|
|
* @return {number} Index in the userlist of the user object if found, otherwise -1.
|
|
*/
|
|
Bot.prototype.getUserIndex = function(username) {
|
|
var i = 0;
|
|
username = username.toLowerCase();
|
|
for (; i < this.CHANNEL.users.length; i++) {
|
|
if (this.CHANNEL.users[i].name.toLowerCase() === username) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Gets the amount of users in the room that are both not AFK and able to voteskip.
|
|
*
|
|
* @return {number} Count of users who can skip
|
|
*/
|
|
Bot.prototype.getUsersWithSkipPerms = function() {
|
|
let min = this.CHANNEL.perms.voteskip,
|
|
users = this.CHANNEL.users,
|
|
i = 0,
|
|
count = 0;
|
|
for (; i < users.length; i++) {
|
|
if (!users[i].meta.afk && users[i].rank >= min) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Gets a list of videos added by the specified user.
|
|
*
|
|
* @param {string} username A username.
|
|
* @return {Object[]} Array of media objects added by the specified user.
|
|
*/
|
|
Bot.prototype.getUserVideos = function(username) {
|
|
username = username.toLowerCase();
|
|
let vids = [],
|
|
pl = this.CHANNEL.playlist,
|
|
i = 0;
|
|
for (;i < pl.length;i++) {
|
|
if (pl[i].queueby.toLowerCase() === username) {
|
|
vids.push(pl[i]);
|
|
}
|
|
}
|
|
return vids;
|
|
}
|
|
|
|
/**
|
|
* Resets some things depending on the bot's rank and permissions.
|
|
*/
|
|
Bot.prototype.handlePermissionChange = function() {
|
|
if (!this.checkChannelPermission("ban")) {
|
|
this.CHANNEL.banlist = [];
|
|
}
|
|
|
|
if (!this.checkChannelPermission("seeplaylist")) {
|
|
this.CHANNEL.playlist = [];
|
|
}
|
|
|
|
if (!this.checkChannelPermission("voteskip")) {
|
|
this.CHANNEL.voteskip.count = 0;
|
|
this.CHANNEL.voteskip.need = 0;
|
|
}
|
|
|
|
if (this.rank < 2) {
|
|
//get our options first just to be able to log that we're disabling it
|
|
let skipopts = this.getOpt("flatSkiprate", {
|
|
managing: false,
|
|
target: -1,
|
|
original_rate: -1
|
|
});
|
|
if (skipopts.managing) {
|
|
this.logger.warn("Disabling automatic flat skiprate. Rank is too low.");
|
|
}
|
|
this.setOpt("flatSkiprate", {
|
|
managing: false,
|
|
target: -1,
|
|
original_rate: -1
|
|
})
|
|
}
|
|
|
|
this.setProgTitle();
|
|
}
|
|
|
|
/**
|
|
* Stops the bot and prepares the process to end.
|
|
*
|
|
* @param {string} reason Reason for killing the bot. Mostly for logging purposes.
|
|
* @param {number} timeout Amount of time in milliseconds to wait until setting the exit code. Minimum is 1000.
|
|
* @param {number=} exitCode Defaults to 0. Sets the process's exit code to something else if needed. An exitcode of 3 is considered safe but prevents restarting.
|
|
*/
|
|
Bot.prototype.kill = async function(reason, timeout, exitCode) {
|
|
if (this.killed) return;
|
|
this.killed = true;
|
|
this.stopAllTimers();
|
|
this.handlingChatCommands = false;
|
|
this.setProgTitle();
|
|
if (this.DiscordBot) {
|
|
this.DiscordBot.client.destroy();
|
|
}
|
|
if (!timeout || timeout < 1000) timeout = 1000;
|
|
if (!reason) reason = "none given";
|
|
var sectext = timeout === 1000 ? " second" : " seconds";
|
|
this.logger.warn(strings.format(this, "EXIT", [(timeout/1000) + sectext, reason]));
|
|
|
|
await this.updateUserRoomTimeAll();
|
|
await this.db.endPool();
|
|
|
|
this.logger.debug("Reached end of await block in bot.kill");
|
|
|
|
if (this.socket && this.socket.connected)
|
|
this.socket.disconnect();
|
|
this.writeSettings();
|
|
this.rl.close();
|
|
|
|
if (exitCode !== null && typeof exitCode !== "number") exitCode = 1;
|
|
else if (typeof exitCode === "number") exitCode = Math.trunc(exitCode);
|
|
|
|
setTimeout(function() {
|
|
process.stdin.destroy();
|
|
process.exitCode = exitCode === null ? 0 : exitCode;
|
|
}, timeout);
|
|
|
|
}
|
|
|
|
/**
|
|
* Searches the banlist and finds usernames associated with subnets that match the given IP's subnet.
|
|
*
|
|
* @param {string} ip A user's IP
|
|
* @param {boolean=} mergeDupes If true, will shorten the output by including the amount of times each username occurs and excluding duplicate names from the output.
|
|
* @return {string[]} Returns array of usernames that were found. If mergeDupes is true, usernames that occurred twice or more will appear like: username (x) where x is the occurrence count
|
|
*/
|
|
Bot.prototype.matchSubnet = function(ip, mergeDupes) {
|
|
ip = ip.match(/.*\./);
|
|
let matches = [],
|
|
i = 0,
|
|
bl = this.CHANNEL.banlist,
|
|
counts = {};
|
|
for (;i < bl.length;i++){
|
|
if (~bl[i]["ip"].indexOf(ip)) {
|
|
if (mergeDupes) {
|
|
if (!counts.hasOwnProperty(bl[i].name)) {
|
|
matches.push(bl[i].name);
|
|
counts[bl[i].name] = 1;
|
|
}
|
|
else counts[bl[i].name]++;
|
|
} else {
|
|
matches.push(bl[i].name);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mergeDupes) {
|
|
i = 0;
|
|
for(;i < matches.length;i++) {
|
|
let count = counts[matches[i]];
|
|
if (count && count > 1) {
|
|
matches[i] += " (" + count + ")";
|
|
}
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
/**
|
|
* Checks if the given media object is blacklisted.
|
|
*
|
|
* @param {Object} media Media object.
|
|
* @return {boolean} True if media is blacklisted, false if not found or invalid object.
|
|
*/
|
|
Bot.prototype.mediaIsBlacklisted = function(media) {
|
|
let _media = media.hasOwnProperty("media") ? media.media : media;
|
|
if (_media && _media.id && _media.type) {
|
|
return ~this.settings.mediaBlacklist.indexOf(utils.formatLink(_media.id, _media.type, true));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* DO NOT CALL MANUALLY. Called whenever a mediaUpdate frame is received. Determines, if the bot is leader, if the current video is finished and the next video should play.
|
|
*
|
|
* @param {Object} data mediaUpdate frame data, usually containing current media states such as current time and paused state
|
|
*/
|
|
Bot.prototype.mediaUpdateTick = function(data) {
|
|
if (this.leader) {
|
|
let media = this.CHANNEL.currentMedia;
|
|
if (media && media.seconds > 0) {
|
|
let duration = media.seconds;
|
|
let diff = duration - data.currentTime;
|
|
if (diff < 1 && !this.leadFinishingMedia) {
|
|
this.leadFinishingMedia = true;
|
|
clearTimeout(this.timeouts.playingNext);
|
|
let next = ()=>{if (this.leader) this.socket.emit("playNext");}
|
|
if (diff === 0) next(); //avoid setting a timeout with 0ms delay
|
|
else this.timeouts.playingNext = setTimeout(next.bind(this), 1000);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Minute-long timeout loop that continously runs. Calling this will reset the currently active timer. Checks timebans every minute.
|
|
*
|
|
*/
|
|
Bot.prototype.minuteTick = function() {
|
|
clearTimeout(this.timeouts.minuteTick);
|
|
if (this.KILLED) return;
|
|
|
|
this.minutesPassed++;
|
|
|
|
if (this.CHANNEL.banlist.length > 0) {
|
|
this.checkTimeBans();
|
|
}
|
|
|
|
if (this.minutesPassed >= 30) {
|
|
this.minutesPassed = 0;
|
|
let bumpCd = this.cfg.media.bumpCooldown,
|
|
bumpUsers = this.bumpStats.users;
|
|
i = 0;
|
|
|
|
//Clean up bump cooldowns
|
|
for (var i in bumpUsers) {
|
|
if (Date.now() - bumpUsers[i] >= bumpCd) {
|
|
delete bumpUsers[i];
|
|
}
|
|
}
|
|
this.updateUserRoomTimeAll();
|
|
}
|
|
|
|
this.timeouts.minuteTick = setTimeout(()=>{
|
|
this.minuteTick();
|
|
}, 1*60*1000);
|
|
}
|
|
|
|
/**
|
|
* Mutes a given user.
|
|
*
|
|
* @param {!string} username Username to mute
|
|
* @param {?boolean=} shadow If true, shadowmutes instead
|
|
* @return {boolean} True if all conditions to mute succeed, otherwise false
|
|
*/
|
|
Bot.prototype.muteUser = function(username, shadow) {
|
|
if (this.checkChannelPermission("mute") && username && username.toLowerCase() !== this.username.toLowerCase()) {
|
|
let user = this.getUser(username);
|
|
if (user && ((!shadow && !user.meta.muted) || (shadow && !user.meta.smuted)) && this.rank > user.rank) {
|
|
let cmd = "/mute ";
|
|
if (shadow) cmd = "/smute ";
|
|
this.sendChatMsg(cmd + user.name, false, true);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Tries to move one media item after another using UIDs.
|
|
* @see {@link Bot#putMediaAtTop}
|
|
* @see {@link Bot#putMediaAtBottom}
|
|
* @param {number} fromUID UID of the media item to move.
|
|
* @param {number|string} afterUID UID of the item to move the media after. Can also be "prepend" or "append" to move the media to the very top/bottom of the playlist, respectively.
|
|
* @return {boolean} Returns false if fromUID is not given or both UIDs are the same, otherwise true.
|
|
*/
|
|
Bot.prototype.moveMedia = function(fromUID, afterUID) {
|
|
if (undefined == fromUID || fromUID === afterUID) {
|
|
return false;
|
|
}
|
|
|
|
let fn = (()=>{
|
|
this.socket.emit("moveMedia", {
|
|
from: fromUID,
|
|
after: afterUID
|
|
});
|
|
});
|
|
this.actionQueue.enqueue([this, fn, []]);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Logs the currently playing video and time if in progress, and sends a Discord embed if enabled.
|
|
*
|
|
*/
|
|
Bot.prototype.notifyVideoState = function() {
|
|
let _media = this.CHANNEL.currentMedia;
|
|
if (_media) {
|
|
let meta = this.getMedia(this.CHANNEL.currentUID);
|
|
if (meta) {
|
|
if (meta.media.id === _media.id && meta.media.type === _media.type) {
|
|
let link = utils.formatLink(_media.id, _media.type, false);
|
|
let username = meta.queueby === "" ? "(anon)" : meta.queueby;
|
|
let _args = [
|
|
utils.colorUsername(this, username),
|
|
utils.colorMediaTitle(this, _media.type, _media.title),
|
|
C.blackBright("[" + link + "]"),
|
|
C.blackBright("[t: " + _media.duration + "]")
|
|
];
|
|
let string = "NOW_PLAYING";
|
|
if (_media.currentTime > 5) {
|
|
_args.push(C.blackBright("[ct: " + utils.secsToTime(Math.floor(_media.currentTime)) + "]"));
|
|
string = "CURRENTLY_PLAYING";
|
|
}
|
|
this.logger.media(strings.format(this, string, _args));
|
|
if (this.DiscordBot && this.cfg.discord.sendNowPlayingMessages && this.cfg.discord.nowPlayingChannelID.trim() !== "") {
|
|
var color = "#00AD53";
|
|
if (this.DiscordBot.lastNowPlayingWasGreen)
|
|
color = "#A4479A";
|
|
this.DiscordBot.lastNowPlayingWasGreen = !this.DiscordBot.lastNowPlayingWasGreen;
|
|
this.DiscordBot.getChannel(this.cfg.discord.nowPlayingChannelID).send(
|
|
this.DiscordBot.createEmbed()
|
|
.setColor(color)
|
|
.setAuthor(':: now playing ::', this.cfg.discord.iconUrl, 'https://'+this.cfg.connection.hostname+'/r/' + this.CHANNEL.room)
|
|
.setTitle(ent.decode(_media.title))
|
|
.setDescription(link)
|
|
.addField("Added by", username, true)
|
|
.addField("Media Duration", _media.duration, true)
|
|
.setTimestamp()
|
|
);
|
|
}
|
|
} else {
|
|
this.logger.error("UID mismatch!");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Opens a poll with the given poll data object.
|
|
*
|
|
* @param {Object} pollData An object with poll data.
|
|
*/
|
|
Bot.prototype.openPoll = function(pollData) {
|
|
if (this.checkChannelPermission("pollctl") && pollData.hasOwnProperty("title") && pollData.hasOwnProperty("opts")) {
|
|
let fn = ()=>{this.socket.emit("newPoll", pollData)};
|
|
this.actionQueue.enqueue([this, fn, []]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleans the playlist of any videos added by the given user.
|
|
* @see {@link Bot#purgeUsers}
|
|
* @param {string} username Username to purge.
|
|
* @param {boolean=} touch If true, will check to see if the user has at least one video before trying to purge.
|
|
* @return {boolean} True if bot can delete videos and if touch conditions pass, otherwise false.
|
|
*/
|
|
Bot.prototype.purgeUser = function(username, touch) {
|
|
if (this.checkChannelPermission("playlistdelete") && username.trim() !== "") {
|
|
if (!touch || (touch && this.touchUserVideos(username))) {
|
|
this.sendChatMsg("/clean " + username, false, true);
|
|
this.logger.mod("Attempted to purge " + username + ".");
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Cleans the playlist of any videos added by the given users.
|
|
* @see {@link Bot#purgeUser}
|
|
* @param {string[]} users Users to purge.
|
|
* @param {boolean=} touch If true, will check to see if the user has at least one video before trying to purge.
|
|
*/
|
|
Bot.prototype.purgeUsers = function(users, touch) {
|
|
if (this.checkChannelPermission("playlistdelete")) {
|
|
let i = 0;
|
|
for (;i < users.length; i++) {
|
|
this.purgeUser(users[i], touch);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves a given media item to the very bottom of the playlist
|
|
* @see {@link Bot#moveMedia}
|
|
* @param {number} uid UID of media to move
|
|
*/
|
|
Bot.prototype.putMediaAtBottom = function(uid) {
|
|
this.moveMedia(uid, "append");
|
|
}
|
|
|
|
/**
|
|
* Moves a given media item to the very top of the playlist
|
|
* @see {@link Bot#moveMedia}
|
|
* @param {number} uid UID of media to move
|
|
*/
|
|
Bot.prototype.putMediaAtTop = function(uid) {
|
|
this.moveMedia(uid, "prepend");
|
|
}
|
|
|
|
/**
|
|
* Adds a piece of media to the playlist
|
|
* @param {string} pos Position to add the video to. Can be either "next" or "end"
|
|
* @param {string} src Link to the media. If it's custom, put the link into content and put "customembed" here instead.
|
|
* @param {boolean=} isTemp Whether the video will be temporary in the queue or not. Will be false if nothing is given.
|
|
* @param {string=} content Content, usually the media link, for custom embeds. Not needed otherwise.
|
|
* @param {string=} title Title for custom embeds.
|
|
*/
|
|
Bot.prototype.queueMedia = function(pos, src, isTemp, content, title) {
|
|
|
|
pos = pos.toLowerCase();
|
|
isTemp = !!isTemp;
|
|
|
|
if (!this.checkChannelPermission("playlistadd"))
|
|
return this.logger.error("Could not add ["+src+"]: insufficient permission to add videos.");
|
|
|
|
if (pos == "next" && !this.checkChannelPermission("playlistnext"))
|
|
return this.logger.error("Could not add ["+src+"], insufficient permission to add it as the next video.");
|
|
|
|
if (src == "customembed") {
|
|
if (!title) title = false;
|
|
|
|
this.actionQueue.enqueue([this, (()=>{
|
|
this.socket.emit("queue", {
|
|
id: content,
|
|
title: title,
|
|
pos: pos,
|
|
type: "cu",
|
|
temp: isTemp
|
|
})
|
|
}), []]);
|
|
|
|
} else {
|
|
let data = utils.parseMediaLink(src);
|
|
|
|
if (!data) return this.logger.error("Could not parse [" + src + "]");
|
|
|
|
if (data.type === "fi") {
|
|
if (data.id.match(/^http:/)) {
|
|
return this.logger.error("Could not queue ["+src+"]. Raw files must begin with 'https'.");
|
|
}
|
|
|
|
if (data.id.match(/kissanime|kimcartoon|kisscartoon|mega\.nz/i)) {
|
|
return this.logger.error("Could not queue ["+src+"]. Unsupported source.");
|
|
}
|
|
|
|
}
|
|
|
|
if (!data.id || !data.type) {
|
|
return this.logger.error("Could not parse [" + src + "]");
|
|
}
|
|
|
|
this.actionQueue.enqueue([this, (()=>{
|
|
this.socket.emit("queue", {
|
|
id: data.id,
|
|
type: data.type,
|
|
pos: pos,
|
|
duration: undefined,
|
|
title: title || false,
|
|
temp: isTemp
|
|
})
|
|
}), []]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits a request to receive the channel log. MUST be Rank >=3 or it will kick you
|
|
*/
|
|
Bot.prototype.readChanLog = function() {
|
|
if (this.rank >= 3) { //ALWAYS PERFORM THIS CHECK! this rank is also hardcoded on CyTube
|
|
var fn = (()=>{this.socket.emit("readChanLog");});
|
|
this.largeDataReqQueue.enqueue([this, fn, []]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads settings.json, looks for missing properties, and stores it
|
|
*
|
|
* @param {Function} callback Callback function to feed the read data into
|
|
* @return {*} Returns data returned by the callback
|
|
*/
|
|
Bot.prototype.readSettings = function(callback) {
|
|
var data;
|
|
try {
|
|
data = fs.readFileSync(path.join(this.ROOTPATH, this.settingsFile), "utf8");
|
|
} catch (err) {
|
|
if (err.code === "ENOENT") {
|
|
this.writeSettings();
|
|
} else {
|
|
this.logger.error(strings.format(this, "FILE_READ_ERROR", [this.settingsFile, err.stack]));
|
|
this.kill("error in settings file", 5000, 1);
|
|
return callback(true);
|
|
}
|
|
}
|
|
var oldSettings = this.settings;
|
|
var newSettings = !data ? {} : JSON.parse(data);
|
|
for (var i in oldSettings) {
|
|
if (!newSettings.hasOwnProperty(i)) {
|
|
this.logger.debug(strings.format(this, "SETTINGS_PROPERTY_MISSING", [i]));
|
|
newSettings[i] = oldSettings[i];
|
|
}
|
|
}
|
|
this.settings = newSettings;
|
|
this.logger.verbose(strings.format(this, "SETTINGS_READ"));
|
|
return callback(false);
|
|
}
|
|
|
|
/**
|
|
* Emits a request to grab the channel ranks. Need to be rank >= 3.
|
|
*/
|
|
Bot.prototype.requestChannelRanks = function() {
|
|
if (this.rank >= 3) {
|
|
var fn = (()=>{this.socket.emit("requestChannelRanks");});
|
|
this.largeDataReqQueue.enqueue([this, fn, []]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears the current cache of stuff retrieved from the YouTube API, such as comments.
|
|
*/
|
|
Bot.prototype.resetCurrentVidData = function() {
|
|
this.currentVideoData = {
|
|
id: null,
|
|
comments: null,
|
|
commentsDisabled: false,
|
|
views: -1,
|
|
likes: 0,
|
|
dislikes: 0,
|
|
ratingsDisabled: false,
|
|
noStats: false
|
|
};
|
|
this.gettingComments = false;
|
|
this.gettingVideoMeta = false;
|
|
}
|
|
|
|
/**
|
|
* Sends a chat message.
|
|
*
|
|
* @param {!string} message Message to send
|
|
* @param {?boolean=} bypassQueue If true, will not be put into the action queue and will be sent immediately instead
|
|
* @param {?boolean=} isCommand If false, will pad the message with a space if the first char is /. If handling commands such as /afk or /ban for example, this MUST be true
|
|
* @param {?boolean=} ignoreMute If true, will send message regardless of bot's mute state. Defaults to false.
|
|
*/
|
|
Bot.prototype.sendChatMsg = function(message, bypassQueue, isCommand, ignoreMute) {
|
|
if (!isCommand && message.indexOf("\/") === 0) message = " " + message;
|
|
let canChat = ()=>{
|
|
return this.checkChannelPermission("chat") && ((this.settings.muted && (message.indexOf("\/") === 0 || ignoreMute)) || !this.settings.muted);
|
|
}
|
|
if (!canChat()) return;
|
|
var fn = ((msg)=>{
|
|
if (!canChat()) return;
|
|
var data = {msg: C.strip(message), meta: {}}
|
|
if (!data.msg) {
|
|
this.logger.warn(strings.format(this, "CHAT_BLANK_MESSAGE"));
|
|
return;
|
|
}
|
|
if (this.cfg.chat.useFlair) data.meta["modflair"] = this.rank;
|
|
this.socket.emit("chatMsg", data);
|
|
});
|
|
if (bypassQueue) fn(message);
|
|
else this.actionQueue.enqueue([this, fn, [message]]);
|
|
}
|
|
|
|
/**
|
|
* Sends a PM to the specified user.
|
|
*
|
|
* @param {!string} username Username to send the message to. Must be online
|
|
* @param {!string} message Message to send to the user
|
|
*/
|
|
Bot.prototype.sendPM = function(username, message) {
|
|
if (this.settings.muted) return;
|
|
var fn = ((username, message)=>{
|
|
this.socket.emit("pm", {
|
|
"to":C.strip(username),
|
|
"msg":C.strip(message)
|
|
});
|
|
});
|
|
this.actionQueue.enqueue([this, fn, [username, message]]);
|
|
}
|
|
|
|
/**
|
|
* Advances the current video's time 5 seconds and emits the player info. Automatically used when the bot is leader
|
|
*
|
|
* @param {?number=} absoluteTime If specified (seconds), will skip to this time instead of adding 5 seconds
|
|
*/
|
|
Bot.prototype.sendVideoUpdate = function(absoluteTime) {
|
|
if (!this.leader || !this.CHANNEL.currentMedia || this.CHANNEL.currentUID === null) return;
|
|
let media = this.CHANNEL.currentMedia,
|
|
currTime = media.currentTime,
|
|
duration = media.seconds,
|
|
newTime = currTime;
|
|
if (media && media.seconds <= 0) return;
|
|
if (absoluteTime !== null && absoluteTime >= 0 && absoluteTime <= duration) {
|
|
newTime = absoluteTime;
|
|
} else if (!media.paused) {
|
|
newTime += 5;
|
|
if (duration > 0 && newTime > duration) newTime = duration;
|
|
}
|
|
this.socket.emit("mediaUpdate", {
|
|
id: media.id,
|
|
currentTime: newTime,
|
|
paused: media.paused,
|
|
type: media.type
|
|
});
|
|
}
|
|
|
|
Bot.prototype.setChannelOpts = function(opts) {
|
|
if (this.rank < 2) return false;
|
|
let changed = false;
|
|
let temp_opts = {...this.CHANNEL.opts};
|
|
for (var i in opts) {
|
|
if (!temp_opts.hasOwnProperty(i)) {
|
|
this.logger.error("setChannelOpts: Tried to change channel option " + i + " but it doesn't already exist!");
|
|
} else if (i !== "password" && typeof temp_opts[i] !== typeof opts[i]) {
|
|
this.logger.error("setChannelOpts: Type mismatch for: " + i);
|
|
} else {
|
|
changed = true;
|
|
temp_opts[i] = opts[i];
|
|
}
|
|
}
|
|
if (changed) {
|
|
if (!temp_opts.password) temp_opts.password = "";
|
|
let fn = ()=>{
|
|
this.socket.emit("setOptions", temp_opts);
|
|
}
|
|
this.actionQueue.enqueue([this, fn, []]);
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
Bot.prototype.setFlatSkiprate = function() {
|
|
let skipopts = this.getOpt("flatSkiprate", {
|
|
managing: false,
|
|
target: -1,
|
|
original_rate: -1
|
|
}
|
|
),
|
|
roomopts = this.CHANNEL.opts;
|
|
if (skipopts.managing) {
|
|
if (this.rank < 2) return false;
|
|
if (skipopts.target <= 0 || skipopts.original_rate < 0) {
|
|
skipopts.managing = false;
|
|
this.logger.warn("Disabling automatic flat skiprate. Target amount is set to 0 or below, or original rate is below 0");
|
|
this.setOpt("flatSkiprate", skipopts);
|
|
return false;
|
|
} else {
|
|
let skippers = this.getUsersWithSkipPerms();
|
|
if (skippers <= 0) return false;
|
|
if (Math.ceil(skippers * roomopts.voteskip_ratio) !== skipopts.target) {
|
|
return this.setChannelOpts({
|
|
voteskip_ratio: (skipopts.target / skippers)
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Adds or removes media to/from the media blacklist.
|
|
*
|
|
* @param {!Object} media Media object, containing at the very least "id" and "type" keys
|
|
* @param {boolean=} state If true, adds the media to the blacklist; otherwise, removes it.
|
|
* @return {boolean} True if the media is not already in the blacklist when adding, or it is already in the list when removing; otherwise false.
|
|
*/
|
|
Bot.prototype.setMediaBlacklistState = function(media, state) {
|
|
let _media = media.hasOwnProperty("media") ? media.media : media;
|
|
if (_media && _media.id && _media.type) {
|
|
let link = utils.formatLink(_media.id, _media.type, true);
|
|
if (link === "") return false;
|
|
|
|
let blacklisted = this.mediaIsBlacklisted(_media);
|
|
if (!blacklisted && state) {
|
|
this.settings.mediaBlacklist.push(link);
|
|
} else if (blacklisted && !state) {
|
|
utils.unsortedRemove(this.settings.mediaBlacklist, ~blacklisted);
|
|
} else {
|
|
return false;
|
|
}
|
|
this.writeSettings();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Sets an option in bot.settings with the given data. Does not check for specific array/object type differences
|
|
*
|
|
* @param {!string} opt description
|
|
* @param {!*} data description
|
|
* @return {boolean} False if datatype mismatch, otherwise
|
|
*/
|
|
Bot.prototype.setOpt = function(opt, data) {
|
|
if (this.settings.hasOwnProperty(opt)) {
|
|
var dataType = typeof this.settings[opt];
|
|
var newDataType = typeof data;
|
|
if (dataType !== newDataType) {
|
|
let err = "setOpt: Tried changing " + opt + " from " + dataType + " to " + newDataType + "!";
|
|
this.logger.error(err);
|
|
throw new TypeError(err);
|
|
return false;
|
|
}
|
|
}
|
|
this.settings[opt] = data;
|
|
this.writeSettings();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Updates the terminal's title if interface.fancyTitle is true.
|
|
*
|
|
* @param {?string=} override If specified, will set the whole title to this string instead
|
|
*/
|
|
Bot.prototype.setProgTitle = function(override) {
|
|
if (!this.cfg.interface.fancyTitle) return;
|
|
var title = "";
|
|
if (override)
|
|
title = override;
|
|
else if (this.KICKED)
|
|
title = "(!KICKED!) | ";
|
|
else if (this.killed)
|
|
title = "(!KILLED!) | ";
|
|
else {
|
|
if (this.username === "" || !this.logged_in)
|
|
title = "not logged in | ";
|
|
else
|
|
title = this.username + " | ";
|
|
title += this.CHANNEL.room + " | ";
|
|
if (this.cfg.rankNames.hasOwnProperty(this.rank))
|
|
title += this.cfg.rankNames[this.rank] + " | ";
|
|
else if (this.rank === 0)
|
|
title += this.cfg.rankNames["unregistered"] + " | ";
|
|
if (this.CHANNEL.usercount > 0)
|
|
title += this.CHANNEL.usercount + " total users (" + (this.CHANNEL.usercount-this.CHANNEL.users.length) + " anon, "+ (this.CHANNEL.users.length-this.getAFKCount()) +" active) | ";
|
|
if (this.checkChannelPermission("seeplaylist", true))
|
|
title += this.CHANNEL.playlistMeta.count + " videos [" + this.CHANNEL.playlistMeta.time + "] "+ (this.CHANNEL.playlistIsLocked ? "L" : "UL") +" | ";
|
|
if (this.checkChannelPermission("viewvoteskip", true))
|
|
title += this.CHANNEL.voteskip.count + "/" + this.CHANNEL.voteskip.need + " skips | ";
|
|
if (this.CHANNEL.poll.active)
|
|
title += "Poll Active | ";
|
|
}
|
|
|
|
title += this.botName + " " + this.version;
|
|
|
|
if (PLATFORM === "win32")
|
|
process.title = title;
|
|
else
|
|
process.stdout.write("\x1B]0;" + title + "\x07");
|
|
}
|
|
|
|
/**
|
|
* Initializes the terminal's readline interface if not already done.
|
|
*/
|
|
Bot.prototype.setupReadline = function() {
|
|
if (this.readlineInitialized) return;
|
|
var rl = this.rl;
|
|
if (this.cfg.interface.allowQuickCLIChat) {
|
|
rl.setPrompt(C.green("chat") + C.greenBright(" > "), 2);
|
|
} else {
|
|
rl.setPrompt(C.red("cmd") + C.redBright(" $ "), 2);
|
|
}
|
|
rl.prompt();
|
|
rl.on('line', input => {
|
|
if (!this.readlineInitialized) {
|
|
this.logger.error(strings.format(this, "CLI_NOT_ACCEPTING_INPUT"));
|
|
} else if (input) {
|
|
if (input.trim() !== "") {
|
|
this.getLogStream().write(utils.getTimestamp(this.cfg.interface.useTwentyFourHourTime) + " " + strings.format(this, "CLI_INPUT", [input]) + "\r\n");
|
|
}
|
|
if (input.substr(0,1) === "/") {
|
|
clicmd.exec(this, input);
|
|
} else if (this.cfg.interface.allowQuickCLIChat) {
|
|
this.sendChatMsg(input, false, true);
|
|
}
|
|
}
|
|
rl.prompt();
|
|
});
|
|
rl.on("SIGINT", () => {
|
|
this.logger.error(strings.format(this, "NO_ABORT"));
|
|
return;
|
|
});
|
|
|
|
this.readlineInitialized = true;
|
|
this.logger.verbose(strings.format(this, "CLI_ACCEPTING_INPUT"));
|
|
}
|
|
|
|
/**
|
|
* Adds or removes a user to/from the user blacklist.
|
|
*
|
|
* @param {!string} name Username
|
|
* @param {?boolean=} state If true, adds the user to the blacklist; otherwise removes the user.
|
|
* @return {boolean} True if the user is blacklisted when removing, or if the user is not blacklisted when adding; false otherwise.
|
|
*/
|
|
Bot.prototype.setUserBlacklistState = function(name, state) {
|
|
name = name.toLowerCase();
|
|
let blacklistIndex = this.settings.userBlacklist.indexOf(name);
|
|
if (!~blacklistIndex && state) {
|
|
this.settings.userBlacklist.push(name);
|
|
} else if (~blacklistIndex && !state) {
|
|
utils.unsortedRemove(this.settings.userBlacklist, blacklistIndex);
|
|
} else {
|
|
return false;
|
|
}
|
|
this.writeSettings();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Sets an option within a user's persistent data (settings.json)
|
|
*
|
|
* @param {!string} username Username
|
|
* @param {!string} opt Key within the user's data to modify
|
|
* @param {!*} data Data to store
|
|
* @return {Object} Returns the user's persistent data
|
|
*/
|
|
Bot.prototype.setUserDataOpt = function(username, opt, data) {
|
|
username = username.toLowerCase();
|
|
if (!this.settings.userData.hasOwnProperty(username)) this.settings.userData[username] = {};
|
|
this.settings.userData[username][opt] = data;
|
|
this.writeSettings();
|
|
return this.settings.userData[username];
|
|
}
|
|
|
|
/**
|
|
* Stops the lead timer and starts it again. Calls sendVideoUpdate every 5 seconds. Requires bot to be leader.
|
|
* @see {@link Bot#sendVideoUpdate}
|
|
*/
|
|
Bot.prototype.startLeadTimer = function() {
|
|
this.stopLeadTimer();
|
|
this.logger.debug("startLeadTimer called");
|
|
if (this.leader) {
|
|
this.logger.debug("bot is leader, starting timer");
|
|
this.leadTimer = setInterval(this.sendVideoUpdate.bind(this), 5000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops all timers and empties the duel list and all queues. Requires the bot to be killed unless force is true.
|
|
*
|
|
* @param {?boolean=} force If force, will disregard the bot's killed state
|
|
*/
|
|
Bot.prototype.stopAllTimers = function(force) {
|
|
if (!force && !this.killed) return this.logger.error(strings.format(bot, "STOPALLTIMERS_FAIL"));
|
|
this.stopLeadTimer();
|
|
clearTimeout(this.timeouts.minuteTick);
|
|
this.actionQueue.clearQueue();
|
|
this.broadcastPMQueue.clearQueue();
|
|
this.largeDataReqQueue.clearQueue();
|
|
let i = 0;
|
|
for (;i < this.duels.length; i++) {
|
|
clearTimeout(this.duels[i][2]);
|
|
}
|
|
for (var j in this.timeouts) {
|
|
clearTimeout(this.timeouts[j]);
|
|
}
|
|
this.duels = [];
|
|
}
|
|
|
|
/**
|
|
* Clears the lead timer interval and sets it to null.
|
|
*/
|
|
Bot.prototype.stopLeadTimer = function() {
|
|
this.logger.debug("stopLeadTimer called");
|
|
if (this.leadTimer) {
|
|
clearInterval(this.leadTimer);
|
|
this.leadTimer = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Does not emit a ban frame, but adds a user to the bot's timeBans list with the given time. If the user is already timebanned, this will overwrite their existing time.
|
|
*
|
|
* @param {!string} name Username
|
|
* @param {!number} time Time in seconds
|
|
*/
|
|
Bot.prototype.timeBan = function(name, time) {
|
|
if (time < 60 || !time) time = 60;
|
|
let delta = Date.now() + (time*1000),
|
|
i = 0,
|
|
bans = this.settings.timeBans[this.CHANNEL.room],
|
|
found = false;
|
|
for (;i < bans.length && !found; i++) {
|
|
if (bans[i].name === name) {
|
|
bans[i].unbanTime = delta;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
bans.push({name: name, unbanTime: delta});
|
|
this.writeSettings();
|
|
}
|
|
|
|
/**
|
|
* Checks if the given user has at least one video in the playlist.
|
|
*
|
|
* @param {!string} username Username
|
|
* @return {boolean} True if the user has a video in the playlist; otherwise false
|
|
*/
|
|
Bot.prototype.touchUserVideos = function(username) {
|
|
username = username.toLowerCase();
|
|
let pl = this.CHANNEL.playlist,
|
|
i = 0;
|
|
for (;i < pl.length;i++) {
|
|
if (pl[i].queueby.toLowerCase() === username) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if a given username is associated with a known bot (list is found in the configuration file)
|
|
*
|
|
* @param {!object|string} user User object or username
|
|
* @return {boolean} True if username found within the bot list or same as this bot; false otherwise
|
|
*/
|
|
Bot.prototype.userIsBot = function(user) {
|
|
let name = null;
|
|
if (utils.isObject(user) && user.name) name = user.name; //lol
|
|
else name = user;
|
|
name = name.toLowerCase();
|
|
if (name === this.username.toLowerCase()) return true;
|
|
let i = 0;
|
|
for (;i < this.cfg.misc.bots.length; i++) {
|
|
if (this.cfg.misc.bots[i].toLowerCase() === name) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if a user is under a timeban.
|
|
*
|
|
* @param {string} name Username
|
|
* @return {boolean} True if the user is found in the timeBans list; otherwise false
|
|
*/
|
|
Bot.prototype.userIsTimebanned = function(name) {
|
|
name = name.toLowerCase();
|
|
let tb = this.settings.timeBans[this.CHANNEL.room],
|
|
i = 0;
|
|
for (;i < tb.length;i++) {
|
|
if (tb[i].name === name) {
|
|
return tb[i];
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Unbans a given user if they are found in the banlist (requires rank 3+)
|
|
*
|
|
* @param {string} name Username to unban
|
|
* @return {boolean} True if bot has permissions and succeeds in sending an unban request; otherwise false
|
|
*/
|
|
Bot.prototype.unbanUser = function(name) {
|
|
name = name.toLowerCase();
|
|
let bl = this.CHANNEL.banlist,
|
|
tb = this.settings.timeBans[this.CHANNEL.room];
|
|
if (bl.length <= 0 || !this.checkChannelPermission("ban")) return false;
|
|
let i = bl.length-1, foundBan = false;
|
|
for (;i >= 0;i--) {
|
|
if (bl[i].name.toLowerCase() === name) {
|
|
let name = bl[i].name,
|
|
id = bl[i].id;
|
|
let fn = ()=>{this.socket.emit("unban", {name: name, id: id})};
|
|
this.actionQueue.enqueue([this, fn, []]);
|
|
foundBan = true;
|
|
}
|
|
}
|
|
return foundBan;
|
|
}
|
|
|
|
/**
|
|
* Unmutes the specified user.
|
|
*
|
|
* @param {string} username Username to unmute
|
|
* @return {boolean} True if muting conditions pass, false otherwise
|
|
*/
|
|
Bot.prototype.unmuteUser = function(username) {
|
|
if (this.checkChannelPermission("mute") && username && username.toLowerCase() !== this.username.toLowerCase()) {
|
|
let user = this.getUser(username);
|
|
if (user && (user.meta.muted || user.meta.smuted) && this.rank > user.rank) {
|
|
this.sendChatMsg("/unmute " + user.name, false, true);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Updates a user's roomtime, AFK time, and last seen in the DB
|
|
*
|
|
* @param {!string} name Username
|
|
* @param {?boolean=} doAfk If true, will also update AFK time
|
|
* @param {?function=} cb If specified, will be called when the DB action is complete
|
|
* @return {Promise} Promise, resolved with false if no user found or DB is not active; otherwise true
|
|
*/
|
|
Bot.prototype.updateUserRoomTime = function(name, doAfk, cb) {
|
|
let user = this.getUser(name);
|
|
if (!(this.cfg.db.use && this.cfg.db.useTables.users)) return Promise.resolve(false);
|
|
if (!user) {if (cb) cb(); return Promise.resolve(false)};
|
|
return new Promise((res)=>{
|
|
if (!this.db || this.CHANNEL.users.length <= 0) return res(false);
|
|
var now = Date.now();
|
|
var time = now - user.lastRoomtimeCheck;
|
|
user.lastRoomtimeCheck = now;
|
|
var afkTime = 0;
|
|
if (doAfk) {
|
|
if (user.timeWentAFK > 0) afkTime = now - user.timeWentAFK;
|
|
if (user.meta.afk) user.timeWentAFK = now;
|
|
else user.timeWentAFK = -1;
|
|
if (afkTime < 0) afkTime = 0;
|
|
}
|
|
if (time < 0) time = 0;
|
|
this.db.run("updateUserRoomTime", [time/1000, afkTime/1000, user.name], ()=> {
|
|
if (cb) cb();
|
|
res(true);
|
|
})
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Updates room and AFK times of all users in the room in the database.
|
|
*
|
|
* @return {Promise} Returns a Promise, resolved with true once the DB action is finished
|
|
*/
|
|
Bot.prototype.updateUserRoomTimeAll = function() {
|
|
return new Promise((res)=>{
|
|
if (!(this.cfg.db.use && this.cfg.db.useTables.users)) {
|
|
res(false);
|
|
} else {
|
|
//this.logger.debug("Running batch updateUserRoomTime query...");
|
|
var users = this.CHANNEL.users;
|
|
var now = Date.now(),
|
|
times = [];
|
|
let i = 0;
|
|
for (; i < users.length; i++) {
|
|
var data = new Array(3);
|
|
var userObj = users[i];
|
|
data[0] = userObj.name;
|
|
var time = (now - userObj.lastRoomtimeCheck) / 1000;
|
|
data[1] = time < 0 ? 0 : time;
|
|
var afkTime = 0;
|
|
if (userObj.timeWentAFK > 0)
|
|
afkTime = (now - userObj.timeWentAFK) / 1000;
|
|
data[2] = afkTime < 0 ? 0 : afkTime;
|
|
userObj.lastRoomtimeCheck = now;
|
|
if (userObj.meta.afk)
|
|
userObj.timeWentAFK = now;
|
|
else
|
|
userObj.timeWentAFK = -1;
|
|
if (data[1] > 0 || data[2] > 0)
|
|
times.push(data);
|
|
}
|
|
this.db.run("updateUserRoomTimeAll", times, ()=>{
|
|
this.logger.debug("Done! (batch active)");
|
|
res(true);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Checks if a user has a channel permission.
|
|
*
|
|
* @param {!string} perm Channel permission to check
|
|
* @param {!number} userRank A user's rank
|
|
* @return {boolean} True if user has permission, false otherwise
|
|
*/
|
|
Bot.prototype.userHasChannelPermission = function(perm, userRank) {
|
|
if (this.first.grabbedPermissions) return false;
|
|
var userRank_ = parseInt(userRank);
|
|
if (!this.CHANNEL.playlistIsLocked && /^playlist/.test(perm) && this.CHANNEL.perms.hasOwnProperty("o" + perm)) perm = "o" + perm;
|
|
if (!isNaN(userRank_) && this.CHANNEL.perms.hasOwnProperty(perm)) {
|
|
return userRank_ >= this.CHANNEL.perms[perm];
|
|
} else if (!this.CHANNEL.perms.hasOwnProperty(perm)) {
|
|
this.logger.error(strings.format(this, "PERMISSION_NOT_FOUND", [perm]));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if the given rank is equal to or above USER (default 1), or below it if allowGuestData is true.
|
|
*
|
|
* @param {!number} rank User's rank
|
|
* @return {boolean} True if the DB is allowed to store the user's data
|
|
*/
|
|
Bot.prototype.userRankDBCheck = function(rank) {
|
|
return rank >= this.RANKS.USER || (rank < this.RANKS.USER && this.cfg.db.allowGuestData);
|
|
}
|
|
|
|
/**
|
|
* Checks the given string among valid trigger characters and returns ! if invalid.
|
|
*
|
|
* @param {!string} trigger A string of length 1
|
|
* @return {string} Returns given trigger if valid, otherwise !
|
|
*/
|
|
Bot.prototype.validateTrigger = function(trigger) {
|
|
if (typeof trigger !== "string" || trigger.length !== 1 || !~'!#$%^&*()_+-=`~.,?'.indexOf(trigger)) {
|
|
this.logger.warn(strings.format(this, "TRIGGER_INVALID", ["!"]));
|
|
return '!';
|
|
} else {
|
|
return trigger;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes text to the terminal and to a stream.
|
|
*
|
|
* @param {!string} out Text to be displayed/written
|
|
* @param {?fs.WriteStream} stream WriteStream to write out to
|
|
* @param {?boolean=} gotoBotLog If true, will go to the standard bot.log stream (will cause duplicate lines if used with the standard log stream!)
|
|
*/
|
|
Bot.prototype.write = function(out, stream, gotoBotLog) {
|
|
if (stream.destroyed) return;
|
|
if (stream) {
|
|
stream.write(C.strip(out) + "\r\n");
|
|
if (gotoBotLog)
|
|
this.getLogStream().write(C.strip(out) + "\r\n");
|
|
}
|
|
//clear the entire line, write output text, reset prompt
|
|
process.stdout.write("\r\x1B[K"+out+"\n");
|
|
this.rl._refreshLine();
|
|
}
|
|
|
|
/**
|
|
* Writes bot.settings into settings.json or settings-ROOMNAME.json, depending on config. Uses fs.writeFileSync.
|
|
*/
|
|
Bot.prototype.writeSettings = function() {
|
|
this.logger.verbose(strings.format(this, "SETTINGS_WRITE"));
|
|
var settings = JSON.stringify(this.settings);
|
|
try {
|
|
fs.writeFileSync(path.join(this.ROOTPATH, this.settingsFile), settings, "utf8");
|
|
} catch (err) {
|
|
this.logger.error(strings.format(this, "FILE_WRITE_ERROR", [this.settingsFile, err.stack]));
|
|
}
|
|
};
|
|
|
|
|
|
module.exports = {
|
|
"init": function(config, readline, ROOT) {
|
|
let bot = new Bot(config, readline, ROOT);
|
|
bot.db.init(bot);
|
|
return bot;
|
|
}
|
|
}
|
|
|
|
/* "Default" (? grabbed from a clean room) permissions
|
|
{
|
|
|
|
//OPEN PLAYLIST
|
|
"oplaylistadd":-1, //Add to playlist
|
|
"oplaylistnext":1.5, //Add/move to next
|
|
"oplaylistmove":1.5, //Move playlist items
|
|
"oplaylistdelete":2, //Delete playlist items
|
|
"oplaylistjump":1.5, //Jump to video
|
|
"oplaylistaddlist":1.5, //Queue playlist
|
|
|
|
//GENERAL PLAYLIST
|
|
"seeplaylist":-1, //View the playlist
|
|
"playlistadd":1.5, //Add to playlist
|
|
"playlistnext":1.5, //Add/move to next
|
|
"playlistmove":1.5, //Move playlist items
|
|
"playlistdelete":2, //Delete playlist items
|
|
"playlistjump":1.5, //Jump to video
|
|
"playlistaddlist":1.5, //Queue playlist
|
|
"playlistaddcustom":3, //Embed custom media
|
|
"playlistaddrawfile":2, //Add raw video file
|
|
"playlistaddlive":1.5, //Queue livestream
|
|
"exceedmaxlength":2, //Exceed maximum media length
|
|
"exceedmaxdurationperuser":2 //Exceed maximum total media length
|
|
"addnontemp":2, //Add nontemporary media
|
|
"settemp":2, //Temp/untemp playlist item
|
|
"playlistshuffle":2, //Shuffle playlist
|
|
"playlistclear":2, //Clear playlist
|
|
"exceedmaxitems":2, //Exceed maximum number of videos per user
|
|
"deletefromchannellib":2, //Delete from channel library
|
|
"playlistlock":2, //Lock/unlock playlist
|
|
|
|
//POLLS
|
|
"pollctl":1.5, //Open/Close Poll
|
|
"pollvote":-1, //Vote
|
|
"viewhiddenpoll":1.5, //View hidden poll results
|
|
"voteskip":-1, //Voteskip
|
|
"viewvoteskip":1.5, //View voteskip results
|
|
|
|
//MODERATION
|
|
"mute":1.5, //Mute users
|
|
"kick":1.5, //Kick users
|
|
"ban":2, //Ban users
|
|
"motdedit":3, //Edit MOTD
|
|
"filteredit":3, //Edit chat filters
|
|
"filterimport":3, //Import chat filters
|
|
"emoteedit":3, //Edit chat emotes
|
|
"emoteimport":3, //Import chat emotes
|
|
"leaderctl":2, //Assign/Remove leader
|
|
|
|
//MISC
|
|
"drink":1000000, //Drink calls
|
|
"chat":0, //Chat
|
|
"chatclear":2, //Clear Chat
|
|
}
|
|
*/
|