├── documentation ├── sample.sqlite ├── p3.sample.php ├── slack-webhook.sample.php ├── config.sample.json ├── schema.sql └── ButtheadScript.js ├── env.sample ├── events ├── error.js ├── tcpEnd.js ├── disconnected.js ├── tcpConnect.js ├── tcpMessage.js ├── disabled │ ├── modAddDj.js │ ├── killSession.js │ ├── modMoveDj.js │ ├── modAmbassador.js │ ├── modRemoveDj.js │ ├── modUnban.js │ ├── modSkip.js │ ├── modMute.js │ ├── modBan.js │ ├── modWaitlistBan.js │ └── djListUpdate.js ├── ready.js ├── pmmed.js ├── nosong.js ├── snagged.js ├── deregistered.js ├── speak.js ├── update_votes.js ├── roomChanged.js ├── endsong.js ├── registered.js └── newsong.js ├── functions ├── util.js ├── time.js ├── init.js ├── file.js ├── webhook.js ├── points.js ├── media.js ├── user.js ├── moderation.js └── chat.js ├── extensions └── README.md ├── .npmignore ├── commands ├── hodor.js ├── freespin.js ├── emoji.js ├── down.js ├── source.js ├── bop.js ├── uptime.js ├── license.js ├── time.js ├── owner.js ├── refresh.js ├── csi.js ├── disabled │ ├── deli.js │ ├── add.js │ ├── delay.js │ ├── locales.js │ ├── callmod.js │ ├── mute.js │ ├── songinfo.js │ ├── gift.js │ ├── move.js │ ├── kick.js │ ├── lastplayed.js │ ├── remove.js │ ├── ban.js │ ├── wlban.js │ ├── debug.js │ ├── idle.js │ ├── blocked.js │ ├── lottery.js │ ├── settings.js │ ├── skip.js │ └── fixsong.js ├── upvote.js ├── rules.js ├── catfact.js ├── downvote.js ├── fact.js ├── today.js ├── commands.js ├── help.js ├── roll.js ├── currency.js ├── quake.js ├── reload.js ├── lastseen.js ├── define.js ├── giphy.js ├── event.js ├── stats.js ├── whois.js ├── theme.js ├── magic8ball.js └── english.js ├── .gitignore ├── .eslintrc.js ├── .travis.yml ├── models ├── karma.js ├── useralias.js ├── game.js ├── songresponse.js ├── blacklist.js ├── eventresponse.js ├── play.js ├── roomevent.js ├── song.js ├── index.js └── user.js ├── docker-compose.yml ├── package.json ├── LICENSE ├── README.md ├── bot.js └── globals.js /documentation/sample.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avatarkava/beavisbot/HEAD/documentation/sample.sqlite -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | PROJECT_ROOT=./ 2 | TZ=Etc/UTC 3 | 4 | DB_ROOT_PASSWORD=password 5 | DB_NAME=beavisbot 6 | DB_USERNAME=beavisbot 7 | DB_PASSWORD=password -------------------------------------------------------------------------------- /events/error.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("error", function (error) { 3 | console.log("[EVENT] Error: " + error); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /functions/util.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | trimCommas = function (str) { 3 | return str.replace(/(^\s*,)|(,\s*$)/g, ""); 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /events/tcpEnd.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("tcpEnd", function (socket) { 3 | console.log("[EVENT] tcpEnd: " + socket); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /events/disconnected.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("disconnected", function (error) { 3 | console.log("[EVENT] Error: " + error); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /events/tcpConnect.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("tcpConnect", function (socket) { 3 | console.log("[EVENT] tcpConnect: " + socket); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /extensions/README.md: -------------------------------------------------------------------------------- 1 | Any extensions to the bot can be placed here and will load on startup. Note that they must have a .js extension. 2 | 3 | Follow the formats in /commands, /functions or /events! -------------------------------------------------------------------------------- /events/tcpMessage.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("tcpConnect", function (socket, msg) { 3 | console.log("[EVENT] tcpConnect"); 4 | console.log(socket); 5 | console.log(msg); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /events/disabled/modAddDj.js: -------------------------------------------------------------------------------- 1 | module.exports = function (bot) { 2 | 3 | bot.on(PlugAPI.events.MODERATE_ADD_DJ, function (data) { 4 | console.log('[EVENT] modAddDJ ', JSON.stringify(data, null, 2)); 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /events/ready.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("ready", function () { 3 | console.log("[EVENT] Ready: Connected..."); 4 | bot.roomRegister(config.roomId); 5 | 6 | writeConfigState(); 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /events/disabled/killSession.js: -------------------------------------------------------------------------------- 1 | module.exports = function (bot) { 2 | 3 | bot.on(PlugAPI.events.KILL_SESSION, function (data) { 4 | console.log('[EVENT] killSession ', JSON.stringify(data, null, 2)); 5 | }); 6 | 7 | }; 8 | -------------------------------------------------------------------------------- /events/disabled/modMoveDj.js: -------------------------------------------------------------------------------- 1 | module.exports = function (bot) { 2 | 3 | bot.on(PlugAPI.events.MODERATE_MOVE_DJ, function (data) { 4 | console.log('[EVENT] modMoveDJ ', JSON.stringify(data, null, 2)); 5 | }); 6 | 7 | }; 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | config.json 18 | 19 | sparkle.sqlite 20 | test.js 21 | 22 | -------------------------------------------------------------------------------- /events/disabled/modAmbassador.js: -------------------------------------------------------------------------------- 1 | module.exports = function (bot) { 2 | 3 | bot.on(PlugAPI.events.MODERATE_AMBASSADOR, function (data) { 4 | console.log('[EVENT] modAmbassador ', JSON.stringify(data, null, 2)); 5 | }); 6 | 7 | }; 8 | -------------------------------------------------------------------------------- /events/disabled/modRemoveDj.js: -------------------------------------------------------------------------------- 1 | module.exports = function (bot) { 2 | 3 | bot.on(PlugAPI.events.MODERATE_REMOVE_DJ, function (data) { 4 | console.log('[EVENT] modRemoveDJ ', JSON.stringify(data, null, 2)); 5 | saveWaitList(true); 6 | }); 7 | 8 | }; -------------------------------------------------------------------------------- /commands/hodor.js: -------------------------------------------------------------------------------- 1 | exports.names = ['hodor']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak('Hodor!'); 10 | }; -------------------------------------------------------------------------------- /commands/freespin.js: -------------------------------------------------------------------------------- 1 | exports.names = ['freespin']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak(config.responses.freeSpin); 10 | }; -------------------------------------------------------------------------------- /events/pmmed.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("pmmed", function (data) { 3 | if (config.verboseLogging) { 4 | console.log("[PM]", JSON.stringify(data, null, 2)); 5 | } else if (data.userid && data.name) { 6 | console.log("[PM]", data.name + ": " + data.text); 7 | } 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /commands/emoji.js: -------------------------------------------------------------------------------- 1 | exports.names = ["emoji"]; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak("Emoji List: http://www.emoji-cheat-sheet.com"); 10 | }; 11 | -------------------------------------------------------------------------------- /events/nosong.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("nosong", function (data) { 3 | if (config.verboseLogging) { 4 | console.log("[SONG]", JSON.stringify(data, null, 2)); 5 | } else if (data.userid && data.name) { 6 | console.log("[SONG]", data.name + ": " + data.text); 7 | } 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /commands/down.js: -------------------------------------------------------------------------------- 1 | // Instructs the bot to downvote a song. Available to VIPs and higher. 2 | exports.names = ['down']; 3 | exports.hidden = true; 4 | exports.enabled = true; 5 | exports.cdAll = 30; 6 | exports.cdUser = 30; 7 | exports.cdStaff = 10; 8 | exports.minRole = PERMISSIONS.RDJ_PLUS; 9 | exports.handler = function (data) { 10 | bot.vote('down'); 11 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | extensions/* 18 | !extensions/README.md 19 | 20 | config.json 21 | configState.json 22 | roomState.json 23 | 24 | *.sqlite 25 | !sample.sqlite 26 | 27 | test.js 28 | .idea 29 | .env 30 | .vscode -------------------------------------------------------------------------------- /commands/source.js: -------------------------------------------------------------------------------- 1 | exports.names = ['source']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak('Source code: https://github.com/avatarkava/beavisbot. Feel free to submit pull requests!'); 10 | }; -------------------------------------------------------------------------------- /commands/bop.js: -------------------------------------------------------------------------------- 1 | // Instructs the bot to upvote a song. Only available for staff (not resident DJs). 2 | exports.names = ['bop', 'woot', 'up']; 3 | exports.hidden = true; 4 | exports.enabled = true; 5 | exports.cdAll = 30; 6 | exports.cdUser = 30; 7 | exports.cdStaff = 10; 8 | exports.minRole = PERMISSIONS.RDJ_PLUS; 9 | exports.handler = function (data) { 10 | bot.bop(); 11 | }; -------------------------------------------------------------------------------- /commands/uptime.js: -------------------------------------------------------------------------------- 1 | exports.names = ["uptime"]; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak(`online ${timeSince(uptime.getTime(), true)} (since ${moment.utc(uptime.getTime()).calendar()} UTC)`); 10 | }; 11 | -------------------------------------------------------------------------------- /commands/license.js: -------------------------------------------------------------------------------- 1 | exports.names = ['license']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak('MIT License - Full license available at https://github.com/avatarkava/beavisbot/blob/master/LICENSE'); 10 | }; 11 | -------------------------------------------------------------------------------- /commands/time.js: -------------------------------------------------------------------------------- 1 | exports.names = ['time', 'utc']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak(`checks the clock: ${moment.utc().format('HH:mm:ss')} UTC on ${ moment.utc().format('dddd MMMM Do, YYYY')}`); 10 | }; -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "linebreak-style": [0, "unix"], 4 | semi: [2, "always"], 5 | "no-unused-vars": [0], 6 | "no-console": "off", 7 | "no-undef": "off" 8 | }, 9 | env: { 10 | es6: true, 11 | node: true, 12 | }, 13 | extends: "eslint:recommended", 14 | parserOptions: { 15 | sourceType: "module", 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 5.6.0 4 | - 4.0.0 5 | env: 6 | - CXX=g++-4.8 7 | addons: 8 | apt: 9 | sources: 10 | - ubuntu-toolchain-r-test 11 | packages: 12 | - g++-4.8 13 | before_install: 14 | - openssl aes-256-cbc -K $encrypted_dae008b4772b_key -iv $encrypted_dae008b4772b_iv 15 | -in secrets.tar.enc -out secrets.tar -d 16 | - tar xvf secrets.tar -------------------------------------------------------------------------------- /commands/owner.js: -------------------------------------------------------------------------------- 1 | exports.names = ['owner', 'feedback']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak('avatarkava is the author of beavisbot. Make bug reports and requests here, please: https://github.com/avatarkava/beavisbot/issues'); 10 | }; 11 | -------------------------------------------------------------------------------- /commands/refresh.js: -------------------------------------------------------------------------------- 1 | exports.names = ['refresh']; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak(`If you can not see the video or it's stuck, please click refresh on the video itself to select an alternate version (this may take more than one try!)`); 10 | }; -------------------------------------------------------------------------------- /events/snagged.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("snagged", function (data) { 3 | if (config.verboseLogging) { 4 | console.log("[SNAG] " + JSON.stringify(data, null, 2)); 5 | } else if (data) { 6 | 7 | roomState.snags++; 8 | getDbUserFromUserId(data.userid, function (user) { 9 | console.log("[SNAG]", user.username + " grabbed this song"); 10 | }); 11 | } 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /events/disabled/modUnban.js: -------------------------------------------------------------------------------- 1 | module.exports = function (bot) { 2 | 3 | bot.on(PlugAPI.events.MODERATE_UNBAN, function (data) { 4 | if (config.verboseLogging) { 5 | console.log('[EVENT] modUnban ', JSON.stringify(data, null, 2)); 6 | } 7 | var message = '[UNBAN] ' + data.username + ' was unbanned by ' + data.moderator; 8 | console.log(message); 9 | sendToWebhooks(message); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /commands/csi.js: -------------------------------------------------------------------------------- 1 | exports.names = ["csi"]; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak("https://media.tenor.com/images/2ff5166ebf3686f71eb681d887540aaa/tenor.gif"); 10 | setTimeout(function () { 11 | bot.speak("YEAAAAAHHHHHHHHHHHHHHHHHHHHHHH"); 12 | }, 8000); 13 | }; 14 | 1; 15 | -------------------------------------------------------------------------------- /commands/disabled/deli.js: -------------------------------------------------------------------------------- 1 | exports.names = ['deli']; 2 | exports.hidden = true; 3 | exports.enabled = false; 4 | exports.cdAll = 10; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | users = bot.getUsers(); 10 | var randomNumber = _.random(1, users.length); 11 | bot.speak(":bell: Now serving customer #" + randomNumber + " - hey there, " + users[(randomNumber - 1)].username + "!"); 12 | }; -------------------------------------------------------------------------------- /commands/upvote.js: -------------------------------------------------------------------------------- 1 | exports.names = ["upvote"]; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.RDJ_PLUS; 8 | exports.handler = function (data) { 9 | var message = ""; 10 | var input = _.rest(data.text.split(" "), 1).join(" ").trim(); 11 | if (input.length > 1) { 12 | message = input + " "; 13 | } 14 | message += config.responses.upvoteReminder; 15 | bot.speak(message); 16 | }; 17 | -------------------------------------------------------------------------------- /commands/rules.js: -------------------------------------------------------------------------------- 1 | exports.names = ['rules']; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | 10 | var message = ''; 11 | var input = _.rest(data.text.split(' '), 1).join(' ').trim(); 12 | if (input.length > 1) { 13 | message = input + ' '; 14 | } 15 | 16 | message += config.responses.rules; 17 | bot.speak(message); 18 | }; -------------------------------------------------------------------------------- /commands/catfact.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | 3 | exports.names = ["catfact", "catfacts"]; 4 | exports.hidden = false; 5 | exports.enabled = true; 6 | exports.cdAll = 10; 7 | exports.cdUser = 30; 8 | exports.cdStaff = 10; 9 | exports.minRole = PERMISSIONS.NONE; 10 | exports.handler = function (data) { 11 | fetch("https://cat-fact.herokuapp.com/facts/random?amount=1") 12 | .then((res) => res.json()) 13 | .then((json) => bot.speak(json.text)) 14 | .catch((err) => console.error(err)); 15 | }; 16 | -------------------------------------------------------------------------------- /commands/downvote.js: -------------------------------------------------------------------------------- 1 | exports.names = ['downvote']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.RDJ_PLUS; 8 | exports.handler = function (data) { 9 | var message = ""; 10 | var input = _.rest(data.text.split(' '), 1).join(' ').trim(); 11 | if (input.length > 1) { 12 | message = input + ' '; 13 | } 14 | message += config.responses.downvoteReminder; 15 | bot.speak(message); 16 | }; -------------------------------------------------------------------------------- /models/karma.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | var Karma = sequelize.define( 3 | "Karma", 4 | { 5 | id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, 6 | type: { type: DataTypes.STRING, allowNull: false }, 7 | details: { type: DataTypes.TEXT }, 8 | }, 9 | { 10 | underscored: true, 11 | tableName: "karmas", 12 | } 13 | ); 14 | 15 | Karma.associate = function (models) { 16 | Karma.belongsTo(models.User); 17 | }; 18 | 19 | return Karma; 20 | }; 21 | -------------------------------------------------------------------------------- /commands/fact.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | 3 | exports.names = ["fact"]; 4 | exports.hidden = false; 5 | exports.enabled = true; 6 | exports.cdAll = 10; 7 | exports.cdUser = 30; 8 | exports.cdStaff = 10; 9 | exports.minRole = PERMISSIONS.NONE; 10 | exports.handler = function (data) { 11 | var month = new Date().getMonth() + 1; 12 | var day = new Date().getDate(); 13 | var factApi = fetch(`http://numbersapi.com/random/trivia`) 14 | .then((res) => res.text()) 15 | .then((body) => bot.speak(body)) 16 | .catch((err) => console.error(err)); 17 | }; 18 | -------------------------------------------------------------------------------- /commands/today.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | 3 | exports.names = ["today"]; 4 | exports.hidden = false; 5 | exports.enabled = true; 6 | exports.cdAll = 10; 7 | exports.cdUser = 30; 8 | exports.cdStaff = 10; 9 | exports.minRole = PERMISSIONS.NONE; 10 | exports.handler = function (data) { 11 | var month = new Date().getMonth() + 1; 12 | var day = new Date().getDate(); 13 | var factApi = fetch(`http://numbersapi.com/${month}/${day}`) 14 | .then((res) => res.text()) 15 | .then((body) => bot.speak(body)) 16 | .catch((err) => console.error(err)); 17 | }; 18 | -------------------------------------------------------------------------------- /events/deregistered.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("deregistered", function (data) { 3 | if (config.verboseLogging) { 4 | console.log("[EVENT] deregistered", data); 5 | } 6 | 7 | data.user.forEach(function (user) { 8 | if (user.name === undefined) { 9 | console.log(`[LEAVE] Guest left`); 10 | } else { 11 | console.log(`[LEAVE] User left: ${user.name}`); 12 | models.User.update({ last_leave: new Date(), last_seen: new Date() }, { where: { site: config.site, site_id: user.userid } }); 13 | } 14 | }); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /commands/commands.js: -------------------------------------------------------------------------------- 1 | exports.names = ["commands"]; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak( 10 | "Commands: " + 11 | _.compact( 12 | _.map(bot.commands, function (command) { 13 | if (command.enabled && !command.hidden && _.first(command.names) != "commands") { 14 | return config.commandLiteral + _.first(command.names); 15 | } 16 | }) 17 | ).join(", ") 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /models/useralias.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | var UserAlias = sequelize.define( 3 | "UserAlias", 4 | { 5 | id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, 6 | username: { type: DataTypes.STRING, allowNull: false }, 7 | }, 8 | { 9 | underscored: true, 10 | tableName: "user_aliases", 11 | indexes: [ 12 | { unique: true, fields: ['username', 'user_id'] } 13 | ] 14 | } 15 | ); 16 | 17 | UserAlias.associate = function (models) { 18 | UserAlias.belongsTo(models.User); 19 | }; 20 | 21 | return UserAlias; 22 | }; 23 | -------------------------------------------------------------------------------- /models/game.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | var Game = sequelize.define( 3 | "Game", 4 | { 5 | id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, 6 | type: { type: DataTypes.STRING, allowNull: false }, 7 | details: { type: DataTypes.TEXT }, 8 | result: { type: DataTypes.TEXT }, 9 | participants: { type: DataTypes.INTEGER.UNSIGNED, defaultValue: 0 }, 10 | }, 11 | { 12 | underscored: true, 13 | tableName: "games", 14 | } 15 | ); 16 | 17 | Game.associate = function (models) { 18 | Game.belongsTo(models.User); 19 | }; 20 | 21 | return Game; 22 | }; 23 | -------------------------------------------------------------------------------- /models/songresponse.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | var SongResponse = sequelize.define( 3 | "SongResponse", 4 | { 5 | id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, 6 | media_type: { type: DataTypes.STRING }, 7 | pattern: { type: DataTypes.STRING, allowNull: false }, 8 | response: { type: DataTypes.STRING }, 9 | rate: { type: DataTypes.INTEGER, defaultValue: 0 }, 10 | is_active: { type: DataTypes.BOOLEAN, defaultValue: true }, 11 | }, 12 | { 13 | underscored: true, 14 | tableName: "song_responses", 15 | } 16 | ); 17 | 18 | return SongResponse; 19 | }; 20 | -------------------------------------------------------------------------------- /commands/help.js: -------------------------------------------------------------------------------- 1 | exports.names = ['help', 'support']; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 60; 5 | exports.cdUser = 60; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | 10 | // @TODO - Use this to show all active staff - console.log(bot.getStaff()); 11 | 12 | if (config.slack?.webhookUrl !== '') { 13 | bot.speak('Bot commands are available using .commands. Need help? Ask a staff member. None around? Try ' + config.commandLiteral + 'callmod!'); 14 | } 15 | else { 16 | bot.speak('Bot commands are available using .commands. Need help? Ask a staff member!'); 17 | } 18 | 19 | }; -------------------------------------------------------------------------------- /commands/roll.js: -------------------------------------------------------------------------------- 1 | exports.names = ["roll"]; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | var maxValue = 6; 10 | var input = _.rest(data.text.split(" "), 1).join(" ").trim(); 11 | 12 | if (input.length > 0) { 13 | maxValue = parseInt(input.replace(/\D/g, "")); 14 | } 15 | 16 | if (maxValue > 0 && maxValue < 99999) { 17 | var roll = _.random(1, maxValue); 18 | bot.speak("@" + data.name + ", you rolled a " + roll + "!"); 19 | } else { 20 | bot.speak("https://media0.giphy.com/media/l4EpciZRNKNrhVKpi/giphy.gif"); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /models/blacklist.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | var Blacklist = sequelize.define( 3 | "Blacklist", 4 | { 5 | id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, 6 | type: { type: DataTypes.STRING, allowNull: false }, 7 | pattern: { type: DataTypes.STRING, allowNull: false }, 8 | details: { type: DataTypes.TEXT }, 9 | is_active: { type: DataTypes.BOOLEAN, defaultValue: 1 }, 10 | }, 11 | { 12 | underscored: true, 13 | tableName: "blacklist", 14 | } 15 | ); 16 | 17 | Blacklist.associate = function (models) { 18 | Blacklist.belongsTo(models.Song); 19 | }; 20 | 21 | return Blacklist; 22 | }; 23 | -------------------------------------------------------------------------------- /events/speak.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("speak", function (data) { 3 | if (config.verboseLogging) { 4 | console.log("[SPEAK]", JSON.stringify(data, null, 2)); 5 | } else if (data.userid && data.name) { 6 | console.log("[SPEAK]", data.name + ": " + data.text); 7 | } 8 | 9 | if (data.name) { 10 | data.text = data.text.trim(); 11 | handleChat(data); 12 | /* 13 | models.User.update({ 14 | last_active: new Date(), 15 | last_seen: new Date(), 16 | locale: data.from.language 17 | }, {where: {site_id: data.from.id.toString(), site: config.site}}); 18 | */ 19 | } 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /models/eventresponse.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | var EventResponse = sequelize.define( 3 | "EventResponse", 4 | { 5 | id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, 6 | event_type: { type: DataTypes.STRING, allowNull: false }, 7 | pattern: { type: DataTypes.STRING }, 8 | response: { type: DataTypes.STRING, allowNull: false }, 9 | cooldown: { type: DataTypes.INTEGER.UNSIGNED, defaultValue: 30 }, 10 | is_active: { type: DataTypes.BOOLEAN, defaultValue: true }, 11 | }, 12 | { 13 | underscored: true, 14 | tableName: "event_responses", 15 | } 16 | ); 17 | 18 | return EventResponse; 19 | }; 20 | -------------------------------------------------------------------------------- /commands/currency.js: -------------------------------------------------------------------------------- 1 | exports.names = ['currency', 'points']; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | if(config.customPointName) { 10 | 11 | var message = "Our custom currency is the " + config.customPointName + ". Points are free and " + 12 | "can be gifted by users or earned from " + bot.user.name + " by DJing or winning games. " + 13 | "Use .gift to give or .info to check your balance! Info on what " + config.customPointName + 14 | " can buy will be available soon!"; 15 | bot.speak(message); 16 | } 17 | }; -------------------------------------------------------------------------------- /commands/disabled/add.js: -------------------------------------------------------------------------------- 1 | exports.names = ['add']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.RDJ_PLUS; 8 | exports.handler = function (data) { 9 | var input = data.message.split(' '); 10 | var username = _.rest(input, 1).join(' ').trim(); 11 | var usernameFormatted = S(username).chompLeft('@').s; 12 | if (usernameFormatted) { 13 | var user = findUserInList(bot.getUsers(), usernameFormatted); 14 | if (user && bot.getWaitListPosition(user.id) === -1) { 15 | bot.moderateAddDJ(user.id, function () { 16 | console.log('[ADD] ' + data.from.username + ' added ' + user.username + ' to waitlist.'); 17 | }); 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | mariadb: 6 | image: mariadb:latest 7 | ports: 8 | - 3306:3306 9 | volumes: 10 | - data:/var/lib/mysql 11 | networks: 12 | - database 13 | environment: 14 | MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}" 15 | MYSQL_DATABASE: "${DB_NAME}" 16 | MYSQL_USER: "${DB_USERNAME}" 17 | MYSQL_PASSWORD: "${DB_PASSWORD}" 18 | TZ: "${TZ}" 19 | 20 | phpmyadmin: 21 | image: phpmyadmin/phpmyadmin 22 | ports: 23 | - 8080:80 24 | networks: 25 | - database 26 | depends_on: 27 | - mariadb 28 | environment: 29 | PMA_HOST: mysql 30 | TZ: "${TZ}" 31 | 32 | volumes: 33 | data: 34 | 35 | networks: 36 | database: 37 | server: -------------------------------------------------------------------------------- /models/play.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | var Play = sequelize.define( 3 | "Play", 4 | { 5 | id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, 6 | site: { type: DataTypes.STRING }, 7 | positive: { type: DataTypes.INTEGER.UNSIGNED, defaultValue: 0 }, 8 | negative: { type: DataTypes.INTEGER.UNSIGNED, defaultValue: 0 }, 9 | grabs: { type: DataTypes.INTEGER.UNSIGNED, defaultValue: 0 }, 10 | listeners: { type: DataTypes.INTEGER.UNSIGNED, defaultValue: 0 }, 11 | skipped: { type: DataTypes.INTEGER.UNSIGNED, defaultValue: 0 }, 12 | }, 13 | { 14 | underscored: true, 15 | tableName: "plays", 16 | } 17 | ); 18 | 19 | Play.associate = function (models) { 20 | Play.belongsTo(models.Song); 21 | Play.belongsTo(models.User); 22 | }; 23 | 24 | return Play; 25 | }; 26 | -------------------------------------------------------------------------------- /commands/disabled/delay.js: -------------------------------------------------------------------------------- 1 | exports.names = ['delay']; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 1800; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | 10 | var input = data.message.split(' '); 11 | var spots = 1; 12 | 13 | if (input.length == 2) { 14 | spots = Math.abs(parseInt(input[1])); 15 | } 16 | 17 | var position = bot.getWaitListPosition(data.from.id); 18 | if (position > 0) { 19 | var newPosition = position + spots; 20 | bot.moderateMoveDJ(data.from.id, newPosition); 21 | console.log('[MOVE]', 'Moving ' + data.from.username + ' to position: ' + newPosition + ' (requested delay)'); 22 | bot.sendChat('Moved you down the wait list @' + data.from.username + '. You can request another delay after 30 minutes.'); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /commands/quake.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | 3 | exports.names = ["quake"]; 4 | exports.hidden = true; 5 | exports.enabled = true; 6 | exports.cdAll = 30; 7 | exports.cdUser = 30; 8 | exports.cdStaff = 10; 9 | exports.minRole = PERMISSIONS.NONE; 10 | exports.handler = function (data) { 11 | var factApi = fetch("http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson") 12 | .then((res) => res.json()) 13 | .then((json) => { 14 | var quakes = json.features.slice(0, 3); 15 | bot.speak( 16 | "Recent earthquakes: " + 17 | _.map(quakes, function (quake) { 18 | var timeElapsed = new Date() - new Date(quake.properties.time); 19 | return quake.properties.title + " (" + Math.floor(timeElapsed / 3600000) + "h" + Math.floor((timeElapsed % 3600000) / 60000) + "m ago)"; 20 | }).join(" · ") 21 | ); 22 | }) 23 | .catch((err) => console.error(err)); 24 | }; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BeavisBot", 3 | "description": "A customizable social music room bot", 4 | "version": "3.0.0", 5 | "keywords": [ 6 | "turntable", 7 | "turntable.fm" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/AvatarKava/beavisbot.git" 12 | }, 13 | "author": "Mike Burke ", 14 | "license": "MIT", 15 | "main": "bot.js", 16 | "directories": { 17 | "commands": "commands" 18 | }, 19 | "engines": { 20 | "node": ">= 14.0.0" 21 | }, 22 | "dependencies": { 23 | "cleverbot-node": "*", 24 | "html-entities": "*", 25 | "mariadb": "*", 26 | "moment": "*", 27 | "node-fetch": "*", 28 | "popyt": "*", 29 | "require-reload": "*", 30 | "sequelize": "~6", 31 | "ttapi": "git+https://github.com/alaingilbert/Turntable-API.git", 32 | "underscore": "*", 33 | "underscore.string": "*" 34 | }, 35 | "devDependencies": { 36 | "sqlite3": "*" 37 | }, 38 | "optionalDependencies": {} 39 | } 40 | -------------------------------------------------------------------------------- /commands/disabled/locales.js: -------------------------------------------------------------------------------- 1 | exports.names = ['locales']; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | 10 | var userList = _.pluck(bot.getUsers(), 'id'); 11 | var users = []; 12 | 13 | models.User.findAll({ 14 | attributes: ['locale', [Sequelize.fn('count', Sequelize.col('locale')), 'user_count']], 15 | where: {site_id: {$in: userList}, site: config.site}, 16 | group: ['locale'], 17 | raw: true 18 | }).then(function (rows) { 19 | for (var x in rows) { 20 | if (rows[x].locale !== 'null' && rows[x].user_count > 0) { 21 | users.push(rows[x].locale + ': ' + rows[x].user_count); 22 | } 23 | } 24 | bot.sendChat(users.join(' · ')); 25 | }); 26 | 27 | // @TODO - Translate and use some iso_languages from context.js 28 | //iso_languages[pair[0]] ? iso_languages[pair[0]] 29 | }; -------------------------------------------------------------------------------- /commands/reload.js: -------------------------------------------------------------------------------- 1 | const { existsSync, readFileSync } = require("fs"); 2 | const reload = require("require-reload")(require); 3 | 4 | exports.names = ["reload"]; 5 | exports.hidden = true; 6 | exports.enabled = true; 7 | exports.cdAll = 60; 8 | exports.cdUser = 60; 9 | exports.cdStaff = 60; 10 | exports.minRole = PERMISSIONS.MANAGER; 11 | exports.handler = function (data) { 12 | // Reload the last existing state of the config file, otherwise revert to the default 13 | if (existsSync("configState.json")) { 14 | config = JSON.parse(readFileSync("configState.json", "utf-8")); 15 | console.log("Loaded config file from configState.json"); 16 | } else { 17 | config = JSON.parse(readFileSync("config.json", "utf-8")); 18 | console.log("Loaded config file from config.json"); 19 | writeConfigState(); 20 | } 21 | 22 | // @TODO - Find a way to reload the events (bot.on bindings need to be purged and reset) 23 | // loadEvents(bot); 24 | loadCommands(); 25 | loadExtensions(); 26 | bot.speak(`Commands and config reloaded, @${data.name}!`); 27 | }; 28 | -------------------------------------------------------------------------------- /events/update_votes.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("update_votes", function (data) { 3 | if (config.verboseLogging) { 4 | console.log("[VOTE]", JSON.stringify(data, null, 2)); 5 | } else if (data.userid && data.name) { 6 | console.log("[VOTE]", data.name + ": " + data.text); 7 | } 8 | 9 | /* 10 | user = bot.getUser(data.i); 11 | 12 | if (config.verboseLogging) { 13 | data.user = user; 14 | console.log('[VOTE] ' + JSON.stringify(data, null, 2)); 15 | } else if (user && data.v == -1) { 16 | console.log('[VOTE] ' + user.username + ' voted ' + data.v); 17 | } 18 | 19 | if (config.queue.prohibitDownvoteInQueue && data.v == -1 && bot.getWaitListPosition(data.i) > 0) { 20 | bot.sendChat('@' + user.username + ', voting down while in queue is prohibited. Please vote up or leave the queue.'); 21 | setTimeout(function () { 22 | removeIfDownvoting(user.username); 23 | }, 10 * 1000); 24 | } 25 | */ 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /events/roomChanged.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("roomChanged", function (data) { 3 | console.log("[EVENT] Ready - joined room: " + data.room.name); 4 | if (config.verboseLogging) { 5 | console.log("[INIT] Room data: " + JSON.stringify(data, null, 2)); 6 | } 7 | 8 | roomState.room = data.room; 9 | roomState.users = data.users; 10 | roomState.waitList = data.djids; 11 | 12 | if (config.responses.botConnect !== "") { 13 | bot.speak(config.responses.botConnect); 14 | } 15 | 16 | data.users.forEach(function (user) { 17 | if (user.userid == config.auth.userId) { 18 | bot.user = user; 19 | if (config.verboseLogging) { 20 | console.log("[INIT] Data loaded for " + bot.user.name + "\n " + JSON.stringify(bot.user, null, 2)); 21 | } 22 | } 23 | 24 | // @TODO bulkify this using bulkCreate() 25 | updateDbUser(user); 26 | }); 27 | 28 | if (config.queue.upvoteSongs == "ALL" && data.room.metadata.current_song) { 29 | bot.bop(); 30 | } 31 | 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /functions/time.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | timeSince = function (timestamp, suppressAgo) { 3 | ago = typeof suppressAgo !== "undefined" ? suppressAgo : false; 4 | var message = moment.utc(timestamp).fromNow(suppressAgo); 5 | 6 | if (moment().isAfter(moment.utc(timestamp).add(24, "hours"))) { 7 | message += " (" + moment.utc(timestamp).calendar() + ")"; 8 | } 9 | 10 | return message; 11 | }; 12 | 13 | timeUntil = function (timestamp, prefixMessage) { 14 | var message = moment.utc(timestamp).fromNow(); 15 | if (prefixMessage !== undefined) { 16 | return "(" + prefixMessage + " " + message + ")"; 17 | } else { 18 | return "(" + message + ")"; 19 | } 20 | }; 21 | 22 | secondsSince = function (timestamp) { 23 | var now = moment.utc(); 24 | timestamp = moment.utc(timestamp); 25 | return now.diff(timestamp, "seconds"); 26 | }; 27 | 28 | secondsUntil = function (timestamp) { 29 | var now = moment.utc(); 30 | timestamp = moment.utc(timestamp); 31 | return timestamp.diff(now, "seconds"); 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /commands/disabled/callmod.js: -------------------------------------------------------------------------------- 1 | exports.names = ['callmod']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 300; 5 | exports.cdUser = 300; 6 | exports.cdStaff = 180; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | 10 | var message = data.message.split(' ').slice(1).join(' ').trim(); 11 | 12 | if (config.slack.webhookUrl === '') { 13 | bot.sendChat('Need help? Ask a staff member!'); 14 | } 15 | else if(message === '') { 16 | bot.sendChat('Need help? Type ' + config.commandLiteral + 'callmod with the nature of your request - for example `' + config.commandLiteral + 'callmod Someone is spamming the chat!` You can also use /sos to contact a Plug Brand Ambassador. Please only use this for emergencies.'); 17 | } 18 | else { 19 | if (sendToWebhooks('@channel - ' + data.from.username + ' requested help in https://plug.dj/' + config.roomName + " \n`" + message + "`")) { 20 | bot.sendChat('A mod has been contacted and will be on the way if available, @' + data.from.username + '. @staff'); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /commands/lastseen.js: -------------------------------------------------------------------------------- 1 | exports.names = ["lastseen", "seen"]; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | var params = _.rest(data.text.split(" "), 1); 10 | if (params.length < 1) { 11 | bot.speak("usage: " + config.commandLiteral + "lastseen username"); 12 | return; 13 | } 14 | 15 | username = params.join(" ").trim(); 16 | usernameFormatted = S.ltrim(username, "@"); 17 | 18 | models.User.findOne({ where: { username: usernameFormatted }, order: [["updatedAt", "DESC"]] }).then(function (row) { 19 | if (row === null) { 20 | bot.speak(usernameFormatted + " was not found."); 21 | } else { 22 | var user = findUserInList(getUsers(), usernameFormatted); 23 | if (user) { 24 | bot.speak(user.username + " is in the room and was last active " + timeSince(row.last_active)); 25 | } else { 26 | bot.speak(row.username + " was last seen " + timeSince(row.last_seen)); 27 | } 28 | } 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /events/disabled/modSkip.js: -------------------------------------------------------------------------------- 1 | module.exports = function (bot) { 2 | 3 | bot.on(PlugAPI.events.MODERATE_SKIP, function (data) { 4 | if (config.verboseLogging) { 5 | console.log('[EVENT] modSkip ', JSON.stringify(data, null, 2)); 6 | } 7 | 8 | // Send this if the skip didn't come via [SKIP] 9 | if (data.user.id != botUser.db.site_id) { 10 | message = '[SKIP] ' + data.user.username + ' skipped a song.'; 11 | console.log(message); 12 | sendToWebhooks(message); 13 | } 14 | 15 | // Data from last song played 16 | var skippedSong = bot.lastPlay; 17 | message = '[SKIP] Skipped song: ' + skippedSong.media.name + ' (https://www.youtube.com/watch?v=' + skippedSong.media.cid + ') played by ' + skippedSong.dj.username + ' (ID: ' + skippedSong.dj.id + ')'; 18 | 19 | if (config.verboseLogging) { 20 | console.log('[SKIP] ' + message, JSON.stringify(skippedSong, null, 2)); 21 | } else { 22 | console.log('[SKIP] ' + message); 23 | } 24 | sendToWebhooks(message); 25 | }); 26 | }; -------------------------------------------------------------------------------- /commands/define.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | 3 | exports.names = ["define"]; 4 | exports.hidden = false; 5 | exports.enabled = true; 6 | exports.cdAll = 10; 7 | exports.cdUser = 30; 8 | exports.cdStaff = 10; 9 | exports.minRole = PERMISSIONS.NONE; 10 | exports.handler = function (data) { 11 | var input = _.rest(data.text.split(" "), 1).join(" ").trim(); 12 | 13 | if (input.length > 0 && config.apiKeys.wordnik) { 14 | var uri = "http://api.wordnik.com:80/v4/word.json/" + input + "/definitions?limit=5&includeRelated=false&useCanonical=true&includeTags=false&api_key=" + config.apiKeys.wordnik; 15 | 16 | fetch(uri) 17 | .then((res) => res.json()) 18 | .then((json) => { 19 | if (config.verboseLogging) { 20 | console.log("[DEFINE]", JSON.stringify(json, null, 2)); 21 | } 22 | 23 | if (json.length == 0) { 24 | bot.speak("No definition for " + input + " found."); 25 | } else { 26 | bot.speak(json[0].word + " [" + json[0].partOfSpeech + "] - " + json[0].text); 27 | } 28 | }) 29 | .catch((err) => console.error(err)); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /models/roomevent.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | var RoomEvent = sequelize.define( 3 | "RoomEvent", 4 | { 5 | id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, 6 | type: { type: DataTypes.STRING, allowNull: false }, 7 | title: { type: DataTypes.STRING, allowNull: false }, 8 | slug: { type: DataTypes.STRING, allowNull: false }, 9 | details: { type: DataTypes.TEXT }, 10 | starts_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, 11 | ends_at: { type: DataTypes.DATE }, 12 | }, 13 | { 14 | underscored: true, 15 | tableName: "room_events", 16 | setterMethods: { 17 | title: function (v) { 18 | this.setDataValue("slug", S.slugify(v)); 19 | return this.setDataValue("title", v); 20 | }, 21 | slug: function (v) { 22 | return this.setDataValue("slug", S.slugify(v)); 23 | }, 24 | }, 25 | } 26 | ); 27 | 28 | RoomEvent.associate = function (models) { 29 | RoomEvent.belongsTo(models.User); 30 | }; 31 | 32 | return RoomEvent; 33 | }; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Kate Wellington 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /commands/disabled/mute.js: -------------------------------------------------------------------------------- 1 | exports.names = ['mute', 'unmute']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.RDJ_PLUS; 8 | exports.handler = function (data) { 9 | 10 | var input = data.message.split(' '); 11 | 12 | var command = _.first(input); 13 | var username = _.rest(input, 1); 14 | var usernameFormatted = S(username).chompLeft('@').s; 15 | var message = ''; 16 | 17 | var user = findUserInList(bot.getUsers(), usernameFormatted); 18 | if (user !== undefined) { 19 | if (command == 'unmute') { 20 | bot.moderateUnmuteUser(user.id); 21 | message = '[UNMUTE] ' + data.from.username + ' unmuted ' + usernameFormatted; 22 | console.log(message); 23 | sendToWebhooks(message); 24 | bot.sendChat(usernameFormatted + ' is now unmuted.'); 25 | } else { 26 | // @TODO - Make this variable 27 | var mute_duration = PlugAPI.MUTE.LONG; 28 | bot.moderateMuteUser(user.id, 1, mute_duration); 29 | } 30 | } 31 | 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /commands/disabled/songinfo.js: -------------------------------------------------------------------------------- 1 | exports.names = ['songinfo']; 2 | exports.hidden = false; 3 | exports.enabled = false; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | var songId; 10 | media = bot.getMedia(); 11 | if (data.message.length > 10) { 12 | songId = data.message.substring(10); 13 | } else if (media != null) { 14 | songId = media.cid; 15 | } else { 16 | bot.sendChat('No song playing.'); 17 | return; 18 | } 19 | 20 | //db.get('SELECT author, title FROM SONGS where id = ?', songId, function (err, row) { 21 | // if (row != null) { 22 | // bot.sendChat('Song ' + songId + ' has this metadata in the DB: Artist: "' 23 | // + row['author'] + '". Title: "' + row['title'] + '". Use .updateauthor or .updatetitle to change.'); 24 | // } else if (songId == media.id) { 25 | // bot.sendChat('Song ' + songId + ' does not exist in the DB and will be added with' 26 | // + ' this metadata: Artist: "' + media.author + '". Title: "' 27 | // + media.title + '".'); 28 | // } else { 29 | // bot.sendChat('Invalid song ID.'); 30 | // } 31 | //}); 32 | }; 33 | -------------------------------------------------------------------------------- /commands/disabled/gift.js: -------------------------------------------------------------------------------- 1 | exports.names = ["gift"]; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | var input = data.text.split(" "); 10 | var params = _.rest(input, 1); 11 | var username = _.initial(params).join(" "); 12 | var amount = parseInt(_.last(params)); 13 | 14 | if (!amount || isNaN(amount) || amount < 1 || !username) { 15 | bot.speak("usage: .gift @username 50 (gifts 50 points)"); 16 | return; 17 | } 18 | 19 | var usernameFormatted = S(username).chompLeft("@").s; 20 | var user = findUserInList(bot.getUsers(), usernameFormatted); 21 | 22 | if (!user) { 23 | bot.speak(`user ${username} was not found.`); 24 | } 25 | 26 | getDbUserFromSiteUser(data.from, function (row) { 27 | if (!row || row.custom_points == 0) { 28 | bot.speak(`You do not have any ${config.customPointName} to give, @${data.name}.`); 29 | return; 30 | } else if (row.custom_points < amount) { 31 | bot.speak(`You only have ${row.custom_points} ${config.customPointName} to give, @${data.name}.`); 32 | return; 33 | } 34 | 35 | transferCustomPoints(data.userid, user, amount); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /commands/giphy.js: -------------------------------------------------------------------------------- 1 | exports.names = ['giphy', 'giphyt', 'giphys']; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | 10 | var command = _.first(data.text.split(' ')); 11 | var params = _.rest(data.text.split(' '), 1); 12 | var api_key = "dc6zaTOxFJmzC"; // public beta key 13 | var rating = "pg"; // PG gifs 14 | var tags = null; 15 | var limit = 20; // How many to randomly choose from 16 | var pointCost = config.giphyPointCost + 0; 17 | 18 | if (params.length == 0) { 19 | //@TODO - Add usage 20 | return; 21 | } 22 | 23 | /* 24 | attemptPurchase(data.from, pointCost, function(success) { 25 | if (success == true) { 26 | tags = params.join('+').trim().replace(/ /g, "+"); 27 | 28 | getGiphy(command, api_key, rating, tags, limit, function (imageurl) { 29 | if (typeof imageurl !== 'undefined') { 30 | bot.sendChat(imageurl); 31 | } else { 32 | bot.sendChat('Could not find any gif tag(s): ' + tags); 33 | } 34 | }, tags != null ? tags : null); 35 | } 36 | }); 37 | */ 38 | 39 | }; -------------------------------------------------------------------------------- /commands/event.js: -------------------------------------------------------------------------------- 1 | exports.names = ['event', 'calendar']; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | 10 | //var input = data.message.split(' '); 11 | 12 | models.RoomEvent.findAll({ 13 | where: {type: 'event', starts_at: {lte: moment.utc().add(1, 'month').toDate()}, ends_at: {gte: new Date()}}, 14 | order: [['starts_at', 'ASC']], 15 | limit: 3 16 | }).then(function (rows) { 17 | if (rows.length === 0) { 18 | bot.speak('No events currently scheduled.'); 19 | } else { 20 | bot.speak(rows.map(function (row) { 21 | var message = row.title; 22 | if (row.details !== null) { 23 | message += ' - ' + row.details; 24 | } 25 | if (row.starts_at > moment.utc().toDate()) { 26 | message = timeUntil(row.starts_at, 'starting') + ' ' + message; 27 | } 28 | else if (row.starts_at <= moment.utc().toDate()) { 29 | message += ' ' + timeUntil(row.ends_at, 'ending'); 30 | } 31 | 32 | return message; 33 | }).join(' • ')); 34 | } 35 | }); 36 | }; -------------------------------------------------------------------------------- /commands/disabled/move.js: -------------------------------------------------------------------------------- 1 | exports.names = ['move', 'mv']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.RDJ_PLUS; 8 | exports.handler = function (data) { 9 | //bot.moderateDeleteChat(data.id); 10 | var input = data.message.split(' '); 11 | if (input.length >= 3) { 12 | var username = _.rest(input, 1); 13 | username = _.initial(username, 1).join(' ').trim(); 14 | var usernameFormatted = S(username).chompLeft('@').s; 15 | var position = parseInt(_.last(input, 1)); 16 | users = bot.getUsers(); 17 | var user = findUserInList(users, usernameFormatted); 18 | if (user !== undefined) { 19 | var currentPosition = bot.getWaitListPosition(user.id); 20 | if (currentPosition === -1) { 21 | bot.moderateAddDJ(user.id, function () { 22 | if (position <= bot.getWaitList().length) { 23 | bot.moderateMoveDJ(user.id, position); 24 | } 25 | }); 26 | } 27 | else if (currentPosition > 0 && currentPosition !== position) { 28 | bot.moderateMoveDJ(user.id, position); 29 | } 30 | console.log('Moving ' + usernameFormatted + ' to position: ' + position); 31 | } 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /commands/stats.js: -------------------------------------------------------------------------------- 1 | exports.names = ['stats', 'bestdjs', 'bestplays', 'busydjs', 'mostgrabbed', 'mostmehed', 'mostplayed', 'mostwooted', 'mymostgrabbed', 'mymostmehed', 'mymostplayed', 'mymostwooted', 'mystats', 'worstdjs']; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | bot.speak('Individual and room stats are being moved to our new website and will be available via API. Stay tuned for details!'); 10 | 11 | //db.get('SELECT COUNT(*) AS total_songs, COUNT(DISTINCT userid) AS total_djs, COUNT(DISTINCT songid) AS unique_songs, SUM(upvotes) AS upvotes, SUM(snags) AS snags, SUM(downvotes) AS downvotes, AVG(upvotes) as avg_upvotes, AVG(snags) AS avg_snags, AVG(downvotes) as avg_downvotes FROM PLAYS', function (err, row) { 12 | // bot.sendChat(row['total_songs'] + ' songs (' 13 | // + row['unique_songs'] + ' unique) have been played by ' 14 | // + row['total_djs'] + ' DJs with a total of ' 15 | // + row['upvotes'] + ' woots, ' 16 | // + row['snags'] + ' grabs and ' 17 | // + row['downvotes'] + ' mehs (avg +' 18 | // + new Number(row['avg_upvotes']).toFixed(1) + '/' 19 | // + new Number(row['avg_snags']).toFixed(1) + '/-' 20 | // + new Number(row['avg_downvotes']).toFixed(1) + ')'); 21 | //}); 22 | }; -------------------------------------------------------------------------------- /commands/disabled/kick.js: -------------------------------------------------------------------------------- 1 | exports.names = ['kick']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.BOUNCER; 8 | exports.handler = function (data) { 9 | 10 | var params = _.rest(data.message.split(' '), 1); 11 | var username; 12 | var message; 13 | 14 | if (params.length >= 2) { 15 | username = _.initial(params).join(' ').trim(); 16 | message = _.last(params).toUpperCase(); 17 | } 18 | else if (params.length == 1) { 19 | username = params.join(' ').trim(); 20 | message = ''; 21 | } 22 | 23 | var usernameFormatted = S(username).chompLeft('@').s; 24 | var user = bot.getUserByName(usernameFormatted, true); 25 | 26 | if (user) { 27 | bot.moderateKickUser(user.id, message); 28 | 29 | getDbUserFromSiteUser(user.id, function (row) { 30 | var userData = { 31 | type: 'kick', 32 | details: 'Kicked ' + username, 33 | user_id: row.id, 34 | mod_user_id: data.from.db.id 35 | }; 36 | models.Karma.create(userData); 37 | console.log('[KICK] ' + username + ' kicked from room'); 38 | models.User.update({queue_position: -1}, {where: {site_id: user.id}}); 39 | }); 40 | 41 | } 42 | else { 43 | bot.sendChat(usernameFormatted + ' not found in the room'); 44 | } 45 | 46 | }; 47 | 48 | -------------------------------------------------------------------------------- /commands/disabled/lastplayed.js: -------------------------------------------------------------------------------- 1 | exports.names = ["lastplayed"]; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | var params = _.rest(data.message.split(" "), 1); 10 | var message = ""; 11 | 12 | var media = bot.getMedia(); 13 | var songid = media.cid; 14 | 15 | if (params.length > 0) { 16 | songid = params.join(" ").trim(); 17 | } 18 | 19 | attemptPurchase(data.from, 1, function (success) { 20 | if (success == true) { 21 | models.Play.findOne({ 22 | include: [ 23 | { 24 | model: models.Song, 25 | where: { $and: [{ site: config.site }, { host: media.format }, { host_id: songid }] }, 26 | }, 27 | models.User, 28 | ], 29 | order: [["created_at", "DESC"]], 30 | }).then(function (row) { 31 | if (!row && params.length == 0) { 32 | bot.sendChat("This is the first time I have seen this video played!"); 33 | } else if (!row) { 34 | bot.sendChat("I have not seen a song with id `" + songid + "` played."); 35 | } else { 36 | message = row.Song.name + " • last played " + timeSince(row.created_at) + " by " + row.User.username + " • " + row.listeners + " :ear: • " + row.positive + " :+1: • " + row.grabs + " :star: • " + row.negative + " :-1:"; 37 | bot.sendChat(message); 38 | } 39 | }); 40 | } 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /events/disabled/modMute.js: -------------------------------------------------------------------------------- 1 | module.exports = function (bot) { 2 | 3 | bot.on(PlugAPI.events.MODERATE_MUTE, function (data) { 4 | if (config.verboseLogging) { 5 | console.log('[EVENT] modMute ', JSON.stringify(data, null, 2)); 6 | } 7 | 8 | var duration = 'unknown'; 9 | switch (data.duration) { 10 | case 'Short': 11 | duration = '15'; 12 | break; 13 | case 'Medium': 14 | duration = '30'; 15 | break; 16 | case 'Long': 17 | duration = '45'; 18 | break; 19 | default: 20 | // maybe this is an unmute? 21 | break; 22 | } 23 | 24 | getDbUserFromUsername(data.user.username, function (dbUser) { 25 | var message; 26 | if (duration == 'unknown') { 27 | message = '[UNMUTE] ' + data.user.username + ' (ID: ' + data.user.id + ') was unmuted by ' + data.moderator.username; 28 | } else if (dbUser == null) { 29 | message = '[MUTE] ' + data.user.username + ' (ID: ' + data.user.id + ') was muted for ' + duration + ' minutes by ' + data.moderator.username; 30 | } else { 31 | message = '[MUTE] ' + data.user.username + ' (ID: ' + data.user.id + ', LVL: ' + dbUser.site_points + ') was muted for ' + duration + ' minutes by ' + data.moderator.username; 32 | } 33 | console.log(message); 34 | sendToWebhooks(message); 35 | }); 36 | }); 37 | }; -------------------------------------------------------------------------------- /commands/whois.js: -------------------------------------------------------------------------------- 1 | exports.names = ['whois', 'info', 'userinfo']; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | 10 | var params = _.rest(data.text.split(' '), 1); 11 | var message = ''; 12 | 13 | if (params.length < 1) { 14 | username = data.name; 15 | } 16 | else { 17 | usernameRaw = params.join(' ').trim(); 18 | username = S.ltrim(usernameRaw, '@'); 19 | } 20 | 21 | models.User.findOne({where: {username: username }, order: [['updatedAt', 'DESC']]}).then(function (row) { 22 | if (row === null) { 23 | bot.speak(username + ' was not found.'); 24 | } else { 25 | // @TODO - store & display data we can get from the site like 'active', 'playedCount', 'songsInQueue', 'dubs' 26 | message = row.username; 27 | /* 28 | if (row.locale !== null && row.locale != 'null') { 29 | message += ' • ' + row.locale; 30 | 31 | } 32 | */ 33 | message += ' • seen ' + timeSince(row.last_seen) + ' • joined ' + moment.utc(row.joined).calendar() 34 | + ' • ID: ' + row.site_id + ' • Pts: ' + row.site_points; 35 | 36 | if (config.customPointName) { 37 | message = message + ' • ' + config.customPointName + ' ' + row.custom_points.toLocaleString(); 38 | } 39 | bot.speak(message); 40 | } 41 | }); 42 | 43 | }; -------------------------------------------------------------------------------- /models/song.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | var Song = sequelize.define( 3 | "Song", 4 | { 5 | id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, 6 | name: { type: DataTypes.STRING, allowNull: false }, 7 | slug: { type: DataTypes.STRING, allowNull: false }, 8 | author: { type: DataTypes.STRING }, 9 | title: { type: DataTypes.STRING }, 10 | description: { type: DataTypes.TEXT }, 11 | release_date: { type: DataTypes.DATEONLY }, 12 | tags: { type: DataTypes.STRING }, 13 | host: { type: DataTypes.STRING, allowNull: false, defaultValue: "youtube" }, 14 | host_id: { type: DataTypes.STRING, allowNull: false }, 15 | permalink: { type: DataTypes.STRING }, 16 | duration: { type: DataTypes.INTEGER.UNSIGNED }, 17 | image: { type: DataTypes.STRING }, 18 | is_banned: { type: DataTypes.BOOLEAN, defaultValue: 0 }, 19 | banned_reason: { type: DataTypes.STRING }, 20 | }, 21 | { 22 | underscored: true, 23 | tableName: "songs", 24 | indexes: [ 25 | { unique: true, fields: ['host', 'host_id'] } 26 | ], 27 | setterMethods: { 28 | name: function (v) { 29 | var formattedSlug = S.slugify(v); 30 | this.setDataValue("slug", formattedSlug); 31 | return this.setDataValue("name", v); 32 | }, 33 | slug: function (v) { 34 | return this.setDataValue("slug", S.slugify(v)); 35 | }, 36 | }, 37 | } 38 | ); 39 | 40 | Song.associate = function (models) { 41 | Song.hasMany(models.Play); 42 | }; 43 | 44 | return Song; 45 | }; 46 | -------------------------------------------------------------------------------- /events/disabled/modBan.js: -------------------------------------------------------------------------------- 1 | module.exports = function(bot) { 2 | 3 | bot.on(PlugAPI.events.MODERATE_BAN, function (data) { 4 | 5 | if (config.verboseLogging) { 6 | console.log('[EVENT] modBan ', JSON.stringify(data, null, 2)); 7 | } 8 | 9 | var duration = 'unknown'; 10 | switch (data.duration) { 11 | case 'Hour': 12 | duration = '1 hour'; 13 | break; 14 | case 'Day': 15 | duration = '1 day'; 16 | break; 17 | case 'Forever': 18 | duration = 'forever'; 19 | break; 20 | } 21 | 22 | getDbUserFromUsername(data.user, function (dbUser) { 23 | var message; 24 | if (dbUser == null) { 25 | message = '[BAN] ' + data.user + ' was banned for ' + duration + ' by ' + data.moderator.username; 26 | } else { 27 | message = '[BAN] ' + data.user + ' (ID: ' + dbUser.site_id + ', LVL: ' + dbUser.site_points + ') was banned for ' + duration + ' by ' + data.moderator.username; 28 | } 29 | console.log(message); 30 | sendToWebhooks(message); 31 | 32 | getDbUserFromUsername(data.moderator.username, function (modUser) { 33 | var userData = { 34 | type: 'ban', 35 | details: 'Banned for ' + duration + ' by ' + data.moderator.username, 36 | user_id: dbUser.id, 37 | mod_user_id: modUser.id 38 | }; 39 | models.Karma.create(userData); 40 | }); 41 | }); 42 | 43 | }); 44 | }; -------------------------------------------------------------------------------- /commands/disabled/remove.js: -------------------------------------------------------------------------------- 1 | exports.names = ['remove', 'rm', 'rmafk', 'rmidle']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.RDJ_PLUS; 8 | exports.handler = function (data) { 9 | var command = _.first(data.message.split(' ')) 10 | var params = _.rest(data.message.split(' '), 1); 11 | var username = params.join(' ').trim() 12 | var usernameFormatted = S(username).chompLeft('@').s; 13 | var user = findUserInList(bot.getUsers(), usernameFormatted); 14 | if (user) { 15 | if (user.songsInQueue == 0) { 16 | bot.sendChat(user.username + ' does not have any songs queued'); 17 | return; 18 | } 19 | bot.moderateRemoveDJ(user.id); 20 | if (command === 'rmafk' || command === 'rmidle') { 21 | bot.sendChat('@' + user.username + ' ' + config.responses.activeDJRemoveMessage); 22 | } 23 | 24 | getDbUserFromSiteUser(user, function (row) { 25 | var userData = { 26 | type: 'remove', 27 | details: 'Removed ' + data.username + ' from the wait list', 28 | user_id: row.id, 29 | mod_user_id: data.from.db.id 30 | }; 31 | models.Karma.create(userData); 32 | var message = '[REMOVE] ' + data.from.username + ' removed ' + user.username + ' from the wait list'; 33 | console.log(message); 34 | sendToWebhooks(message); 35 | models.User.update({queue_position: -1}, {where: {id: row.id}}); 36 | }); 37 | 38 | } 39 | else { 40 | bot.sendChat(usernameFormatted + ' not found in the room'); 41 | } 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /functions/init.js: -------------------------------------------------------------------------------- 1 | const { readdirSync } = require("fs"); 2 | const reload = require('require-reload')(require); 3 | 4 | module.exports = function () { 5 | 6 | loadCommands = function () { 7 | 8 | // Load commands 9 | try { 10 | bot.commands = []; 11 | readdirSync("./commands/").forEach(function (file) { 12 | if (file.indexOf(".js") > -1) { 13 | var command = reload(`../commands/${file}`); 14 | command.lastRun = 0; 15 | command.lastRunUsers = {}; 16 | if (command.minRole === undefined) { 17 | command.minRole = PERMISSIONS.NONE; 18 | } 19 | bot.commands.push(command); 20 | } 21 | }); 22 | console.log("[INIT] Commands loaded..."); 23 | } catch (e) { 24 | console.error("Unable to load command: ", e.stack); 25 | } 26 | }; 27 | 28 | // @TODO Change events to work the same as commands (exports without a constructor?) 29 | loadEvents = function () { 30 | 31 | try { 32 | readdirSync("./events/").forEach(function (file) { 33 | if (file.indexOf(".js") > -1) { 34 | reload(`../events/${file}`)(); 35 | } 36 | }); 37 | console.log("[INIT] Events loaded..."); 38 | } catch (e) { 39 | console.error("Unable to load event: ", e.stack); 40 | } 41 | }; 42 | 43 | loadExtensions = function () { 44 | try { 45 | readdirSync("./extensions").forEach(function (file) { 46 | if (file.indexOf(".js") > -1) { 47 | reload(`../extensions/${file}`)(); 48 | } 49 | }); 50 | console.log("[INIT] Extensions loaded..."); 51 | } catch (e) { 52 | console.error("Unable to load extension: ", e.stack); 53 | } 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /functions/file.js: -------------------------------------------------------------------------------- 1 | const { writeFile } = require("fs"); 2 | 3 | module.exports = function () { 4 | console.log('Updating config state...'); 5 | writeConfigState = function () { 6 | /* 7 | writeFile( 8 | "../configState.json", 9 | JSON.stringify( 10 | config, 11 | function (key, value) { 12 | if (key == "parent") { 13 | return value.id; 14 | } else { 15 | return value; 16 | } 17 | }, 18 | 2 19 | ), 20 | 21 | function (err) { 22 | if (err) { 23 | console.log(err); 24 | return console.log(err); 25 | } 26 | } 27 | ); 28 | */ 29 | }; 30 | 31 | writeRoomState = function (permalink) { 32 | console.log('Updating room state...'); 33 | // Writes current room state to outfile so it can be used for the web 34 | if (config.roomStateFile) { 35 | var JSONstats = {}; 36 | 37 | JSONstats.media = bot.getMedia(); 38 | JSONstats.permalink = permalink; 39 | JSONstats.dj = bot.getDJ(); 40 | JSONstats.roomQueue = bot.getWaitList(); 41 | JSONstats.users = bot.getUsers(); 42 | JSONstats.staff = bot.getStaff(); 43 | JSONstats.lastPlay = bot.lastPlay; 44 | JSONstats.mediaHistory = bot.mediaHistory; 45 | 46 | writeFile( 47 | config.roomStateFile, 48 | JSON.stringify( 49 | JSONstats, 50 | function (key, value) { 51 | if (key == "parent") { 52 | return value.id; 53 | } else { 54 | return value; 55 | } 56 | }, 57 | 2 58 | ), 59 | 60 | function (err) { 61 | if (err) { 62 | console.log(err); 63 | return console.log(err); 64 | } 65 | } 66 | ); 67 | } 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /functions/webhook.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | sendToWebhooks = function (message) { 3 | if (message == "") { 4 | return false; 5 | } 6 | 7 | if (config.webhooks.discord.webhookUrl != null) { 8 | sendToDiscord(message); 9 | } 10 | 11 | if (config.webhooks.slack.webhookUrl != null) { 12 | sendToSlack(message); 13 | } 14 | }; 15 | 16 | sendToDiscord = function (message) { 17 | var formPayload = { 18 | content: message, 19 | username: bot.getSelf().username, 20 | avatar_url: config.webhooks.discord.default.avatarUrl, 21 | }; 22 | 23 | formPayload = JSON.stringify(formPayload); 24 | 25 | request.post(config.webhooks.discord.webhookUrl, { form: { payload_json: formPayload } }, function (error, response, body) { 26 | if (!error && response.statusCode == 200) { 27 | if (body == "ok") { 28 | return true; 29 | } else { 30 | return false; 31 | } 32 | } else { 33 | console.log(error); 34 | return false; 35 | } 36 | }); 37 | }; 38 | 39 | sendToSlack = function (message) { 40 | var formPayload = { 41 | text: message, 42 | username: bot.getSelf().username, 43 | link_names: 1, 44 | channel: config.webhooks.slack.default.channel, 45 | icon_url: config.webhooks.slack.default.iconUrl, 46 | }; 47 | 48 | formPayload = JSON.stringify(formPayload); 49 | 50 | request.post(config.webhooks.slack.webhookUrl, { form: { payload: formPayload } }, function (error, response, body) { 51 | if (!error && response.statusCode == 200) { 52 | if (body == "ok") { 53 | return true; 54 | } else { 55 | return false; 56 | } 57 | } else { 58 | console.log(error); 59 | return false; 60 | } 61 | }); 62 | }; 63 | return exports; 64 | }; 65 | -------------------------------------------------------------------------------- /events/disabled/modWaitlistBan.js: -------------------------------------------------------------------------------- 1 | module.exports = function (bot) { 2 | 3 | bot.on(PlugAPI.events.MODERATE_WLBAN, function (data) { 4 | 5 | if (config.verboseLogging) { 6 | console.log('[EVENT] modWaitlistBan ', JSON.stringify(data, null, 2)); 7 | } 8 | 9 | if (data.user == undefined) { 10 | console.log('[WARNING] data.user was undefined in modWaitlistBan event'); 11 | return; 12 | } 13 | 14 | var duration = 'unknown'; 15 | switch (data.duration) { 16 | case 'Short': 17 | duration = '15 min'; 18 | break; 19 | case 'Medium': 20 | duration = '30 min'; 21 | break; 22 | case 'Long': 23 | duration = '45 min'; 24 | break; 25 | case 'Forever': 26 | duration = 'forever'; 27 | break; 28 | default: 29 | // maybe this is an unmute? 30 | break; 31 | } 32 | 33 | getDbUserFromUsername(data.user.username, function (dbUser) { 34 | var message; 35 | if (duration == 'unknown') { 36 | message = '[WLUNBAN] ' + data.user.username + ' (ID: ' + data.user.id + ') was unbanned from the waitlist by ' + data.moderator.username; 37 | } else if (dbUser == null) { 38 | message = '[WLBAN] ' + data.user.username + ' (ID: ' + data.user.id + ') was banned from the waitlist for ' + duration + ' by ' + data.moderator.username; 39 | } else { 40 | message = '[WLBAN] ' + data.user.username + ' (ID: ' + data.user.id + ', LVL: ' + dbUser.site_points + ') was banned from the waitlist for ' + duration + ' by ' + data.moderator.username; 41 | } 42 | console.log(message); 43 | sendToWebhooks(message); 44 | }); 45 | }); 46 | 47 | }; 48 | -------------------------------------------------------------------------------- /commands/theme.js: -------------------------------------------------------------------------------- 1 | exports.names = ["theme"]; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | var input = _.rest(data.text.split(" "), 1); 10 | 11 | if (data.role >= PERMISSIONS.BOUNCER && input && input.length > 0) { 12 | var message = input.join(" "); 13 | 14 | if (message == "clear" && config.themeOverride) { 15 | delete config.themeOverride; 16 | bot.speak("The theme has been cleared!"); 17 | } else { 18 | config.themeOverride = message; 19 | bot.speak("The theme has been updated!"); 20 | } 21 | } else if (config.themeOverride) { 22 | bot.speak(config.themeOverride); 23 | } else { 24 | models.RoomEvent.findAll({ 25 | where: { 26 | type: "theme", 27 | starts_at: { 28 | lte: moment.utc().add(1, "day").toDate(), 29 | }, 30 | ends_at: { 31 | gte: new Date(), 32 | }, 33 | }, 34 | order: [["starts_at", "ASC"]], 35 | limit: 3, 36 | }).then(function (rows) { 37 | if (rows.length === 0) { 38 | bot.speak(config.responses.theme); 39 | } else { 40 | bot.speak( 41 | rows 42 | .map(function (row) { 43 | var message = row.title; 44 | if (row.details !== null) { 45 | message += " - " + row.details; 46 | } 47 | 48 | if (row.starts_at > moment.utc().toDate()) { 49 | message = timeUntil(row.starts_at, "starting") + " " + message; 50 | } else if (row.starts_at <= moment.utc().toDate()) { 51 | message += " " + timeUntil(row.ends_at, "ending"); 52 | } 53 | 54 | return message; 55 | }) 56 | .join(" • ") 57 | ); 58 | } 59 | }); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | let Sequelize = require("sequelize"); 2 | const { readdirSync } = require("fs"); 3 | let db = {}; 4 | 5 | let logLevel = false; 6 | if (config.verboseLogging) { 7 | logLevel = console.log; 8 | } 9 | 10 | let sequelize = undefined; 11 | if (config.db.dialect === "sqlite") { 12 | sequelize = new Sequelize(null, null, null, { 13 | dialect: config.db.dialect, 14 | storage: config.db.storage, 15 | logging: logLevel, 16 | }); 17 | } else if (config.db.dialect === "mariadb" || config.db.dialect === "mysql") { 18 | sequelize = new Sequelize(config.db.database, config.db.username, config.db.password, { 19 | host: config.db.host, 20 | dialect: config.db.dialect, 21 | port: config.db.port, 22 | logging: logLevel, 23 | charset: "utf8", 24 | retry: { match: "ER_LOCK_DEADLOCK: Deadlock found when trying to get lock; try restarting transaction", max: 3 }, 25 | }); 26 | } 27 | 28 | try { 29 | sequelize.authenticate().then(function () { 30 | console.log("Connected to " + config.db.dialect + " database: " + config.db.database); 31 | }); 32 | } catch (error) { 33 | console.error("Unable to connect to the database:", error); 34 | } 35 | 36 | // @TODO Do we do a sequelize sync here? 37 | try { 38 | sequelize.sync({ alter: config.db.forceSequelizeSync }).then(function () { 39 | console.log("Synced sequelize to database: " + config.db.database); 40 | }); 41 | } catch (error) { 42 | console.error("Unable to sync the database:", error); 43 | } 44 | 45 | readdirSync("./models").forEach(function (file) { 46 | if (file.indexOf(".js") > -1 && file !== "index.js") { 47 | let model = require("./" + file)(sequelize, Sequelize); 48 | db[model.name] = model; 49 | } 50 | }); 51 | 52 | Object.keys(db).forEach(function (modelName) { 53 | if (db[modelName].associate) { 54 | db[modelName].associate(db); 55 | } 56 | }); 57 | 58 | db.sequelize = sequelize; 59 | db.Sequelize = Sequelize; 60 | 61 | module.exports = db; 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BeavisBot 2 | ========== 3 | 4 | BeavisBot is a heavily-adapted port of the turntable.fm SparkleBot that has migrated from TT.fm to plug.dj to dubtrack.fm. 5 | 6 | This bot uses https://github.com/anjanms/DubAPI as its API dependency and is designed to be run using a node.JS instance. 7 | 8 | Quick Installation 9 | ----------------------- 10 | 1. Run `npm install` in the root folder of the checkout 11 | 2. Copy the config.sample.json from /documentation to the root folder and rename it config.json 12 | 3. Edit config.json to suit your needs 13 | 4a. If you are using MySQL, run the documentation/tables.sql file against the database to create all the necessary tables 14 | 4b. If you are using SQLite, copy the supplied sample.sqlite to the root folder and rename it to align with your settings in config.json 15 | 16 | Known issues are located at https://github.com/AvatarKava/beavisbot/issues - please submit any bug reports or feature requests there! 17 | 18 | Logging and Log Rotation 19 | ------------------------ 20 | 21 | Full details are here: https://github.com/Unitech/pm2 22 | 23 | Start the process in pm2 with custom log locations: 24 | ``` 25 | pm2 start /srv/web/apps/plug.dj/BeavisBot/bot.js --name beavisbot -o /var/log/node/beavisbot.log -e /var/log/node/beavisbot.err --log-date-format 'YYYY-MM-DD HH:mm:ss' 26 | ``` 27 | Set pm2 to automatically run on startup 28 | ``` 29 | pm2 startup 30 | ``` 31 | Save the processes running so they get restored any time pm2 is started 32 | ``` 33 | pm2 save 34 | ``` 35 | 36 | then in /etc/logrotate.d, create a file (name it whatever you like, "node" works well here) and use 37 | this or something along these lines as the contents: 38 | ``` 39 | /var/log/node/* { 40 | daily 41 | rotate 30 42 | missingok 43 | notifempty 44 | sharedscripts 45 | copytruncate 46 | compress 47 | delaycompress 48 | dateext 49 | } 50 | ``` 51 | This will do a daily rotation of the logs and save the last 30 days. -------------------------------------------------------------------------------- /commands/magic8ball.js: -------------------------------------------------------------------------------- 1 | exports.names = ["magic8ball", "8ball"]; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | var responses = [ 10 | "Signs point to yes.", 11 | "Yes.", 12 | "Reply hazy, try again.", 13 | "Without a doubt.", 14 | "My sources say no.", 15 | "As I see it, yes.", 16 | "You may rely on it.", 17 | "Concentrate and ask again.", 18 | "Outlook not so good.", 19 | "It is decidedly so.", 20 | "Better not tell you now.", 21 | "Very doubtful.", 22 | "Yes - definitely.", 23 | "It is certain.", 24 | "Cannot predict now.", 25 | "Most likely.", 26 | "Ask again later.", 27 | "My reply is no.", 28 | "Outlook good.", 29 | "Don't count on it.", 30 | "Yes, in due time.", 31 | "Definitely not.", 32 | "You will have to wait.", 33 | "I have my doubts.", 34 | "Outlook so so.", 35 | "Looks good to me!", 36 | "Who knows?", 37 | "Looking good!", 38 | "Probably.", 39 | "Are you kidding?", 40 | "Go for it!", 41 | "Don't bet on it.", 42 | "Forget about it.", 43 | "Nah, man.", 44 | "Does a bear shit in the woods? Not if it's a polar bear. So I'm afraid the answer is no.", 45 | "That was a dumb question. Think of a better one and try again.", 46 | "Do I look like your psychiatrist?", 47 | "My mind is telling me no, but my body, my body's telling me yes.", 48 | "I can't believe you just asked me that. You're a real piece of work, you know that?", 49 | "That is LITERALLY the dumbest question. No. Just... no.", 50 | "If I say no, will you be upset? Because it's no.", 51 | "Fo sho.", 52 | "Nope nope nope nope nope nope nope nope nope nope nope...", 53 | "Are tacos fucking delicious?", 54 | "YAAAAASSS", 55 | ]; 56 | var randomNumber = _.random(1, responses.length); 57 | bot.speak(":8ball: " + responses[randomNumber - 1] + " @" + data.name); 58 | }; 59 | -------------------------------------------------------------------------------- /commands/english.js: -------------------------------------------------------------------------------- 1 | exports.names = ['english']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.RDJ_PLUS; 8 | exports.handler = function (data) { 9 | var params = _.rest(data.text.split(' '), 1); 10 | var ch = ''; 11 | var lang = 'en'; 12 | 13 | 14 | if (params.length >= 1) { 15 | username = params.join(' ').trim(); 16 | usernameFormatted = S(username).chompLeft('@').s; 17 | // Currently TT doesn't support languages in profiles 18 | //var user = findUserInList(bot.getUsers(), usernameFormatted); 19 | //if (user) { 20 | ch += '@' + usernameFormatted + ' '; 21 | //lang = user.language; 22 | //} 23 | } 24 | 25 | switch (lang) { 26 | case 'da': 27 | ch += 'Vær venlig at tale engelsk.'; 28 | break; 29 | case 'sv': 30 | ch += 'Vänligen tala engelska.'; 31 | break; 32 | case 'de': 33 | ch += 'Bitte sprechen Sie Englisch.'; 34 | break; 35 | case 'es': 36 | ch += 'Por favor, hable Inglés.'; 37 | break; 38 | case 'fr': 39 | ch += 'Parlez anglais, s\'il vous plaît.'; 40 | break; 41 | case 'nl': 42 | ch += 'Spreek Engels, alstublieft.'; 43 | break; 44 | case 'pl': 45 | ch += 'Proszę mówić po angielsku.'; 46 | break; 47 | case 'pt': 48 | ch += 'Por favor, fale Inglês.'; 49 | break; 50 | case 'sk': 51 | ch += 'Hovorte po anglicky, prosím.'; 52 | break; 53 | case 'cs': 54 | ch += 'Mluvte prosím anglicky.'; 55 | break; 56 | case 'sr': 57 | ch += 'Молим Вас, говорите енглески.'; 58 | break; 59 | default: 60 | ch += 'This is an English-speaking community. Please use English in chat.'; 61 | break; 62 | } 63 | 64 | bot.speak(ch); 65 | 66 | }; -------------------------------------------------------------------------------- /events/endsong.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("endsong", function (data) { 3 | const song = data.room.metadata.current_song; 4 | if (song.source == "yt") { 5 | song.source = "youtube"; 6 | } 7 | 8 | if (config.verboseLogging) { 9 | console.log("[SONG END]", JSON.stringify(data, null, 2)); 10 | } else { 11 | console.log(`[SONG END] ${song.djname} (${song.djid}) played: ${song.metadata.artist} - ${song.metadata.song} (${song.source}: ${song.sourceid})`); 12 | } 13 | 14 | // Write current song data to DB 15 | const songData = { 16 | author: song.metadata.artist, 17 | title: song.metadata.song, 18 | name: song.metadata.artist + " - " + song.metadata.song, 19 | host: song.source, 20 | host_id: song.sourceid, 21 | duration: song.metadata.length, 22 | image: song.metadata.coverart, 23 | }; 24 | 25 | models.Song.upsert(songData, { returning: true }) 26 | .then(() => { 27 | let userRecord = models.User.findOne({ where: { site: config.site, site_id: song.djid } }); 28 | let songRecord = models.Song.findOne({ where: { host: song.source, host_id: song.sourceid } }); 29 | return Promise.all([userRecord, songRecord]); 30 | }) 31 | .then(([userRecord, songRecord]) => { 32 | if (!userRecord) throw new Error("No user found to associate on song insertion"); 33 | if (!songRecord) throw new Error("No song found - maybe the insert failed?"); 34 | 35 | songRecord.createPlay({ 36 | UserId: userRecord.dataValues.id, 37 | site: config.site, 38 | positive: data.room.metadata.upvotes, 39 | negative: data.room.metadata.downvotes, 40 | grabs: roomState.snags, 41 | listeners: data.room.metadata.listeners, 42 | skipped: false, // @FIXME any way to detect this? 43 | }); 44 | 45 | let pointsToAward = Math.floor(roomState.snags + (data.room.metadata.upvotes / 10) - data.room.metadata.downvotes); 46 | if (pointsToAward > 0) { 47 | transferCustomPoints(null, song.djid, pointsToAward); 48 | } 49 | 50 | //writeRoomState(); 51 | }) 52 | .catch((err) => console.log("[ERROR]", err)); 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /commands/disabled/ban.js: -------------------------------------------------------------------------------- 1 | exports.names = ['ban', 'unban']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.BOUNCER; 8 | exports.handler = function (data) { 9 | 10 | var input = data.message.split(' '); 11 | var command = _.first(input); 12 | var params = _.rest(input); 13 | var username = ''; 14 | var duration = 'HOUR'; 15 | var message = ''; 16 | 17 | if (params.length >= 2) { 18 | username = _.initial(params).join(' ').trim(); 19 | duration = _.last(params).toUpperCase(); 20 | } else if (params.length >= 1) { 21 | username = params.join(' ').trim(); 22 | } else { 23 | bot.sendChat('Usage: .[ban|unban|kick] username [PERMA|DAY|HOUR]'); 24 | return; 25 | } 26 | 27 | var usernameFormatted = S(username).chompLeft('@').s; 28 | 29 | switch (duration) { 30 | case 'DAY': 31 | apiDuration = PlugAPI.BAN.DAY; 32 | break; 33 | case 'PERMA': 34 | apiDuration = PlugAPI.BAN.PERMA; 35 | break; 36 | case 'HOUR': 37 | default: 38 | apiDuration = PlugAPI.BAN.HOUR; 39 | break; 40 | 41 | } 42 | 43 | models.User.find({ 44 | where: { 45 | username: usernameFormatted, 46 | site: config.site 47 | }, 48 | order: 'id DESC' 49 | }).then(function (row) { 50 | if (row === null) { 51 | bot.sendChat(usernameFormatted + ' was not found.'); 52 | } else { 53 | switch (command) { 54 | case 'ban': 55 | console.log('[BAN] ' + data.from.username + ' attempting to ban ' + usernameFormatted + ' for ' + duration + ' (' + apiDuration + ')'); 56 | bot.moderateBanUser(row.site_id, PlugAPI.BAN_REASON.OFFENSIVE_MEDIA, apiDuration); 57 | break; 58 | case 'unban': 59 | bot.moderateUnbanUser(row.site_id, function () { 60 | bot.sendChat('unbanning ' + usernameFormatted + '. This can take a few moments...'); 61 | }); 62 | break; 63 | } 64 | } 65 | }); 66 | 67 | 68 | }; -------------------------------------------------------------------------------- /commands/disabled/wlban.js: -------------------------------------------------------------------------------- 1 | exports.names = ['wlban', 'wlunban']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.BOUNCER; 8 | exports.handler = function (data) { 9 | 10 | var input = data.message.split(' '); 11 | var command = _.first(input); 12 | var params = _.rest(input); 13 | var username = ''; 14 | var duration = 'HOUR'; 15 | var message = ''; 16 | 17 | if (params.length >= 2) { 18 | username = _.initial(params).join(' ').trim(); 19 | duration = _.last(params).toUpperCase(); 20 | } else if (params.length >= 1) { 21 | username = params.join(' ').trim(); 22 | } else { 23 | bot.sendChat('Usage: .[wlban|wlunban] username [15|HOUR|DAY|PERMA]'); 24 | return; 25 | } 26 | 27 | var usernameFormatted = S(username).chompLeft('@').s; 28 | 29 | switch (duration) { 30 | case '15': 31 | apiDuration = bot.WLBAN.SHORT; 32 | break; 33 | case 'DAY': 34 | apiDuration = bot.WLBAN.LONG; 35 | break; 36 | case 'PERMA': 37 | apiDuration = bot.WLBAN.PERMA; 38 | break; 39 | case 'HOUR': 40 | default: 41 | apiDuration = bot.WLBAN.MEDIUM; 42 | break; 43 | } 44 | 45 | models.User.find({ 46 | where: { 47 | username: usernameFormatted, 48 | site: config.site 49 | }, 50 | order: 'id DESC' 51 | }).then(function (row) { 52 | if (row === null) { 53 | bot.sendChat(usernameFormatted + ' was not found.'); 54 | } else { 55 | switch (command) { 56 | case 'wlban': 57 | console.log('[WLBAN] ' + data.from.username + ' attempting to ban ' + usernameFormatted + ' for ' + duration + ' (' + apiDuration + ')'); 58 | bot.moderateWaitListBan(row.site_id, bot.WLBAN_REASON.INAPPROPRIATE_GENRE, apiDuration); 59 | break; 60 | case 'wlunban': 61 | bot.moderateWaitListUnbanUser(row.site_id, function () { 62 | bot.sendChat('unbanning ' + usernameFormatted + '. This can take a few moments...'); 63 | }); 64 | break; 65 | } 66 | } 67 | }); 68 | }; -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | var User = sequelize.define( 3 | "User", 4 | { 5 | id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true }, 6 | site: { type: DataTypes.STRING, allowNull: false, defaultValue: "turntable", unique: "site_id" }, 7 | site_id: { type: DataTypes.STRING, allowNull: false, unique: "site_id" }, 8 | username: { type: DataTypes.STRING, allowNull: false }, 9 | slug: { type: DataTypes.STRING, allowNull: false }, 10 | locale: { type: DataTypes.STRING, defaultValue: "en_US" }, 11 | avatar: { type: DataTypes.STRING }, 12 | badge: { type: DataTypes.STRING }, 13 | bio: { type: DataTypes.TEXT }, 14 | role: { type: DataTypes.STRING }, 15 | site_points: { type: DataTypes.INTEGER.UNSIGNED, defaultValue: 0 }, 16 | custom_points: { type: DataTypes.INTEGER.UNSIGNED, defaultValue: 0 }, 17 | joined: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, 18 | birthday: { type: DataTypes.DATEONLY }, 19 | queue_position: { type: DataTypes.INTEGER, defaultValue: -1 }, 20 | last_seen: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, 21 | last_active: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, 22 | last_leave: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, 23 | dj_timeout_until: { type: DataTypes.DATE, allowNull: true }, 24 | }, 25 | { 26 | underscored: true, 27 | tableName: "users", 28 | setterMethods: { 29 | username: function (v) { 30 | this.setDataValue("slug", S.slugify(v)); 31 | return this.setDataValue("username", v); 32 | }, 33 | slug: function (v) { 34 | return this.setDataValue("slug", S.slugify(v)); 35 | }, 36 | }, 37 | } 38 | ); 39 | 40 | User.associate = function (models) { 41 | User.hasMany(models.Game, { as: "UserGames", foreignKey: "user_id" }); 42 | User.hasMany(models.Game, { as: "ModUserGames", foreignKey: "mod_user_id" }); 43 | User.hasMany(models.Karma, { as: "UserKarmas", foreignKey: "user_id" }); 44 | User.hasMany(models.Karma, { as: "ModUserKarmas", foreignKey: "mod_user_id" }); 45 | User.hasMany(models.Play, { as: "UserPlays", foreignKey: "user_id" }); 46 | User.hasMany(models.RoomEvent, { as: "ModUserRoomEvents", foreignKey: "mod_user_id" }); 47 | User.hasMany(models.UserAlias, { as: "UserAliases", foreignKey: "user_id"}); 48 | }; 49 | 50 | return User; 51 | }; 52 | -------------------------------------------------------------------------------- /commands/disabled/debug.js: -------------------------------------------------------------------------------- 1 | exports.names = ['debug']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.MANAGER; 8 | exports.handler = function (data) { 9 | 10 | var input = data.message.split(' '); 11 | var debugData = null; 12 | 13 | console.log('[DEBUG] ' + JSON.stringify(data, null, 2)); 14 | 15 | switch (input[1]) { 16 | case 'admins': 17 | debugData = bot.getAdmins(); 18 | break; 19 | case 'ambassadors': 20 | debugData = bot.getAmbassadors(); 21 | break; 22 | case 'audience': 23 | debugData = bot.getAudience(); 24 | break; 25 | case 'dj': 26 | debugData = bot.getDJ(); 27 | break; 28 | case 'djs': 29 | debugData = bot.getDJs(); 30 | break; 31 | case 'history': 32 | debugData = bot.getHistory(); 33 | break; 34 | case 'host': 35 | debugData = bot.getHost(); 36 | break; 37 | case 'media': 38 | debugData = bot.getMedia(); 39 | break; 40 | case 'roomScore': 41 | debugData = bot.getRoomScore(); 42 | break; 43 | case 'self': 44 | debugData = bot.getSelf(); 45 | break; 46 | case 'staff': 47 | debugData = bot.getStaff(); 48 | break; 49 | case 'timeElapsed': 50 | debugData = bot.getTimeElapsed(); 51 | break; 52 | case 'timeRemaining': 53 | debugData = bot.getTimeRemaining(); 54 | break; 55 | case 'user': 56 | debugData = bot.getUser(input[2]); 57 | break; 58 | case 'waitList': 59 | debugData = bot.getWaitList(); 60 | break; 61 | case 'waitListPosition': 62 | debugData = bot.getWaitListPosition(parseInt(input[2])); 63 | break; 64 | default: 65 | bot.sendChat('Command not supported.'); 66 | return; 67 | break; 68 | } 69 | 70 | if (debugData !== null) { 71 | bot.sendChat('Debugging data logged to the console, @' + data.from.username + '...'); 72 | console.log('[DEBUG] ', data.message + ': ' + JSON.stringify(debugData, null, 2)); 73 | } else { 74 | console.log('[DEBUG] ', data.message + ': null returned'); 75 | } 76 | 77 | }; 78 | 79 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | // Utility libraries used throughout the app 2 | global._ = require("underscore"); 3 | global.S = require("underscore.string"); 4 | global.moment = require("moment"); 5 | 6 | 7 | const { existsSync, readFileSync, readdirSync } = require("fs"); 8 | require("popyt"); 9 | //import { decode, encode } from "html-entities"; 10 | 11 | // Reload the last existing state of the config file, otherwise revert to the default 12 | global.config = {}; 13 | if (existsSync("configState.json")) { 14 | config = JSON.parse(readFileSync("configState.json", "utf-8")); 15 | console.log("Loaded config file from configState.json"); 16 | } else { 17 | config = JSON.parse(readFileSync("config.json", "utf-8")); 18 | console.log("Loaded config file from config.json"); 19 | } 20 | 21 | global.models = require('./models/index'); 22 | 23 | // @FIXME - YouTube connectivity 24 | /* 25 | if (config.apiKeys.youtube !== undefined) { 26 | console.log("[YOUTUBE]", "Authenticating with youtube..."); 27 | var oauth = YouTube.authenticate({ 28 | type: "key", 29 | key: config.apiKeys.youtube.api_key, 30 | }); 31 | console.log("[YOUTUBE]", "Authenticated! " + JSON.stringify(oauth, null, 2)); 32 | } 33 | */ 34 | 35 | /** 36 | * Custom functions accessible to commands 37 | */ 38 | 39 | /* 40 | const getActiveDJs = function (maxIdleMins, startPosition, callback) { 41 | var activeUsers = []; 42 | if (startPosition === undefined) { 43 | startPosition = 0; 44 | } 45 | 46 | Promise.map(_.rest(bot.getDJs(), startPosition), function (dj) { 47 | return models.User.find({ 48 | where: { site_id: dj.id, site: config.site }, 49 | }).then(function (dbUser) { 50 | if (dbUser !== null && dbUser.site_id !== bot.getSelf().id) { 51 | if (secondsSince(dbUser.last_active) <= maxIdleMins * 60) { 52 | activeUsers.push(dbUser); 53 | } 54 | } 55 | }); 56 | }).then(function () { 57 | callback(activeUsers); 58 | }); 59 | }; 60 | */ 61 | 62 | const Bot = require("ttapi"); 63 | global.bot = new Bot(config.auth.authKey, config.auth.userId); 64 | bot.commands = []; 65 | bot.user = {}; 66 | //bot.debug = config.verboseLogging; 67 | 68 | global.roomState = { 69 | mentions: { lastRunAll : 0, lastRunUsers: [] }, 70 | snags: 0, 71 | }; 72 | 73 | require("./globals.js")(); 74 | 75 | try { 76 | readdirSync("./functions").forEach(function (file) { 77 | if (file.indexOf(".js") > -1) { 78 | require(`./functions/${file}`)(); 79 | } 80 | }); 81 | } catch (e) { 82 | console.error("Unable to load function: ", e.stack); 83 | } 84 | 85 | loadEvents(); 86 | loadCommands(); 87 | loadExtensions(); 88 | -------------------------------------------------------------------------------- /commands/disabled/idle.js: -------------------------------------------------------------------------------- 1 | exports.names = ['idle']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.RDJ_PLUS; 8 | exports.handler = function (data) { 9 | 10 | var input = data.message.split(' '); 11 | 12 | if (config.queue.djIdleAfterMins > 0) { 13 | 14 | if (input.length >= 2) { 15 | var username = _.rest(input, 1); 16 | var usernameFormatted = S(username).chompLeft('@').s; 17 | 18 | models.User.update({ 19 | last_active: new Date(), 20 | last_seen: new Date() 21 | }, {where: {username: usernameFormatted}}); 22 | bot.sendChat("reset the idle timer for " + usernameFormatted); 23 | console.log('[IDLE]', data.from.username + ' reset the idle timer for ' + usernameFormatted); 24 | 25 | 26 | } 27 | else if (secondsSince(startupTimestamp) < config.queue.djIdleAfterMins * 60) { 28 | bot.sendChat("I've been connected less than " + config.queue.djIdleAfterMins + " minutes"); 29 | } 30 | else { 31 | var maxIdleTime = config.queue.djIdleAfterMins * 60; 32 | idleDJs = []; 33 | 34 | Promise.map(bot.getWaitList(), function (dj) { 35 | return models.User.find({where: {site_id: dj.id}}).then(function (dbUser) { 36 | var position = bot.getWaitListPosition(dj.id); 37 | if (dbUser !== null) { 38 | if (secondsSince(dbUser.last_active) >= maxIdleTime && moment.utc().isAfter(moment.utc(startupTimestamp).add(config.queue.djIdleAfterMins, 'minutes'))) { 39 | console.log('[WL-IDLE]', position + '. ' + dbUser.username + ' last active ' + timeSince(dbUser.last_active)); 40 | idleDJs.push(dbUser.username + ' (' + timeSince(dbUser.last_active, true) + ')'); 41 | 42 | } 43 | else { 44 | console.log('[WL-ACTIVE]', position + '. ' + dbUser.username + ' last active ' + timeSince(dbUser.last_active)); 45 | } 46 | } 47 | }); 48 | }).then(function () { 49 | if (idleDJs.length > 0) { 50 | var idleDJsList = idleDJs.join(' • '); 51 | bot.sendChat("Currently idle: " + idleDJsList); 52 | } 53 | else { 54 | bot.sendChat("Everyone's currently active! :thumbsup:"); 55 | } 56 | }); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /functions/points.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | attemptPurchase = function (user, points, callback) { 3 | getDbUserFromSiteUser(user, function (row) { 4 | if (!row || row.custom_points < points) { 5 | console.log("[POINTS] Purchase failed: " + row.username + " only has " + row.custom_points + " points (" + points + " needed)"); 6 | bot.sendChat("Sorry @" + row.username + ", you need " + points + config.customPointName + " for that."); 7 | callback(false); 8 | return; 9 | } 10 | 11 | // Deduct the points from the sender's balance and add to the recipient 12 | models.User.update( 13 | { 14 | custom_points: models.sequelize.literal("(custom_points - " + points + ")"), 15 | }, 16 | { where: { site_id: row.site_id } } 17 | ); 18 | console.log("[POINTS] " + row.username + " spent " + points + " points"); 19 | callback(true); 20 | }); 21 | return; 22 | }; 23 | 24 | transferCustomPoints = function (fromUserId, toUserId, points) { 25 | getDbUserFromUserId(toUserId, function (toUser) { 26 | // Create them out of thin air! 27 | if (fromUserId === null) { 28 | getDbUserFromUserId(bot.user.userid, function (fromUser) { 29 | models.User.update( 30 | { 31 | custom_points: models.sequelize.literal("(custom_points + " + points + ")"), 32 | }, 33 | { where: { site_id: toUserId } } 34 | ); 35 | console.log(`[GIFT] ${fromUser.username} awarded ${points} points to ${toUser.username}`); 36 | bot.speak(`:gift: ${fromUser.username} awarded ${points} ${config.customPointName} to @${toUser.username}`); 37 | return; 38 | }); 39 | } else { 40 | getDbUserFromUserId(fromUserId, function (row) { 41 | if (!row || row.custom_points < points) { 42 | console.log("Gift failed"); 43 | return false; 44 | } 45 | 46 | // Deduct the points from the sender's balance and add to the recipient 47 | models.User.update( 48 | { 49 | custom_points: models.sequelize.literal("(custom_points - " + points + ")"), 50 | }, 51 | { where: { site_id: fromUserId } } 52 | ); 53 | models.User.update( 54 | { 55 | custom_points: models.sequelize.literal("(custom_points + " + points + ")"), 56 | }, 57 | { where: { site_id: toUserId } } 58 | ); 59 | 60 | console.log(`[GIFT] ${fromUser.username} gave ${points} points to ${toUser.username}`); 61 | bot.speak(`:gift: @${fromUser.username} gave ${points} ${config.customPointName} to @${toUser.username} :gift:`); 62 | }); 63 | } 64 | }); 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /functions/media.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | correctMetadata = function () { 3 | var media = bot.getMedia(); 4 | 5 | // first, see if the song exists in the db 6 | //db.get('SELECT id FROM SONGS WHERE id = ?', [media.id], function (error, row) { 7 | // if (row == null) { 8 | // // if the song isn't in the db yet, check it for suspicious strings 9 | // artistTitlePair = S((media.author + ' ' + media.title).toLowerCase()); 10 | // if (artistTitlePair.contains('official music video') 11 | // || artistTitlePair.contains('lyrics') 12 | // || artistTitlePair.contains('|') 13 | // || artistTitlePair.contains('official video') 14 | // || artistTitlePair.contains('[') 15 | // || artistTitlePair.contains('"') 16 | // || artistTitlePair.contains('*') 17 | // || artistTitlePair.contains('(HD)') 18 | // || artistTitlePair.contains('(HQ)') 19 | // || artistTitlePair.contains('1080p') 20 | // || artistTitlePair.contains('720p') 21 | // || artistTitlePair.contains(' - ') 22 | // || artistTitlePair.contains('full version') 23 | // || artistTitlePair.contains('album version')) { 24 | // suggestNewSongMetadata(media.author + ' ' + media.title); 25 | // } 26 | // } 27 | //}); 28 | }; 29 | 30 | const suggestNewSongMetadata = function (valueToCorrect) { 31 | var media = bot.getMedia(); 32 | // @FIXME - don't use the room. construct. 33 | //request('http://developer.echonest.com/api/v4/song/search?api_key=' + config.apiKeys.echoNest + '&format=json&results=1&combined=' + S(valueToCorrect).escapeHTML().stripPunctuation().s, function (error, response, body) { 34 | // console.log('echonest body', body); 35 | // if (error) { 36 | // bot.sendChat('An error occurred while connecting to EchoNest.'); 37 | // bot.error('EchoNest error', error); 38 | // } else { 39 | // response = JSON.parse(body).response; 40 | // 41 | // room.media.suggested = { 42 | // author: response.songs[0].artist_name, 43 | // title: response.songs[0].title 44 | // }; 45 | // 46 | // // log 47 | // console.log('[EchoNest] Original: "' + media.author + '" - "' + media.title + '". Suggestion: "' + room.media.suggested.author + '" - "' + room.media.suggested.title); 48 | // 49 | // if (media.author != room.media.suggested.author || media.title != room.media.suggested.title) { 50 | // bot.sendChat('Hey, the metadata for this song looks wrong! Suggested Artist: "' + room.media.suggested.author + '". Title: "' + room.media.suggested.title + '". Type ".fixsong yes" to use the suggested tags.'); 51 | // } 52 | // } 53 | //}); 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /documentation/p3.sample.php: -------------------------------------------------------------------------------- 1 | 10 | { 11 | "instructions": "http://issue.plugcubed.net/wiki/Plug3%3ARSS", 12 | "author": "AvatarKava", 13 | "colors": { 14 | "background": "", 15 | "chat": { 16 | "admin": "", 17 | "ambassador": "", 18 | "bouncer": "", 19 | "cohost": "", 20 | "residentdj": "", 21 | "host": "", 22 | "manager": "" 23 | }, 24 | "footer": "", 25 | "header": "" 26 | }, 27 | "css": { 28 | "font": [ 29 | ], 30 | "import": [ 31 | ], 32 | "rule": { 33 | "#chat .mention": { 34 | "background-color": "rgba(24, 0, 38, 0.5) !important", 35 | "border": "1px solid #009cdd", 36 | "border-left": "0px !important", 37 | "border-right": "0px !important", 38 | "border-collapse": "collapse !important" 39 | }, 40 | "#chat .mention.is-staff": { 41 | }, 42 | "#chat .welcome": { 43 | "background-color": "rgba(24, 0, 38, 0.5) !important", 44 | "border": "3px solid #009cdd", 45 | "border-left": "0px !important", 46 | "border-right": "0px !important", 47 | "font-weight": "bold", 48 | "padding": "5px 2px 5px 25px" 49 | }, 50 | "#chat .from-": { 51 | "border-left": "#ac76ff 0px solid", 52 | "background-color": "rgba(24, 0, 38, 0.5) !important" 53 | }, 54 | "#chat .from- .from.staff.clickable": { 55 | }, 56 | ".from- .icon-chat-manager": { 57 | "background": "url('//beavisbot.phpmonkeys.com/images/icons/beavis_icon.png') !important" 58 | }, 59 | "#chat .from- .from": { 60 | }, 61 | "#chat .from- .text": { 62 | "color": "rgb(238, 238, 238) !important", 63 | "font-style": "normal", 64 | "font-weight": "normal" 65 | }, 66 | "#chat .nxnotif": { 67 | "border": "0 !important" 68 | } 69 | } 70 | }, 71 | "images": { 72 | "background": "//beavisbot.phpmonkeys.com/images/background/default.jpg", 73 | "booth": "", 74 | "icons": { 75 | "admin": "", 76 | "ambassador": "", 77 | "bouncer": "", 78 | "cohost": "", 79 | "residentdj": "", 80 | "host": "", 81 | "manager": "" 82 | }, 83 | "playback": "//beavisbot.phpmonkeys.com/images/tv-background.gif" 84 | }, 85 | "rules": { 86 | "allowAutorespond": "false", 87 | "allowAutowoot": "true", 88 | "allowAutojoin": "false" 89 | } 90 | } -------------------------------------------------------------------------------- /events/disabled/djListUpdate.js: -------------------------------------------------------------------------------- 1 | module.exports = function (bot) { 2 | 3 | bot.on(PlugAPI.events.DJ_LIST_UPDATE, function (data) { 4 | if (config.verboseLogging) { 5 | console.log('[EVENT] djListUpdate ', JSON.stringify(data, null, 2)); 6 | } 7 | saveWaitList(false); 8 | 9 | data.forEach(function (listUser) { 10 | getDbUserFromSiteUser(listUser, function (row) { 11 | remove = false; 12 | var banLength = bot.WLBAN.SHORT; 13 | 14 | if (listUser['role'] == 0 && listUser['gRole'] == 0 && listUser['level'] < config.queue.djMinLevel) { 15 | logMessage = '[REMOVE] Removed ' + listUser.username + ' joining the wait list below minimum level ' + config.queue.djMinLevel; 16 | message = 'Sorry @' + listUser.username + ', this community does not allow joining the wait list until you reach level ' + config.queue.djMinLevel + '. Enjoy listening until then!'; 17 | banLength = bot.WLBAN.SHORT; 18 | remove = true; 19 | } else { 20 | // @TODO - This is not really needed thanks to the waitlist ban function on plug 21 | if (secondsUntil(row.dj_timeout_until) > 0) { 22 | expiry = moment.utc(row.dj_timeout_until).format('MMM DD, YYYY') + ' at ' + moment.utc(row.dj_timeout_until).format('h:mmA') + ' UTC'; 23 | logMessage = '[REMOVE] Removed ' + listUser.username + ' joining the wait list while timed out until ' + expiry; 24 | message = 'Sorry @' + listUser.username + ', you are prohibited from entering the wait list until ' + expiry + ' ' + timeUntil(row.dj_timeout_until) + '. Enjoy listening until then!'; 25 | banLength = bot.WLBAN.SHORT; 26 | remove = true; 27 | } 28 | } 29 | 30 | if (remove) { 31 | console.log(logMessage); 32 | sendToWebhooks(logMessage); 33 | bot.sendChat(message); 34 | bot.moderateWaitListBan(row.site_id, bot.WLBAN_REASON.INAPPROPRIATE_GENRE, banLength); 35 | models.User.update({ 36 | queue_position: -1 37 | }, { 38 | where: { 39 | id: row.id 40 | } 41 | }); 42 | 43 | var data = { 44 | type: 'ban', 45 | details: 'Banned ' + row.username + ' from the wait list', 46 | username: row.username, 47 | site_id: listUser.id, 48 | user_id: row.id, 49 | mod_user_id: botUser.db.id, 50 | message: '[WLBAN] Banned ' + row.username + ' from the wait list for 15 minutes (' + banLength + ')' 51 | } 52 | addKarma(data); 53 | } 54 | }); 55 | }); 56 | 57 | }); 58 | 59 | } -------------------------------------------------------------------------------- /globals.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | 3 | PERMISSIONS = { 4 | NONE: 0, 5 | RDJ: 1, 6 | RDJ_PLUS: 1.5, 7 | BOUNCER: 2, 8 | BOUNCER_PLUS: 2.5, 9 | MANAGER: 3, 10 | COHOST: 4, 11 | HOST: 5, 12 | }; 13 | 14 | SETTING_NAMES = { 15 | autoskip: "Autoskip", 16 | timeguard: "Timeguard", 17 | maxdctime: "DC Protection Time", 18 | maxsonglength: "Max Song Length", 19 | maxdjidletime: "Max DJ Idle Time", 20 | djidleminqueue: "DJ Idle Min Queue", 21 | djcyclemaxqueue: "DJ Cycle Max Queue", 22 | lockdown: "Lockdown Mode", 23 | cleverbot: "Cleverbot", 24 | rdjplus: "RDJ+ Mode", 25 | bouncerplus: "Bouncer+ Mode", 26 | }; 27 | 28 | ISO_LANGUAGES = { 29 | af: "Afrikkans", 30 | ar: "Arabic", 31 | be: "Belarusian", 32 | bg: "Bulgarian", 33 | ca: "Catalan", 34 | cs: "Czech", 35 | da: "Danish", 36 | de: "German", 37 | el: "Greek", 38 | en: "English", 39 | es: "Spanish", 40 | et: "Estonian", 41 | eu: "Basque", 42 | fa: "Farsi", 43 | fi: "Finnish", 44 | fo: "Faeroese", 45 | fr: "French", 46 | ga: "Irish", 47 | gd: "Gaelic", 48 | hi: "Hindi", 49 | hr: "Croatian", 50 | hu: "Hungarian", 51 | id: "Indonesian", 52 | is: "Icelandic", 53 | it: "Italian", 54 | ja: "Japanese", 55 | ji: "Yiddish", 56 | ko: "Korean", 57 | ku: "Kurdish", 58 | lt: "Lithuanian", 59 | lv: "Latvian", 60 | mk: "Macedonian", 61 | ml: "Malayalam", 62 | ms: "Malasian", 63 | mt: "Maltese", 64 | nl: "Dutch", 65 | nb: "Norwegian", 66 | no: "Norwegian", 67 | pa: "Punjabi", 68 | pl: "Polish", 69 | pt: "Portuguese", 70 | rm: "Rhaeto-Romanic", 71 | ro: "Romanian", 72 | ru: "Russian", 73 | sb: "Sorbian", 74 | sk: "Slovak", 75 | sl: "Slovenian", 76 | sq: "Albanian", 77 | sr: "Serbian", 78 | sv: "Swedish", 79 | th: "Thai", 80 | tn: "Tswana", 81 | tr: "Turkish", 82 | ts: "Tsonga", 83 | uk: "Ukranian", 84 | ur: "Urdu", 85 | ve: "Venda", 86 | vi: "Vietnamese", 87 | xh: "Xhosa", 88 | zh: "Chinese", 89 | zu: "Zulu", 90 | }; 91 | 92 | settings = { 93 | autoskip: false, 94 | timeguard: false, 95 | maxdctime: 15 * 60, 96 | maxsonglength: config.queue.maxSongLengthSecs, 97 | maxdjidletime: config.queue.djIdleAfterMins * 60, 98 | djidle: false, 99 | djidleminqueue: config.queue.djIdleMinQueueLengthToEnforce, 100 | djcyclemaxqueue: config.queue.djCycleMaxQueueLength, 101 | lockdown: false, 102 | cleverbot: false, 103 | rdjplus: false, 104 | bouncerplus: false, 105 | }; 106 | 107 | uptime = new Date(); 108 | lastRpcMessage = new Date(); 109 | roomHasActiveStaff = true; 110 | 111 | /** 112 | * Set default time thresholds for moment 113 | * (round up a little less aggressively) 114 | */ 115 | moment.relativeTimeThreshold("s", 55); 116 | moment.relativeTimeThreshold("m", 90); 117 | moment.relativeTimeThreshold("h", 24); 118 | moment.relativeTimeThreshold("d", 30); 119 | moment.relativeTimeThreshold("M", 12); 120 | 121 | startupTimestamp = moment.utc().toDate(); 122 | 123 | }; 124 | -------------------------------------------------------------------------------- /commands/disabled/blocked.js: -------------------------------------------------------------------------------- 1 | exports.names = ['blocked']; 2 | exports.hidden = false; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.NONE; 8 | exports.handler = function (data) { 9 | 10 | function getBlockedCountries(ytid, func) { 11 | var reqparams = {ytid: ytid}; 12 | request({ 13 | url: 'http://polsy.org.uk/stuff/ytrestrict.cgi?', 14 | qs: reqparams, 15 | method: 'GET' 16 | }, function (error, response, body) { 17 | if (error) { 18 | console.log(error); 19 | func(null); 20 | } else { 21 | try { 22 | var tablestart = ''; 23 | var copy = body.replace(/(?:\r\n|\r|\n)/g, ''); 24 | var index = copy.indexOf(tablestart); 25 | if (index > -1) { 26 | var index2 = copy.indexOf("
"); 27 | copy = copy.substr(index + tablestart.length, index2); 28 | } 29 | copy = copy.split(""); 30 | 31 | var status = ''; 32 | var allowed = []; 33 | var blocked = []; 34 | var htmltagregex = /(<([^>]+)>)/ig 35 | for (var i = 0; i < copy.length; i++) { 36 | if (copy[i].substr(0, 4) == '') { 37 | var rest = copy[i].substr(4); 38 | index = rest.indexOf(''); 39 | if (index > -1) { 40 | var country = rest.substr(index + 4).replace(htmltagregex, "").split("-"); 41 | if (country.length > 1) { 42 | country.splice(0, 1); 43 | } 44 | country = country.join('-').trim(); 45 | if (country != '') 46 | blocked.push(country); 47 | } 48 | } 49 | } 50 | 51 | func(blocked); 52 | } 53 | catch (error) { 54 | func(null); 55 | } 56 | } 57 | }); 58 | } 59 | 60 | if (bot.getMedia() !== undefined && bot.getMedia().cid !== undefined) { 61 | 62 | getBlockedCountries(bot.getMedia().cid, function (blockedcountries) { 63 | 64 | if (blockedcountries === undefined || blockedcountries === null) { 65 | bot.sendChat('Sorry, I was\'nt able to check for blocked countries'); 66 | } 67 | else if (blockedcountries.length == 0) { 68 | bot.sendChat('Yay! This song has no restrictions'); 69 | } 70 | else if (blockedcountries.length > 24) { 71 | bot.sendChat('This song is blocked in ' + blockedcountries.length + ' countries, so I won\'t list them all.'); 72 | } 73 | else { 74 | bot.sendChat('This song is blocked in: ' + blockedcountries.join(', ')); 75 | } 76 | }); 77 | } 78 | else { 79 | bot.sendChat("There's no song running or it's not from YouTube"); 80 | } 81 | 82 | }; -------------------------------------------------------------------------------- /commands/disabled/lottery.js: -------------------------------------------------------------------------------- 1 | exports.names = ['lottery', 'roulette']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 60; 5 | exports.cdUser = 60; 6 | exports.cdStaff = 60; 7 | exports.minRole = PERMISSIONS.BOUNCER; 8 | exports.handler = function (data) { 9 | 10 | var input = data.message.split(' '); 11 | var mins = 1; 12 | 13 | 14 | if (input.length >= 2) { 15 | mins = parseInt(_.last(input, 1)); 16 | } 17 | else { 18 | mins = 5; 19 | } 20 | 21 | // Sanity check! 22 | if (mins < 1 || mins > 120) { 23 | mins = 5; 24 | } 25 | 26 | if (input[0].toLowerCase() === 'roulette') { 27 | bot.sendChat('Wait list roulette in ' + mins + ' minutes! Chat and be in line within ' + mins + ' minutes to enter. Winner gets moved up a random number of spots! @djs'); 28 | } 29 | else { 30 | bot.sendChat('Wait list lottery in ' + mins + ' minutes! Chat and be in line within ' + mins + ' minutes to enter. Winner gets the #2 spot! @djs'); 31 | } 32 | 33 | if (mins > 1) { 34 | setTimeout(function () { 35 | bot.sendChat("Contest ending in ONE MINUTE - be in line and chat to enter! @djs"); 36 | }, (mins - 1) * 60 * 1000); 37 | } 38 | 39 | setTimeout(function () { 40 | // Only select from users active during the lottery 41 | var minPosition = 2; 42 | getActiveDJs(mins, minPosition, function (activeDJs) { 43 | if (activeDJs.length > 0) { 44 | var randomNumber = _.random(0, activeDJs.length - 1); 45 | var winner = activeDJs[randomNumber]; 46 | var winnerId = parseInt(winner.site_id); 47 | var message = ":tada: @" + winner.username + " emerges victorious!"; 48 | transferCustomPoints(null, bot.getUser(winnerId), 1); 49 | var currentPosition = bot.getWaitListPosition(winnerId); 50 | var position = minPosition; 51 | if (input[0] === 'roulette') { 52 | position = _.random(minPosition, currentPosition - 1); 53 | } 54 | if (currentPosition > minPosition && currentPosition > position) { 55 | bot.moderateMoveDJ(winnerId, position); 56 | console.log('[LOTTO] Moving ' + winner.username + ' from position ' + currentPosition + ' to position: ' + position); 57 | message += ' Moving to position ' + position; 58 | } else if (currentPosition == -1) { 59 | console.log('[LOTTO] ' + winner.username + ' detected with waitlist position of -1'); 60 | message += ' Not finding ' + winner.username + '\'s position in the wait list. Need some help @staff'; 61 | } else if (currentPosition <= position) { 62 | console.log('[LOTTO] Leaving ' + winner.username + ' in position: ' + currentPosition + '(' + position + ')'); 63 | message += ' Leaving in position ' + currentPosition; 64 | } 65 | bot.sendChat(message); 66 | } 67 | else { 68 | bot.sendChat(":thumbsdown: No one is eligible to win the contest."); 69 | console.log('[LOTTO] No one is eligible to win the contest.'); 70 | } 71 | } 72 | ); 73 | }, mins * 60 * 1000); 74 | 75 | }; 76 | -------------------------------------------------------------------------------- /documentation/slack-webhook.sample.php: -------------------------------------------------------------------------------- 1 | slack->pm2->instance_name; 10 | $pm2_path = $config->slack->pm2->bin_path; 11 | $pm2_log_path = $config->slack->pm2->log_path; 12 | 13 | // You made need the below commands in order to get this working on your specific operating system 14 | // $pm2_name = $pm2_name . ' 2>&1'; 15 | // $pm2_path = 'HOME="${HOME:=/root}" ' . $pm2_path; 16 | 17 | // Deal with expected incoming data from $_POST 18 | $token = $_POST['token']; 19 | $team_id = $_POST['team_id']; 20 | $channel_id = $_POST['channel_id']; 21 | $channel_name = $_POST['channel_name']; 22 | $timestamp = $_POST['timestamp']; 23 | $user_id = $_POST['user_id']; 24 | $user_name = $_POST['user_name']; 25 | $text = $_POST['text']; 26 | $trigger_word = $_POST['trigger_word']; 27 | 28 | // Strip the trigger word from text 29 | $text = trim(str_replace($trigger_word, '', $text)); 30 | list($command, $parameters) = explode(' ', $text, 2); 31 | 32 | if ($_POST['team_id'] == $config->slack->pm2->team_id && $_POST['token'] == $config->slack->pm2->token) { 33 | switch ($command) { 34 | case 'reload': 35 | case 'reset': 36 | case 'restart': 37 | $result = shell_exec($pm2_path . ' restart ' . $pm2_name); 38 | $message = "I'm restarting..."; 39 | break; 40 | case 'die': 41 | case 'kill': 42 | case 'stop': 43 | $result = shell_exec($pm2_path . ' stop ' . $pm2_name); 44 | $message = "I'm shutting down..."; 45 | break; 46 | case 'status': 47 | $output = shell_exec($pm2_path . ' jlist'); 48 | $json = json_decode($output); 49 | file_put_contents('output.txt', $output); 50 | $message = "I'm currently " . $json[0]->pm2_env->status . " and have restarted " . $json[0]->pm2_env->restart_time . " times."; 51 | break; 52 | case 'log': 53 | if (intval($parameters) > 0 && intval($parameters) < 1000) { 54 | $lines = intval($parameters); 55 | } 56 | else { 57 | $lines = 10; 58 | } 59 | $result = shell_exec('tail -n ' . $lines . ' ' . $pm2_log_path . ' 2>&1'); 60 | $message = "Here are the last $lines lines from my log:\n```" . $result . "```"; 61 | break; 62 | case 'start': 63 | $result = shell_exec($pm2_path . ' restart ' . $pm2_name); 64 | $message = "I'm starting..."; 65 | break; 66 | case 'help': 67 | default: 68 | $message = "To control me, you need to type `" . $trigger_word . "` followed by one of the commands below. 69 | E.g. `" . $trigger_word . " status` to show my on/offline status 70 | Available commands are: 71 | `status` — Show my online/offline status. 72 | `start` — Start me up f I'm offline. 73 | `stop` — Shut me down. 74 | `restart` — Restart me. 75 | `log #` — Show the last # things I see in my consle (default 10). 76 | `help` — Display this lst"; 77 | } 78 | } 79 | 80 | // Return data 81 | $json = [ 82 | 'text' => $message, 83 | 'link_names' => 1 84 | ]; 85 | echo json_encode($json); 86 | -------------------------------------------------------------------------------- /documentation/config.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "site": "plug", 3 | "roomName": "room-name-slug-from-url", 4 | "superAdmin": "YourUsername", 5 | "roomStateFile": false, 6 | "auth": { 7 | "username": "account-email-address@tld.com", 8 | "password": "account-password" 9 | }, 10 | "db": { 11 | "dialect": "mysql", 12 | "host": "localhost", 13 | "port": "3306", 14 | "username": "root", 15 | "password": "password", 16 | "database": "beavisbot", 17 | "forceSequelizeSync": false 18 | }, 19 | "apiKeys": { 20 | "cleverbot": "cleverbotApiKey", 21 | "echoNest": "###", 22 | "lastFm": "lastFmApiKey", 23 | "lastFm_secret": "lastfmSecretKey", 24 | "wordnik": "wordnikApiKey", 25 | "youtube": { 26 | "api_key": "youtubeApiKey", 27 | "client_id": "clientid.apps.googleusercontent.com", 28 | "client_secret": "clientSecret" 29 | } 30 | 31 | }, 32 | "webhooks": { 33 | "discord": { 34 | "webhookUrl": "https://discordapp.com/api/webhooks/{webhook.id}/{webhook.token}", 35 | "default": { 36 | "username": "", 37 | "avatarUrl": "" 38 | } 39 | }, 40 | "slack": { 41 | "webhookUrl": "https://xxxxxx.slack.com/services/hooks/incoming-webhook?token=xxxxxxxxxxx", 42 | "default": { 43 | "channel": "#general", 44 | "iconUrl": "" 45 | } 46 | } 47 | }, 48 | "pm2": { 49 | "instance_name": "beavisbot", 50 | "token": "xxx", 51 | "team_id": "xxx" 52 | }, 53 | "responses": { 54 | "botConnect": "", 55 | "freeSpin": "At DJ point milestones (500, 1000, 2000 and every 1k after), you may spin from any genre/date (still under 7 mins). It should be played within 150 points of reaching and we call it a \"free spin\". Check .rules for details.", 56 | "mention": "", 57 | "welcome": { 58 | "newUser": ":wave: Welcome, @{username}! Community rules: http://someurl.com", 59 | "oldUser": ":wave: Welcome back, @{username}!" 60 | }, 61 | "rules": "Rules: http://someurl.com.", 62 | "theme": "No current theme. Spin any track released from 1978-2002 under 7 mins. See .rules for more.", 63 | "activeDJReminder": "Are you still here? Looks like you've been AFK for more than an hour. We require DJs in the wait list be active participants in chat. :smile:", 64 | "activeDJRemoveMessage": "Everyone is welcome to AFK and listen to the music but we ask that if you wish to DJ you must be able to at the very least respond to an @ mention in chat.", 65 | "downvoteReminder": "We like to maintain a positive environment. Repeated meh'ing of songs probably means this is not the community for you and may result in a ban.", 66 | "upvoteReminder": "Help us maintain an active, positive environment! Wooting all songs while in line to DJ is required. For auto-woot info, click the music note at the top left of the page." 67 | }, 68 | "queue": { 69 | "djCycleMaxQueueLength": 4, 70 | "djIdleAfterMins": 600, 71 | "djIdleMinQueueLengthToEnforce": 5, 72 | "djMinLevel": 1, 73 | "maxSongLengthSecs": 420, 74 | "minSongReleaseDate": "1978-01-01", 75 | "maxSongReleaseDate": "2002-12-31", 76 | "prohibitDownvoteInQueue": false, 77 | "skipStuckSongs": false, 78 | "upvoteSongs": "ALL" 79 | }, 80 | "autoSuggestCorrections": false, 81 | "chatRandomnessPercentage": 5, 82 | "commandLiteral": ".", 83 | "customPointName": ":eggplant:", 84 | "developmentMode": false, 85 | "quietMode": false, 86 | "verboseLogging": false, 87 | "welcomeUsers": "ALL", 88 | "welcomeUsersMinLevel": 4 89 | } 90 | -------------------------------------------------------------------------------- /commands/disabled/settings.js: -------------------------------------------------------------------------------- 1 | exports.names = ['settings', 'set']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 5; 7 | exports.minRole = PERMISSIONS.BOUNCER_PLUS; 8 | exports.handler = function (data) { 9 | 10 | var input = data.message.split(' '); 11 | var logMessage = ''; 12 | var chatMessage = ''; 13 | var result; 14 | 15 | var translation = [ 16 | { configName: 'djIdleAfterMins', chatName: 'djidle', english: 'DJ Idle Seconds', notify: true }, 17 | { 18 | configName: 'djIdleMinQueueLengthToEnforce', 19 | chatName: 'minidlequeue', 20 | english: 'Min Idle Queue', 21 | notify: false 22 | }, 23 | { configName: 'djCycleMaxQueueLength', chatName: 'maxcyclequeue', english: 'Max Cycle Queue', notify: false }, 24 | { configName: 'maxSongLengthSecs', chatName: 'maxsonglength', english: 'Max Song Seconds', notify: true }, 25 | { configName: 'minSongReleaseDate', chatName: 'minreleasedate', english: 'Min Release Date ', notify: true }, 26 | { configName: 'maxSongReleaseDate', chatName: 'maxreleasedate', english: 'Max Release Date', notify: true }, 27 | { configName: 'prohibitDownvoteInQueue', chatName: 'nomehsinqueue', english: 'No Mehs in Queue', notify: false }, 28 | { configName: 'quietMode', chatName: 'quietmode', english: 'Quiet Mode', notify: false }, 29 | { configName: 'verboseLogging', chatName: 'verboselogging', english: 'Verbose Logging', notify: false } 30 | ]; 31 | 32 | if (input.length < 3) { 33 | for (var key in config.queue) { 34 | result = _.findWhere(translation, { configName: key }); 35 | if (config.queue.hasOwnProperty(key) && result) { 36 | chatMessage += result.chatName + ': ' + config.queue[key] + ', '; 37 | } 38 | } 39 | for (var key in config) { 40 | result = _.findWhere(translation, { configName: key }); 41 | if (config.hasOwnProperty(key) && result) { 42 | chatMessage += result.chatName + ': ' + config[key] + ', '; 43 | } 44 | } 45 | if (chatMessage != '') { 46 | bot.sendChat('Settings: ' + trimCommas(chatMessage)); 47 | } 48 | } 49 | else { 50 | var setting = input[1]; 51 | var newValue = _.rest(input, 2).join(' ').trim(); 52 | result = _.findWhere(translation, { chatName: setting }); 53 | 54 | if (result !== undefined) { 55 | 56 | if (newValue === 'false') { 57 | newValue = new Boolean(false); 58 | } else if (newValue === 'true') { 59 | newValue = new Boolean(true); 60 | } else if (newValue.match(/^\d+$/)) { 61 | newValue = parseInt(newValue); 62 | } 63 | 64 | if (config.queue.hasOwnProperty(result.configName)) { 65 | config.queue[result.configName] = newValue; 66 | } 67 | if (config.hasOwnProperty(result.configName)) { 68 | config[result.configName] = newValue; 69 | } 70 | if (result.notify) { 71 | bot.sendChat(result.english + ' now set to: ' + newValue + ' @djs'); 72 | } 73 | logMessage = '[CONFIG] ' + data.from.username + ' set `' + result.configName + '` to `' + newValue + '`'; 74 | console.log(logMessage); 75 | sendToWebhooks(logMessage); 76 | } 77 | else { 78 | bot.sendChat('unknown setting: ' + setting); 79 | } 80 | 81 | // @TODO - need to merge down configState and settings{} 82 | settings.maxsonglength = parseInt(config.queue.maxSongLengthSecs); 83 | settings.maxdjidletime = parseInt(config.queue.djIdleAfterMins) * 60; 84 | settings.djidleminqueue = parseInt(config.queue.djIdleMinQueueLengthToEnforce); 85 | settings.djcyclemaxqueue = parseInt(config.queue.djCycleMaxQueueLength); 86 | 87 | writeConfigState(); 88 | } 89 | 90 | }; 91 | 92 | -------------------------------------------------------------------------------- /commands/disabled/skip.js: -------------------------------------------------------------------------------- 1 | exports.names = ['skip', 'skipban', 'skipbroken', 'skipnsfw', 'skipoor', 'skiptroll', 'blacklist']; 2 | exports.hidden = true; 3 | exports.enabled = true; 4 | exports.cdAll = 10; 5 | exports.cdUser = 10; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.RDJ_PLUS; 8 | exports.handler = function (data) { 9 | 10 | var dj = bot.getDJ(); 11 | var media = bot.getMedia(); 12 | 13 | if (dj == null) { 14 | return; 15 | } 16 | 17 | var input = data.message.split(' '); 18 | var command = _.first(input); 19 | var params = _.rest(input, 1); 20 | 21 | var message = ''; 22 | var reason = ''; 23 | var notes = ''; 24 | 25 | var addToBlacklist = false; 26 | var banUser = false; 27 | 28 | if (params.length > 0) { 29 | notes = params.join(' ').trim(); 30 | } 31 | 32 | if (media) { 33 | 34 | switch (command) { 35 | case 'blacklist': 36 | reason = ''; 37 | addToBlacklist = true; 38 | banUser = false; 39 | break; 40 | case 'skipban': 41 | reason = ''; 42 | addToBlacklist = true; 43 | banUser = true; 44 | break; 45 | case 'skipbroken': 46 | reason = 'Video broken/removed'; 47 | addToBlacklist = true; 48 | banUser = false; 49 | break; 50 | case 'skipnsfw': 51 | reason = 'NSFW/Nudity'; 52 | addToBlacklist = true; 53 | banUser = false; 54 | break; 55 | case 'skiptroll': 56 | reason = 'Troll/Not Music'; 57 | addToBlacklist = true; 58 | banUser = false; 59 | break; 60 | case 'skipoor': 61 | reason = 'Out of range for theme'; 62 | addToBlacklist = false; 63 | banUser = false; 64 | break; 65 | default: 66 | reason = ''; 67 | addToBlacklist = false; 68 | banUser = false; 69 | break; 70 | } 71 | 72 | // Blacklist this song 73 | if (addToBlacklist && params.length == 0) { 74 | models.Song.update({is_banned: 1, banned_reason: reason}, {where: {host_id: media.cid}}); 75 | bot.sendChat("@" + dj.username + ", the song \"" + media.name + "\" has been blacklisted."); 76 | message = '[BLACKLIST] ' + data.from.username + ' blacklisted ' + media.name + ' (ID:' + media.cid + ')'; 77 | } else if (addToBlacklist) { 78 | blacklistSongById(notes, data.from); 79 | return; 80 | } else if (command == 'skipoor' && params.length > 0) { 81 | if (notes.match(/\d{4}$/)) { 82 | var releaseDate = notes + '-01-01'; 83 | models.Song.update({release_date: releaseDate}, {where: {host_id: media.cid}}); 84 | } 85 | bot.sendChat("@" + dj.username + ", the song \"" + media.name + "\" has been marked out of range (released in " + notes + ")."); 86 | message = '[SKIP] ' + data.from.username + ' skipped a song.'; 87 | 88 | } else { 89 | message = '[SKIP] ' + data.from.username + ' skipped a song.'; 90 | } 91 | 92 | if (reason != '') { 93 | message += ' Reason: ' + reason; 94 | } 95 | if (notes != '') { 96 | message += ' Notes: ' + notes; 97 | } 98 | 99 | if (message != '') { 100 | console.log(message + ' ' + JSON.stringify(data, null, 2)); 101 | sendToWebhooks(message); 102 | } 103 | 104 | if (banUser) { 105 | bot.moderateBanUser(dj.id, PlugAPI.BAN_REASON.OFFENSIVE_MEDIA, PlugAPI.BAN.PERMA); 106 | } else { 107 | bot.moderateForceSkip(); 108 | } 109 | 110 | getDbUserFromSiteUser(dj, function (row) { 111 | var userData = { 112 | type: 'skip', 113 | details: media.id + ': ' + media.name + ' (skipped by ' + data.from.username + '): ' + message, 114 | user_id: row.id, 115 | mod_user_id: data.from.db.id 116 | }; 117 | models.Karma.create(userData); 118 | }); 119 | 120 | } 121 | 122 | 123 | }; 124 | 125 | -------------------------------------------------------------------------------- /functions/user.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | // Case-insensitive search for user 3 | findUserInList = function (list, username) { 4 | var lowerUser = username.toLowerCase(); 5 | return _.find(list, function (term) { 6 | return term.username.toLowerCase() == lowerUser; 7 | }); 8 | }; 9 | 10 | getDbUserFromUserId = function (siteUserId, callback) { 11 | models.User.findOne({ 12 | where: { site_id: siteUserId, site: config.site }, 13 | }).then(function (row) { 14 | callback(row); 15 | }); 16 | }; 17 | 18 | getDbUserFromUsername = function (siteUsername, callback) { 19 | models.User.findOne({ 20 | where: { username: siteUsername, site: config.site }, 21 | order: "id ASC", 22 | }).then(function (row) { 23 | callback(row); 24 | }); 25 | }; 26 | 27 | hasPermission = function (user, minRole) { 28 | // @FIXME We aren't checking anything yet!!! 29 | return true; 30 | /* 31 | if (user.role == PlugAPI.ROOM_ROLE.RESIDENTDJ) { 32 | return user.role >= minRole || (minRole == PERMISSIONS.RDJ_PLUS && settings["rdjplus"]); 33 | } else if (user.role == PlugAPI.ROOM_ROLE.BOUNCER) { 34 | return user.role >= minRole || (minRole == PERMISSIONS.BOUNCER_PLUS && settings["bouncerplus"]); 35 | } 36 | 37 | return user.role >= minRole; 38 | */ 39 | }; 40 | 41 | getUsers = function () { 42 | return []; 43 | }; 44 | 45 | getWaitListPosition = function (userId) { 46 | return -1; 47 | }; 48 | 49 | saveWaitList = function (wholeRoom) { 50 | if (wholeRoom) { 51 | var userList = bot.getUsers(); 52 | } else { 53 | var userList = bot.getWaitList(); 54 | } 55 | userList.forEach(function (user) { 56 | var position = bot.getWaitListPosition(user.id); 57 | // user last seen in 900 seconds 58 | if (position > 0) { 59 | models.User.update( 60 | { 61 | queue_position: position, 62 | last_seen: moment.utc().toDate(), 63 | }, 64 | { where: { site: config.site, site_id: user.id.toString() } } 65 | ); 66 | } else { 67 | models.User.update({ queue_position: -1 }, { where: { site: config.site, site_id: user.id.toString() } }); 68 | } 69 | 70 | if (config.verboseLogging) { 71 | console.log("[WL-UPDATE]", user.username + " => " + position); 72 | } 73 | }); 74 | models.User.update( 75 | { queue_position: -1 }, 76 | { 77 | where: { 78 | last_seen: { lte: moment.utc().subtract(15, "minutes").toDate() }, 79 | last_active: { lte: moment.utc().subtract(15, "minutes").toDate() }, 80 | queue_position: { ne: -1 }, 81 | }, 82 | } 83 | ); 84 | }; 85 | 86 | updateDbUser = function (user) { 87 | if (user.name == "Guest") return; 88 | 89 | const userData = { 90 | site: config.site, 91 | site_id: user.userid, 92 | username: user.name, 93 | avatar: user.avatarid, 94 | role: user.acl, 95 | site_points: user.points, 96 | last_seen: new Date(), 97 | }; 98 | 99 | models.User.findOrCreate({ 100 | where: { site_id: user.userid, site: config.site }, 101 | defaults: userData, 102 | }) 103 | .then(([dbUser, created]) => { 104 | // Save the alias if the user has changed username 105 | if (dbUser.username && userData.username != dbUser.username) { 106 | console.log("[USER]", userData.username + " has changed their username from " + dbUser.username + ". Saving alias..."); 107 | addAlias(dbUser.dataValues); 108 | } 109 | 110 | // Reset the user's AFK timer if they've been gone for long enough (so we don't reset on disconnects) 111 | if (secondsSince(dbUser.last_seen) >= 900) { 112 | dbUser.last_active = new Date(); 113 | dbUser.queue_position = getWaitListPosition(user.userid); 114 | } 115 | 116 | dbUser.username = userData.username; 117 | dbUser.last_seen = new Date(); 118 | return dbUser.save(); 119 | }) 120 | .catch((err) => { 121 | console.log("[ERROR]", err); 122 | }); 123 | }; 124 | 125 | addAlias = function (user) { 126 | models.UserAlias.upsert({ 127 | username: user.username, 128 | user_id: user.id, 129 | }).catch(function (err) { 130 | console.log("[ERROR]", err); 131 | }); 132 | }; 133 | }; 134 | -------------------------------------------------------------------------------- /commands/disabled/fixsong.js: -------------------------------------------------------------------------------- 1 | exports.names = ['fixsong']; 2 | exports.hidden = true; 3 | exports.enabled = false; 4 | exports.cdAll = 30; 5 | exports.cdUser = 30; 6 | exports.cdStaff = 10; 7 | exports.minRole = PERMISSIONS.RDJ_PLUS; 8 | exports.handler = function (data) { 9 | function checkEchoNest(valueToCorrect) { 10 | request('http://developer.echonest.com/api/v4/song/search?api_key=' + config.apiKeys.echoNest + '&format=json&results=1&combined=' + S(valueToCorrect).escapeHTML().stripPunctuation().s, function (error, response, body) { 11 | logger.info('echonest body', body); 12 | if (error) { 13 | bot.sendChat('An error occurred while connecting to EchoNest.'); 14 | logger.error('EchoNest error', error); 15 | } else { 16 | response = JSON.parse(body).response; 17 | 18 | bot.getMedia().suggested = { 19 | author: response.songs[0].artist_name, 20 | title: response.songs[0].title 21 | }; 22 | bot.sendChat('Suggested Artist: "' + bot.getMedia().suggested.author + '". Title: "' + bot.getMedia().suggested.title + '". Type ".fixsong yes" to use the suggested tags.'); 23 | } 24 | }); 25 | } 26 | 27 | if (config.apiKeys.echoNest == null || config.apiKeys.echoNest == '###') { 28 | bot.sendChat('A valid EchoNest API key is needed to run this command.'); 29 | return; 30 | } 31 | 32 | var input = data.message.split(' '); 33 | 34 | if (data.from.role > 1 || (data.from.id == bot.getMedia().currentDJ && input[1] != 'yes')) { 35 | bot.sendChat('This command is only available to bouncers, managers, and hosts.'); 36 | return; 37 | } 38 | 39 | if (input[1] == 'yes') { 40 | // commit suggested song value to DB and room.media 41 | if (bot.getMedia().suggested) { 42 | bot.getMedia().author = bot.getMedia().suggested.author; 43 | bot.getMedia().title = bot.getMedia().suggested.title; 44 | //db.run('INSERT OR REPLACE INTO SONGS VALUES (?, ?, ?, ?, ?, ?)', [bot.getMedia().id, bot.getMedia().title, bot.getMedia().format, bot.getMedia().author, bot.getMedia().cid, bot.getMedia().duration]); 45 | bot.sendChat('Database updated with corrected values.'); 46 | } else { 47 | bot.sendChat('No suggested values present.'); 48 | } 49 | } else if (input[1] == 'artist') { 50 | // commit corrected artist value to DB and room.media 51 | var artist = _.rest(input, 2).join(' '); 52 | bot.getMedia().author = artist; 53 | //db.run('INSERT OR REPLACE INTO SONGS VALUES (?, ?, ?, ?, ?, ?)', [bot.getMedia().id, bot.getMedia().title, bot.getMedia().format, bot.getMedia().author, bot.getMedia().cid, bot.getMedia().duration], 54 | // function (error) { 55 | // if (error) { 56 | // bot.sendChat('An error occurred.'); 57 | // logger.error('Error while updating song ' + bot.getMedia().id, error); 58 | // } else { 59 | // bot.sendChat('Author updated.') 60 | // } 61 | // }); 62 | } else if (input[1] == 'title') { 63 | // commit corrected title value to DB and room.media 64 | var title = _.rest(input, 2).join(' '); 65 | bot.getMedia().title = title; 66 | //db.run('INSERT OR REPLACE INTO SONGS VALUES (?, ?, ?, ?, ?, ?)', [bot.getMedia().id, bot.getMedia().title, bot.getMedia().format, bot.getMedia().author, bot.getMedia().cid, bot.getMedia().duration], 67 | // function (error) { 68 | // if (error) { 69 | // bot.sendChat('An error occurred.'); 70 | // logger.error('Error while updating song ' + bot.getMedia().id, error); 71 | // } else { 72 | // bot.sendChat('Title updated.') 73 | // } 74 | // }); 75 | } else if (input[1] == 'check') { 76 | // search echonest 77 | checkEchoNest(bot.getMedia().author + ' ' + bot.getMedia().title); 78 | } else { 79 | // first, search db 80 | //db.get('SELECT author, title FROM SONGS WHERE id = ?', [bot.getMedia().id], 81 | // function (error, row) { 82 | // logger.info('db response: ', row); 83 | // if (row != null) { 84 | // bot.sendChat('Database values: Artist: "' + row['author'] + '". Title: "' + row['title'] + '". Use .fixsong check if this looks wrong.'); 85 | // } else { 86 | // // check echonest 87 | // logger.info('checking echonest'); 88 | // checkEchoNest(bot.getMedia().author + ' ' + bot.getMedia().title); 89 | // } 90 | // }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /events/registered.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("registered", function (data) { 3 | if (config.verboseLogging) { 4 | console.log("[JOIN]", JSON.stringify(data, null, 2)); 5 | } 6 | 7 | data.user.forEach(function (user) { 8 | if (user.name === undefined) { 9 | console.log(`[JOIN] Guest joined`); 10 | } 11 | 12 | if (user.userid !== config.auth.userId) { 13 | 14 | var newUser = false; 15 | var message = ""; 16 | 17 | getDbUserFromUserId(user.userid, function (dbUser) { 18 | if (dbUser == null) { 19 | newUser = true; 20 | message = config.responses.welcome.newUser.replace("{username}", user.name); 21 | if (!roomHasActiveStaff) { 22 | message += " Type .help if you need it!"; 23 | } 24 | models.RoomEvent.findOne({ 25 | where: { 26 | starts_at: { lte: new Date() }, 27 | ends_at: { gte: new Date() }, 28 | }, 29 | }).then(function (event) { 30 | if (event !== null) { 31 | if (event.type == "event") { 32 | message += " :star: SPECIAL EVENT :star: " + event.title + " (.event for details)"; 33 | } else if (event.type == "theme") { 34 | message += " Theme: " + event.title + " (.theme for details)"; 35 | } 36 | } 37 | 38 | console.log("[JOIN]", user.name + " is a first-time visitor to the room!"); 39 | if (config.welcomeUsersMinLevel <= user.points && (config.welcomeUsers == "NEW" || config.welcomeUsers == "ALL")) { 40 | setTimeout(function () { 41 | bot.speak(message); 42 | }, 5000); 43 | } 44 | }); 45 | } else { 46 | models.EventResponse.findOne({ 47 | where: { event_type: "userJoin", pattern: user.name, is_active: true }, 48 | order: models.sequelize.random() 49 | }) 50 | .then(function (eventResponse) { 51 | if (eventResponse == null) { 52 | message = config.responses.welcome.oldUser.replace("{username}", user.name); 53 | } else { 54 | message = eventResponse.response.replace("{username}", user.name); 55 | } 56 | }) 57 | .then(function () { 58 | models.RoomEvent.findOne({ 59 | where: { 60 | starts_at: { lte: new Date() }, 61 | ends_at: { gte: new Date() }, 62 | }, 63 | }).then(function (event) { 64 | if (event !== null) { 65 | if (event.type == "event") { 66 | message += " :star: SPECIAL EVENT :star: " + event.title + " (.event for details)"; 67 | } else if (event.type == "theme") { 68 | message += " Theme: " + event.title + " (.theme for details)"; 69 | } 70 | } 71 | 72 | if (message && config.welcomeUsersMinLevel <= user.points && config.welcomeUsers == "ALL" && secondsSince(dbUser.last_active) >= 900 && secondsSince(dbUser.last_seen) >= 900) { 73 | setTimeout(function () { 74 | bot.speak(message); 75 | }, 5000); 76 | } 77 | 78 | console.log("[JOIN]", user.name + " last seen " + timeSince(dbUser.last_seen)); 79 | }); 80 | }); 81 | } 82 | 83 | // Restore spot in line if user has been gone < 15 mins 84 | var position = getWaitListPosition(user.userid); 85 | 86 | /* 87 | if (!newUser && dbUser.queue_position > -1 && secondsSince(dbUser.last_seen) <= 900 && (position === -1 || (position > -1 && position > dbUser.queue_position))) { 88 | bot.moderateAddDJ(user.id, function () { 89 | if (dbUser.queue_position < bot.getWaitList().length && position !== dbUser.queue_position) { 90 | bot.moderateMoveDJ(user._id, dbUser.queue_position); 91 | } 92 | 93 | var userData = { 94 | type: "restored", 95 | details: "Restored to position " + dbUser.queue_position + " (disconnected for " + timeSince(dbUser.last_seen, true) + ")", 96 | user_id: dbUser.id, 97 | mod_user_id: botUser.db.id, 98 | }; 99 | models.Karma.create(userData); 100 | 101 | setTimeout(function () { 102 | bot.speak("put @" + user.name + " back in line (reconnected after " + timeSince(dbUser.last_seen, true) + ") :thumbsup:"); 103 | }, 5000); 104 | }); 105 | } 106 | */ 107 | }); 108 | updateDbUser(user); 109 | } 110 | }); 111 | }); 112 | }; 113 | -------------------------------------------------------------------------------- /functions/moderation.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | blacklistSongById = function (songid, from) { 3 | models.Play.findOne({ 4 | include: [ 5 | { 6 | model: models.Song, 7 | where: { $and: [{ site: config.site }, { host_id: songid }] }, 8 | }, 9 | models.User, 10 | ], 11 | order: [["created_at", "DESC"]], 12 | }).then(function (row) { 13 | if (!row) { 14 | bot.sendChat("I have not seen a song with id `" + songid + "` played in this room!"); 15 | } else { 16 | var userData = { 17 | type: "blacklist", 18 | details: "Blacklisted " + row.Song.name + " (spun by " + row.User.username + ")", 19 | user_id: row.User.id, 20 | mod_user_id: from.db.id, 21 | }; 22 | models.Karma.create(userData); 23 | models.Song.update({ is_banned: 1 }, { where: { host_id: songid } }); 24 | bot.sendChat('The song "' + row.Song.name + '" has been blacklisted.'); 25 | message = "[BLACKLIST] " + from.username + " blacklisted " + row.Song.name + " (ID:" + row.Song.host_id + ")"; 26 | console.log(message); 27 | sendToWebhooks(message); 28 | } 29 | }); 30 | }; 31 | 32 | monitorDJList = function () {}; 33 | 34 | idleWaitListProcess = function () { 35 | // Check for active mods 36 | roomHasActiveStaff = false; 37 | 38 | // @FIXME - This should probably be recoded using Promise.all so it's in order 39 | var idleDJs = []; 40 | Promise.map(bot.getUsers(), function (dj) { 41 | if (dj.id) { 42 | return models.User.find({ 43 | where: { site_id: dj.id, site: config.site }, 44 | include: { 45 | model: models.Karma, 46 | required: false, 47 | where: { 48 | type: "warn", 49 | created_at: { 50 | gte: moment.utc().subtract(config.queue.djIdleAfterMins, "minutes").toDate(), 51 | }, 52 | }, 53 | limit: 1, 54 | order: [["created_at", "DESC"]], 55 | }, 56 | }).then(function (dbUser) { 57 | if (dbUser) { 58 | var position = bot.getWaitListPosition(dj.id); 59 | 60 | if (botUser.db.id !== dbUser.id && dbUser.role > 1 && (secondsSince(dbUser.last_active) <= 300 || position >= 0)) { 61 | console.log("[STAFF-ACTIVE]", dbUser.username + " last active " + timeSince(dbUser.last_active)); 62 | roomHasActiveStaff = true; 63 | } 64 | 65 | if (position < 1) { 66 | // Don't do anything, user is not in line 67 | } else if (settings.djidle && secondsSince(dbUser.last_active) >= settings.maxdjidletime && moment.utc().isAfter(moment.utc(startupTimestamp).add(config.queue.djIdleAfterMins, "minutes"))) { 68 | console.log("[WL-IDLE]", position + ". " + dbUser.username + " last active " + timeSince(dbUser.last_active)); 69 | if (dbUser.Karmas.length > 0) { 70 | console.log("[WL-IDLE]", dbUser.username + " was last warned " + timeSince(dbUser.Karmas[0].created_at)); 71 | bot.moderateRemoveDJ(dj.id); 72 | bot.sendChat("@" + dbUser.username + " " + config.responses.activeDJRemoveMessage); 73 | var userData = { 74 | type: "remove", 75 | details: "Removed from position " + position + ": AFK for " + timeSince(dbUser.last_active, true), 76 | user_id: dbUser.id, 77 | mod_user_id: botUser.db.id, 78 | }; 79 | models.Karma.create(userData); 80 | } else if (position > 1) { 81 | var userData = { 82 | type: "warn", 83 | details: "Warned in position " + position + ": AFK for " + timeSince(dbUser.last_active, true), 84 | user_id: dbUser.id, 85 | mod_user_id: botUser.db.id, 86 | }; 87 | models.Karma.create(userData); 88 | idleDJs.push(dbUser.username); 89 | } 90 | } else { 91 | console.log("[WL-ACTIVE]", position + ". " + dbUser.username + " last active " + timeSince(dbUser.last_active)); 92 | } 93 | } 94 | }); 95 | } 96 | }).then(function () { 97 | if (idleDJs.length > 0) { 98 | var idleDJsList = idleDJs.join(" @"); 99 | bot.sendChat("@" + idleDJsList + " " + config.responses.activeDJReminder); 100 | } 101 | 102 | if (moment.utc().isAfter(moment.utc(startupTimestamp).add(5, "minutes"))) { 103 | if (roomHasActiveStaff && (settings.rdjplus || settings.bouncerplus)) { 104 | bot.sendChat("Active @staff detected. Revoking temporary extra permissions @rdjs"); 105 | settings.rdjplus = false; 106 | settings.bouncerplus = false; 107 | } else if (!roomHasActiveStaff && (!settings.rdjplus || !settings.bouncerplus)) { 108 | bot.sendChat("No active @staff detected. Granting Bouncers and @rdjs temporary extra permissions"); 109 | settings.rdjplus = true; 110 | settings.bouncerplus = true; 111 | } 112 | } 113 | }); 114 | saveWaitList(true); 115 | 116 | var waitListSize = bot.getWaitList().length; 117 | 118 | if (waitListSize >= settings.djidleminqueue && settings.djidle == false) { 119 | settings.djidle = true; 120 | bot.changeDJCycle(false); 121 | bot.sendChat("Wait List at " + waitListSize + " @djs. Idle timer enabled and cycle disabled"); 122 | } else if (waitListSize < settings.djidleminqueue && settings.djidle == true) { 123 | settings.djidle = false; 124 | bot.changeDJCycle(true); 125 | bot.sendChat("Wait List at " + waitListSize + " @djs. Idle timer disabled and cycle enabled"); 126 | } 127 | }; 128 | 129 | removeIfDownvoting = function (mehUsername) { 130 | var mehWaitList = bot.getWaitList(); 131 | var mehUser = findUserInList(mehWaitList, mehUsername); 132 | 133 | if (config.verboseLogging) { 134 | console.log("[WAITLIST]" + JSON.stringify(mehWaitList, null, 2)); 135 | } 136 | 137 | if (mehUser !== undefined && mehUser.vote == -1) { 138 | console.log("[REMOVE] Removed " + mehUser.username + " from wait list for mehing"); 139 | var position = bot.getWaitListPosition(mehUser.id); 140 | bot.moderateRemoveDJ(mehUser.id); 141 | bot.sendChat("@" + mehUser.username + ", voting MEH/Chato/:thumbsdown: while in line is prohibited. Check .rules."); 142 | getDbUserFromSiteUser(mehUser.id, function (row) { 143 | var userData = { 144 | type: "remove", 145 | details: "Removed from position " + position + " for mehing", 146 | user_id: row.id, 147 | mod_user_id: botUser.db.id, 148 | }; 149 | models.Karma.create(userData); 150 | }); 151 | } 152 | }; 153 | 154 | addKarma = function (data) { 155 | var userData = { 156 | type: data.type, 157 | details: data.details, 158 | user_id: data.user_id, 159 | mod_user_id: data.mod_user_id, 160 | }; 161 | models.Karma.create(userData).then(function () { 162 | models.Karma.findAndCount({ 163 | where: { 164 | user_id: data.user_id, 165 | created_at: { gte: moment.utc().subtract(1, "hours").toDate() }, 166 | }, 167 | }).then(function (results) { 168 | if (results.count > 2) { 169 | bot.moderateBanUser(data.site_id, PlugAPI.BAN_REASON.SPAMMING_TROLLING, PlugAPI.BAN.DAY); 170 | } 171 | }); 172 | }); 173 | console.log(data.message); 174 | sendToWebhooks(data.message); 175 | }; 176 | }; 177 | -------------------------------------------------------------------------------- /functions/chat.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | const Cleverbot = require("cleverbot-node"); 3 | let cleverbot = new Cleverbot(); 4 | cleverbot.configure({ botapi: config.apiKeys.cleverbot }); 5 | 6 | chatResponse = function (data) { 7 | if (data.userid === undefined) { 8 | return; 9 | } 10 | 11 | const input = data.text.split("@", 2); 12 | const command = input[0].trim(); 13 | let target = null; 14 | if (input.length > 1) { 15 | target = input[1].trim(); 16 | } 17 | 18 | models.EventResponse.findOne({ 19 | where: { event_type: "chat", pattern: command, is_active: true }, 20 | order: models.sequelize.random(), 21 | }).then(function (row) { 22 | if (row === null) { 23 | return; 24 | } else if (target != null) { 25 | // Remove /me from the beginning of targeting since it won't alert someone 26 | if (row.response.indexOf("/me") === 0) { 27 | row.response = "@" + target + " " + S(row.response).chompLeft("/me").s; 28 | } else { 29 | row.response = "@" + target + " " + row.response; 30 | } 31 | } 32 | 33 | bot.speak(row.response.replace("{sender}", data.name)); 34 | }); 35 | }; 36 | 37 | handleChat = function (data) { 38 | // Only listen to the superAdmin when in development mode 39 | if (data.userid === undefined || (config.developmentMode && data.name !== config.superAdmin)) { 40 | return; 41 | } 42 | 43 | // unescape message 44 | /* 45 | // data.text = S(data.text).unescapeHTML().s; 46 | data.text = data.text.replace(/'/g, "'"); 47 | data.text = data.text.replace(/"/g, '"'); 48 | data.text = data.text.replace(/&/g, "&"); 49 | data.text = data.text.replace(/</gi, "<"); 50 | data.text = data.text.replace(/>/gi, ">"); 51 | */ 52 | 53 | if (data.text.charAt(0) === config.commandLiteral) { 54 | // Chop off the command literal 55 | data.text = data.text.substr(1); 56 | 57 | // Don't allow @mention to the bot - prevent loopback 58 | data.text = data.text.replace("@" + bot.user.name, ""); 59 | 60 | const command = bot.commands.filter(function (cmd) { 61 | let found = false; 62 | for (i = 0; i < cmd.names.length; i++) { 63 | if (!found) { 64 | found = cmd.names[i] == _.first(data.text.toLowerCase().split(" ")); 65 | } 66 | } 67 | return found; 68 | })[0]; 69 | 70 | if (command && command.enabled) { 71 | let can_run_command = true; 72 | const cur_time = Date.now() / 1000; 73 | const time_diff = cur_time - command.lastRun; 74 | let time_diff_user = cur_time; 75 | if (data.userid in command.lastRunUsers) { 76 | time_diff_user -= command.lastRunUsers[data.userid]; 77 | } 78 | 79 | /* 80 | if (data.from.role >= PlugAPI.ROOM_ROLE.BOUNCER) { 81 | if (command.cdStaff >= time_diff) { 82 | console.log("[ANTISPAM]", data.name + " cannot run the command due to antispam (cdStaff) " + time_diff); 83 | can_run_command = false; 84 | } 85 | } else { 86 | if (command.cdAll >= time_diff) { 87 | console.log("[ANTISPAM]", data.name + " cannot run the command due to antispam (cdAll) " + time_diff); 88 | can_run_command = false; 89 | } else if (command.cdUser >= time_diff_user) { 90 | console.log("[ANTISPAM]", data.name + " cannot run the command due to antispam (cdUser) " + time_diff_user); 91 | can_run_command = false; 92 | } 93 | } 94 | */ 95 | 96 | if (config.verboseLogging) { 97 | console.log("[COMMAND]", JSON.stringify(data, null, 2)); 98 | } 99 | 100 | if (can_run_command && hasPermission(data.userid, command.minRole)) { 101 | const r = command.handler(data); 102 | if (typeof r === "object" && "cdAll" in r && "cdUser" in r) { 103 | command.lastRun = cur_time - command.cdAll + r.cdAll; 104 | command.lastRunUsers[data.userid] = cur_time - command.cdUser + r.cdUser; 105 | } else if (r !== false) { 106 | command.lastRun = cur_time; 107 | command.lastRunUsers[data.userid] = cur_time; 108 | } 109 | } 110 | } else if (!config.quietMode) { 111 | // @TODO - Build the list of possible commands on init() instead of querying every time 112 | chatResponse(data); 113 | } 114 | } else if (!config.quietMode && data.text.indexOf("@" + bot.user.name) > -1) { 115 | mentionResponse(data); 116 | } 117 | }; 118 | 119 | mentionResponse = function (data) { 120 | if (data.userid === undefined) { 121 | return; 122 | } 123 | 124 | // Antispam 125 | const cooldown_all = 10; 126 | const cooldown_user = 30; 127 | const cur_time = Date.now() / 1000; 128 | const time_diff = cur_time - roomState.mentions.lastRunAll; 129 | let time_diff_user = cur_time; 130 | 131 | if (data.userid in roomState.mentions.lastRunUsers) { 132 | time_diff_user -= roomState.mentions.lastRunUsers[data.userid]; 133 | } 134 | 135 | if (cooldown_all >= time_diff) { 136 | console.log("[ANTISPAM]", data.name + " cannot chat with the bot - antispam (all) " + time_diff); 137 | } else if (cooldown_user >= time_diff_user) { 138 | console.log("[ANTISPAM]", data.name + " cannot chat with the bot - antispam (user) " + time_diff_user); 139 | } else { 140 | if (config.verboseLogging) { 141 | console.log(`[ANTISPAM] ${data.name} passed antispam (user) ${time_diff_user}:${time_diff}`); 142 | } 143 | roomState.mentions.lastRunAll = cur_time; 144 | roomState.mentions.lastRunUsers[data.userid] = cur_time; 145 | 146 | // How much ADHD does the bot have? 147 | const chatRandomnessPercentage = config.chatRandomnessPercentage; 148 | 149 | if (_.random(1, 100) > chatRandomnessPercentage) { 150 | const cleverMessage = data.text.replace("@" + bot.user.name, "").trim(); 151 | cleverbot.write(cleverMessage, function (response) { 152 | if (config.verboseLogging) { 153 | console.log("[CLEVERBOT]", JSON.stringify(response, null, 2)); 154 | } 155 | 156 | if (response != null) { 157 | bot.speak("@" + data.name + ", " + response.output); 158 | } 159 | }); 160 | } else { 161 | models.EventResponse.find({ 162 | where: { event_type: "mention", is_active: true }, 163 | order: "RAND()", 164 | }).then(function (row) { 165 | if (row === null) { 166 | return; 167 | } else { 168 | bot.speak(row.response.replace("{sender}", data.name)); 169 | } 170 | }); 171 | } 172 | } 173 | }; 174 | 175 | getGiphy = function (type, api_key, rating, tags, limit, returnData) { 176 | let reqparams = { 177 | format: "json", 178 | api_key: api_key, 179 | rating: rating, 180 | limit: limit, 181 | }; 182 | if (type == "giphyt") { 183 | endpoint = "/v1/gifs/translate"; 184 | search_param = "s"; 185 | } else if (type == "giphys") { 186 | endpoint = "/v1/stickers/random"; 187 | search_param = "tag"; 188 | tags = tags.replace(/\+/g, "-"); 189 | } else { 190 | endpoint = "/v1/gifs/search"; 191 | search_param = "q"; 192 | } 193 | 194 | if (tags !== undefined) { 195 | reqparams[search_param] = tags; 196 | } 197 | 198 | /* 199 | request( 200 | { 201 | url: "https://api.giphy.com" + endpoint + "?", 202 | qs: reqparams, 203 | method: "GET", 204 | }, 205 | function (error, response, body) { 206 | if (error) { 207 | console.log(error); 208 | returnData(null); 209 | } else { 210 | try { 211 | var data = JSON.parse(body); 212 | 213 | if (config.verboseLogging) { 214 | data.calloutendpoint = endpoint; 215 | data.calloutqs = reqparams; 216 | console.log("[GIPHY] ", JSON.stringify(data, null, 2)); 217 | } 218 | var randomNumber = _.random(0, data.data.length); 219 | if (type == "giphys") { 220 | returnData(data.data[randomNumber].image_url.split(/[?#]/)[0]); 221 | } else { 222 | returnData( 223 | data.data[randomNumber].images.fixed_height.url.split(/[?#]/)[0] 224 | ); 225 | } 226 | } catch (error) { 227 | returnData(null); 228 | } 229 | } 230 | } 231 | ); 232 | */ 233 | }; 234 | }; 235 | -------------------------------------------------------------------------------- /documentation/schema.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.16 Distrib 10.1.47-MariaDB, for debian-linux-gnu (x86_64) 2 | -- 3 | -- Host: localhost Database: beavisbot 4 | -- ------------------------------------------------------ 5 | -- Server version 10.1.47-MariaDB-0ubuntu0.18.04.1 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | /*!40101 SET NAMES utf8mb4 */; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Table structure for table `blacklist` 20 | -- 21 | 22 | DROP TABLE IF EXISTS `blacklist`; 23 | /*!40101 SET @saved_cs_client = @@character_set_client */; 24 | /*!40101 SET character_set_client = utf8 */; 25 | CREATE TABLE `blacklist` ( 26 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 27 | `type` varchar(255) NOT NULL, 28 | `pattern` varchar(255) NOT NULL, 29 | `details` text, 30 | `is_active` tinyint(1) DEFAULT '1', 31 | `created_at` datetime NOT NULL, 32 | `updated_at` datetime NOT NULL, 33 | `song_id` int(10) unsigned DEFAULT NULL, 34 | PRIMARY KEY (`id`), 35 | KEY `song_id` (`song_id`), 36 | CONSTRAINT `blacklist_ibfk_1` FOREIGN KEY (`song_id`) REFERENCES `songs` (`id`) ON DELETE SET NULL ON UPDATE CASCADE 37 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 38 | /*!40101 SET character_set_client = @saved_cs_client */; 39 | 40 | -- 41 | -- Table structure for table `event_responses` 42 | -- 43 | 44 | DROP TABLE IF EXISTS `event_responses`; 45 | /*!40101 SET @saved_cs_client = @@character_set_client */; 46 | /*!40101 SET character_set_client = utf8 */; 47 | CREATE TABLE `event_responses` ( 48 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 49 | `event_type` varchar(255) NOT NULL, 50 | `pattern` varchar(255) DEFAULT NULL, 51 | `response` varchar(255) NOT NULL, 52 | `cooldown` int(10) unsigned NOT NULL DEFAULT '10', 53 | `is_active` tinyint(1) DEFAULT '1', 54 | `created_at` datetime NOT NULL, 55 | `updated_at` datetime NOT NULL, 56 | PRIMARY KEY (`id`) 57 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 58 | /*!40101 SET character_set_client = @saved_cs_client */; 59 | 60 | -- 61 | -- Table structure for table `games` 62 | -- 63 | 64 | DROP TABLE IF EXISTS `games`; 65 | /*!40101 SET @saved_cs_client = @@character_set_client */; 66 | /*!40101 SET character_set_client = utf8 */; 67 | CREATE TABLE `games` ( 68 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 69 | `type` varchar(255) NOT NULL, 70 | `details` text, 71 | `result` text, 72 | `participants` int(10) unsigned DEFAULT '0', 73 | `created_at` datetime NOT NULL, 74 | `updated_at` datetime NOT NULL, 75 | `user_id` int(10) unsigned DEFAULT NULL, 76 | `mod_user_id` int(10) unsigned DEFAULT NULL, 77 | PRIMARY KEY (`id`), 78 | KEY `user_id` (`user_id`), 79 | KEY `mod_user_id` (`mod_user_id`), 80 | CONSTRAINT `games_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, 81 | CONSTRAINT `games_ibfk_2` FOREIGN KEY (`mod_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE 82 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 83 | /*!40101 SET character_set_client = @saved_cs_client */; 84 | 85 | -- 86 | -- Table structure for table `karmas` 87 | -- 88 | 89 | DROP TABLE IF EXISTS `karmas`; 90 | /*!40101 SET @saved_cs_client = @@character_set_client */; 91 | /*!40101 SET character_set_client = utf8 */; 92 | CREATE TABLE `karmas` ( 93 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 94 | `type` varchar(255) NOT NULL, 95 | `details` text, 96 | `created_at` datetime NOT NULL, 97 | `updated_at` datetime NOT NULL, 98 | `user_id` int(10) unsigned DEFAULT NULL, 99 | `mod_user_id` int(10) unsigned DEFAULT NULL, 100 | PRIMARY KEY (`id`), 101 | KEY `user_id` (`user_id`), 102 | KEY `mod_user_id` (`mod_user_id`), 103 | CONSTRAINT `karmas_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, 104 | CONSTRAINT `karmas_ibfk_2` FOREIGN KEY (`mod_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE 105 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 106 | /*!40101 SET character_set_client = @saved_cs_client */; 107 | 108 | -- 109 | -- Table structure for table `plays` 110 | -- 111 | 112 | DROP TABLE IF EXISTS `plays`; 113 | /*!40101 SET @saved_cs_client = @@character_set_client */; 114 | /*!40101 SET character_set_client = utf8 */; 115 | CREATE TABLE `plays` ( 116 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 117 | `site_id` varchar(255) NOT NULL, 118 | `positive` int(10) unsigned DEFAULT '0', 119 | `negative` int(10) unsigned DEFAULT '0', 120 | `grabs` int(10) unsigned DEFAULT '0', 121 | `listeners` int(10) unsigned DEFAULT '0', 122 | `skipped` int(10) unsigned DEFAULT '0', 123 | `created_at` datetime NOT NULL, 124 | `updated_at` datetime NOT NULL, 125 | `song_id` int(10) unsigned DEFAULT NULL, 126 | `user_id` int(10) unsigned DEFAULT NULL, 127 | PRIMARY KEY (`id`), 128 | KEY `song_id` (`song_id`), 129 | KEY `user_id` (`user_id`), 130 | CONSTRAINT `plays_ibfk_1` FOREIGN KEY (`song_id`) REFERENCES `songs` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, 131 | CONSTRAINT `plays_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE 132 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 133 | /*!40101 SET character_set_client = @saved_cs_client */; 134 | 135 | -- 136 | -- Table structure for table `room_events` 137 | -- 138 | 139 | DROP TABLE IF EXISTS `room_events`; 140 | /*!40101 SET @saved_cs_client = @@character_set_client */; 141 | /*!40101 SET character_set_client = utf8 */; 142 | CREATE TABLE `room_events` ( 143 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 144 | `type` varchar(255) NOT NULL, 145 | `title` varchar(255) NOT NULL, 146 | `slug` varchar(255) NOT NULL, 147 | `details` text, 148 | `starts_at` datetime DEFAULT NULL, 149 | `ends_at` datetime DEFAULT NULL, 150 | `created_at` datetime NOT NULL, 151 | `updated_at` datetime NOT NULL, 152 | `mod_user_id` int(10) unsigned DEFAULT NULL, 153 | PRIMARY KEY (`id`), 154 | KEY `mod_user_id` (`mod_user_id`), 155 | CONSTRAINT `room_events_ibfk_1` FOREIGN KEY (`mod_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE 156 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 157 | /*!40101 SET character_set_client = @saved_cs_client */; 158 | 159 | -- 160 | -- Table structure for table `song_responses` 161 | -- 162 | 163 | DROP TABLE IF EXISTS `song_responses`; 164 | /*!40101 SET @saved_cs_client = @@character_set_client */; 165 | /*!40101 SET character_set_client = utf8 */; 166 | CREATE TABLE `song_responses` ( 167 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 168 | `media_type` varchar(255) DEFAULT NULL, 169 | `pattern` varchar(255) NOT NULL, 170 | `response` varchar(255) DEFAULT NULL, 171 | `rate` int(11) DEFAULT '0', 172 | `is_active` tinyint(1) DEFAULT '1', 173 | `created_at` datetime NOT NULL, 174 | `updated_at` datetime NOT NULL, 175 | PRIMARY KEY (`id`) 176 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 177 | /*!40101 SET character_set_client = @saved_cs_client */; 178 | 179 | -- 180 | -- Table structure for table `songs` 181 | -- 182 | 183 | DROP TABLE IF EXISTS `songs`; 184 | /*!40101 SET @saved_cs_client = @@character_set_client */; 185 | /*!40101 SET character_set_client = utf8 */; 186 | CREATE TABLE `songs` ( 187 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 188 | `site` varchar(255) NOT NULL DEFAULT 'dubtrack', 189 | `site_id` varchar(255) NOT NULL, 190 | `name` varchar(255) NOT NULL, 191 | `slug` varchar(255) NOT NULL, 192 | `author` varchar(255) DEFAULT NULL, 193 | `title` varchar(255) DEFAULT NULL, 194 | `description` text, 195 | `release_date` date DEFAULT NULL, 196 | `tags` varchar(255) DEFAULT NULL, 197 | `host` varchar(255) NOT NULL DEFAULT 'youtube', 198 | `host_id` varchar(255) NOT NULL, 199 | `permalink` varchar(255) DEFAULT NULL, 200 | `duration` int(10) unsigned DEFAULT NULL, 201 | `image` varchar(255) DEFAULT NULL, 202 | `is_banned` tinyint(1) DEFAULT '0', 203 | `banned_reason` varchar(255) DEFAULT NULL, 204 | `created_at` datetime NOT NULL, 205 | `updated_at` datetime NOT NULL, 206 | PRIMARY KEY (`id`), 207 | UNIQUE KEY `site_id` (`site_id`), 208 | UNIQUE KEY `site_host_id` (`site`,`host`,`host_id`) 209 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 210 | /*!40101 SET character_set_client = @saved_cs_client */; 211 | 212 | -- 213 | -- Table structure for table `user_aliases` 214 | -- 215 | 216 | DROP TABLE IF EXISTS `user_aliases`; 217 | /*!40101 SET @saved_cs_client = @@character_set_client */; 218 | /*!40101 SET character_set_client = utf8 */; 219 | CREATE TABLE `user_aliases` ( 220 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 221 | `username` varchar(255) NOT NULL, 222 | `created_at` datetime NOT NULL, 223 | `updated_at` datetime NOT NULL, 224 | `user_id` int(10) unsigned DEFAULT NULL, 225 | PRIMARY KEY (`id`), 226 | KEY `user_id` (`user_id`), 227 | CONSTRAINT `user_aliases_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE 228 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 229 | /*!40101 SET character_set_client = @saved_cs_client */; 230 | 231 | -- 232 | -- Table structure for table `users` 233 | -- 234 | 235 | DROP TABLE IF EXISTS `users`; 236 | /*!40101 SET @saved_cs_client = @@character_set_client */; 237 | /*!40101 SET character_set_client = utf8 */; 238 | CREATE TABLE `users` ( 239 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 240 | `site` varchar(50) NOT NULL DEFAULT 'dubtrack', 241 | `site_id` varchar(255) NOT NULL, 242 | `username` varchar(255) NOT NULL, 243 | `slug` varchar(255) NOT NULL, 244 | `locale` varchar(255) DEFAULT 'en_US', 245 | `avatar` varchar(255) DEFAULT NULL, 246 | `badge` varchar(255) DEFAULT NULL, 247 | `bio` text, 248 | `role` varchar(255) DEFAULT NULL, 249 | `site_points` int(10) unsigned DEFAULT '0', 250 | `custom_points` int(10) unsigned DEFAULT '0', 251 | `joined` datetime DEFAULT NULL, 252 | `birthday` date DEFAULT NULL, 253 | `queue_position` int(11) DEFAULT '-1', 254 | `last_seen` datetime DEFAULT NULL, 255 | `last_active` datetime DEFAULT NULL, 256 | `last_leave` datetime DEFAULT NULL, 257 | `dj_timeout_until` datetime DEFAULT NULL, 258 | `created_at` datetime NOT NULL, 259 | `updated_at` datetime NOT NULL, 260 | PRIMARY KEY (`id`), 261 | UNIQUE KEY `site_id` (`site`,`site_id`) USING BTREE 262 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 263 | /*!40101 SET character_set_client = @saved_cs_client */; 264 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 265 | 266 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 267 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 268 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 269 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 270 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 271 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 272 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 273 | -------------------------------------------------------------------------------- /events/newsong.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | bot.on("newsong", function (data) { 3 | if (config.verboseLogging) { 4 | console.log("[SONG START]", JSON.stringify(data, null, 2)); 5 | } 6 | 7 | const song = data.room.metadata.current_song; 8 | if (song.source == "yt") { 9 | song.source = "youtube"; 10 | } 11 | console.log("********************************************************************"); 12 | console.log("[UPTIME]", "Bot online " + timeSince(startupTimestamp, true)); 13 | console.log(`[SONG START] ${song.djname} (${song.djid}) played: ${song.metadata.artist} - ${song.metadata.song} (${song.source}: ${song.sourceid})`); 14 | 15 | roomState.snags = 0; 16 | if (config.queue.upvoteSongs == "ALL" && data.room.metadata.current_song) { 17 | bot.bop(); 18 | } 19 | 20 | // Perform automatic song metadata correction 21 | if (config.autoSuggestCorrections) { 22 | correctMetadata(); 23 | } 24 | 25 | models.Play.findOne({ 26 | include: [ 27 | { model: models.Song, required: true, where: { host: song.source, host_id: song.sourceid } }, 28 | { model: models.User, required: true }, 29 | ], 30 | order: [["created_at", "DESC"]], 31 | }).then(function (row) { 32 | if (!row) { 33 | bot.speak(`This is the first time I have seen this video of ${song.metadata.artist} - ${song.metadata.song} played!`); 34 | } else { 35 | /* 36 | if (song.is_banned) { 37 | logMessage = "[SKIP] Skipped " + data.currentDJ.username + " spinning a blacklisted song: " + data.media.name + " (id: " + data.media.id + ")"; 38 | message = "Sorry @" + data.currentDJ.username + ", this video has been blacklisted in our song database."; 39 | if (song.banned_reason) { 40 | message += " (" + song.banned_reason + ")"; 41 | logMessage += " (" + song.banned_reason + ")"; 42 | } 43 | console.log(logMessage); 44 | sendToWebhooks(logMessage); 45 | bot.sendChat(message); 46 | bot.moderateForceSkip(); 47 | getDbUserFromSiteUser(data.currentDJ, function (dbuser) { 48 | var userData = { 49 | type: "skip", 50 | details: "Skipped for playing a blacklisted song: " + data.media.name + " (id: " + data.media.id + ")", 51 | user_id: dbuser.id, 52 | mod_user_id: botUser.db.id, 53 | }; 54 | models.Karma.create(userData); 55 | }); 56 | } else if (song.release_date != null && config.queue.minSongReleaseDate != null && config.queue.maxSongReleaseDate != null) { 57 | if (moment(song.release_date).isBefore(config.queue.minSongReleaseDate) || moment(song.release_date).isAfter(config.queue.maxSongReleaseDate)) { 58 | var releaseYear = moment(song.release_date).format("Y"); 59 | logMessage = "[SKIP] Skipped " + data.currentDJ.username + " spinning an out-of-range song from " + releaseYear + ": " + data.media.name + " (id: " + data.media.id + ")"; 60 | message = "Sorry @" + data.currentDJ.username + ", this song is out of range for the current theme (" + releaseYear + ")."; 61 | console.log(logMessage); 62 | sendToWebhooks(logMessage); 63 | bot.sendChat(message); 64 | bot.moderateForceSkip(); 65 | getDbUserFromSiteUser(data.currentDJ, function (dbuser) { 66 | var userData = { 67 | type: "skip", 68 | details: "Skipped for playing an out-of-range song: " + data.media.name + " (id: " + data.media.id + ")", 69 | user_id: dbuser.id, 70 | mod_user_id: botUser.db.id, 71 | }; 72 | models.Karma.create(userData); 73 | }); 74 | } 75 | } else if (config.queue.maxSongLengthSecs > 0 && data.media.duration > config.queue.maxSongLengthSecs) { 76 | // Check if the song is too long for room settings. Then check to see if it's blacklisted 77 | logMessage = "[SKIP] Skipped " + data.currentDJ.username + " spinning a song of " + data.media.duration + " seconds"; 78 | console.log(logMessage); 79 | sendToWebhooks(logMessage); 80 | var maxLengthMins = Math.floor(config.queue.maxSongLengthSecs / 60); 81 | var maxLengthSecs = config.queue.maxSongLengthSecs % 60; 82 | if (maxLengthSecs < 10) { 83 | maxLengthSecs = "0" + maxLengthSecs; 84 | } 85 | bot.sendChat("Sorry @" + data.currentDJ.username + ", this song is over our maximum room length of " + maxLengthMins + ":" + maxLengthSecs + "."); 86 | bot.moderateForceSkip(); 87 | getDbUserFromSiteUser(data.currentDJ, function (dbuser) { 88 | var userData = { 89 | type: "skip", 90 | details: "Skipped for playing a song of " + data.media.duration + " (room configured for max of " + config.queue.maxSongLengthSecs + "s)", 91 | user_id: dbuser.id, 92 | mod_user_id: botUser.db.id, 93 | }; 94 | models.Karma.create(userData); 95 | }); 96 | } else if (data.media.duration == 0) { 97 | console.log("[ZEROLENGTH] Song was advanced by the site because it reported a duration of 0"); 98 | bot.sendChat("@" + data.currentDJ.username + ", this song was reported as 0:00 long. Please check your playlist or try .zerolength for more info."); 99 | } else if (data.media.format == 1) { 100 | song.permalink = "https://youtu.be/" + data.media.cid; 101 | } else if (data.media.format == 2) { 102 | soundcloud_get_track(data.media.cid, function (json_data) { 103 | song.permalink = json_data.permalink_url; 104 | 105 | if (!json_data.streamable) { 106 | console.log("[SKIP] Song was skipped because it is not available or embeddable"); 107 | bot.sendChat("Skipping this video because it is not available or embeddable."); 108 | bot.moderateForceSkip(); 109 | } 110 | }); 111 | } else { 112 | */ 113 | bot.speak(`${row.Song.name} • last played ${timeSince(row.createdAt)} by ${row.User.username} • ${row.listeners} :ear: • ${row.positive} :thumbsup: • ${row.negative} :thumbsdown: • ${row.grabs} :star:`); 114 | //} 115 | } 116 | }); 117 | 118 | //idleWaitListProcess(); 119 | 120 | /* 121 | // Auto skip for "stuck" songs 122 | if (typeof skipTimer !== "undefined") { 123 | clearTimeout(skipTimer); 124 | } 125 | var nextTimerDelay = (data.media.duration + 10) * 1000; 126 | if (config.queue.skipStuckSongs) { 127 | skipTimer = setTimeout(function () { 128 | if (bot.getMedia() && bot.getMedia().id == data.media.id) { 129 | var message = "[SKIP] Skipping " + data.media.name + " because it appears to be stuck..."; 130 | console.log(message); 131 | sendToWebhooks(message); 132 | bot.sendChat("Skipping " + data.media.name + " because it appears to be stuck..."); 133 | bot.moderateForceSkip(); 134 | } 135 | }, nextTimerDelay); 136 | } 137 | */ 138 | 139 | /* 140 | if (song.metadata.source == 'yt' && config.apiKeys.youtube.client_id) { 141 | YouTube.videos.list( 142 | { 143 | part: "id,snippet,status,statistics", 144 | id: data.media.cid, 145 | }, 146 | function (err, api_data) { 147 | if (api_data) { 148 | if (config.verboseLogging) { 149 | console.log("[YOUTUBE] " + JSON.stringify(api_data, null, 2)); 150 | } 151 | 152 | var available = true; 153 | var banned = false; 154 | var lowViewCount = false; 155 | 156 | if (api_data.items.length === 0) { 157 | available = false; 158 | } else { 159 | var item = _.first(api_data.items); 160 | if (!item.status || item.status.embeddable === false) { 161 | available = false; 162 | } 163 | if (item.statistics && item.statistics.viewCount < 100) { 164 | lowViewCount = true; 165 | } 166 | 167 | // See if this channel is blacklisted 168 | models.Blacklist.find({ where: { $and: [{ type: "channel" }, { is_active: true }, { pattern: item.snippet.channelId }] } }).then(function (row) { 169 | if (row) { 170 | banned = true; 171 | } 172 | 173 | if (banned) { 174 | bot.moderateBanUser(data.currentDJ.id, PlugAPI.BAN_REASON.OFFENSIVE_MEDIA, PlugAPI.BAN.PERMA); 175 | bot.sendChat("NOOOOOOOOOPE. https://media.giphy.com/media/9wBub5vhSsTDi/giphy.gif"); 176 | models.Song.update({ is_banned: 1 }, { where: { host_id: data.media.cid } }); 177 | var message = "[SKIPBAN] Song https://youtu.be/" + data.media.cid + " skipped and " + data.currentDJ.username + "(ID: " + data.currentDJ.id + ") banned because they used a song from a blacklisted channel."; 178 | console.log(message); 179 | sendToWebhooks(message); 180 | } else if (!available) { 181 | var message = "[SKIP] Song was skipped because it is not available or embeddable"; 182 | console.log(message); 183 | sendToWebhooks(message); 184 | bot.sendChat("@" + data.currentDJ.username + ", skipping this video because it is not available or embeddable. Please update your playlist!"); 185 | bot.moderateForceSkip(); 186 | } else if (lowViewCount) { 187 | var message = 188 | "[YOUTUBE] The current video played has very few views. You may want to check it for :trollface:... " + 189 | data.media.name + 190 | " (https://youtu.be/" + 191 | data.media.cid + 192 | ") played by " + 193 | data.currentDJ.username + 194 | " (ID: " + 195 | data.currentDJ.id + 196 | ")"; 197 | console.log(message); 198 | sendToWebhooks(message); 199 | } 200 | }); 201 | } 202 | } else { 203 | console.log(err); 204 | } 205 | } 206 | ); 207 | } 208 | */ 209 | 210 | /* 211 | models.SongResponse.findOne({ 212 | where: { 213 | $or: [ 214 | { 215 | $and: [{ media_type: "author" }, { pattern: { $like: "%" + song.metadata.artist + "%" } }], 216 | $and: [{ media_type: "title" }, { pattern: { $like: "%" + song.metadata.song + "%" } }], 217 | $and: [{ media_type: "id" }, { pattern: song.sourceid }], 218 | }, 219 | ], 220 | is_active: true, 221 | }, 222 | }).then(function (songresponse) { 223 | if (songresponse !== null) { 224 | if (songresponse.response != "") { 225 | bot.speak(songresponse.response); 226 | } 227 | if (songresponse.rate === 1) { 228 | bot.bop(); 229 | } else if (songresponse.rate === -1) { 230 | bot.vote("down"); 231 | } 232 | } 233 | }); 234 | */ 235 | }); 236 | }; 237 | -------------------------------------------------------------------------------- /documentation/ButtheadScript.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * ButtheadScript by AvatarKava - beavisbot(at)phpmonkeys(dot)com 4 | * adapted from SimplePlugScript by Enyxx - arkaenyx(at)gmail(dot)com 5 | * This work is under CreativeCommons BY-NC-SA 3.0 6 | * http://creativecommons.org/licenses/by-nc-sa/3.0/legalcode*/ 7 | // "use strict"; 8 | var nxVersion = "2.4"; 9 | var notice = "ButtheadScript v" + nxVersion + " by AvatarKava!\n - Options in the plug menu (top left)"; 10 | var forceReload = false; 11 | var AFKArray = []; 12 | 13 | var nx = { 14 | initial: function() { 15 | ToogleSelect = 0; 16 | }, 17 | OnFocus: function() { 18 | nx.DoMeh(); 19 | }, 20 | OnChat: function(a) { 21 | nx.resetAFK(a); 22 | }, 23 | OnAdvance: function(a) { 24 | null !== a && (1 == AutoWoot && nx.Woot(), 1 == AutoList && 2 == ListSort ? nx.ListSortUpdate(3) : 1 == AutoList && 1 == ListPos && nx.ListPosUpdate(3), nx.DoMeh()) 25 | $('.discogs a').attr('href', 'https://www.discogs.com/search/?&sort=year%2Casc&type=release&q=' + encodeURI(a.media.author + ' - ' + a.media.title)); 26 | }, 27 | OnWlUpdate: function(a) { 28 | 1 == AutoList && 2 == ListSort ? nx.ListSortUpdate(3) : 1 == AutoList && 1 == ListPos && nx.ListPosUpdate(3) 29 | }, 30 | OnJoin: function(a) { 31 | 1 == AutoList && nx.UpdateUserlist(1, a); 32 | nx.addAFK(a); 33 | }, 34 | OnLeave: function(a) { 35 | 1 == AutoList && nx.UpdateUserlist(2, a); 36 | nx.removeAFK(a); 37 | }, 38 | Woot: function() { 39 | $("div#woot").delay(3800).trigger("click"); 40 | }, 41 | SetButton: function() { 42 | 1 == AutoWoot ? ($(".nxvalwoot").css("color", "green"), $(".nxvalwoot").html("ON")) : ($(".nxvalwoot").css("color", "red"), $(".nxvalwoot").html("OFF")); 43 | 1 == AutoList ? ($(".nxvallist").css("color", "green"), $(".nxvallist").html("ON")) : ($(".nxvallist").css("color", "red"), $(".nxvallist").html("OFF")); 44 | }, 45 | ClicButton: function(a) { 46 | // Auto woot 47 | 1 == a && (1 == AutoWoot ? (AutoWoot = 0, "undefined" != typeof localStorage && localStorage.setItem("NXAutoWoot", "0")) : (AutoWoot = 1, "undefined" != typeof localStorage && localStorage.setItem("NXAutoWoot", "1"), nx.Woot())); 48 | // Auto List 49 | 3 == a && (1 == AutoList ? (AutoList = 0, "undefined" != typeof localStorage && localStorage.setItem("NXAutoList", "0"), nx.DeleteUserlist()) : (AutoList = 1, "undefined" != typeof localStorage && localStorage.setItem("NXAutoList", "1"), nx.CreateUserlist(), nx.showAFK())); 50 | nx.SetButton(); 51 | // List position toggle 52 | 60 == a && (1 == ListPos ? (ListPos = 0, "undefined" != typeof localStorage && localStorage.setItem("NXListPos", "0"), nx.ListPosUpdate(0)) : (ListPos = 1, "undefined" != typeof localStorage && localStorage.setItem("NXListPos", "1"), nx.ListPosUpdate(1))); 53 | 71 == a && 1 != ListSort && (ListSort = 1, "undefined" != typeof localStorage && localStorage.setItem("NXListSort", "1"), nx.ListSortUpdate(1)); 54 | 72 == a && 2 != ListSort && (ListSort = 2, "undefined" != typeof localStorage && localStorage.setItem("NXListSort", "2"), nx.ListSortUpdate(1)); 55 | }, 56 | DoButton: function() { 57 | 0 === nxButtons && ($("#app-menu .list .nxmenu").remove(), $("#app-menu .list").append('
AutoWoot:
\n
User List :
'), 58 | $("#app-menu .list .nxautowoot span,#app-menu .list .nxautolist span").css({ 59 | "top": "0px", 60 | "position": "relative" 61 | }), $("#app-menu .list .nxclass").css({ 62 | "top": "10px", 63 | "position": "absolute", 64 | "padding-left": "15px !important" 65 | }), nx.SetButton(), nxButtons = 1); 66 | $("#app-menu .list .nxclass span").css({ 67 | float: "none" 68 | }); 69 | $(".nxautowoot").mousedown(function() { 70 | nx.ClicButton(1); 71 | }); 72 | $(".nxautolist").mousedown(function() { 73 | nx.ClicButton(3); 74 | }) 75 | }, 76 | ShowMeh: function() { 77 | $.each(API.getUsers(), function(a, b) { 78 | if(b.username) { 79 | $(document).find(".nxuser:contains('" + nxEscape(b.username) + "') i.nxmehi").remove(); 80 | 1 == b.grab ? $(document).find(".nxlist .nxuser:contains('" + nxEscape(b.username) + "')").remove("i.icon-woot,i.icon-meh,i.icon-grab").append('') : "-1" == b.vote ? $(document).find(".nxuser:contains('" + 81 | nxEscape(b.username) + "')").remove("i.icon-woot,i.icon-meh,i.icon-grab").append('') : "1" == AutoList && "1" == b.vote && $(document).find(".nxlist .nxuser:contains('" + nxEscape(b.username) + "')").remove("i.icon-woot,i.icon-meh,i.icon-grab").append('') 82 | } 83 | }); 84 | $("#user-lists .list.room .nxuser .icon-woot,#user-lists .list.room .nxuser .icon-meh").css({ 85 | "left": "auto", 86 | "right": "9px", 87 | "top": "-1px" 88 | }); 89 | $(".nxlist .nxuser .icon-woot").css({ left: "auto", right: "0px", top: "-5px" }); 90 | $(".nxlist .nxuser .icon-meh").css({ left: "auto", right: "-1px", top: "-5px" }); 91 | $(".nxlist .nxuser .icon-grab").css({ left: "auto", right: "-1px", top: "-5px" }) 92 | }, 93 | DoMeh: function() { 94 | 0 < $(".list.room").length && nx.ShowMeh() 95 | }, 96 | GetSettings: function() { 97 | "undefined" != typeof localStorage ? ("NXAutoWoot" in localStorage ? AutoWoot = localStorage.getItem("NXAutoWoot") : (console.log("First use ?"), localStorage.setItem("NXAutoWoot", AutoWoot)), 98 | "NXAutoList" in localStorage ? AutoList = localStorage.getItem("NXAutoList") : (console.log("Update >2.25 ?"), localStorage.setItem("NXAutoList", AutoList)), "NXListPos" in localStorage ? ListPos = localStorage.getItem("NXListPos") : (console.log("Update >2.90 ?"), localStorage.setItem("NXListPos", ListPos)), "NXListSort" in localStorage ? ListSort = localStorage.getItem("NXListSort") : (console.log("Update >2.92 ?"), localStorage.setItem("NXListSort", ListSort))) : console.log("Storage isn't supported by this browser") 99 | }, 100 | CreateUserlist: function() { 101 | ListDisplay = ""; 102 | "0" === ListPos && (ListDisplay = "display: none"); 103 | $("#app").append('
\n
List Sort Position
\n'); 105 | $("#nxlisttooglepos").mousedown(function(a) { 106 | a.preventDefault(); 107 | nx.ClicButton(60); 108 | return !1 109 | }); 110 | $("#nxlistorder").mousedown(function(a) { 111 | a.preventDefault(); 112 | nx.toogleListSelect(this) 113 | }); 114 | $nxUsers = API.getUsers(); 115 | $nxWL = API.getWaitList(); 116 | $nxDJ = API.getDJ(); 117 | if (1 == ListSort) { 118 | for (x = 0; x < $nxUsers.length; x++) { 119 | nx.AppendUserList("div.nxuser:last", $nxUsers[x]); 120 | } 121 | } else if (2 == ListSort) { 122 | nx.AppendUserList("div.nxuser:last", $nxDJ); 123 | for (x = 0; x < $nxWL.length; x++) { 124 | $nxWL[x].wlIndex = x; 125 | for (i = 0; i < $nxUsers.length; i++) { 126 | if ($nxWL[x].username === $nxUsers[i].username) { 127 | $nxUsers[i].wlIndex = x; 128 | } 129 | } 130 | nx.AppendUserList("div.nxuser:last", $nxWL[x]); 131 | } 132 | $(".nxlist").append('
\t
'); 133 | for (x = 0; x < $nxUsers.length; x++) { 134 | nxEscape($nxDJ.username) != nxEscape($nxUsers[x].username) && (0 > $nxUsers[x].wlIndex || void 0 === $nxUsers[x].wlIndex) && nx.AppendUserList("div.nxuser:last", $nxUsers[x]) 135 | } 136 | } 137 | 138 | $("#nxdivlist").css({ 139 | "position": "absolute", 140 | "left": 50, 141 | "top": 50, 142 | "font-size": "0.8rem", 143 | "height": "100%", 144 | "z-index": 17, 145 | "background-color": "rgba(0, 0, 0, 0.7)", 146 | "width": "250px" 147 | }); 148 | $("#nxlistmenu").css({ 149 | "height": "20px", 150 | "position": "relative", 151 | "z-index": "17", 152 | "background-color": "#0a0a0a", 153 | "padding": "5px 0px 5px 10px", 154 | "color": "#808691", 155 | "border-bottom": "1px solid #323742" 156 | }); 157 | $("#nxdivlist .nxlist").css({ 158 | "position": "relative", 159 | "height": "calc(100% - 50)", 160 | "padding": "5px 0 10px 0", 161 | "overflow-y": "auto", 162 | "overflow-x": "hidden" 163 | }); 164 | $('.discogs a').css({ 165 | "color": "white" 166 | }); 167 | 168 | $(".nxlist .nxuser").mouseenter(function() { 169 | $(this).css({ "background-color": "#282c35"}); 170 | }).mouseleave(function() { 171 | $(this).css({ "background-color": "transparent"}); 172 | }); 173 | $(".nxlist .name").css({ position: "relative", left: "20px" }); 174 | $(".nxlist .listtxt").css({ width: "100%" }); 175 | $(".nxlist .nxtimer").css({"display": "none","color": "#42c1ee","position": "relative","left": "20px"}); 176 | nx.DoMeh(); 177 | $(".nxlist .nxuser").not(".nxtimer").each(function() { 178 | $(this).css({ position: "relative", cursor: "pointer" }).unbind().on("click", { param: this }, nx.ClickUserlist) 179 | }); 180 | $('.discogs a').attr('href', 'https://www.discogs.com/search/?&type=release&q=' + encodeURI(API.getMedia().author + ' - ' + API.getMedia().title)); 181 | nx.ListButtonUpdate(); 182 | $(window).on("click", nx.ListButtonUpdate).on("resize", nx.ListButtonUpdate); 183 | }, 184 | ClickUserlist: function(a) { 185 | a.preventDefault(); 186 | uid = $.trim($(a.data.param).find(".uid").not(".nxtimer").text()); 187 | // @TODO Replace with something that doesn't require RCS to be running 188 | //console.log('Clicking UID: ' + uid); 189 | //require(['plug-modules!plug/core/Events'], function (Events) {Events.trigger('notify', 'icon-plug-dj', 'Clicking UID: ' + uid);}); 190 | //_$context.dispatch(new _$userRolloverEvent(_$userRolloverEvent.SHOW, new _$userModel(API.getUser(uid)), {x: 550, y: a.pageY })); 191 | username = $.trim($(a.data.param).find(".name").not(".nxtimer").text()); 192 | a = $("#chat-input-field"); 193 | a.val(a.val() + "@" + username + " ").focus() 194 | 195 | //$(".list.room .user.id-" + uid).click(); 196 | }, 197 | DeleteUserlist: function() { 198 | VotePos = $("#vote").position(); 199 | $("#nxdivlist").remove(); 200 | $(".nxlist .nxuser").unbind(); 201 | $(window).off("click", nx.ListButtonUpdate).off("resize", nx.ListButtonUpdate); 202 | }, 203 | UpdateUserlist: function(a, b) { 204 | if (1 == a) { 205 | $nxUsers = API.getUsers(); 206 | var c, e, d = 1; 207 | for (x = 0; x < $nxUsers.length && 1 == d; x++) { 208 | $nxUsers[x].username == b.username && (c = x, d = 0); 209 | } 210 | if (1 == ListSort && 0 !== c) { 211 | $previousUser = ".nxuser:contains('" + nxEscape($nxUsers[c - 1].username) + "')"; 212 | } else if (2 == ListSort && 0 !== c) { 213 | d = 1; 214 | for (y = c - 1; 0 <= y && 1 == d; y--) { 215 | if (void 0 === $nxUsers[y].wlIndex || 0 > $nxUsers[y].wlIndex) { 216 | e = y, d = 0; 217 | } 218 | } 219 | $previousUser = 0 === d ? ".nxuser:contains('" + nxEscape($nxUsers[e].username) + "')" : ".nxuser.spacer" 220 | } else { 221 | $previousUser = 2 == ListSort ? ".nxuser.spacer" : ".nxfirstuser"; 222 | } 223 | nx.AppendUserList($previousUser, $nxUsers[c]); 224 | $(".nxlist .name").css({ 225 | position: "relative", 226 | left: "20px" 227 | }); 228 | $(".nxlist .nxuser").css({ position: "relative", cursor: "pointer" }); 229 | $(".listtxt").css({ width: "100%" }); 230 | $(".nxlist .nxtimer").css({ display: "none", color: "#42c1ee", position: "relative", left: "20px" }); 231 | $(".nxlist .nxuser").unbind(); 232 | $(".nxlist .nxuser").not(".nxtimer").each(function() { 233 | $(this).css({ 234 | position: "relative", 235 | cursor: "pointer" 236 | }).unbind().on("click", { param: this }, nx.ClickUserlist) 237 | }) 238 | } 239 | 2 == a && $(document).find(".nxlist .nxuser:contains('" + nxEscape(b.username) + "')").remove() 240 | }, 241 | AppendUserList: function(a, b) { 242 | var c = 0; 243 | $previousUser = 0 !== a ? a : ""; 244 | //nxUserDiv1 = '
\n' + b.username.replace(/\'/g, "\\'").replace(/\"/g, '\\"').replace(/\\/g, "\\\\") + '\n\n\n'; 245 | nxUserDiv1 = '
\n' + nxEscape(b.username) + '\n\n\n'; 246 | idleSeconds = 0; 247 | if (typeof AFKArray !== 'undefined') { 248 | for (i = 0; i < AFKArray.length; i++) { 249 | if (b.username == AFKArray[i].username) { 250 | idleSeconds = (new Date - AFKArray[i].Stime) / 1000; 251 | } 252 | } 253 | } 254 | if (idleSeconds > 3600) { 255 | nxUserPosition = '\n'; 256 | } else { 257 | nxUserPosition = '\n' + (b.wlIndex + 1) + ""; 258 | } 259 | API.getDJ().username == b.username && (c = 1, nxUserPosition = '\nDJ'); 260 | nxUserDiv2 = "
\n"; 261 | nxUserContent = 1 == ListPos && (0 <= b.wlIndex || 1 == c) ? nxUserDiv1 + nxUserPosition + nxUserDiv2 : nxUserDiv1 + nxUserDiv2; 262 | "10000" == b.role && $(".nxlist " + $previousUser).after('
\n' + nxUserContent + "
\n"); 263 | "1000" == b.gRole || "2000" == b.gRole || "3000" == b.gRole ? $(".nxlist " + $previousUser).after('
\n' + nxUserContent + "
\n") : "5000" == b.role ? $(".nxlist " + 264 | $previousUser).after('
\n' + nxUserContent + "
\n") : "4000" == b.role ? $(".nxlist " + $previousUser).after('
\n' + nxUserContent + "
\n") : "3000" == b.role ? $(".nxlist " + $previousUser).after('
\n' + nxUserContent + "
\n") : "2000" == b.role ? $(".nxlist " + $previousUser).after('
\n' + 265 | nxUserContent + "
\n") : "1000" == b.role ? $(".nxlist " + $previousUser).after('
\n' + nxUserContent + "
\n") : "0" == b.role && $(".nxlist " + $previousUser).after('
\n' + nxUserContent + "
\n") 266 | }, 267 | ListButtonUpdate: function() { 268 | //$("#dj-button").css({ margin: "5px", left: "0px", top: "0px", position: "relative", width: "230px" }); 269 | //$("#nxdivlist").css("height", $(".app-right").height() - $("#dj-button").height() - 25 - 30); 270 | $("#nxdivlist").css("height", $(".app-right").height()); 271 | }, 272 | getUserID: function(a) { 273 | var b = null; 274 | $.each(API.getUsers(), 275 | function(c, e) { 276 | if (e.username == a) return b = e.id, !1 277 | }); 278 | return b 279 | }, 280 | initAFK: function() { 281 | window.AFKArray = []; 282 | $.each(API.getUsers(), function(a, b) { 283 | AFKArray.push({ id: b.id, username: b.username, Stime: new Date, join: new Date }) 284 | }); 285 | }, 286 | showAFK: function() { 287 | $(".listtxt").each(function() { 288 | nx.bindAFK(this); 289 | }); 290 | }, 291 | bindAFK: function(a) { 292 | var b; 293 | $(a).on("mouseenter", function() { 294 | $(this).find(".name").not(".nxtimer").hide(); 295 | ListTimeName = $(this).find(".name").not(".nxtimer").text(); 296 | for (i = 0; i < AFKArray.length; i++) { 297 | ListTimeName == AFKArray[i].username && (b = i); 298 | } 299 | $(this).find(".nxtimer").show(); 300 | this.nxtime = nx.convertMS(new Date() - AFKArray[b].Stime); 301 | $(this).find(".nxtimer").text(" AFK: " + this.nxtime.h + "h" + this.nxtime.m + "m" + this.nxtime.s + "s"); 302 | }).on("mouseleave", function() { 303 | $(this).find(".nxtimer").hide(); 304 | $(this).find(".name").not(".nxtimer").show(); 305 | }); 306 | }, 307 | addAFK: function(a) { 308 | AFKArray.push({ id: a.id, username: a.username, Stime: new Date(), join: new Date() }); 309 | nx.bindAFK($(document).find(".nxlist .nxuser:contains('" + nxEscape(a.username) + "')")); 310 | }, 311 | removeAFK: function(a) { 312 | AFKArray = $.map(AFKArray, function(b) { 313 | return b.username == a.username ? null : b 314 | }) 315 | }, 316 | resetAFK: function(a) { 317 | for (i = 0; i < AFKArray.length; i++) a.un == AFKArray[i].username && (AFKArray[i].Stime = new Date) 318 | }, 319 | convertMS: function(a) { 320 | var b, c, e; 321 | e = Math.floor(a / 1E3); 322 | c = Math.floor(e / 60); 323 | b = Math.floor(c / 60); 324 | a = Math.floor(b / 24); 325 | return { d: a, h: b % 24, m: c % 60, s: e % 60 } 326 | }, 327 | toogleListSelect: function(a) { 328 | 0 === ToogleSelect ? (nx.showListSelect(a), ToogleSelect = 1) : ($("div#nxpopup").remove(), ToogleSelect = 0) 329 | }, 330 | showListSelect: function(a) { 331 | var b = $(a).height(), 332 | c = $(a).position(), 333 | b = c.top + b + 7; 334 | $("#app").append('
\n \t
\n \t\t
\n \t\t\t
\n \t\t\t\t\n \t\t\t\tRank First \n \t\t\t
\n \t\t\t
\n \t\t\t\t\n \t\t\t\tWaitlist First \n \t\t\t
\n \t\t
\n \t
\n
'); 335 | 1 == ListSort ? ($(".nxpopcontainer.rankf i").css("display", ""), $(".nxpopcontainer.wlf i").css("display", "none")) : 2 == ListSort && ($(".nxpopcontainer.wlf i").css("display", ""), $(".nxpopcontainer.rankf i").css("display", "none")); 336 | $(".nxpopcontainer.rankf").mousedown(function(b) { 337 | b.preventDefault(); 338 | nx.ClicButton(71); 339 | nx.toogleListSelect(a) 340 | }); 341 | $(".nxpopcontainer.wlf").mousedown(function(b) { 342 | b.preventDefault(); 343 | nx.ClicButton(72); 344 | nx.toogleListSelect(a) 345 | }) 346 | }, 347 | ListSortUpdate: function(a) { 348 | 1 == a ? (nx.DeleteUserlist(), nx.CreateUserlist(), 349 | nx.showAFK()) : 3 == a && (nx.DeleteUserlist(), nx.CreateUserlist(), nx.showAFK()) 350 | }, 351 | ListPosUpdate: function(a) { 352 | 0 === a ? $("#nxlisttooglepos i").css("display", "none") : 1 == a && $("#nxlisttooglepos i").css("display", ""); 353 | 0 !== a && 3 != a || $("span.pos").remove(); 354 | 1 != a && 3 != a || $(".listtxt").each(function() { 355 | UserDJ = API.getDJ(); 356 | UserArray = API.getUsers(); 357 | ListKey = void 0; 358 | var a = $(this).find(".name").not(".nxtimer").text(); 359 | for (i = 0; i < UserArray.length; i++) a == UserArray[i].username && (ListKey = i); 360 | void 0 === ListKey ? console.log("error ListKey not found") : 361 | 0 <= UserArray[ListKey].wlIndex ? $(this).append('\n' + (UserArray[ListKey].wlIndex + 1) + "") : a == UserDJ.username && $(this).append('\nDJ') 362 | }) 363 | }, 364 | }; 365 | 366 | function nxdestroy() { 367 | API.off(API.ADVANCE, nx.OnAdvance); 368 | API.off(API.VOTE_UPDATE, nx.DoMeh); 369 | API.off(API.USER_JOIN, nx.OnJoin); 370 | API.off(API.USER_LEAVE, nx.OnLeave); 371 | API.off(API.CHAT, nx.OnChat); 372 | API.off(API.WAIT_LIST_UPDATE, nx.OnWlUpdate); 373 | $("#app").off("mousedown", nx.DoButton); 374 | $("#users-button").off("click", nx.DoMeh); 375 | $(".button.room").off("click", nx.DoMeh); 376 | nxButtons = nxScript = 0; 377 | window.AFKArray = []; 378 | $("#app").one("mousedown", function() { 379 | $(".nxmenu").remove() 380 | }).mousedown().mouseup().mousedown().mouseup(); 381 | "undefined" != typeof nx && (nx.DeleteUserlist(), $(window).off("click", nx.ListButtonUpdate).off("resize", nx.ListButtonUpdate)); 382 | clearInterval(showafktimer); 383 | console.log("Unloaded old version") 384 | } 385 | 386 | function nxEscape(a) { 387 | a.replace(/\'/g, "\\'").replace(/\"/g, '\\"').replace(/\\/g, "\\\\"); 388 | return a; 389 | } 390 | 391 | function init() { 392 | nxButtons = AutoWoot = 0; 393 | AutoList = ListPos = AFKTmr = 1; 394 | ListSort = 2; 395 | API.on(API.ADVANCE, nx.OnAdvance); 396 | API.on(API.VOTE_UPDATE, nx.DoMeh); 397 | API.on(API.USER_JOIN, nx.OnJoin); 398 | API.on(API.USER_LEAVE, nx.OnLeave); 399 | API.on(API.CHAT, nx.OnChat); 400 | API.on(API.WAIT_LIST_UPDATE, nx.OnWlUpdate); 401 | nx.GetSettings(); 402 | console.log(notice); 403 | $("#chat-messages").append('
' + notice + '
'); 404 | $("#chat-messages").scrollTop($("#chat-messages div.message").last().position().top - $("#chat-messages div.message").first().position().top + 100); 405 | $(window).bind('beforeunload', function() { 406 | return "You are about to navigate away from this plug.dj community!" 407 | }); 408 | $(".nxnotif").css("color", "#89be6c"); 409 | } 410 | 411 | function base() { 412 | 413 | init(); 414 | if (AutoWoot == 1) { 415 | nx.Woot(); 416 | } 417 | if (AutoList == 1) { 418 | nx.CreateUserlist(); 419 | } 420 | nx.initial(); 421 | nx.initAFK(); 422 | nx.showAFK(); 423 | nxScript = 1; 424 | $(window).on("focus", nx.OnFocus); 425 | $("#app").on("mousedown", nx.DoButton); 426 | $("#users-button").on("click", nx.DoMeh); 427 | $(".button.room").on("click", nx.DoMeh); 428 | $("#user-lists .list.room .nxuser .icon-meh").css({ left: "auto", right: "9px", top: "-1px" }); 429 | showafktimer = setInterval(nx.showAFK, 150000); 430 | if ("undefined" != typeof localStorage) { 431 | localStorage.setItem("NXVersion", nxVersion); 432 | } else { 433 | sessionStorage.setItem("NXVersion", nxVersion); 434 | } 435 | 436 | requirejs.config({paths: {'plug-modules': 'https://unpkg.com/plug-modules/plug-modules'}}); 437 | require(['plug-modules!plug/core/Events'], function (Events) {Events.trigger('notify', 'icon-plug-dj', notice);}); 438 | 439 | } 440 | 441 | $(".bottom__playback-controls--desktop .stream").before('
  • '); 442 | 443 | $.getScript('https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.2/require.min.js') 444 | .done(function(script, textStatus) { 445 | if ("undefined" == typeof nxScript || 0 === nxScript) { 446 | base(); 447 | } else if (forceReload || sessionStorage.getItem("NXVersion") < (nxVersion)) { 448 | console.log("Update available"); 449 | nxdestroy(); 450 | base(); 451 | console.log("loaded new version"); 452 | } else { 453 | console.log("Already running"); 454 | } 455 | }).fail(function(jqxhr, settings, exception) { 456 | console.log('Something did not work'); 457 | }); 458 | --------------------------------------------------------------------------------