diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..731ef49 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM codecatt/reddit-radio:base + +ENV CONFIG_FILE="./config/config.toml" + +WORKDIR /app + +COPY . /app + +CMD ["node", "index.js"] diff --git a/Dockerfile.base b/Dockerfile.base new file mode 100644 index 0000000..fa55f3e --- /dev/null +++ b/Dockerfile.base @@ -0,0 +1,18 @@ +FROM node:16-alpine + +WORKDIR /app + +COPY ./package.json /app/package.json + +RUN apk add --no-cache ffmpeg \ + && apk add --no-cache --virtual .build-deps \ + g++ \ + gcc \ + libgcc \ + make \ + autoconf \ + libtool \ + automake \ + python3 \ + && npm install \ + && apk del .build-deps diff --git a/Radio.js b/Radio.js new file mode 100644 index 0000000..daa7627 --- /dev/null +++ b/Radio.js @@ -0,0 +1,118 @@ +var discord = require("discord.js"); + +class Radio +{ + constructor(config, radioconfig) + { + this.config = config; + + this.name = radioconfig.name; + this.url = radioconfig.url; + + this.client = new discord.Client({ + intents: [ + // List of intents: https://discord.com/developers/docs/topics/gateway#list-of-intents + discord.Intents.FLAGS.GUILDS, + discord.Intents.FLAGS.GUILD_VOICE_STATES, + ], + }); + + this.running = false; + this.voice_connection = false; + this.voice_dispatcher = false; + + this.channel = false; + + this.client.on("ready", () => { + console.log("Radio client \"" + this.name + "\" connected!"); + + this.client.channels.fetch(radioconfig.channel).then(channel => { + this.channel = channel; + if (channel.members.size > 0) { + this.joinChannel(); + } + }); + }); + + this.client.on("error", (e) => { + console.log("Radio bot error:", e); + }); + + this.client.on("voiceStateUpdate", (o, n) => { + if (n.channel == this.channel) { + console.log("Someone joined \"" + this.name + "\": " + this.channel.members.size); + if (!this.running) { + this.running = true; + this.joinChannel(); + } + } else if (o.channel == this.channel && n.channel != this.channel) { + console.log("Someone left \"" + this.name + "\": " + this.channel.members.size); + if (this.running && this.channel.members.size == 1) { + this.running = false; + this.leaveChannel(); + } + } + }); + + this.client.login(radioconfig.token); + } + + stop() + { + console.log("Stopping radio \"" + this.name + "\"..."); + return this.client.destroy(); + } + + joinChannel() + { + console.log("Joining and starting \"" + this.name + "\"!"); + this.running = true; + + this.channel.join().then((conn) => { + this.voice_connection = conn; + this.voice_connection.on("disconnect", () => { + this.voice_connection = false; + }); + this.voice_connection.on("newSession", () => { + this.startBroadcast(); + }); + this.voice_connection.on("error", (err) => { + console.log("Error: " + err); + }); + this.startBroadcast(); + }).catch(console.error); + } + + leaveChannel() + { + console.log("Leaving \"" + this.name + "\"."); + this.running = false; + + if (this.voice_dispatcher !== false) { + this.voice_dispatcher.end(); + } + + if (this.voice_connection !== false) { + this.voice_connection.disconnect(); + } + } + + startBroadcast() + { + if (this.voice_connection === false) { + return; + } + + if (this.voice_dispatcher !== false) { + this.voice_dispatcher.end(); + } + + this.voice_dispatcher = this.voice_connection.play(this.url, this.config.voice); + this.voice_dispatcher.on("end", (reason) => { + console.log("Radio voice \"" + this.name + "\" ended: \"" + reason + "\""); + this.voice_dispatcher = false; + }); + } +} + +module.exports = Radio; diff --git a/RedditRadio.js b/RedditRadio.js new file mode 100644 index 0000000..33a650c --- /dev/null +++ b/RedditRadio.js @@ -0,0 +1,445 @@ +var discord = require("discord.js"); +var colors = require("colors"); +var moment = require("moment-timezone"); + +var process = require("process"); +var fs = require("fs"); +var https = require("https"); + +var cmdsplit = require("./cmdsplit"); +var Radio = require("./Radio"); +var MongoClient = require("mongodb").MongoClient; + +function findCommand(obj, cmdID) +{ + var cmdName = "onCmd" + cmdID; + var cmdRegex = new RegExp("^" + cmdName + "$", "i"); + + var prototype = Object.getPrototypeOf(obj); + var props = Object.getOwnPropertyNames(prototype); + for (var i = 0; i < props.length; i++) { + var key = props[i]; + if (!key.startsWith("onCmd")) { + continue; + } + if (key.match(cmdRegex)) { + return obj[key]; + } + } + return null; +} + +class RedditRadio +{ + constructor(config) + { + this.config = config; + this.readyPromises = []; + + moment.tz.setDefault(this.config.discord.timezone || "Europe/Amsterdam"); + + console.log('Discord.js version', discord.version); + + this.client = new discord.Client({ + intents: [ + // List of intents: https://discord.com/developers/docs/topics/gateway#list-of-intents + discord.Intents.FLAGS.GUILDS, + discord.Intents.FLAGS.GUILD_MEMBERS, + discord.Intents.FLAGS.GUILD_BANS, + discord.Intents.FLAGS.GUILD_MESSAGES, + discord.Intents.FLAGS.GUILD_MESSAGE_REACTIONS, + discord.Intents.FLAGS.DIRECT_MESSAGES, + ], + }); + this.client.on("messageCreate", (msg) => { this.onMessage(msg, false); }); + this.client.on("messageUpdate", (oldMsg, newMsg) => { this.onMessageUpdate(oldMsg, newMsg); }); + this.client.on("messageDelete", (msg) => { this.onMessageDelete(msg); }); + this.client.on("guildMemberAdd", (member) => { this.onMemberJoin(member); }); + this.readyPromises.push(this.client.login(this.config.discord.token)); + + this.modules = []; + + if (this.config.database) { + this.mongoclient = new MongoClient(this.config.database.url, { useUnifiedTopology: true }); + this.readyPromises.push(this.mongoclient.connect()); + } + + /** @type {discord.TextChannel} */ + this.logChannel = null; + /** @type {discord.TextChannel} */ + this.dmChannel = null; + /** @type {discord.TextChannel} */ + this.errorChannel = null; + } + + loadConfigModules() + { + if (!this.config.modules) { + return; + } + + for (var name in this.config.modules) { + var moduleClass = require('./modules/' + name); + if (!moduleClass) { + console.error('Unable to find module with name "' + name + '"!'); + continue; + } + + var configs = this.config.modules[name]; + + console.log('Module: "' + name + '" (' + configs.length + ' instances)'); + + for (let config of configs) { + var newModule = new moduleClass(config, this.client, this); + this.modules.push(newModule); + } + } + } + + onReady() + { + /* + this.client.guilds.cache.tap(guild => { + guild.members.fetch().then(() => { + console.log('Cached ' + guild.members.size + ' members in ' + guild.name); + }); + }); + */ + + this.client.user.setActivity(this.config.discord.activity); + + this.client.channels.fetch(this.config.discord.logchannel).then(logChannel => this.logChannel = logChannel); + this.client.channels.fetch(this.config.discord.dmchannel).then(dmChannel => this.dmChannel = dmChannel); + this.client.channels.fetch(this.config.discord.errorchannel).then(errorChannel => this.errorChannel = errorChannel); + + if (this.mongoclient) { + this.mongodb = this.mongoclient.db(this.config.database.db); + console.log("Database connected."); + } + + console.log("Client ready, loading modules..."); + + this.loadConfigModules(); + + console.log("Modules loaded!"); + + setInterval(() => { this.onTick(); }, 1000); + } + + start() + { + Promise.all(this.readyPromises).then(() => { + this.onReady(); + }).catch((err) => { + console.error(err); + }); + } + + stop() + { + var promises = []; + + console.log("Stopping client..."); + promises.push(this.client.destroy()); + + if (this.mongoclient) { + console.log("Stopping MongoDB..."); + promises.push(this.mongoclient.close()); + } + + Promise.all(promises).then(() => { + console.log("Client stopped."); + process.exit(); + }); + } + + addLogMessage(text, fromMember) + { + if (!this.logChannel) { + console.log("Couldn't log because we couldn't find the log channel:", text); + return; + } + + if (fromMember) { + text += " (via " + fromMember.user.username + ")"; + } + + console.log("Log: " + text); + this.logChannel.send(":robot: " + text); + } + + /** + * Checks if the given member is an admin. + * @param {discord.GuildMember} member + */ + isAdmin(member) + { + return member.permissions.has(discord.Permissions.FLAGS.ADMINISTRATOR); + } + + /** + * Checks if the given member is a moderator. + * @param {discord.GuildMember} member + */ + isMod(member) + { + return member.permissions.has(discord.Permissions.FLAGS.MANAGE_MESSAGES); + } + + onTick() + { + for (var i = 0; i < this.modules.length; i++) { + var m = this.modules[i]; + if (m.onTick) { + m.onTick(); + } + } + } + + onMemberJoin(member) + { + console.log("User joined: " + member + " (" + member.user.username + ")"); + + for (var i = 0; i < this.modules.length; i++) { + var m = this.modules[i]; + if (m.onMemberJoin) { + m.onMemberJoin(member); + } + } + } + + handleError(ex) + { + console.log(ex); + if (this.errorChannel) { + this.errorChannel.send(":octagonal_sign: Bot error!\n```\n" + ex.toString() + "\n```"); + } + } + + /** + * @param {discord.Message} msg + * @param {Boolean} edited + */ + async onMessage(msg, edited) + { + // Ignore our own messages + if (msg.author == this.client.user) { + return; + } + + // Ignore DM's + if (msg.member === null && msg.guild === null) { + // Log DM's + var logUsername = msg.author.username + '#' + msg.author.discriminator; + console.warn("Ignored a DM from " + logUsername.brightWhite + ": \"" + msg.content + "\""); + + // Send DM's to the DM channel + if (this.dmChannel) { + this.dmChannel.send(":mailbox_with_mail: " + msg.author.toString() + ": `" + msg.content + "`"); + } + + return; + } + + // Ignore webhooks + if (msg.webhookID) { + console.warn("Ignored webhook: \"" + msg.content + "\""); + return; + } + + // Ensure we have a member (sometimes this is null if their status is offline) + if (msg.member === null) { + console.warn("Member is null, fetching member now"); + msg.member = await msg.guild.fetchMember(msg.author); + } + + // Log line + var logUsername = msg.author.username + '#' + msg.author.discriminator; + if (this.isAdmin(msg.member)) { + logUsername = logUsername.red; + } else if (this.isMod(msg.member)) { + logUsername = logUsername.yellow; + } else { + logUsername = logUsername.brightWhite; + } + + console.log('[' + moment().format('MMM Do LTS') + '] ' + + logUsername + + ' in ' + ('#' + msg.channel.name).green.underline + ': ' + + (edited ? '(edited) '.gray : '') + + '"' + msg.content + '"'); + + for (var i = 0; i < this.modules.length; i++) { + var m = this.modules[i]; + if (m.onMessage) { + try { + if (m.onMessage(msg, edited)) { + return; + } + } catch (ex) { this.handleError(ex); } + } + } + + if (!msg.content.startsWith(".")) { + return; + } + + var parse = cmdsplit(msg.content); + var cmdID = parse[0].slice(1); + cmdID = cmdID.charAt(0).toUpperCase() + cmdID.slice(1); + if (!cmdID.match(/^[a-z]+$/i)) { + return; + } + + var cmdFound = false; + + var cmdFunc = findCommand(this, cmdID); + if (cmdFunc) { + if (msg.member !== null) { + console.log("Built-in command from \"" + msg.member.user.username + "\": " + cmdID); + } else { + console.log("Module command from offline member: " + cmdID); + } + + try { + var r = cmdFunc.apply(this, [ msg ].concat(parse.slice(1))); + if (r && r.catch) { + r.catch(ex => this.handleError(ex)); + } + } catch (ex) { this.handleError(ex); } + cmdFound = true; + } + + for (var i = 0; i < this.modules.length; i++) { + var m = this.modules[i]; + + var cmdFunc = findCommand(m, cmdID); + if (!cmdFunc) { + continue; + } + + if (msg.member !== null) { + console.log("Module command from \"" + msg.member.user.username + "\": " + cmdID); + } else { + console.log("Module command from offline member: " + cmdID); + } + + try { + var r = cmdFunc.apply(m, [ msg ].concat(parse.slice(1))); + if (r && r.catch) { + r.catch(ex => this.handleError(ex)); + } + } catch (ex) { this.handleError(ex); } + cmdFound = true; + } + + if (!cmdFound) { + console.log("Unknown command: \"" + cmdID + "\""); + } + } + + async onMessageUpdate(oldMsg, newMsg) + { + if (oldMsg.content != newMsg.content) { + this.onMessage(newMsg, true); + } + } + + async onMessageDelete(msg) + { + console.log("Message deleted: \"" + msg.content + "\""); + } + + onCmdGithub(msg) + { + msg.channel.send("My code is on Github! :robot: https://github.com/codecat/reddit-radio"); + } + + /* + //TODO: Move this to a module + onCmdWeather(msg) + { + var url = "https://api.darksky.net/forecast/" + this.config.weather.apikey + "/" + this.config.weather.coords + "?units=auto"; + https.get(url, (res) => { + var data = ""; + res.setEncoding("utf8"); + res.on("data", function(chunk) { data += chunk; }); + res.on("end", () => { + try { + var obj = JSON.parse(data); + var ret = "**The weather at Defqon.1 is currently:** (powered by darksky.net)\n"; + ret += "*" + obj.currently.summary + "* / **" + obj.currently.temperature + "\u2103 (" + Math.round((obj.currently.temperature * 9/5) + 32) + "\u2109)** / " + Math.round(obj.currently.humidity * 100) + "% humidity\n"; + ret += "UV index " + obj.currently.uvIndex + ", wind speed " + obj.currently.windSpeed + " m/s"; + msg.channel.send(ret); + } catch (err) { + msg.channel.send("I failed to get the weather... :sob:"); + console.log(err); + } + }); + }); + } + */ + + //TODO: Change this to .timeout and move this to its own module + /* + onCmdMute(msg) + { + if (!this.isMod(msg.member)) { + return; + } + + for (var memberID of msg.mentions.members.keys()) { + var member = msg.mentions.members.get(memberID); + member.roles.remove(this.config.discord.mutedrole); + + this.addLogMessage("Muted " + member.user.username, msg.member); + } + + msg.delete(); + } + + onCmdUnmute(msg) + { + if (!this.isMod(msg.member)) { + return; + } + + for (var memberID of msg.mentions.members.keys()) { + var member = msg.mentions.members.get(memberID); + member.roles.add(this.config.discord.mutedrole); + + this.addLogMessage("Unmuted " + member.user.username, msg.member); + } + + msg.delete(); + } + */ + + formatMilliseconds(ms) + { + var sec = Math.floor(ms / 1000); + + var secs = sec % 60; + var mins = Math.floor(sec / 60) % 60; + var hours = Math.floor(sec / 60 / 60) % 60; + + var ret = ""; + if (hours > 0) { + ret += hours + "h"; + } + if (mins > 0) { + ret += mins + "m"; + } + ret += secs + "s"; + return ret; + } + + onCmdTime(msg) + { + var date = moment(); + var text = "The local time is: **" + date.format("HH:mm") + "** ()"; + msg.channel.send(text); + } +} + +module.exports = RedditRadio; diff --git a/Startup.js b/Startup.js new file mode 100644 index 0000000..e2f0613 --- /dev/null +++ b/Startup.js @@ -0,0 +1,101 @@ +var RedditRadio = require("./RedditRadio"); +var toml = require("toml"); +var fs = require("fs"); +var Radio = require("./Radio"); + +class Startup +{ + constructor() + { + this.RADIO = 'radio'; + this.RADIOS = 'radios'; + this.NO_RADIO = 'no-radio'; + this.REGULAR_BOT = 'regular-bot'; + + this.radios = []; + + let configFile = process.env.CONFIG_FILE || "config.toml"; + this.config = toml.parse(fs.readFileSync(configFile, "utf8")); + } + + run() + { + let args = this.parseArgs(); + switch (args.type) { + case this.RADIO: this.setupRadio(args.name); break; + case this.RADIOS: this.setupRadios(); break; + case this.NO_RADIO: this.setupNoRadio(); break; + default: this.startBot(this.config); break; + } + } + + setupNoRadio() + { + let config = this.config; + delete config.radios; + this.startBot(config); + } + + setupRadio(name) + { + if (this.config.radios === undefined) { + console.error('No radios are defined in the config file, exiting...'); + process.exit(1); + } + for (let i = 0; i < this.config.radios.length; i++) { + if (this.config.radios[i].name === name) { + this.startRadio(this.config.radios[i]); + break; + } + } + this.setupSignals(); + } + + setupRadios() + { + for (var i = 0; i < this.config.radios.length; i++) { + this.startRadio(this.config.radios[i]); + } + this.setupSignals(); + } + + startRadio(radioConfig) + { + this.radios.push(new Radio(this.config, radioConfig)); + } + + startBot(config) + { + this.bot = new RedditRadio(config); + this.bot.start(); + + this.setupSignals(); + } + + setupSignals() + { + var stopHandler = () => { + if (this.bot) { + this.bot.stop(); + } + for (var i = 0; i < this.radios.length; i++) { + this.radios[i].stop(); + } + }; + process.on("SIGINT", stopHandler); // Ctrl+C + process.on("SIGTERM", stopHandler); // Terminate + } + + parseArgs() + { + let args = process.argv.splice(2); + switch (args[0]) { + case '--radio': return { type: this.RADIO, name: args[1]}; + case '--radios': return { type: this.RADIOS }; + case '--no-radios': return { type: this.NO_RADIO }; + default: return { type: this.REGULAR_BOT }; + } + } +} + +module.exports = Startup; diff --git a/cmdsplit.js b/cmdsplit.js new file mode 100644 index 0000000..f5017fd --- /dev/null +++ b/cmdsplit.js @@ -0,0 +1,54 @@ +module.exports = function(str, options) { + var ret = []; + + var buffer = ""; + var inString = false; + + for (var i = 0; i < str.length; i++) { + var c = str[i]; + + // literals + if (c == "\\" && i + 1 < str.length) { + buffer += str[++i]; + continue; + } + + // strings + if (c == "\"") { + if (inString) { + // string ends + inString = false; + ret.push(buffer); + buffer = ""; + if (i + 1 < str.length && str[i + 1] == " ") { + i++; + } + } else { + // string starts + inString = true; + } + continue; + } + + // words + if (c == " ") { + if (inString) { + buffer += " "; + } else { + ret.push(buffer); + buffer = ""; + } + continue; + } + + // characters + buffer += c; + } + + // last word + if (buffer != "") { + ret.push(buffer); + } + + return ret; +}; diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..5074960 --- /dev/null +++ b/config.example.toml @@ -0,0 +1,30 @@ +[discord] +token = "main_bot_token" +timezone = "Europe/Amsterdam" +logchannel = "647767937054277642" +dmchannel = "736529983706628146" + +#[database] +#url = "mongodb://localhost:27017" +#db = "redditradio" + +[filter] +badwords = "(thepiratebay|badwords)" + +[[modules.qdance]] + +#[[modules.event]] +#file = "events/Qlimax2019.json" + +#[[modules.eventquick]] +#channel = "651911168650248208" + +[[radios]] +token = "radio_bot_1_token" +channel = "330121740758024193" +name = "Q-dance Radio" +url = "http://audio.true.nl/qdance-hard" + +[voice] +passes = 2 +bitrate = 44100 diff --git a/events/Corona_2020_03_14.json b/events/Corona_2020_03_14.json new file mode 100644 index 0000000..866b5cb --- /dev/null +++ b/events/Corona_2020_03_14.json @@ -0,0 +1,62 @@ +[ + { + "stage": "I AM HARDSTYLE - Born & Raised", + "channel": "319525278978277407", + "emoji": ":warning:", + "url": "https://www.youtube.com/watch?v=Y9KxiNOKaCI", + "unconfirmed": false, + "streamdelay": 0, + "sets": [ + [ 2020, 3, 14, 20, 0, "Toneshifterz" ], + [ 2020, 3, 14, 21, 0, "Brennan Heart" ], + [ 2020, 3, 14, 22, 0, "Code Black" ], + [ 2020, 3, 14, 23, 0, "Brennan Heart, Code Black, Dailucia & Toneshifterz" ], + [ 2020, 3, 15, 0, 0 ] + ], + "responses": { + "^\\.(url|stream|link|watch)$": ":tv: Tune in to the livestream here: ****", + "^\\.(corona)$": "Due to many hardstyle events being canceled due to the COVID-19 pandemic, several DJ's are setting up their own livestream events. For all Corona/Event related news: " + } + }, + { + "stage": "Quarantine Hardstyle Sessions", + "channel": "688383827407667272", + "emoji": ":wolf:", + "url": "https://www.facebook.com/Qdance/videos/518799009013830/", + "unconfirmed": false, + "streamdelay": 0, + "sets": [ + [ 2020, 3, 14, 22, 0, "Early Hardstyle by Jones" ], + [ 2020, 3, 14, 22, 30, "Frequencerz" ], + [ 2020, 3, 14, 23, 0, "Ran-D & Adaro" ], + [ 2020, 3, 14, 23, 30, "Devin Wild" ], + [ 2020, 3, 15, 0, 0, "B-Front" ], + [ 2020, 3, 15, 0, 30, "Ending" ], + [ 2020, 3, 15, 1, 35 ] + ], + "responses": { + "^\\.(url|stream|link|watch)$": ":tv: Tune in to the livestream here: ****", + "^\\.(mc|host)$": ":microphone: Hosted by **MC Da Syndrone**.", + "^\\.(corona)$": "Due to many hardstyle events being canceled due to the COVID-19 pandemic, several DJ's are setting up their own livestream events. For all Corona/Event related news: " + } + }, + { + "stage": "United We Stand", + "channel": "688470315579605189", + "emoji": "<:frontliner:376092032361299979>", + "url": "https://www.youtube.com/watch?v=6B8OU5aQR_4", + "unconfirmed": true, + "streamdelay": 0, + "sets": [ + [ 2020, 3, 14, 21, 0, "Blasterjaxx" ], + [ 2020, 3, 14, 22, 0, "Frontliner" ], + [ 2020, 3, 14, 22, 45, "Blasterjaxx & Frontliner" ], + [ 2020, 3, 14, 23, 15, "Mr. Polska" ], + [ 2020, 3, 14, 23, 30 ] + ], + "responses": { + "^\\.(url|stream|link|watch)$": ":tv: Tune in to the livestream here: ****", + "^\\.(corona)$": "Due to many hardstyle events being canceled due to the COVID-19 pandemic, several DJ's are setting up their own livestream events. For all Corona/Event related news: " + } + } +] diff --git a/events/Corona_2020_03_20.json b/events/Corona_2020_03_20.json new file mode 100644 index 0000000..07565c2 --- /dev/null +++ b/events/Corona_2020_03_20.json @@ -0,0 +1,27 @@ +[ + { + "stage": "Quarantine Hardstyle Sessions", + "channel": "319525278978277407", + "emoji": ":wolf:", + "url": "https://www.youtube.com/watch?v=tA6ZsLKm_SU", + "unconfirmed": false, + "streamdelay": 0, + "sets": [ + [ 2020, 3, 20, 19, 0, "Unrivalled Podcast ft. Delius" ], + [ 2020, 3, 20, 20, 0, "Frequencerz (Hardstyle Classics)" ], + [ 2020, 3, 20, 20, 30, "B-Freqz" ], + [ 2020, 3, 20, 21, 0, "Rejecta" ], + [ 2020, 3, 20, 21, 30, "Radianze" ], + [ 2020, 3, 20, 22, 0, "Digital Punk" ], + [ 2020, 3, 20, 22, 30, "D-Fence" ], + [ 2020, 3, 20, 23, 0, "Ending" ], + [ 2020, 3, 21, 0, 0, "Feels birthday man" ], + [ 2020, 3, 21, 0, 17 ] + ], + "responses": { + "^\\.(url|stream|link|watch)$": ":tv: Tune in to the livestream here: ****", + "^\\.(mc|host)$": ":microphone: Hosted by **MC Da Syndrome**.", + "^\\.(corona)$": "Due to many hardstyle events being canceled due to the COVID-19 pandemic, several DJ's are setting up their own livestream events. For all Corona/Event related news: " + } + } +] diff --git a/events/Corona_2020_04_12.json b/events/Corona_2020_04_12.json new file mode 100644 index 0000000..1037f82 --- /dev/null +++ b/events/Corona_2020_04_12.json @@ -0,0 +1,24 @@ +[ + { + "stage": "I AM HARDSTYLE", + "channel": "319525278978277407", + "emoji": ":warning:", + "url": "https://www.youtube.com/watch?v=0jWt1ZQd7Gg", + "unconfirmed": false, + "streamdelay": 0, + "sets": [ + [ 2020, 4, 12, 19, 0, "Code Black" ], + [ 2020, 4, 12, 19, 45, "Dailucia" ], + [ 2020, 4, 12, 20, 15, "Aftershock" ], + [ 2020, 4, 12, 20, 45, "Toneshifterz" ], + [ 2020, 4, 12, 21, 15, "Brennan Heart" ], + [ 2020, 4, 12, 22, 15, "Radical Redemption & MC Nolz" ], + [ 2020, 4, 12, 23, 0 ] + ], + "responses": { + "^\\.(url|stream|link|watch)$": ":tv: Tune in to the livestream here: ****", + "^\\.(corona)$": "Due to many hardstyle events being canceled due to the COVID-19 pandemic, several DJ's are setting up their own livestream events. For all Corona/Event related news: ", + "^\\.(ultrawide)$": "For the ultrawide viewers, this userscript is nice: " + } + } +] diff --git a/events/Dediqated2020.json b/events/Dediqated2020.json new file mode 100644 index 0000000..7b2bd29 --- /dev/null +++ b/events/Dediqated2020.json @@ -0,0 +1,37 @@ +[ + { + "stage": "mainstage", + "channel": "319525278978277407", + "emoji": "<:qdance:328585093553586176>", + "url": "https://www.q-dance.com/en/videos/dediqated-live", + "unconfirmed": true, + "streamdelay": 3, + "sets": [ + [ 2020, 2, 8, 12, 50, "Early Rave Rebels", "Buzz Fuzz, Franky Jones, Gizmo, The Darkraver" ], + [ 2020, 2, 8, 13, 13, "The Qlubtempo Parade", "ASYS, DJ Luna, Dana, Pavo, Pila" ], + [ 2020, 2, 8, 13, 41, "Fusion Records", "Donkey Rollers, The Pitcher, Zany" ], + [ 2020, 2, 8, 13, 58, "Bella Hardstyle Italia", "Tatanka, TNT, Zatox" ], + [ 2020, 2, 8, 14, 30, "Masters of Melody Part 1", "Bass Modulators, Frontliner, Max Enforcer" ], + [ 2020, 2, 8, 15, 0, "Praise the Reverse Bass", "DJ Isaac, Technoboy, Tuneboy" ], + [ 2020, 2, 8, 15, 20, "Viva Hollandia", "Deepack, Dr. Rude, Outsiders, Ruthless, The Viper" ], + [ 2020, 2, 8, 15, 58, "World of Madness", "Headhunterz, Noisecontrollers, Wildstylez" ], + [ 2020, 2, 8, 16, 44, "Millenium Hardcore Mayhem", "Art of Fighters, Endymion, Evil Activities, Noize Supperssor, Nosferatu, Promo" ], + [ 2020, 2, 8, 17, 16, "Scantraxx Recordz", "D-Block & S-te-Fan, Devin Wild, The Prophet" ], + [ 2020, 2, 8, 17, 37, "The Land Down Under", "Audiofreq, Code Black, Outbreak" ], + [ 2020, 2, 8, 17, 52, "The Best of Power Hour", "Neophyte, The Stunned Guys, Zazafront and many more" ], + [ 2020, 2, 8, 18, 53, "Dirty Workz", "Coone, Da Tweekaz, Hard Driver, Psyko Punkz" ], + [ 2020, 2, 8, 19, 20, "Birth of Raw", "Adaro, Alpha Twins, B-Front, Crypsis, Digital Punk, E-Force, Ran-D" ], + [ 2020, 2, 8, 20, 4, "Masters of Melody Part 2", "Atmozfears, Audiotricz, KELTEK, Sound Rush" ], + [ 2020, 2, 8, 20, 36, "Future Heroes", "D-Sturb, Frequencerz, Rebelion, Sefa, Sub Zero Project, Warface" ], + [ 2020, 2, 8, 21, 22, "Hardcore 2.0", "Dr. Peacock, Korsakoff, Mad Dog, Partyraiser" ], + [ 2020, 2, 8, 22, 0, "Hardstyle Top 25 of All-Time" ], + [ 2020, 2, 8, 22, 50, "The Endshow" ], + [ 2020, 2, 8, 23, 0 ] + ], + "responses": { + "^\\.(top100|top25)": "Find the all-time top 100 here: <#675627527208697856>", + "^\\.(url|stream|link|watch)$": ":tv: Tune in to the livestream here: ****", + "^\\.(mc|host)$": ":microphone: The MCs are **Villain**, **DV8**, and **Ruffian**." + } + } +] diff --git a/events/Defqon2018.json b/events/Defqon2018.json new file mode 100644 index 0000000..6188623 --- /dev/null +++ b/events/Defqon2018.json @@ -0,0 +1,117 @@ +[ + { + "stage": "red", + "mc": "DV8 & Villain", + "channel": "459077085013606401", + "emoji": ":red_circle:", + "url": "https://www.q-dance.com/en/videos/defqon-1-livestream-red", + "sets": [ + [ 23, 13, 0, "ANDY SVGE" ], + [ 23, 14, 0, "Sound Rush" ], + [ 23, 15, 0, "Noisecontrollers" ], + [ 23, 16, 0, "POWER HOUR" ], + [ 23, 17, 0, "Code Black" ], + [ 23, 18, 0, "D-Block & S-te-Fan" ], + [ 23, 19, 0, "Wildstylez" ], + [ 23, 20, 0, "Ran-D (LIVE)" ], + [ 23, 20, 45, "Warface" ], + [ 23, 21, 45, "Sub Zero Project" ], + [ 23, 22, 30, "The Endshow" ], + [ 23, 23, 0, "Nothing" ], + + [ 24, 15, 0, "Zazafront" ], + [ 24, 15, 30, "Wasted Penguinz & Adrenalize" ], + [ 24, 16, 15, "Ransomnia & Friends" ], + [ 24, 16, 45, "Psyko Punkz" ], + [ 24, 17, 30, "Dee-Block & S-te-Pack" ], + [ 24, 18, 15, "Zatox (LIVE)" ], + [ 24, 18, 45, "Peacock in Concert" ], + [ 24, 19, 15, "Tweekacore & Darren Styles" ], + [ 24, 20, 0, "Brennan Heart (Uncertain)" ], + [ 24, 21, 0, "Coone" ], + [ 24, 22, 0, "Project One" ], + [ 24, 23, 0, "Nothing" ] + ] + }, + + { + "stage": "blue", + "mc": "Nolz", + "channel": "459077096665382942", + "emoji": ":large_blue_circle:", + "url": "https://www.q-dance.com/en/videos/defqon-1-livestream-blue", + "sets": [ + [ 22, 20, 0, "Headhunterz" ], + [ 22, 21, 0, "Bass Modulators" ], + [ 22, 22, 0, "Atmozfears" ], + [ 22, 23, 0, "B-Front & Phuture Noize" ], + [ 23, 0, 0, "Frequencerz" ], + [ 23, 1, 0, "Nothing" ], + + [ 23, 13, 0, "Requiem" ], + [ 23, 14, 0, "Digital Punk" ], + [ 23, 15, 0, "E-Force" ], + [ 23, 16, 0, "Regain (LIVE) (Uncertain)" ], + [ 23, 16, 30, "Jason Payne" ], + [ 23, 18, 0, "B-Freqz" ], + [ 23, 18, 30, "Adaro" ], + [ 23, 19, 30, "Radical Redemption (LIVE)" ], + [ 23, 20, 0, "Rebelion" ], + [ 23, 21, 0, "Main Concern" ], + [ 23, 22, 0, "Nothing" ], + + [ 24, 13, 0, "Myst" ], + [ 24, 14, 30, "Act of Rage" ], + [ 24, 15, 30, "Clockartz" ], + [ 24, 16, 30, "Endymion (LIVE)" ], + [ 24, 17, 0, "Crypsis & Chain Reaction present \"Unlike Others\"" ], + [ 24, 17, 45, "Sub Sonik" ], + [ 24, 18, 45, "D-Sturb (LIVE)" ], + [ 24, 19, 15, "Frequencerz \"Stealth Mode\"" ], + [ 24, 19, 45, "Phuture Noize presents Black Mirror Society (Showcase)" ], + [ 24, 20, 30, "Deetox - \"Bring the Riot\" (LIVE)" ], + [ 24, 21, 0, "Delete VIP (Uncertain)" ], + [ 24, 21, 30, "Rebel[ut]ion" ], + [ 24, 22, 0, "Nothing" ] + ] + }, + + { + "stage": "black", + "mc": "Diesel", + "channel": "459077105137745941", + "emoji": ":black_circle:", + "url": "https://www.q-dance.com/en/videos/defqon-1-livestream-black", + "sets": [ + [ 22, 20, 0, "Paul Elstak" ], + [ 22, 21, 0, "20 Years of Endymion (LIVE)" ], + [ 22, 22, 0, "Korsakoff" ], + [ 22, 23, 0, "Tha Playah" ], + [ 23, 0, 0, "Destructive Tendencies" ], + [ 23, 1, 0, "Nothing" ], + + [ 23, 13, 0, "Neophyte" ], + [ 23, 14, 0, "Mad Dog" ], + [ 23, 15, 0, "Noize Suppressor" ], + [ 23, 16, 0, "Art of Fighters" ], + [ 23, 17, 0, "Miss K8" ], + [ 23, 18, 0, "N-Vitral presents \"Bombsquad\" (Uncertain)" ], + [ 23, 18, 30, "Angerfist" ], + [ 23, 19, 30, "Andy The Core" ], + [ 23, 20, 30, "The Sickest Squad" ], + [ 23, 21, 30, "Spitnoise" ], + [ 23, 22, 30, "Nothing" ], + + [ 24, 14, 0, "The Melodyst" ], + [ 24, 15, 0, "Anime" ], + [ 24, 16, 0, "Nosferatu" ], + [ 24, 17, 0, "Evil Activities" ], + [ 24, 18, 0, "I:GOR (Uncertain)" ], + [ 24, 19, 0, "Deadly Guns" ], + [ 24, 20, 0, "D-Fence" ], + [ 24, 21, 0, "Partyraiser & Bulletproof" ], + [ 24, 22, 0, "Sefa" ], + [ 24, 23, 0, "Nothing" ] + ] + } +] diff --git a/events/Defqon2018_Aus.json b/events/Defqon2018_Aus.json new file mode 100644 index 0000000..d5b4fb8 --- /dev/null +++ b/events/Defqon2018_Aus.json @@ -0,0 +1,27 @@ +[ + { + "stage": "red", + "mc": "Villain", + "channel": "489754437624266766", + "emoji": ":red_circle:", + "url": "https://www.q-dance.com/en/", + "sets": [ + [ 15, 8, 15, "TNT" ], + [ 15, 9, 15, "Defqon.1 Legends presents: A Decade of Dedication" ], + [ 15, 10, 15, "Sub Zero Project" ], + [ 15, 10, 40, "Da Tweekaz" ], + [ 15, 11, 38, "Coone" ], + [ 15, 12, 38, "Headhunterz" ], + [ 15, 13, 50, "The Endshow" ], + [ 15, 14, 6, "Nothing" ], + [ 15, 16, 0, "TNT (Rebroadcast)" ], + [ 15, 17, 0, "Defqon.1 Legends presents: A Decade of Dedication (Rebroadcast)" ], + [ 15, 18, 0, "Sub Zero Project (Rebroadcast)" ], + [ 15, 18, 30, "Da Tweekaz (Rebroadcast)" ], + [ 15, 19, 30, "Coone (Rebroadcast)" ], + [ 15, 20, 30, "Headhunterz (Rebroadcast)" ], + [ 15, 21, 45, "The Endshow (Rebroadcast)" ], + [ 15, 22, 0, "Nothing" ] + ] + } +] diff --git a/events/Defqon2018_test.json b/events/Defqon2018_test.json new file mode 100644 index 0000000..c166041 --- /dev/null +++ b/events/Defqon2018_test.json @@ -0,0 +1,117 @@ +[ + { + "stage": "red", + "mc": "DV8 & Villain", + "channel": "459077085013606401", + "emoji": ":red_circle:", + "url": "https://www.q-dance.com/en/news/defqon-1-2018-or-livestream", + "sets": [ + [ 23, 13, 0, "ANDY SVGE" ], + [ 23, 14, 0, "Sound Rush" ], + [ 23, 15, 0, "Noisecontrollers" ], + [ 23, 16, 0, "POWER HOUR" ], + [ 23, 17, 0, "Code Black" ], + [ 23, 18, 0, "D-Block & S-te-Fan" ], + [ 23, 19, 0, "Wildstylez" ], + [ 23, 20, 0, "Ran-D (LIVE)" ], + [ 23, 20, 45, "Warface" ], + [ 23, 21, 45, "Sub Zero Project" ], + [ 23, 22, 30, "The Endshow" ], + [ 23, 23, 0, "Nothing" ], + + [ 24, 15, 0, "Zazafront" ], + [ 24, 15, 30, "Wasted Penguinz & Adrenalize" ], + [ 24, 16, 15, "Ransomnia & Friends" ], + [ 24, 16, 45, "Psyko Punkz" ], + [ 24, 17, 30, "Dee-Block & S-te-Pack" ], + [ 24, 18, 15, "Zatox (LIVE)" ], + [ 24, 18, 45, "Tweekacore & Darren Styles" ], + [ 24, 19, 30, "Peacock in Concert" ], + [ 24, 20, 0, "Nothing" ], + [ 24, 21, 0, "Coone" ], + [ 24, 22, 0, "Project One" ], + [ 24, 23, 0, "Nothing" ] + ] + }, + + { + "stage": "blue", + "mc": "Villain", + "channel": "459077096665382942", + "emoji": ":large_blue_circle:", + "url": "https://www.q-dance.com/en/news/defqon-1-2018-or-livestream", + "sets": [ + [ 20, 21, 38, "Headhunterz" ], + [ 22, 21, 0, "Bass Modulators" ], + [ 22, 22, 0, "Atmozfears" ], + [ 22, 23, 0, "B-Front & Phuture Noize" ], + [ 23, 0, 0, "Frequencerz" ], + [ 23, 1, 0, "Nothing" ], + + [ 23, 13, 0, "Requiem" ], + [ 23, 14, 0, "Digital Punk" ], + [ 23, 15, 0, "E-Force" ], + [ 23, 16, 0, "Nothing" ], + [ 23, 16, 30, "Jason Payne" ], + [ 23, 18, 0, "B-Freqz" ], + [ 23, 18, 30, "Adaro" ], + [ 23, 19, 30, "Radical Redemption (LIVE)" ], + [ 23, 20, 0, "Rebelion" ], + [ 23, 21, 0, "Main Concern" ], + [ 23, 22, 0, "Nothing" ], + + [ 24, 13, 0, "Myst" ], + [ 24, 14, 30, "Act of Rage" ], + [ 24, 15, 30, "Clockartz" ], + [ 24, 16, 30, "Endymion (LIVE)" ], + [ 24, 17, 0, "Crypsis & Chain Reaction present \"Unlike Others\"" ], + [ 24, 17, 45, "Sub Sonik" ], + [ 24, 18, 45, "D-Sturb (LIVE)" ], + [ 24, 19, 15, "Frequencerz \"Stealth Mode\"" ], + [ 24, 19, 45, "Phuture Noize presents Black Mirror Society (Showcase)" ], + [ 24, 20, 30, "Deetox - \"Bring the Riot\" (LIVE)" ], + [ 24, 21, 0, "Nothing" ], + [ 24, 21, 30, "Rebel[ut]ion" ], + [ 24, 22, 0, "Nothing" ] + ] + }, + + { + "stage": "black", + "mc": "Ruffian", + "channel": "459077105137745941", + "emoji": ":black_circle:", + "url": "https://www.q-dance.com/en/news/defqon-1-2018-or-livestream", + "sets": [ + [ 22, 20, 0, "Paul Elstak" ], + [ 22, 21, 0, "20 Years of Endymion (LIVE)" ], + [ 22, 22, 0, "Korsakoff" ], + [ 22, 23, 0, "Tha Playah" ], + [ 23, 0, 0, "Destructive Tendencies" ], + [ 23, 1, 0, "Nothing" ], + + [ 23, 13, 0, "Neophyte" ], + [ 23, 14, 0, "Mad Dog" ], + [ 23, 15, 0, "Nothing" ], + [ 23, 16, 0, "Art of Fighters" ], + [ 23, 17, 0, "Miss K8" ], + [ 23, 18, 0, "Nothing" ], + [ 23, 18, 30, "Angerfist" ], + [ 23, 19, 30, "Andy The Core" ], + [ 23, 20, 30, "The Sickest Squad" ], + [ 23, 21, 30, "Spitnoise" ], + [ 23, 22, 30, "Nothing" ], + + [ 24, 14, 0, "The Melodyst" ], + [ 24, 15, 0, "Anime" ], + [ 24, 16, 0, "Nosferatu" ], + [ 24, 17, 0, "Evil Activities" ], + [ 24, 18, 0, "Nothing" ], + [ 24, 19, 0, "Deadly Guns" ], + [ 24, 20, 0, "D-Fence" ], + [ 24, 21, 0, "Partyraiser & Bulletproof" ], + [ 24, 22, 0, "Sefa" ], + [ 24, 23, 0, "Nothing" ] + ] + } +] diff --git a/events/Defqon2019.json b/events/Defqon2019.json new file mode 100644 index 0000000..033d6e8 --- /dev/null +++ b/events/Defqon2019.json @@ -0,0 +1,352 @@ +[ + { + "stage": "Red", + "mc": "Villain & DV8", + "channel": "591927725594378240", + "emoji": ":heart:", + "url": "https://www.q-dance.com/en/videos/red-live-2019", + "sets": [ + [ 2019, 6, 29, 11, 3, "Toneshifterz" ], + [ 2019, 6, 29, 12, 3, "Adrenalize & Devin Wild" ], + [ 2019, 6, 29, 13, 3, "Coone" ], + [ 2019, 6, 29, 13, 48, "Noisecontrollers" ], + [ 2019, 6, 29, 14, 33, "Sound Rush" ], + [ 2019, 6, 29, 15, 18, "Headhunterz" ], + [ 2019, 6, 29, 16, 3, "Power Hour" ], + [ 2019, 6, 29, 17, 3, "Coone, Headhunterz,Noisecontrollers & Sound Rush" ], + [ 2019, 6, 29, 18, 3, "KELTEK" ], + [ 2019, 6, 29, 19, 3, "Frequencerz & Phuture Noize" ], + [ 2019, 6, 29, 20, 3, "D-Sturb" ], + [ 2019, 6, 29, 21, 3, "Radical Redemption" ], + [ 2019, 6, 29, 21, 48, "B-Front" ], + [ 2019, 6, 29, 22, 38, "The Endshow" ], + [ 2019, 6, 29, 23, 0, "Nothing" ], + [ 2019, 6, 30, 15, 3, "Code Black & Atmozfears" ], + [ 2019, 6, 30, 16, 18, "Audiotricz: The Next Chapter [LIVE]" ], + [ 2019, 6, 30, 17, 3, "D-Block & S-te-Fan & DJ Isaac" ], + [ 2019, 6, 30, 18, 3, "Wildstylez" ], + [ 2019, 6, 30, 19, 3, "Gunz for Hire" ], + [ 2019, 6, 30, 20, 3, "Defqon.1 Legends" ], + [ 2019, 6, 30, 21, 18, "Sub Zero Project" ], + [ 2019, 6, 30, 22, 13, "Sefa" ], + [ 2019, 6, 30, 22, 53, "The Closing Ritual" ], + [ 2019, 6, 30, 23, 0, "Nothing" ] + ], + "faq": [ + ] + }, + { + "stage": "Blue", + "mc": "Villain & Livid & Nolz", + "channel": "591928007351074816", + "emoji": ":blue_heart:", + "url": "https://www.q-dance.com/en/videos/blue-live-2019", + "sets": [ + [ 2019, 6, 28, 20, 3, "Brennan Heart" ], + [ 2019, 6, 28, 21, 33, "Bass Modulators" ], + [ 2019, 6, 28, 22, 33, "D-Block & S-te-Fan" ], + [ 2019, 6, 28, 23, 33, "Ran-D" ], + [ 2019, 6, 29, 0, 33, "Phuture Noize [THE SPOTLIGHT]" ], + [ 2019, 6, 29, 1, 0, "Nothing" ], + [ 2019, 6, 29, 11, 3, "D-Attack" ], + [ 2019, 6, 29, 12, 3, "Bass Chaserz" ], + [ 2019, 6, 29, 13, 3, "Sub Sonik" ], + [ 2019, 6, 29, 14, 3, "Deetox presents Revival" ], + [ 2019, 6, 29, 15, 3, "Hard Driver [LIVE]" ], + [ 2019, 6, 29, 15, 33, "Malice" ], + [ 2019, 6, 29, 17, 3, "Digital Punk" ], + [ 2019, 6, 29, 18, 3, "Rejecta" ], + [ 2019, 6, 29, 19, 3, "Warface" ], + [ 2019, 6, 29, 20, 3, "Regain" ], + [ 2019, 6, 29, 21, 3, "Rebelion" ], + [ 2019, 6, 29, 22, 0, "Nothing" ], + [ 2019, 6, 30, 0, 0, "Psyko Punkz" ], + [ 2019, 6, 30, 1, 0, "Zatox" ], + [ 2019, 6, 30, 2, 0, "Rebelion & Delete" ], + [ 2019, 6, 30, 3, 0, "The Sickest Squad" ], + [ 2019, 6, 30, 4, 0, "Nothing" ], + [ 2019, 6, 30, 11, 3, "Chain Reaction" ], + [ 2019, 6, 30, 12, 3, "Degos & Re-Done" ], + [ 2019, 6, 30, 13, 3, "Endymion" ], + [ 2019, 6, 30, 14, 3, "Clockartz" ], + [ 2019, 6, 30, 14, 48, "Requiem presents The Reckoning" ], + [ 2019, 6, 30, 15, 18, "Titan" ], + [ 2019, 6, 30, 15, 33, "Act of Rage" ], + [ 2019, 6, 30, 16, 33, "Adaro" ], + [ 2019, 6, 30, 17, 33, "MYST" ], + [ 2019, 6, 30, 18, 33, "Jason Payne" ], + [ 2019, 6, 30, 19, 33, "Killshot" ], + [ 2019, 6, 30, 20, 33, "E-Force & Luna" ], + [ 2019, 6, 30, 21, 33, "10 Years of Thera" ], + [ 2019, 6, 30, 22, 33, "Delete VIP" ], + [ 2019, 6, 30, 23, 0, "Nothing" ] + ], + "faq": [ + ] + }, + { + "stage": "Black", + "mc": "Da Mouth of Madness & Alee", + "channel": "591928033523269632", + "emoji": ":black_heart:", + "url": "https://www.q-dance.com/en/videos/black-live-2019", + "sets": [ + [ 2019, 6, 28, 20, 3, "Promo" ], + [ 2019, 6, 28, 21, 33, "Evil Activities" ], + [ 2019, 6, 28, 22, 33, "AniMe" ], + [ 2019, 6, 28, 23, 33, "Angerfist" ], + [ 2019, 6, 29, 0, 33, "Dr. Peacock [THE SPOTLIGHT]" ], + [ 2019, 6, 29, 1, 0, "Nothing" ], + [ 2019, 6, 29, 11, 3, "Access One" ], + [ 2019, 6, 29, 12, 33, "Amada & Yoshiko" ], + [ 2019, 6, 29, 14, 3, "Wasted Mind" ], + [ 2019, 6, 29, 15, 3, "Dither presents Tools of Demolition" ], + [ 2019, 6, 29, 16, 3, "Neophyte" ], + [ 2019, 6, 29, 17, 3, "N-Vitral" ], + [ 2019, 6, 29, 18, 3, "Nosferatu" ], + [ 2019, 6, 29, 19, 3, "Angerfist & I:GOR" ], + [ 2019, 6, 29, 20, 3, "Deadly Guns" ], + [ 2019, 6, 29, 21, 3, "Andy The Core" ], + [ 2019, 6, 29, 22, 3, "Destructive Tendencies" ], + [ 2019, 6, 29, 23, 0, "Nothing" ], + [ 2019, 6, 30, 12, 3, "The Viper" ], + [ 2019, 6, 30, 13, 3, "The Melodyst" ], + [ 2019, 6, 30, 14, 3, "Mad Dog" ], + [ 2019, 6, 30, 15, 3, "Korsakoff" ], + [ 2019, 6, 30, 16, 3, "Tha Playah 'Sick and Twisted showcase'" ], + [ 2019, 6, 30, 17, 3, "D-Fence" ], + [ 2019, 6, 30, 18, 3, "Partyraiser, Hard Effectz & Bulletproof" ], + [ 2019, 6, 30, 19, 3, "GPF [THE PRERECORDED SEX SHOW V69 UNMASTERED TEST.WMA]" ], + [ 2019, 6, 30, 19, 45, "Nothing" ] + ], + "faq": [ + ] + }, + { + "stage": "UV", + "mc": "DL & DV8 & Da Syndrome", + "channel": "591928065416757248", + "emoji": ":purple_heart:", + "url": "https://www.q-dance.com/en/videos/uv-live-2019", + "sets": [ + [ 2019, 6, 29, 11, 0, "Sephyx" ], + [ 2019, 6, 29, 12, 0, "Retrospect" ], + [ 2019, 6, 29, 13, 0, "Primeshock" ], + [ 2019, 6, 29, 14, 0, "Audiofreq" ], + [ 2019, 6, 29, 15, 0, "Max Enforcer & ANDY SVGE" ], + [ 2019, 6, 29, 16, 0, "Jay Reeve presents A Higher State" ], + [ 2019, 6, 29, 17, 0, "DJ The Prophet" ], + [ 2019, 6, 29, 17, 45, "Zany presents DNA" ], + [ 2019, 6, 29, 18, 45, "Demi Kanon" ], + [ 2019, 6, 29, 19, 30, "Frontliner" ], + [ 2019, 6, 29, 20, 30, "Da Tweekaz" ], + [ 2019, 6, 29, 21, 30, "Wasted Penguinz" ], + [ 2019, 6, 29, 22, 30, "Nothing" ], + [ 2019, 6, 30, 13, 3, "Mental Twister" ], + [ 2019, 6, 30, 14, 3, "Dr. Phunk & MANDY" ], + [ 2019, 6, 30, 15, 3, "Ransom" ], + [ 2019, 6, 30, 15, 48, "Mark with a K & MC Chucky" ], + [ 2019, 6, 30, 16, 33, "Dr. Ruthless" ], + [ 2019, 6, 30, 17, 18, "Outsiders & The Partysquad & The Darkraver" ], + [ 2019, 6, 30, 18, 18, "Frequencerz presents GET WACK!" ], + [ 2019, 6, 30, 19, 18, "ZazaFront" ], + [ 2019, 6, 30, 20, 3, "Mashup Jack" ], + [ 2019, 6, 30, 20, 48, "Paul Elstak" ], + [ 2019, 6, 30, 21, 30, "Nothing" ] + ], + "faq": [ + ] + }, + { + "stage": "Magenta", + "mc": "Ruffian & DL", + "channel": "591928254877794324", + "emoji": ":loud_sound:", + "url": "https://www.q-dance.com/en/videos/magenta-audio-live-2019", + "sets": [ + [ 2019, 6, 29, 11, 0, "Sunny D" ], + [ 2019, 6, 29, 12, 30, "Pila & The Scientist" ], + [ 2019, 6, 29, 14, 0, "Trilok & Chiren" ], + [ 2019, 6, 29, 15, 30, "ASYS" ], + [ 2019, 6, 29, 17, 0, "Dana" ], + [ 2019, 6, 29, 18, 0, "Activator" ], + [ 2019, 6, 29, 19, 0, "DJ Isaac" ], + [ 2019, 6, 29, 20, 0, "Luna" ], + [ 2019, 6, 29, 21, 30, "Pavo" ], + [ 2019, 6, 29, 23, 0, "Nothing" ], + [ 2019, 6, 30, 11, 0, "Lip DJ" ], + [ 2019, 6, 30, 12, 30, "A-Lusion" ], + [ 2019, 6, 30, 14, 0, "Ivan Carsten" ], + [ 2019, 6, 30, 15, 0, "Geck-o" ], + [ 2019, 6, 30, 16, 0, "The Pitcher" ], + [ 2019, 6, 30, 17, 0, "Tat & Zat" ], + [ 2019, 6, 30, 18, 0, "Charly Lownoise" ], + [ 2019, 6, 30, 19, 0, "Deepack" ], + [ 2019, 6, 30, 20, 0, "Nothing" ] + ], + "faq": [ + ] + }, + { + "stage": "Indigo", + "mc": "Focus & Livid", + "channel": "591928282295828480", + "emoji": ":loud_sound:", + "url": "https://www.q-dance.com/en/videos/indigo-audio-live-2019", + "sets": [ + [ 2019, 6, 29, 11, 0, "Imperatorz" ], + [ 2019, 6, 29, 12, 0, "Aversion" ], + [ 2019, 6, 29, 13, 0, "Crystal Mad" ], + [ 2019, 6, 29, 14, 0, "Neroz" ], + [ 2019, 6, 29, 15, 0, "Luminite" ], + [ 2019, 6, 29, 16, 0, "Retaliation" ], + [ 2019, 6, 29, 17, 0, "Ncrypta" ], + [ 2019, 6, 29, 18, 0, "The Apexx Machine" ], + [ 2019, 6, 29, 19, 0, "Rooler" ], + [ 2019, 6, 29, 20, 0, "The Purge" ], + [ 2019, 6, 29, 21, 0, "Mind Dimension" ], + [ 2019, 6, 29, 22, 0, "Riot Shift" ], + [ 2019, 6, 29, 23, 0, "Nothing" ], + [ 2019, 6, 30, 11, 0, "Dark Pact" ], + [ 2019, 6, 30, 12, 0, "Tartaros" ], + [ 2019, 6, 30, 13, 0, "The Classics Machine" ], + [ 2019, 6, 30, 14, 0, "RVAGE" ], + [ 2019, 6, 30, 15, 0, "Vertile" ], + [ 2019, 6, 30, 16, 0, "Prefix & Density" ], + [ 2019, 6, 30, 17, 0, "Typhoon" ], + [ 2019, 6, 30, 18, 0, "Bloodlust" ], + [ 2019, 6, 30, 19, 0, "Thyron" ], + [ 2019, 6, 30, 20, 0, "Chris One" ], + [ 2019, 6, 30, 21, 0, "Unresolved" ], + [ 2019, 6, 30, 22, 0, "Caine" ], + [ 2019, 6, 30, 23, 0, "Nothing" ] + ], + "faq": [ + ] + }, + { + "stage": "Yellow", + "mc": "No-iD & RG", + "channel": "591928310582345738", + "emoji": ":loud_sound:", + "url": "https://www.q-dance.com/en/videos/yellow-audio-live-2019", + "sets": [ + [ 2019, 6, 29, 11, 0, "Streiks & Kratchs" ], + [ 2019, 6, 29, 12, 0, "Mr. Ivex" ], + [ 2019, 6, 29, 13, 0, "Trespassed & Hard Effectz" ], + [ 2019, 6, 29, 14, 0, "Lady Dammage" ], + [ 2019, 6, 29, 15, 0, "Unproven" ], + [ 2019, 6, 29, 16, 0, "BillX" ], + [ 2019, 6, 29, 17, 0, "Crypton" ], + [ 2019, 6, 29, 18, 0, "Spitnoise" ], + [ 2019, 6, 29, 19, 0, "Partyraiser & Bulletproof" ], + [ 2019, 6, 29, 20, 0, "F. NøIzE" ], + [ 2019, 6, 29, 21, 0, "The Destroyer" ], + [ 2019, 6, 29, 22, 0, "Dissoactive" ], + [ 2019, 6, 29, 23, 0, "Nothing" ], + [ 2019, 6, 30, 12, 0, "Sprinky" ], + [ 2019, 6, 30, 13, 30, "Hyrule War" ], + [ 2019, 6, 30, 14, 30, "Remzcore" ], + [ 2019, 6, 30, 15, 30, "Super Trash Bros. [LIVE]" ], + [ 2019, 6, 30, 16, 0, "Frenchcore Familia" ], + [ 2019, 6, 30, 17, 0, "Lunatic" ], + [ 2019, 6, 30, 18, 0, "Vandal!sm" ], + [ 2019, 6, 30, 19, 0, "System Overload" ], + [ 2019, 6, 30, 20, 0, "Angernoizer & Cryogenic" ], + [ 2019, 6, 30, 21, 0, "Chaotic Hostility" ], + [ 2019, 6, 30, 22, 0, "DRS" ], + [ 2019, 6, 30, 23, 0, "Nothing" ] + ], + "faq": [ + ] + }, + { + "stage": "Gold", + "mc": "Da Mouth of Madness & Ruffian", + "channel": "591928331159601154", + "emoji": ":loud_sound:", + "url": "https://www.q-dance.com/en/videos/gold-audio-live-2019", + "sets": [ + [ 2019, 6, 29, 11, 0, "The Raver" ], + [ 2019, 6, 29, 12, 30, "Petrov" ], + [ 2019, 6, 29, 14, 0, "Panic" ], + [ 2019, 6, 29, 15, 0, "The Stunned Guys" ], + [ 2019, 6, 29, 16, 0, "Uzi" ], + [ 2019, 6, 29, 17, 0, "DJ DUNE" ], + [ 2019, 6, 29, 18, 0, "Mental Theo" ], + [ 2019, 6, 29, 19, 0, "The Darkraver & Vince" ], + [ 2019, 6, 29, 20, 0, "Buzz Fuzz" ], + [ 2019, 6, 29, 21, 30, "Frantic Freak" ], + [ 2019, 6, 29, 23, 0, "Nothing" ], + [ 2019, 6, 30, 11, 0, "Sequence & Ominous" ], + [ 2019, 6, 30, 12, 30, "Ophidian & Ruffneck" ], + [ 2019, 6, 30, 14, 0, "Re-Style" ], + [ 2019, 6, 30, 15, 0, "Art of Fighters" ], + [ 2019, 6, 30, 16, 0, "Endymion" ], + [ 2019, 6, 30, 17, 0, "Noize Suppressor" ], + [ 2019, 6, 30, 18, 0, "Synapse & Sei2ure" ], + [ 2019, 6, 30, 19, 0, "Unexist" ], + [ 2019, 6, 30, 20, 0, "Nothing" ] + ], + "faq": [ + ] + }, + { + "stage": "Silver", + "mc": "Dart", + "channel": "591928389938446356", + "emoji": ":loud_sound:", + "url": "https://www.q-dance.com/en/videos/silver-audio-live-2019", + "sets": [ + [ 2019, 6, 29, 12, 0, "Mindustries" ], + [ 2019, 6, 29, 13, 0, "Manu Le Malin" ], + [ 2019, 6, 29, 14, 0, "The Clamps" ], + [ 2019, 6, 29, 15, 30, "Penta" ], + [ 2019, 6, 29, 16, 30, "Deathmachine" ], + [ 2019, 6, 29, 18, 0, "Khaoz Engine" ], + [ 2019, 6, 29, 19, 30, "Bryan Fury" ], + [ 2019, 6, 29, 20, 30, "Hellfish" ], + [ 2019, 6, 29, 22, 0, "Nothing" ], + [ 2019, 6, 30, 12, 0, "Strange Arrival" ], + [ 2019, 6, 30, 13, 0, "Somniac One" ], + [ 2019, 6, 30, 14, 0, "Katharsys" ], + [ 2019, 6, 30, 15, 0, "Ophidian as Raziel" ], + [ 2019, 6, 30, 16, 0, "The Outside Agency" ], + [ 2019, 6, 30, 17, 0, "The DJ Producer" ], + [ 2019, 6, 30, 18, 0, "The Satan" ], + [ 2019, 6, 30, 19, 0, "Akira" ], + [ 2019, 6, 30, 20, 0, "Nothing" ] + ], + "faq": [ + ] + }, + { + "stage": "Purple", + "mc": "Dash & Le Prince", + "channel": "591928410209517568", + "emoji": ":loud_sound:", + "url": "https://www.q-dance.com/en/videos/purple-audio-live-2019", + "sets": [ + [ 2019, 6, 29, 11, 0, "Solstice" ], + [ 2019, 6, 29, 12, 30, "Nexone" ], + [ 2019, 6, 29, 14, 0, "Jesse Jax" ], + [ 2019, 6, 29, 15, 0, "PRDX" ], + [ 2019, 6, 29, 16, 0, "STK" ], + [ 2019, 6, 29, 17, 0, "Bestia" ], + [ 2019, 6, 29, 18, 0, "Kuzak" ], + [ 2019, 6, 29, 19, 0, "Dawnfire" ], + [ 2019, 6, 29, 20, 0, "Imperial" ], + [ 2019, 6, 29, 21, 0, "Unifire" ], + [ 2019, 6, 29, 22, 0, "Nothing" ], + [ 2019, 6, 30, 15, 0, "Charter" ], + [ 2019, 6, 30, 15, 45, "Envine" ], + [ 2019, 6, 30, 16, 30, "Helix" ], + [ 2019, 6, 30, 17, 15, "Attack" ], + [ 2019, 6, 30, 18, 0, "Stormerz" ], + [ 2019, 6, 30, 19, 0, "Emphasis" ], + [ 2019, 6, 30, 20, 0, "Nothing" ] + ], + "faq": [ + ] + } +] diff --git a/events/Epiq2019.json b/events/Epiq2019.json new file mode 100644 index 0000000..377f02f --- /dev/null +++ b/events/Epiq2019.json @@ -0,0 +1,35 @@ +[ + { + "stage": "mainstage", + "channel": "319525278978277407", + "emoji": "<:epiqE:661533593356468244><:epiqP:661533593088294933><:epiqI:661533593323044874><:epiqQ:661533593343885353>", + "url": "https://www.q-dance.com/en/radio/", + "unconfirmed": false, + "streamdelay": 1, + "sets": [ + [ 2019, 12, 31, 17, 45, "Q-dance Hardstyle Top 100 to 11" ], + [ 2019, 12, 31, 20, 30, "Primeshock" ], + [ 2019, 12, 31, 21, 30, "Demi Kanon" ], + [ 2019, 12, 31, 22, 15, "Sound Rush vs. Demi Kanon" ], + [ 2019, 12, 31, 22, 45, "Sound Rush" ], + [ 2019, 12, 31, 23, 40, "Hardstyle Top 10" ], + [ 2020, 1, 1, 0, 0, "Frequencerz" ], + [ 2020, 1, 1, 0, 45, "Devin Wild" ], + [ 2020, 1, 1, 1, 30, "Devin Wild vs. Rebelion" ], + [ 2020, 1, 1, 2, 0, "Psyko Punkz vs. Frequencerz" ], + [ 2020, 1, 1, 2, 30, "Psyko Punkz" ], + [ 2020, 1, 1, 3, 0, "D-Block & S-te-Fan vs. D-Sturb" ], + [ 2020, 1, 1, 4, 0, "TILT MODE" ], + [ 2020, 1, 1, 4, 10, "Rebelion" ], + [ 2020, 1, 1, 5, 0, "Act of Rage" ], + [ 2020, 1, 1, 5, 45, "Act of Rage vs. Sefa" ], + [ 2020, 1, 1, 6, 15, "Sefa" ], + [ 2020, 1, 1, 7, 0 ] + ], + "responses": { + "^\\.(top100|top10)": "Find the Q-dance Top 100 here: <#661622907109244939>", + "^\\.(url|stream|link|watch)$": ":tv: Listen to the livestream in our **Q-dance Radio voice channel** or here: ****", + "^\\.(mc|host)$": ":microphone: The top 100 is hosted by **Tellem**. The MC during Epiq is **Villan**." + } + } +] diff --git a/events/HardBass2019.json b/events/HardBass2019.json new file mode 100644 index 0000000..99840da --- /dev/null +++ b/events/HardBass2019.json @@ -0,0 +1,29 @@ +[ + { + "stage": "mainstage", + "mc": "DV8 & Villain", + "channel": "319525278978277407", + "emoji": ":blue_heart::green_heart::yellow_heart::heart:", + "url": "https://live.b2s.nl/", + "sets": [ + [ 2019, 2, 9, 21, 0, "Zany" ], + [ 2019, 2, 9, 22, 0, "TEAM BLUE (Coone, D-Block & S-te-Fan, Wildstylez)" ], + [ 2019, 2, 9, 23, 30, "Headhunterz (LIVE)" ], + [ 2019, 2, 10, 0, 0, "TEAM GREEN (Atmozfears, B-Front, Noisecontrollers)" ], + [ 2019, 2, 10, 1, 30, "I AM HARDSTYLE Take Over (Brennan Heart, Code Black, Toneshifterz) (LIVE)" ], + [ 2019, 2, 10, 2, 15, "Heroes of Hard Bass show" ], + [ 2019, 2, 10, 3, 0, "TEAM YELLOW (Frequencerz, Phuture Noize, Sub Zero Project)" ], + [ 2019, 2, 10, 4, 30, "Ran-D - We Rule The Night (Showcase) (LIVE)" ], + [ 2019, 2, 10, 5, 0, "TEAM RED (E-Force, Radical Redemption, Rejecta)" ], + [ 2019, 2, 10, 6, 30, "End of Line (Warface, Delete, Killshot) (LIVE)" ], + [ 2019, 2, 10, 7, 0, "Nothing" ] + ], + "faq": [ + "You can remove ads by logging in to the b2s website with a b2s account (or a Q-dance account).", + "Disable your adblocker to fix common issues with the stream player.", + "There's only 1 camera for the warmup set, it'll change soon.", + "Yes, <@193774894075346944> is recording: ", + "The timer is counting down until the end of Hard Bass. (hh:mm:ss.mm)" + ] + } +] diff --git a/events/Holland.json b/events/Holland.json new file mode 100644 index 0000000..3734cdb --- /dev/null +++ b/events/Holland.json @@ -0,0 +1,23 @@ +[ + { + "stage": "de-huiskamer", + "mc": "DV8", + "channel": "495260892535980032", + "emoji": ":flag_nl:", + "url": "https://www.q-dance.com/en/videos/q-dance-live-stream-from-x-qlusive-holland-xxl", + "sets": [ + [ 29, 22, 5, "Zany" ], + [ 29, 23, 5, "Demi Kanon" ], + [ 29, 23, 53, "Bass Modulators" ], + [ 30, 0, 50, "The Partysquad & Outsiders" ], + [ 30, 1, 48, "Coone's Inburgeringscursus" ], + [ 30, 2, 35, "Ransom & Dr. Rude" ], + [ 30, 3, 20, "Psyko Punkz" ], + [ 30, 4, 5, "Frequencerz" ], + [ 30, 5, 5, "Zazafront" ], + [ 30, 5, 35, "Bass Chaserz" ], + [ 30, 6, 20, "Dr. Peacock" ], + [ 30, 7, 5, "Nothing" ] + ] + } +] diff --git a/events/Holland2019.json b/events/Holland2019.json new file mode 100644 index 0000000..474f8cb --- /dev/null +++ b/events/Holland2019.json @@ -0,0 +1,20 @@ +[ + { + "stage": "mainstage", + "mc": "DV8", + "channel": "319525278978277407", + "emoji": ":flag_nl:", + "url": "https://www.q-dance.com/en/radio/", + "sets": [ + [ 2019, 9, 28, 23, 0, "Retroshock (Primeshock & Retrospect)" ], + [ 2019, 9, 29, 0, 0, "Dr. Ruthless" ], + [ 2019, 9, 29, 1, 0, "Sound Rush" ], + [ 2019, 9, 29, 2, 0, "Nothing" ], + [ 2019, 9, 29, 3, 0, "Outsiders" ], + [ 2019, 9, 29, 4, 0, "De Nachtbrakers: Bass Chaserz, Degos & Re-Done, Endymion" ], + [ 2019, 9, 29, 5, 0, "Warface presenteert Warfeest" ], + [ 2019, 9, 29, 6, 0, "D-Fence" ], + [ 2019, 9, 29, 7, 0, "Nothing" ] + ] + } +] diff --git a/events/Impaqt2019.json b/events/Impaqt2019.json new file mode 100644 index 0000000..564c1f9 --- /dev/null +++ b/events/Impaqt2019.json @@ -0,0 +1,25 @@ +[ + { + "stage": "Colossus", + "mc": "Villain", + "channel": "319525278978277407", + "emoji": ":airplane:", + "url": "https://www.q-dance.com/en/radio/", + "sets": [ + [ 2019, 9, 7, 14, 0, "Primeshock" ], + [ 2019, 9, 7, 15, 0, "KELTEK" ], + [ 2019, 9, 7, 16, 0, "Atmozfears & Sound Rush present: 2//\\\\\\\\1" ], + [ 2019, 9, 7, 16, 30, "Noisecontrollers & Devin Wild" ], + [ 2019, 9, 7, 17, 30, "Wildstylez" ], + [ 2019, 9, 7, 18, 30, "Sefa & Rooler" ], + [ 2019, 9, 7, 19, 0, "Zatox" ], + [ 2019, 9, 7, 20, 0, "Phuture Noize" ], + [ 2019, 9, 7, 21, 0, "D-Block & S-te-Fan" ], + [ 2019, 9, 7, 22, 0, "Ran-D" ], + [ 2019, 9, 7, 23, 0, "Warface" ], + [ 2019, 9, 8, 0, 0, "Rebelion" ], + [ 2019, 9, 8, 1, 0, "Miss K8" ], + [ 2019, 9, 8, 2, 0, "Nothing" ] + ] + } +] diff --git a/events/Mysteryland.json b/events/Mysteryland.json new file mode 100644 index 0000000..486cc28 --- /dev/null +++ b/events/Mysteryland.json @@ -0,0 +1,22 @@ +[ + { + "stage": "mysteryland", + "mc": "Villain", + "channel": "481392155022196747", + "emoji": ":bird:", + "url": "https://www.q-dance.com/en/videos/q-dance-live-stream", + "sets": [ + [ 25, 14, 0, "ANDY SVGE" ], + [ 25, 15, 30, "Ruthless" ], + [ 25, 16, 30, "Brennan Heart" ], + [ 25, 17, 30, "Code Black" ], + [ 25, 19, 0, "Nothing" ], + [ 25, 19, 33, "Wildstylez (w/o Hardwell)" ], + [ 25, 20, 0, "Atmozfears" ], + [ 25, 21, 0, "B-Front" ], + [ 25, 22, 0, "Sub Zero Project" ], + [ 25, 22, 50, "Endshow" ], + [ 25, 23, 0, "Nothing" ] + ] + } +] diff --git a/events/Qbase2018.json b/events/Qbase2018.json new file mode 100644 index 0000000..a76fa45 --- /dev/null +++ b/events/Qbase2018.json @@ -0,0 +1,112 @@ +[ + { + "stage": "openair", + "mc": "DV8", + "channel": "487970506910203914", + "emoji": ":airplane:", + "url": "https://www.q-dance.com/en/videos/q-dance-live-open-air", + "sets": [ + [ 8, 17, 0, "Retrospect" ], + [ 8, 18, 30, "Adrenalize" ], + [ 8, 20, 0, "KELTEK" ], + [ 8, 21, 0, "D-Block & S-te-Fan" ], + [ 8, 22, 0, "Sound Rush" ], + [ 8, 23, 0, "Coone" ], + [ 9, 0, 0, "Atmozfears" ], + [ 9, 1, 30, "Noisecontrollers" ], + [ 9, 3, 0, "Ran-D & B-Front" ], + [ 9, 4, 0, "Nothing" ], + [ 9, 6, 30, "Sefa" ], + [ 9, 7, 0, "Nothing" ] + ] + }, + + { + "stage": "hangar", + "mc": "Livid & Nolz", + "channel": "487970523784019979", + "emoji": ":airplane:", + "url": "https://www.q-dance.com/en/videos/q-dance-live-hangar", + "sets": [ + [ 8, 17, 0, "D-Attack" ], + [ 8, 18, 30, "Rejecta" ], + [ 8, 20, 0, "Myst vs. Degos & Re-Done" ], + [ 8, 21, 30, "Deetox" ], + [ 8, 22, 45, "Adaro" ], + [ 9, 0, 0, "Frequencerz" ], + [ 9, 1, 0, "Bass Chaserz" ], + [ 9, 2, 0, "Nothing" ], + [ 9, 2, 30, "E-Force" ], + [ 9, 4, 0, "Nothing" ], + [ 9, 5, 0, "Rebelion" ], + [ 9, 6, 0, "Jason Payne & Apexx" ], + [ 9, 7, 0, "Nothing" ] + ] + }, + + { + "stage": "thunderdome", + "mc": "Da Mouth of Madness", + "channel": "488016911838347276", + "emoji": ":airplane:", + "url": "https://www.q-dance.com/en/videos/q-dance-live-thunderdome", + "sets": [ + [ 8, 17, 0, "Waxweazle" ], + [ 8, 18, 30, "Vince" ], + [ 8, 20, 0, "MD&A" ], + [ 8, 20, 30, "Predator" ], + [ 8, 22, 0, "Evil Activities" ], + [ 8, 23, 30, "Nosferatu" ], + [ 9, 0, 30, "The Melodyst" ], + [ 9, 1, 30, "Promo" ], + [ 9, 3, 0, "I:GOR" ], + [ 9, 4, 0, "Unexist" ], + [ 9, 5, 0, "Andy The Core & Lady Dammage" ], + [ 9, 6, 0, "Spitnoise" ], + [ 9, 7, 0, "Nothing" ] + ] + }, + + { + "stage": "bkjn", + "mc": "No-ID & RG", + "channel": "488018562124873741", + "emoji": ":airplane:", + "url": "https://www.q-dance.com/en/videos/q-dance-live-bkjn", + "sets": [ + [ 8, 18, 0, "Hard Effectz vs. Stampede" ], + [ 8, 19, 0, "Hyrule War vs. Mr. Ivex" ], + [ 8, 20, 0, "Cryogenic" ], + [ 8, 21, 0, "Para Italia vs. Aggressive" ], + [ 8, 22, 0, "Super Trash Bros [LIVE]" ], + [ 8, 22, 30, "Rob Gee & Lunatic present: BKJN Music Showcase" ], + [ 8, 23, 45, "Sefa" ], + [ 9, 0, 15, "Vandalism" ], + [ 9, 1, 0, "Repix" ], + [ 9, 2, 0, "Omkara [LIVE]" ], + [ 9, 2, 30, "Dr. Peacock" ], + [ 9, 3, 30, "Chaotic Hostility & Chrono" ], + [ 9, 4, 30, "The Vizitor vs. MBK" ], + [ 9, 5, 30, "ELITE [LIVE]" ], + [ 9, 6, 0, "Nothing"] + ] + }, + + { + "stage": "evolution", + "mc": "DL", + "channel": "488019382094528512", + "emoji": ":airplane:", + "url": "https://www.q-dance.com/en/videos/q-dance-live-evolution", + "sets": [ + [ 8, 20, 0, "The Pitcher & Retrospect" ], + [ 8, 21, 45, "Coone & Ruthless" ], + [ 8, 22, 30, "Nightbreed Crew" ], + [ 9, 0, 0, "DJ The Prophet & Devin Wild" ], + [ 9, 1, 0, "Deetox & Sound Rush" ], + [ 9, 2, 30, "D-Fence & Panic" ], + [ 9, 3, 45, "Rooler" ], + [ 9, 5, 0, "Nothing" ] + ] + } +] diff --git a/events/Qbase2018_Single.json b/events/Qbase2018_Single.json new file mode 100644 index 0000000..99601a0 --- /dev/null +++ b/events/Qbase2018_Single.json @@ -0,0 +1,30 @@ +[ + { + "stage": "qbase", + "mc": "DV8 & Livid & Nolz", + "channel": "487537296581722112", + "emoji": ":airplane:", + "url": "https://www.q-dance.com/en/radio/", + "sets": [ + [ 8, 17, 0, "Retrospect / D-Attack" ], + [ 8, 18, 30, "Adrenalize / Rejecta" ], + [ 8, 20, 0, "KELTEK / Myst vs. Degos & Re-Done" ], + [ 8, 21, 0, "D-Block & S-te-Fan / Myst vs. Degos & Re-Done" ], + [ 8, 21, 30, "D-Block & S-te-Fan / Deetox" ], + [ 8, 22, 0, "Sound Rush / Deetox" ], + [ 8, 22, 45, "Sound Rush / Adaro" ], + [ 8, 23, 0, "Coone / Adaro" ], + [ 9, 0, 0, "Atmozfears / Frequencerz" ], + [ 9, 1, 0, "Atmozfears / Bass Chaserz" ], + [ 9, 1, 30, "Noisecontrollers / Bass Chaserz" ], + [ 9, 2, 0, "Noisecontrollers" ], + [ 9, 2, 30, "Noisecontrollers / E-Force" ], + [ 9, 3, 0, "Ran-D & B-Front / E-Force" ], + [ 9, 4, 0, "???" ], + [ 9, 5, 0, "Rebelion" ], + [ 9, 6, 0, "Jason Payne & Apexx" ], + [ 9, 6, 30, "Sefa / Jason Payne & Apexx" ], + [ 9, 7, 0, "Nothing" ] + ] + } +] diff --git a/events/Qbase2018_Two.json b/events/Qbase2018_Two.json new file mode 100644 index 0000000..fef0ccc --- /dev/null +++ b/events/Qbase2018_Two.json @@ -0,0 +1,46 @@ +[ + { + "stage": "openair", + "mc": "DV8", + "channel": "487970506910203914", + "emoji": ":airplane:", + "url": "https://www.q-dance.com/en/videos/q-dance-live-open-air", + "sets": [ + [ 8, 17, 0, "Retrospect" ], + [ 8, 18, 30, "Adrenalize" ], + [ 8, 20, 0, "KELTEK" ], + [ 8, 21, 0, "D-Block & S-te-Fan" ], + [ 8, 22, 0, "Sound Rush" ], + [ 8, 23, 0, "Coone" ], + [ 9, 0, 0, "Atmozfears" ], + [ 9, 1, 30, "Noisecontrollers" ], + [ 9, 3, 0, "Ran-D & B-Front" ], + [ 9, 4, 0, "Nothing" ], + [ 9, 6, 30, "Sefa" ], + [ 9, 7, 0, "Nothing" ] + ] + }, + + { + "stage": "hangar", + "mc": "Livid & Nolz", + "channel": "487970523784019979", + "emoji": ":airplane:", + "url": "https://www.q-dance.com/en/videos/q-dance-live-hangar", + "sets": [ + [ 8, 17, 0, "D-Attack" ], + [ 8, 18, 30, "Rejecta" ], + [ 8, 20, 0, "Myst vs. Degos & Re-Done" ], + [ 8, 21, 30, "Deetox" ], + [ 8, 22, 45, "Adaro" ], + [ 9, 0, 0, "Frequencerz" ], + [ 9, 1, 0, "Bass Chaserz" ], + [ 9, 2, 0, "Nothing" ], + [ 9, 2, 30, "E-Force" ], + [ 9, 4, 0, "Nothing" ], + [ 9, 5, 0, "Rebelion" ], + [ 9, 6, 0, "Jason Payne & Apexx" ], + [ 9, 7, 0, "Nothing" ] + ] + } +] diff --git a/events/Qlimax2018.json b/events/Qlimax2018.json new file mode 100644 index 0000000..a81ea1b --- /dev/null +++ b/events/Qlimax2018.json @@ -0,0 +1,23 @@ +[ + { + "stage": "mainstage", + "mc": "Villain", + "channel": "319525278978277407", + "emoji": ":flag_nl:", + "url": "https://www.q-dance.com/en/", + "sets": [ + [ 24, 21, 30, "Luna" ], + [ 24, 22, 30, "Sound Rush" ], + [ 24, 23, 30, "Coone" ], + [ 25, 0, 30, "Bass Modulators" ], + [ 25, 1, 30, "Wildstylez (Live)" ], + [ 25, 2, 1, "Sub Zero Project" ], + [ 25, 3, 16, "Tweekacore" ], + [ 25, 4, 1, "Phuture Noize" ], + [ 25, 5, 1, "Sub Zero Project & Phuture Noize" ], + [ 25, 5, 16, "B-Freqz" ], + [ 25, 6, 1, "Dr. Peacock" ], + [ 25, 7, 0, "Nothing" ] + ] + } +] diff --git a/events/Qlimax2019.json b/events/Qlimax2019.json new file mode 100644 index 0000000..9baaceb --- /dev/null +++ b/events/Qlimax2019.json @@ -0,0 +1,31 @@ +[ + { + "stage": "mainstage", + "channel": "319525278978277407", + "emoji": "<:qlimax:438815681706721281>", + "url": "https://www.q-dance.com/en/events/qlimax/qlimax-2019/live", + "unconfirmed": false, + "streamdelay": 4, + "sets": [ + [ 2019, 11, 23, 21, 45, "The Qreator" ], + [ 2019, 11, 23, 22, 45, "KELTEK" ], + [ 2019, 11, 23, 23, 30, "Sound Rush" ], + [ 2019, 11, 24, 0, 30, "B-Front" ], + [ 2019, 11, 24, 0, 40, "D-Block & S-te-Fan" ], + [ 2019, 11, 24, 1, 30, "Headhunterz" ], + [ 2019, 11, 24, 2, 30, "B-Front" ], + [ 2019, 11, 24, 3, 30, "Ran-D \"We Rule The Night\"" ], + [ 2019, 11, 24, 4, 15, "D-Sturb" ], + [ 2019, 11, 24, 5, 0, "Rejecta (LIVE)" ], + [ 2019, 11, 24, 5, 30, "Radical Redemption" ], + [ 2019, 11, 24, 6, 15, "Miss K8" ], + [ 2019, 11, 24, 7, 0 ] + ], + "responses": { + "^\\.(url|stream|link|watch)$": ":tv: Watch the livestream here: **** (Note: This is a paid premium livestream!)", + "^\\.(mc|host)$": ":microphone: The MC is **Villan**, and **Nolz** for the final 2 sets", + "^\\.restream$": "The Qlimax stream is a **premium** livestream. As such, we consider all unofficial free re-streams as piracy. Do not post or ask for restreams.", + "^\\.qreator$": "It is not confirmed yet who *The Qreator* is. It was not revealed during their set." + } + } +] diff --git a/events/Qonnect.json b/events/Qonnect.json new file mode 100644 index 0000000..b76dfad --- /dev/null +++ b/events/Qonnect.json @@ -0,0 +1,29 @@ +[ + { + "stage": "The Studio", + "channel": "319525278978277407", + "emoji": "<:qdance:328585093553586176>", + "url": "https://www.q-dance.com/en/videos/livestream-qonnect", + "unconfirmed": false, + "streamdelay": 3, + "sets": [ + [ 2020, 3, 21, 13, 0, "Demi Kanon" ], + [ 2020, 3, 21, 14, 0, "Primeshock" ], + [ 2020, 3, 21, 15, 0, "Clockartz" ], + [ 2020, 3, 21, 16, 0, "Adrenalize" ], + [ 2020, 3, 21, 16, 45, "Audiotricz" ], + [ 2020, 3, 21, 17, 50, "Outsiders" ], + [ 2020, 3, 21, 18, 45, "Sound Rush" ], + [ 2020, 3, 21, 19, 45, "Audiofreq" ], + [ 2020, 3, 21, 20, 0, "Headhunterz" ], + [ 2020, 3, 21, 21, 0, "Devin Wild" ], + [ 2020, 3, 21, 22, 10, "Hard Driver" ], + [ 2020, 3, 21, 23, 10, "Dr. Peacock" ], + [ 2020, 3, 21, 23, 40 ] + ], + "responses": { + "^\\.(url|stream|link|watch)$": ":tv: Tune in to the livestream here: ****", + "^\\.(mc|host)$": ":microphone: The hosts between 13:00 and 19:00 CET are **Tellem** and **Adrenalize**. Between 19:00 and 23:00 CET it's **E-Life** and **Audiofreq**." + } + } +] diff --git a/events/Qonnect2.json b/events/Qonnect2.json new file mode 100644 index 0000000..799606e --- /dev/null +++ b/events/Qonnect2.json @@ -0,0 +1,33 @@ +[ + { + "stage": "The Studio", + "channel": "319525278978277407", + "emoji": "<:qdance:328585093553586176>", + "url": "https://live.q-dance.com/", + "unconfirmed": false, + "streamdelay": 0, + "sets": [ + [ 2020, 4, 4, 18, 5, "Degos & Re-Done" ], + [ 2020, 4, 4, 18, 35, "MYST" ], + [ 2020, 4, 4, 19, 15, "Hard Driver" ], + [ 2020, 4, 4, 19, 45, "Apexx" ], + [ 2020, 4, 4, 20, 20, "B-Front" ], + [ 2020, 4, 4, 20, 55, "Adaro" ], + [ 2020, 4, 4, 21, 30, "Bloodlust" ], + [ 2020, 4, 4, 22, 5, "Vertile" ], + [ 2020, 4, 4, 22, 35, "Rejecta" ], + [ 2020, 4, 4, 23, 15, "The Qreator: The Ultimate QAPITAL Mix" ], + [ 2020, 4, 4, 23, 45, "Act of Rage" ], + [ 2020, 4, 5, 0, 20, "Rebelion" ], + [ 2020, 4, 5, 0, 50, "Rooler" ], + [ 2020, 4, 5, 1, 25, "Invector" ], + [ 2020, 4, 5, 1, 55, "Thyron" ], + [ 2020, 4, 5, 2, 25, "Imperatorz" ], + [ 2020, 4, 5, 3, 0 ] + ], + "responses": { + "^\\.(url|stream|link|watch)$": ":tv: Tune in to the livestream here: ****", + "^\\.(mc|host)$": ":microphone: The hosts are Tellem & Livid." + } + } +] diff --git a/events/Qonnect3.json b/events/Qonnect3.json new file mode 100644 index 0000000..ffb5918 --- /dev/null +++ b/events/Qonnect3.json @@ -0,0 +1,29 @@ +[ + { + "stage": "The Studio", + "channel": "319525278978277407", + "emoji": "<:qdance:328585093553586176>", + "url": "https://live.q-dance.com/", + "unconfirmed": false, + "streamdelay": 3, + "sets": [ + [ 2020, 4, 11, 13, 0, "Retrospect" ], + [ 2020, 4, 11, 14, 0, "Villain" ], + [ 2020, 4, 11, 15, 0, "Coone" ], + [ 2020, 4, 11, 16, 0, "Da Tweekaz" ], + [ 2020, 4, 11, 17, 0, "Zatox" ], + [ 2020, 4, 11, 18, 0, "Darkraver (Vinyl Mix at Six)" ], + [ 2020, 4, 11, 19, 0, "KELTEK" ], + [ 2020, 4, 11, 20, 0, "Headhunterz" ], + [ 2020, 4, 11, 21, 0, "Atmozfears" ], + [ 2020, 4, 11, 22, 0, "Crypsis" ], + [ 2020, 4, 11, 23, 0, "Sefa (LIVE)" ], + [ 2020, 4, 12, 0, 0 ] + ], + "responses": { + "^\\.(url|stream|link|watch)$": ":tv: Tune in to the livestream here: ****", + "^\\.(mc|host)$": ":microphone: The hosts between 13:00 and 18:00 are Adrenalize and Tellem. During 18:00 and 23:30 it's Audiofreq & E-life.", + "^\\.(hidechat|removechat|fuckchat)$": ":thinking: Press F12 and paste this into the console to hide the chat on live.q-dance.com: ```sc=document.getElementById(\"scrollContainer\");sc.classList.remove(\"col-l--9\");sc.classList.remove(\"col-m--8\");sc.classList.add(\"col-l--12\");sc.classList.remove(\"col-m--12\");```" + } + } +] diff --git a/events/Reverze2019.json b/events/Reverze2019.json new file mode 100644 index 0000000..5b15975 --- /dev/null +++ b/events/Reverze2019.json @@ -0,0 +1,26 @@ +[ + { + "stage": "sportpaleis", + "mc": "Villain & Chucky", + "channel": "319525278978277407", + "emoji": ":fire:", + "url": "https://www.youtube.com/user/bassevents/live", + "sets": [ + [ 2019, 2, 23, 21, 30, "Sound Rush" ], + [ 2019, 2, 23, 22, 30, "Sephyx vs Devin Wild" ], + [ 2019, 2, 23, 23, 30, "Refuzion (recorded from 20:30)" ], + [ 2019, 2, 24, 0, 0, "Headhunterz" ], + [ 2019, 2, 24, 1, 0, "KELTEK (LIVE)" ], + [ 2019, 2, 24, 1, 30, "Coone & Brennan Heart" ], + [ 2019, 2, 24, 2, 30, "Tweekacore (LIVE)" ], + [ 2019, 2, 24, 3, 0, "Mark with a K & Warface (LIVE)" ], + [ 2019, 2, 24, 3, 30, "Reverze Flashback by Dark-E & Pat B" ], + [ 2019, 2, 24, 4, 15, "Phuture Noize \"Black Mirror Society\" (LIVE)" ], + [ 2019, 2, 24, 4, 45, "Nothing" ] + ], + "faq": [ + "D-Block & S-te-Fan's \"Ghost Stories\" set is not livestreamed. It is instead replaced with the first half of Refuzion's set.", + "Yes, someone is definitely recording this. Including Bass Events." + ] + } +] diff --git a/events/Reverze2020.json b/events/Reverze2020.json new file mode 100644 index 0000000..fd41dfe --- /dev/null +++ b/events/Reverze2020.json @@ -0,0 +1,29 @@ +[ + { + "stage": "sportpaleis", + "channel": "319525278978277407", + "emoji": "<:bassevents:685859019973460136>", + "url": "https://www.youtube.com/watch?v=QP0AGJ0avRs", + "unconfirmed": false, + "streamdelay": 3, + "sets": [ + [ 2020, 3, 7, 21, 0, "Refuzion" ], + [ 2020, 3, 7, 21, 30, "Mandy" ], + [ 2020, 3, 7, 22, 15, "Keltek vs. Sound Rush" ], + [ 2020, 3, 7, 23, 15, "Psyko Punkz" ], + [ 2020, 3, 8, 0, 0, "Brennan Heart: 15 Years Reverze Special" ], + [ 2020, 3, 8, 0, 45, "The Elite", "Da Tweekaz, Coone, Hard Driver" ], + [ 2020, 3, 8, 1, 45, "D-Block & S-te-Fan" ], + [ 2020, 3, 8, 2, 45, "Sub Zero Project: Rave Into Space (LIVE)" ], + [ 2020, 3, 8, 3, 15, "15 Years Reverze Flashback", "Pat B, Dark-E, Mark with a K & MC Chucky" ], + [ 2020, 3, 8, 4, 0, "Warface & D-Sturb: Synchronised" ], + [ 2020, 3, 8, 4, 45, "Minus Militia: The Code of Conduct (LIVE)" ], + [ 2020, 3, 8, 5, 15 ] + ], + "responses": { + "^\\.(url|stream|link|watch)$": ":tv: Tune in to the livestream here: ****", + "^\\.(mc|host)$": ":microphone: The MC is **Villain**.", + "^\\.(lotto|arena|stages)$": ":warning: **ONLY** the Sportpaleis is being streamed, the Lotto Arena stage is **NOT** being streamed." + } + } +] diff --git a/events/TomorrowlandDominator2018.json b/events/TomorrowlandDominator2018.json new file mode 100644 index 0000000..9d2a854 --- /dev/null +++ b/events/TomorrowlandDominator2018.json @@ -0,0 +1,47 @@ +[ + { + "stage": "tomorrowland", + "mc": "Villain", + "channel": "469913276592029706", + "emoji": ":one:", + "url": "https://live.tomorrowland.com/", + "sets": [ + [ 21, 13, 0, "Demi Kanon" ], + [ 21, 14, 0, "Mandy & Adrenalize" ], + [ 21, 15, 0, "Audiotricz" ], + [ 21, 16, 0, "Da Tweekaz" ], + [ 21, 17, 0, "Wildstylez" ], + [ 21, 18, 0, "Brennan Heart" ], + [ 21, 19, 0, "Atmozfears" ], + [ 21, 20, 0, "Zatox" ], + [ 21, 21, 0, "Ran-D" ], + [ 21, 22, 0, "Sub Zero Project" ], + [ 21, 23, 0, "Korsakoff" ], + [ 22, 0, 0, "Nothing" ], + + [ 28, 12, 0, "Ransom" ], + [ 28, 13, 0, "Refuzion" ], + [ 28, 14, 0, "Sound Rush" ], + [ 28, 15, 0, "Wasted Penguinz" ], + [ 28, 16, 0, "Mark With a K & MC Chucky" ], + [ 28, 17, 0, "Bass Modulators" ], + [ 28, 18, 0, "Code Black" ], + [ 28, 19, 0, "Noisecontrollers" ], + [ 28, 20, 0, "Psyko Punkz" ], + [ 28, 21, 0, "Frequencerz" ], + [ 28, 22, 0, "B-Front" ], + [ 28, 23, 0, "Partyraiser" ], + [ 29, 0, 0, "Nothing" ], + + [ 29, 16, 0, "Da Tweekaz" ], + [ 29, 17, 0, "Wildstylez" ], + [ 29, 18, 0, "Coone" ], + [ 29, 19, 0, "Pablo Discobar" ], + [ 29, 19, 30, "Darren Styles" ], + [ 29, 20, 15, "Zatox" ], + [ 29, 21, 15, "Coone vs. Hard Driver" ], + [ 29, 22, 15, "Gunz for Hire" ], + [ 29, 23, 0, "Nothing" ] + ] + } +] diff --git a/events/Tweekaz.json b/events/Tweekaz.json new file mode 100644 index 0000000..6328d9f --- /dev/null +++ b/events/Tweekaz.json @@ -0,0 +1,21 @@ +[ + { + "stage": "mainstage", + "mc": "Villain", + "channel": "319525278978277407", + "emoji": ":duck::wine_glass:", + "url": "https://www.q-dance.com/en/radio/", + "sets": [ + [ 2019, 1, 26, 23, 0, "Sephyx & Refuzion" ], + [ 2019, 1, 27, 0, 0, "Code Black" ], + [ 2019, 1, 27, 1, 0, "Da Tweekaz" ], + [ 2019, 1, 27, 2, 0, "Coone, Hard Driver & Da Tweekaz \"Dirty Workz Elite\"" ], + [ 2019, 1, 27, 3, 15, "D-Block & S-te-Fan" ], + [ 2019, 1, 27, 4, 15, "Tweekacore" ], + [ 2019, 1, 27, 4, 45, "Sub Zero Project vs. Da Tweekaz" ], + [ 2019, 1, 27, 5, 30, "D-Sturb" ], + [ 2019, 1, 27, 6, 15, "Destructive Tendencies vs. Da Tweekaz" ], + [ 2019, 1, 27, 7, 0, "Nothing" ] + ] + } +] diff --git a/events/WOWWOW2018.json b/events/WOWWOW2018.json new file mode 100644 index 0000000..d9a9139 --- /dev/null +++ b/events/WOWWOW2018.json @@ -0,0 +1,28 @@ +[ + { + "stage": "mainstage", + "mc": "Villain", + "channel": "319525278978277407", + "emoji": ":champagne::tada:", + "url": "https://www.q-dance.com/en/radio/", + "sets": [ + [ 2018, 12, 31, 21, 0, "Max Enforcer" ], + [ 2018, 12, 31, 22, 0, "Mark with a K presents: Belgium" ], + [ 2018, 12, 31, 23, 0, "KELTEK: Best euphoric breakthrough" ], + [ 2018, 12, 31, 23, 40, "Hardstyle Top 10" ], + [ 2019, 1, 1, 0, 0, "Brennan Heart" ], + [ 2019, 1, 1, 0, 45, "Best of X-Qlusive Frequencerz" ], + [ 2019, 1, 1, 1, 30, "POWER HOUR in 10 minutes" ], + [ 2019, 1, 1, 1, 40, "Noisecontrollers & Audiotricz presents: Spirit of Hardstyle" ], + [ 2019, 1, 1, 2, 20, "Atmozfears: Q-BASE anthem 2018" ], + [ 2019, 1, 1, 3, 0, "Ran-D presents: Roughstate 2018" ], + [ 2019, 1, 1, 3, 30, "ZazaFront: X-Qlusive Holland set" ], + [ 2019, 1, 1, 4, 0, "Sub Zero Project: Qlimax anthem 2018" ], + [ 2019, 1, 1, 4, 15, "Rejecta: Best RAW Talent" ], + [ 2019, 1, 1, 4, 45, "Phuture Noize: QAPITAL anthem 2018" ], + [ 2019, 1, 1, 5, 15, "Rebelion: QAPITAL anthem 2019" ], + [ 2019, 1, 1, 6, 0, "Partyraiser presents: Hardcore 2018" ], + [ 2019, 1, 1, 7, 0, "Nothing" ] + ] + } +] diff --git a/index.js b/index.js new file mode 100644 index 0000000..ecfd9ed --- /dev/null +++ b/index.js @@ -0,0 +1,4 @@ +var Startup = require("./Startup"); + +let startup = new Startup(); +startup.run(); diff --git a/modules/autoreact.js b/modules/autoreact.js new file mode 100644 index 0000000..e4431b7 --- /dev/null +++ b/modules/autoreact.js @@ -0,0 +1,29 @@ +const discord = require("discord.js"); + +class AutoReactModule +{ + constructor(config, client, bot) + { + this.config = config; + /** @type {discord.Client} */ + this.client = client; + this.bot = bot; + } + + /** + * @param {discord.Message} msg + * @param {Boolean} edited + */ + onMessage(msg, edited) + { + if (edited) { + return; + } + + if (msg.content.match(new RegExp(this.config.match, "i"))) { + msg.react(this.config.emoji); + } + } +} + +module.exports = AutoReactModule; diff --git a/modules/event.js b/modules/event.js new file mode 100644 index 0000000..e68070c --- /dev/null +++ b/modules/event.js @@ -0,0 +1,513 @@ +var fs = require("fs"); +var moment = require("moment"); + +var cmdsplit = require("./../cmdsplit"); + +class EventSchedule +{ + constructor(config, client, bot) + { + this.event = config; + this.client = client; + this.bot = bot; + + this.lastNotFound = new Date(1970, 1, 1); + this.lastNow = new Date(1970, 1, 1); + + this.loadSchedule(this.event.file); + } + + loadSchedule(filename) + { + console.log('Loading schedule: "' + filename + '"'); + + this.schedule = JSON.parse(fs.readFileSync(filename)); + + for (var i = 0; i < this.schedule.length; i++) { + let stage = this.schedule[i]; + + console.log("Event channel: " + stage.channel); + this.client.channels.fetch(stage.channel).then(channel => { + stage.channel = channel; + this.updateChannel(stage); + }).catch(() => { + console.error("Unable to find channel for stage \"" + stage.stage + "\""); + }); + + var newResponses = []; + for (var expression in stage.responses) { + let newResponse = { + match: new RegExp(expression, "i"), + msg: stage.responses[expression] + }; + newResponses.push(newResponse); + } + stage.responses = newResponses; + + stage.channelExtra = null; + if (stage.extra_channel !== undefined) { + this.client.channels.fetch(stage.extra_channel).then(channel => { + stage.channelExtra = channel; + }).catch(() => { + console.error("Unable to find channel for stage \"" + stage.stage + "\""); + }); + } + + var streamDelay = stage.streamdelay; + + for (var j = 0; j < stage.sets.length; j++) { + var set = stage.sets[j]; + var dateArray = set.slice(0, 5); + dateArray[1] -= 1; // months are 0-indexed, for some reason. even in the moment library! + + var setDate = moment(dateArray).add(streamDelay, 'm'); + var newSet = { + date: setDate, + name: set[5], + report: moment() > setDate, + report_5min: moment().add(5, 'm') > setDate, + nothing: (set[5] === undefined || set[5] == "Nothing"), + who: set[6] + }; + + stage.sets[j] = newSet; + } + } + } + + getStage(stage) + { + for (var i = 0; i < this.schedule.length; i++) { + var s = this.schedule[i]; + if (s.stage == stage) { + return s; + } + } + return null; + } + + getStageByChannel(channel) + { + for (var i = 0; i < this.schedule.length; i++) { + var s = this.schedule[i]; + if (s.channel == channel) { + return s; + } + } + return null; + } + + findSets(query) + { + query = query.toLowerCase(); + + var ret = []; + for (var i = 0; i < this.schedule.length; i++) { + var stage = this.schedule[i]; + for (var j = 0; j < stage.sets.length; j++) { + var set = stage.sets[j]; + if (set.nothing) { + continue; + } + if (set.name.toLowerCase().indexOf(query) != -1 || (set.who && set.who.toLowerCase().indexOf(query) != -1)) { + ret.push({ + set: set, + stage: stage + }); + } + } + } + return ret; + } + + getCurrentSet(stage) + { + var date = new Date(); + + for (var i = 0; i < stage.sets.length; i++) { + var set = stage.sets[i]; + if (date < set.date) { + if (i == 0) { + // Happens if no set has started yet on this stage + return null; + } + return stage.sets[i - 1]; + } + } + + // Happens if the last set has been played + return stage.sets[stage.sets.length - 1]; + } + + getNextSet(stage) + { + var date = new Date(); + + for (var i = 0; i < stage.sets.length; i++) { + var set = stage.sets[i]; + if (date < set.date) { + return set; + } + } + + // Happens if this is the final set on this stage + return null; + } + + getNextLiveSet(stage) + { + var date = new Date(); + + for (var i = 0; i < stage.sets.length; i++) { + var set = stage.sets[i]; + if (date < set.date && !set.nothing) { + return set; + } + } + + // Happens if this is the final set on this stage + return null; + } + + onTick() + { + var date = moment(); + + for (var i = 0; i < this.schedule.length; i++) { + var stage = this.schedule[i]; + + var current = this.getCurrentSet(stage); + if (current !== null) { + if (!current.report) { + current.report = true; + if (!current.nothing) { + console.log("Starting now: " + current.name); + var msg = ":red_circle: STARTING NOW: **" + current.name + "**"; + stage.channel.send(msg); + if (stage.channelExtra) { + stage.channelExtra.send(msg); + } + } else { + console.log("Stream is not live anymore."); + var next = this.getNextSet(stage); + if (next !== null && !next.nothing) { + var msg = ":no_entry_sign: Stream is no longer live. Next set is at **" + this.getTimeString(next.date) + "**!"; + stage.channel.send(msg); + if (stage.channelExtra) { + stage.channelExtra.send(msg); + } + } else { + var msg = ":tada: This is the end of the livestream. Thanks for watching."; + stage.channel.send(msg); + if (stage.channelExtra) { + stage.channelExtra.send(msg); + } + } + } + this.updateChannel(stage); + } + } + + var next = this.getNextSet(stage); + if (next !== null && !next.nothing) { + if (date.clone().add(5, 'm') > next.date && !next.report_5min) { + next.report_5min = true; + console.log("Starting in 5 minutes: " + next.name); + var msg = ":warning: **" + next.name + "** starts in 5 minutes!"; + stage.channel.send(msg); + if (stage.channelExtra) { + stage.channelExtra.send(msg); + } + } + } + } + } + + onCmdCurrent(msg) { this.onCmdNp(msg); } + onCmdNow(msg) { this.onCmdNp(msg); } + onCmdNp(msg) + { + var stage = this.getStageByChannel(msg.channel); + if (!stage) { + return; + } + + var current = this.getCurrentSet(stage); + if (current !== null && !current.nothing) { + if (current.who) { + msg.channel.send(":red_circle: Now playing: **" + current.name + "**, started ! (" + current.who + ")"); + } else { + msg.channel.send(":red_circle: Now playing: **" + current.name + "**, started !"); + } + } else { + msg.channel.send("Nobody's playing right now."); + } + } + + onCmdNext(msg) + { + var stage = this.getStageByChannel(msg.channel); + if (!stage) { + return; + } + + var next = this.getNextSet(stage); + if (next) { + var localTime = "**" + this.getTimeString(next.date) + "**"; + localTime += " ()"; + + if (next.name) { + if (next.who) { + msg.channel.send(":arrow_forward: Next up: **" + next.name + "**, at " + localTime + " (" + next.who + ")"); + } else { + msg.channel.send(":arrow_forward: Next up: **" + next.name + "**, at " + localTime); + } + } else { + msg.channel.send(":arrow_forward: The stream ends at " + localTime); + } + } else { + msg.channel.send("There's nothing playing next."); + } + } + + getScheduleString(stage, limit, starttime) + { + var ret = ""; + + if (stage.unconfirmed) { + ret = ":warning: **Note:** Set times are not confirmed!\n"; + } + + if (!starttime) { + starttime = moment(stage.sets[0].date).clone().subtract(1, 'm'); + } + + var lines = 0; + for (var i = 0; i < stage.sets.length; i++) { + var set = stage.sets[i]; + if (starttime > set.date) { + continue; + } + + if (limit && lines == limit) { + ret += "(limited, use `.fullschedule` for the full schedule)\n"; + break; + } + lines++; + + if (set.nothing) { + ret += "- (), the stream will be offline :no_entry_sign:\n"; + } else { + if (set.who) { + ret += "- (): **" + set.name + "** (" + set.who + ")\n"; + } else { + ret += "- (): **" + set.name + "**\n"; + } + } + } + + if (lines == 0) { + ret = "We have nothing left! :frowning:"; + } else if (limit) { + ret = ":calendar_spiral: Next " + limit + " sets are:\n" + ret.trim(); + } else { + ret = ":calendar_spiral: The full schedule:\n" + ret.trim(); + } + + return ret; + } + + onCmdTimetable(msg) { this.onCmdSchedule(msg); } + onCmdSched(msg) { this.onCmdSchedule(msg); } + onCmdSchedule(msg) + { + var stage = this.getStageByChannel(msg.channel); + if (!stage) { + return; + } + + msg.channel.send(this.getScheduleString(stage, 5, moment())); + } + + onCmdFullSched(msg) { this.onCmdFullSchedule(msg); } + onCmdFullSchedule(msg) + { + var stage = this.getStageByChannel(msg.channel); + if (!stage) { + return; + } + + var text = this.getScheduleString(stage); + while (text.length > 0) { + msg.author.send(text.substr(0, 2000)).catch(console.error); + text = text.substr(2000); + } + + msg.reply("I've DM'd you the full schedule."); + } + + onCmdFind(msg) + { + var query = Array.from(arguments).slice(1).join(" ").trim(); + if (query.length < 3) { + return; + } + + var results = this.findSets(query); + + var ret = ""; + if (results.length == 0) { + ret = "I found nothing :frowning:"; + // Avoid spamming "I found nothing" when jokers do .find a meaning of life + var now = new Date(); + if ((now - this.lastNotFound) < 60 * 1000) { + return; + } + this.lastNotFound = now; + } else { + var date = new Date(); + for (var i = 0; i < results.length; i++) { + var res = results[i]; + + var localTime = "**" + this.getTimeString(res.set.date) + "**"; + localTime += " ()"; + + var stageMessage = ""; + if (this.schedule.length > 1) { + stageMessage = " on **" + res.stage.stage + "** " + res.stage.emoji + " stage!"; + } + + if (date > res.date) { + ret += res.set.name + " already played on \n"; + } else { + ret += res.set.name + " plays on \n"; + } + } + } + + msg.channel.send(":calendar_spiral: " + ret.trim()); + } + + onCmdReloadSchedule(msg) + { + if (!this.bot.isAdmin(msg.member)) { + return; + } + + this.loadSchedule(this.event.file); + msg.reply("schedule reloaded!"); + } + + onMessage(msg) + { + var inStage = false; + + for (var i = 0; i < this.schedule.length; i++) { + var stage = this.schedule[i]; + if (stage.channel.id != msg.channel.id) { + continue; + } + + inStage = true; + + for (var j = 0; j < stage.responses.length; j++) { + var r = stage.responses[j]; + var match = msg.content.match(r.match); + if (match) { + var sendMessage = r.msg; + for (var k = 0; k < match.length; k++) { + sendMessage = sendMessage.replace("$" + k, match[k]); + } + msg.channel.send(sendMessage); + return true; + } + } + } + + var isCommand = msg.content.startsWith("."); + + var parse = []; + if (isCommand) { + parse = cmdsplit(msg.content); + } + + // Outside-channel schedule command + if (isCommand && this.schedule.length > 1 && !inStage && (parse[0] == ".schedule" || parse[0] == ".timetable" || parse[0] == ".sched" || parse[0] == ".current" || parse[0] == ".now")) { + // Avoid spamming long .now message when jokers spam .now + var now = new Date(); + if ((now - this.lastNow) < 60 * 1000) { + return true; + } + this.lastNow = now; + + var ret = "**LIVE**\n"; + for (var i = 0; i < this.schedule.length; i++) { + var stage = this.schedule[i]; + + var current = this.getCurrentSet(stage); + var next = this.getNextSet(stage); + + if (current === null && next !== null && next.date.date() != moment(now).date()) { + continue; + } + + if (current !== null && !current.nothing) { + ret += stage.emoji + " " + stage.stage + ": **" + current.name + "**"; + } else { + ret += stage.emoji + " " + stage.stage + ": Not live"; + } + + if (next !== null && !next.nothing) { + ret += ", next: " + next.name; + } else { + ret += "."; + } + + ret += " " + stage.channel.toString() + "\n"; + } + msg.channel.send(ret.trim()); + return true; + } + + return false; + } + + getTimeString(date) + { + return ""; + } + + updateChannel(stage) + { + if (typeof(stage.channel) == "string") { + return; + } + + var line = ""; + + var current = this.getCurrentSet(stage); + var next = this.getNextLiveSet(stage); + + if ((current === null || current.nothing) && next === null) { + line += " :tada: Thanks for watching."; + } else { + if (current !== null && !current.nothing) { + line += " __" + current.name + "__ "; + } else { + line += " :no_entry_sign: __Not currently live__."; + } + + if (next !== null) { + line += " :arrow_forward: Next: __" + next.name + "__ "; + } else { + line += " :warning: This is the last set!"; + } + + line += " :link: " + stage.url; + } + + stage.channel.setTopic(stage.emoji + " " + line, "Automated bot action for event"); + } +} + +module.exports = EventSchedule; diff --git a/modules/eventquick.js b/modules/eventquick.js new file mode 100644 index 0000000..10ca353 --- /dev/null +++ b/modules/eventquick.js @@ -0,0 +1,133 @@ +class EventQuickModule +{ + constructor(config, client, bot) + { + this.event = config; + this.client = client; + this.bot = bot; + + this.current = ''; + this.ended = false; + if (this.event.current !== undefined) { + this.current = this.event.current; + } + + this.channel = client.channels.resolve(this.event.channel); + this.updateChannel(); + } + + onCmdIlink(msg, link) + { + if (!this.bot.isMod(msg.member)) { + return; + } + + this.event.link = link; + + console.log("New impromptu event link: " + this.event.link); + msg.channel.send(":link: The livestream can be found here: <" + this.event.link + ">"); + + msg.delete(); + this.updateChannel(); + } + + onCmdInow(msg) + { + if (!this.bot.isMod(msg.member)) { + return; + } + + if (arguments.length == 1) { + return; + } + + this.current = Array.from(arguments).slice(1).join(" ").trim(); + this.ended = false; + + console.log("Starting now: " + this.current); + msg.channel.send(":red_circle: STARTING NOW: **" + this.current + "**"); + + msg.delete(); + this.updateChannel(); + } + + onCmdIoffline(msg) + { + if (!this.bot.isMod(msg.member)) { + return; + } + + this.current = ""; + this.ended = false; + + console.log("Stream is offline!"); + msg.channel.send(":no_entry_sign: Stream is temporarily offline."); + + msg.delete(); + this.updateChannel(); + } + + onCmdIend(msg) + { + if (!this.bot.isMod(msg.member)) { + return; + } + + this.current = ""; + this.ended = true; + + console.log("End of event!"); + msg.channel.send(":tada: Thank you for tuning in."); + + msg.delete(); + this.updateChannel(); + } + + onCmdCurrent(msg) { this.onCmdNp(msg); } + onCmdNow(msg) { this.onCmdNp(msg); } + onCmdNp(msg) + { + if (this.current != "") { + msg.channel.send(":red_circle: Now playing: **" + this.current + "**"); + } else { + msg.channel.send(":robot: There's currently no set playing."); + } + } + + onCmdNext(msg) + { + msg.channel.send(":robot: This is an event without a timetable."); + } + + onCmdLink(msg) + { + msg.channel.send(":link: The livestream can be found here: <" + this.event.link + ">"); + } + + updateChannel() + { + var line = ""; + + if (this.ended) { + line = ":tada: Thank you for tuning in."; + } else { + if (this.current == "") { + line = ":no_entry_sign: Not currently live."; + } else { + line = "__" + this.current + "__"; + } + + if (this.event.link !== undefined) { + line += " :link: " + this.event.link; + } + } + + if (this.event.emoji) { + line = this.event.emoji + " " + line; + } + + this.channel.setTopic(line); + } +} + +module.exports = EventQuickModule; diff --git a/modules/filter.js b/modules/filter.js new file mode 100644 index 0000000..44f7ae4 --- /dev/null +++ b/modules/filter.js @@ -0,0 +1,70 @@ +const discord = require("discord.js"); +const RedditRadio = require("../RedditRadio"); + +class FilterModule +{ + constructor(config, client, bot) + { + this.config = config; + + /** @type {discord.Client} */ + this.client = client; + + /** @type {RedditRadio} */ + this.bot = bot; + + if (this.config.channel) { + if (!this.config.channels) { + this.config.channels = []; + } + this.config.channels.push(this.config.channel); + } + } + + /** + * @param {discord.Message} msg + * @param {Boolean} edited + */ + onMessage(msg, edited) + { + if (this.bot.isMod(msg.member)) { + return; + } + + var shouldDelete = false; + + // Only filter if we're in the right channel + if (this.config.channels) { + var isInChannel = false; + for (const channelID of this.config.channels) { + if (msg.channel.id == channelID) { + isInChannel = true; + break; + } + } + if (!isInChannel) { + return; + } + } + + // Check for bad words (case insensitive) + if (this.config.words && msg.content.toLowerCase().match(this.config.words)) { + shouldDelete = true; + } + + // Check for bad tokens (case sensitive) + if (this.config.tokens && msg.content.match(this.config.tokens)) { + shouldDelete = true; + } + + if (shouldDelete) { + var usermessage = this.config.usermessage || "Your recent message has been automatically deleted. Please take another look at the rules in #info. We automatically delete messages for things like piracy and advertising."; + + msg.delete(); + this.bot.addLogMessage("Deleted unwanted message from " + msg.author.toString() + " in " + msg.channel.toString() + ": `" + msg.content.replace('`', '\\`') + "`"); + msg.author.send(usermessage).catch(console.error); + } + } +} + +module.exports = FilterModule; diff --git a/modules/filteremotes.js b/modules/filteremotes.js new file mode 100644 index 0000000..1628d73 --- /dev/null +++ b/modules/filteremotes.js @@ -0,0 +1,34 @@ +const discord = require("discord.js"); +const RedditRadio = require("../RedditRadio"); + +class FilterEmotesModule +{ + constructor(config, client, bot) + { + this.config = config; + + /** @type {discord.Client} */ + this.client = client; + + /** @type {RedditRadio} */ + this.bot = bot; + } + + /** + * @param {discord.Message} msg + * @param {Boolean} edited + */ + onMessage(msg, edited) + { + var limit = this.config.limit || 14; + + var emotes = msg.content.toLowerCase().match(/(|\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g); + if (emotes && emotes.length > limit) { + msg.delete(); + this.bot.addLogMessage("Deleted message from " + msg.member.toString() + " in " + msg.channel.toString() + " that contained " + emotes.length + " emotes"); + msg.author.send("You posted too many emojis. Calm down a little bit!").catch(console.error); + } + } +} + +module.exports = FilterEmotesModule; diff --git a/modules/filterinvite.js b/modules/filterinvite.js new file mode 100644 index 0000000..f14c019 --- /dev/null +++ b/modules/filterinvite.js @@ -0,0 +1,45 @@ +const discord = require("discord.js"); +const RedditRadio = require("../RedditRadio"); + +class FilterInviteModule +{ + constructor(config, client, bot) + { + this.config = config; + + /** @type {discord.Client} */ + this.client = client; + + /** @type {RedditRadio} */ + this.bot = bot; + } + + /** + * @param {discord.Message} msg + * @param {Boolean} edited + */ + onMessage(msg, edited) + { + if (this.bot.isMod(msg.member)) { + return; + } + + var inviteLinks = msg.content.matchAll(/(discord\.gg|discord\.com\/invite|discordapp\.com\/invite)\/([A-Za-z0-9]+)/gi); + for (const link of inviteLinks) { + var inviteCode = link[2]; + + var isWhitelisted = false; + if (this.config.whitelist) { + isWhitelisted = (this.config.whitelist.indexOf(inviteCode) != -1); + } + + if (!isWhitelisted) { + msg.delete(); + this.bot.addLogMessage("Deleted Discord invite link from " + msg.author.toString() + " in " + msg.channel.toString() + ": `" + msg.content.replace('`', '\\`') + "`"); + msg.author.send("Your recent message has been automatically deleted. Please do not post Discord invite links without prior permission from a moderator or admin.").catch(console.error); + } + } + } +} + +module.exports = FilterInviteModule; diff --git a/modules/filterlink.js b/modules/filterlink.js new file mode 100644 index 0000000..8f9ec14 --- /dev/null +++ b/modules/filterlink.js @@ -0,0 +1,92 @@ +const discord = require("discord.js"); +const RedditRadio = require("../RedditRadio"); + +var moment = require("moment"); + +class FilterLinkModule +{ + constructor(config, client, bot) + { + this.config = config; + + /** @type {discord.Client} */ + this.client = client; + + /** @type {RedditRadio} */ + this.bot = bot; + + /** @type {String[]} */ + this.permitted = []; + } + + isPermitted(member) + { + var delay = this.config.minutes || 60; + var minutes = moment().diff(member.joinedTimestamp, "minutes"); + + if (minutes >= delay) { + return true; + } + + // Check the list of permitted users with .permit + var permittedIndex = this.permitted.indexOf(member.id); + if (permittedIndex != -1) { + if (minutes >= delay) { + this.permitted.splice(permittedIndex, 1); + } + return true; + } + + return false; + } + + /** + * @param {discord.Message} msg + * @param {Boolean} edited + */ + onMessage(msg, edited) + { + if (msg.content.match(/https?:\/\//i) || msg.content.match(/\.[a-z]{2,3}\//i) || msg.content.match(/(bit.ly|shorturl.at|tiny.cc)/i)) { + msg.guild.members.fetch(msg.author).then((member) => { + if (this.isPermitted(member)) { + return; + } + + msg.delete(); + msg.author.send("Your recent message has been automatically deleted. Brand new members can't post links for a short while, to combat spam. Check #info for more information about the rules. If you think this message is in error, please DM one of the mods.").catch(console.error); + + var minutes = moment().diff(member.joinedTimestamp, "minutes"); + this.bot.addLogMessage("Deleted link from " + member.toString() + " in " + msg.channel.toString() + " who joined " + minutes + " minutes ago. Deleted message:\n```" + msg.content + "```"); + }); + } + } + + /** + * @param {discord.Message} msg + * @param {String} user + */ + onCmdPermit(msg, user) + { + if (!this.bot.isMod(msg.member)) { + return; + } + + var mentions = ""; + var num = 0; + msg.mentions.members.each(member => { + if (this.isPermitted(member)) { + return; + } + this.permitted.push(member.id); + mentions += member.toString() + " "; + num++; + }); + + if (num > 0) { + msg.channel.send(mentions + "A moderator has permitted you to post links!"); + this.bot.addLogMessage(msg.member.toString() + " has permitted " + mentions + "to post links"); + } + } +} + +module.exports = FilterLinkModule; diff --git a/modules/fun.js b/modules/fun.js new file mode 100644 index 0000000..5f0471a --- /dev/null +++ b/modules/fun.js @@ -0,0 +1,55 @@ +const discord = require("discord.js"); +const RedditRadio = require("../RedditRadio"); + +class FunModule +{ + constructor(config, client, bot) + { + this.config = config; + + /** @type {discord.Client} */ + this.client = client; + + /** @type {RedditRadio} */ + this.bot = bot; + } + + /** + * @param {discord.Message} msg + * @param {Boolean} edited + */ + onMessage(msg, edited) + { + if (msg.content.toLowerCase() == "good bot") { + msg.channel.send(msg.member.toString() + " Thanks"); + return; + } + + if (msg.content.toLowerCase() == "bad bot") { + msg.channel.send(msg.member.toString() + " I'm sorry :sob: If I did something wrong, you can report a bug! "); + return; + } + + if (msg.content.toLowerCase() == "kut bot") { + msg.channel.send(msg.member.toString() + " nou sorry hoor"); + return; + } + + if (msg.content.toLowerCase().indexOf("am i the only one") != -1 && msg.member !== null) { + msg.channel.send(msg.member.toString() + " Probably not."); + return; + } + + if (msg.content.toLowerCase().indexOf(".shrug") != -1) { + msg.channel.send("\xaf\\\\\\_<:headykappa:330110432209797123>\\_/\xaf"); + return; + } + + if (msg.content.toLowerCase() == "<@!327816989114630145> ") { + msg.channel.send(""); + return; + } + } +} + +module.exports = FunModule; diff --git a/modules/joinreact.js b/modules/joinreact.js new file mode 100644 index 0000000..105d3da --- /dev/null +++ b/modules/joinreact.js @@ -0,0 +1,29 @@ +const discord = require("discord.js"); + +class JoinReactModule +{ + constructor(config, client, bot) + { + this.config = config; + /** @type {discord.Client} */ + this.client = client; + this.bot = bot; + } + + /** + * @param {discord.Message} msg + * @param {Boolean} edited + */ + onMessage(msg, edited) + { + if (msg.system && msg.type == "GUILD_MEMBER_JOIN") { + setTimeout(() => { + try { + msg.react(this.config.emoji || "πŸ‘‹"); + } catch (err) { console.error(err); } + }, 2000); + } + } +} + +module.exports = JoinReactModule; diff --git a/modules/poll.js b/modules/poll.js new file mode 100644 index 0000000..9ff4a1b --- /dev/null +++ b/modules/poll.js @@ -0,0 +1,29 @@ +const discord = require("discord.js"); +const RedditRadio = require("../RedditRadio"); + +class PollModule +{ + constructor(config, client, bot) + { + this.config = config; + + /** @type {discord.Client} */ + this.client = client; + + /** @type {RedditRadio} */ + this.bot = bot; + } + + /** + * @param {discord.Message} msg + */ + onCmdPoll(msg) + { + (async () => { + await msg.react("πŸ‘"); + await msg.react("πŸ‘Ž"); + })(); + } +} + +module.exports = PollModule; diff --git a/modules/producing.js b/modules/producing.js new file mode 100644 index 0000000..9927fef --- /dev/null +++ b/modules/producing.js @@ -0,0 +1,212 @@ +var colors = require("colors"); +var moment = require("moment"); + +const ffmpeg = require('fluent-ffmpeg'); + +class ProducingModule +{ + constructor(config, client, bot) + { + this.config = config; + this.client = client; + this.bot = bot; + + this.client.on("messageReactionAdd", (r, user) => { this.onMessageReactionAdd(r, user); }); + this.client.on("messageReactionRemove", (r, user) => { this.onMessageReactionRemove(r, user); }); + + if (!this.bot.mongodb) { + console.error("The producing module requires MongoDB to be connected to a database!"); + return; + } + + this.collUsers = this.bot.mongodb.collection("users"); + this.collFiles = this.bot.mongodb.collection("files"); + this.collFilesFeedback = this.bot.mongodb.collection("files_feedback"); + } + + async getOrCreateUser(id) + { + var user = await this.collUsers.findOne({ id: id }); + if (!user) { + user = { + id: id, + files_uploaded: 0, + feedback_given: 0 + }; + await this.collUsers.insertOne(user); + } + return user; + } + + async getNumberOfFeedbackGiven(userId) + { + var result = await this.collFilesFeedback.aggregate([ + { $match: { user: userId } }, + { $group: { _id: "$msg", count: { $sum: 1 } } }, + { $count: "count" } + ]).next(); + + if (!result) { + return 0; + } + return result.count; + } + + onMessageReactionAdd(r, user) + { + if (user == this.client.user) { + return; + } + + var msg = r.message; + if (msg.channel.id != this.config.channel) { + return; + } + + if (user == msg.author) { + return; + } + + if (this.config.reactions.indexOf(r.emoji.name) == -1) { + return; + } + + this.collFilesFeedback.insertOne({ + time: new Date(), + msg: msg.id, + msg_user: msg.author.id, + user: user.id, + emoji: r.emoji.name + }); + } + + onMessageReactionRemove(r, user) + { + if (user == this.client.user) { + return; + } + + var msg = r.message; + if (msg.channel.id != this.config.channel) { + return; + } + + if (user == msg.author) { + return; + } + + if (this.config.reactions.indexOf(r.emoji.name) == -1) { + return; + } + + this.collFilesFeedback.deleteOne({ + msg: msg.id, + user: user.id, + emoji: r.emoji.name + }); + } + + onMessage(msg, edited) + { + if (msg.channel.id != this.config.channel) { + return false; + } + + if (edited) { + return false; + } + + (async () => { + var user = await this.getOrCreateUser(msg.author.id); + + var filenames = ""; + var numFiles = 0; + + msg.attachments.each(async a => { + if (!a.name.match(/.*\.(wav|mp3|ogg|flac)/)) { + return; + } + + filenames += a.name + " "; + numFiles++; + + var logUsername = msg.author.username + '#' + msg.author.discriminator; + console.log(logUsername + " uploaded " + a.name.red.underline); + + this.collUsers.updateOne({ id: user.id }, { + $inc: { files_uploaded: 1 } + }); + + /* + statsmessage = false + feedbackmessage = false + spectrumpic = false + */ + + if (this.config.spectrumpic) { + new Promise((resolve, reject) => { + let path = '/tmp/waveform-' + msg.id + '.png'; + let cmd = ffmpeg(a.url); + cmd.complexFilter([ + '[0:a] showspectrumpic=s=400x70:color=nebulae:legend=false [tmp1]', + //'[0:a] showwavespic=s=400x70:colors=0xFFFFFFFF:filter=peak [tmp2]', + //'[tmp1][tmp2] overlay=y=0:format=rgb:alpha=premultiplied [tmp3]', + '[tmp1] drawbox=0:0:400:70:black', + ]); + cmd.frames(1); + cmd.on('error', err => { + reject(err); + }); + cmd.on('end', () => { + msg.channel.send({ + files: [{ + attachment: path, + name: 'waveform.png', + }], + }).then(resolve).catch(reject); + }); + cmd.save(path); + }).catch(err => { + console.error('ffmpeg waveform failed!', err); + }); + } + + for (var i = 0; i < this.config.reactions.length; i++) { + await msg.react(this.config.reactions[i]); + } + }); + + if (numFiles > 0) { + var numFeedbackGiven = await this.getNumberOfFeedbackGiven(user.id); + + if (this.config.statsmessage) { + msg.channel.send("**Give " + msg.member.displayName + " your feedback!** :outbox_tray: " + (user.files_uploaded + 1) + " / :bulb: " + numFeedbackGiven); + } + + if (this.config.feedbackmessage) { + if (numFeedbackGiven < user.files_uploaded) { + msg.channel.send(msg.member.toString() + " Remember to give others feedback, too! :ok_hand:"); + } + } + } + })(); + + return false; + } + + async onCmdStats(msg) + { + if (msg.channel.id != this.config.channel) { + return; + } + + var user = await this.getOrCreateUser(msg.author.id); + + var numFeedbackReceived = await this.collFilesFeedback.countDocuments({ msg_user: user.id }); + var numFeedbackGiven = await this.getNumberOfFeedbackGiven(user.id); + + msg.channel.send(":bar_chart: " + msg.member.toString() + ", you have uploaded **" + (user.files_uploaded || 0) + "** files, given **" + numFeedbackGiven + "** feedback reactions, and received **" + numFeedbackReceived + "**."); + } +} + +module.exports = ProducingModule; diff --git a/modules/qdance.js b/modules/qdance.js new file mode 100644 index 0000000..4462c65 --- /dev/null +++ b/modules/qdance.js @@ -0,0 +1,65 @@ +var discord = require("discord.js"); +var http = require("https"); + +class QdanceModule +{ + get(dir) + { + return new Promise((resolve, reject) => { + http.get("https://feed.q-dance.com/onair", function(res) { + var data = ""; + res.setEncoding("utf8"); + res.on("data", function(chunk) { data += chunk; }); + res.on("end", function() { + var obj = JSON.parse(data); + if (dir == -1) { + resolve(obj.TrackData.PreviousPlaying); + } else if (dir == 0) { + resolve(obj.TrackData.NowPlaying); + } else if (dir == 1) { + resolve(obj.TrackData.NextPlaying); + } + reject('Unknown track direction!'); + }); + }); + }); + } + + makeEmbed(track, title) + { + var embed = new discord.MessageEmbed({ + title: title, + description: track.Artist + " - " + track.Title, + hexColor: "#D26F1C" + }); + embed.setAuthor("Q-dance Radio", "https://4o4.nl/20170908JHxVy.png"); + embed.setThumbnail(track.CoverImage); + return embed; + } + + async onCmdQdnp(msg) + { + var track = await this.get(0); + msg.channel.send({ + embeds: [ this.makeEmbed(track, "Q-dance Radio is now playing:") ], + }); + } + + async onCmdQdnext(msg) + { + var track = await this.get(1); + msg.channel.send({ + embeds: [ this.makeEmbed(track, "Next track on Q-dance Radio:") ], + }); + } + + async onCmdQdprev(msg) + { + var track = await this.get(-1); + msg.channel.send({ + embeds: [ this.makeEmbed(track, "Previous track on Q-dance Radio:") ], + }); + } +} + +module.exports = QdanceModule; diff --git a/modules/sus.js b/modules/sus.js new file mode 100644 index 0000000..53b49b7 --- /dev/null +++ b/modules/sus.js @@ -0,0 +1,27 @@ +const discord = require("discord.js"); + +var moment = require("moment"); + +class SusModule +{ + constructor(config, client, bot) + { + this.config = config; + /** @type {discord.Client} */ + this.client = client; + this.bot = bot; + } + + /** + * @param {discord.GuildMember} member + */ + onMemberJoin(member) + { + if (moment().diff(member.user.createdAt, "hours") < 48) { + this.bot.addLogMessage("<:skepticalpepe:743455915935662133> Brand new user account joined: " + member.user.toString() + + " (account created )"); + } + } +} + +module.exports = SusModule; diff --git a/modules/tools.js b/modules/tools.js new file mode 100644 index 0000000..84f6297 --- /dev/null +++ b/modules/tools.js @@ -0,0 +1,60 @@ +var moment = require("moment"); + +class ToolsModule +{ + constructor(config, client, bot) + { + this.client = client; + this.bot = bot; + } + + onCmdJoinTime(msg) { this.onCmdJoinDate(msg); } + onCmdJoinDate(msg) + { + msg.guild.members.fetch(msg.author).then((member) => { + var joinedAt = moment(member.joinedTimestamp); + msg.reply("you joined this server **** - "); + }); + } + + onCmdSlow(msg, seconds) + { + if (!this.bot.isMod(msg.member)) { + return; + } + + msg.channel.setRateLimitPerUser(parseInt(seconds)); + msg.delete(); + } + + async onCmdUserInfo(msg, user) + { + if (!this.bot.isMod(msg.member)) { + return; + } + + try { + var member = await msg.guild.members.fetch(user); + } catch { + var fetcheduser = await this.client.users.fetch(user); + var createdAt = moment(fetcheduser.createdTimestamp); + + msg.channel.send( + "**Info for non-member " + fetcheduser.tag + "**:\n" + + ":alarm_clock: Account age: **** - " + ); + return; + } + + var joinedAt = moment(member.joinedTimestamp); + var createdAt = moment(member.user.createdTimestamp); + + msg.channel.send( + "**Info for member " + member.toString() + "**:\n" + + ":alarm_clock: Join time: **** - \n" + + ":alarm_clock: Account age: **** - " + ); + } +} + +module.exports = ToolsModule; diff --git a/modules/welcome.js b/modules/welcome.js new file mode 100644 index 0000000..d897b08 --- /dev/null +++ b/modules/welcome.js @@ -0,0 +1,40 @@ +const discord = require("discord.js"); + +class WelcomeModule +{ + constructor(config, client, bot) + { + this.config = config; + /** @type {discord.Client} */ + this.client = client; + this.bot = bot; + } + + /** + * @param {discord.GuildMember} member + */ + async onMemberJoin(member) + { + var msg = this.config.messageprefix; + + var msgIndex = Math.floor(Math.random() * this.config.messages.length); + msg += ' ' + this.config.messages[msgIndex]; + msg = msg.replace('', '**' + member.user.username + '**'); + //msg = msg.replace('', member.toString()); + //^ Commented out because mentions for newly joined members are broken on the client + + var channel = await this.client.channels.fetch(this.config.channel); + var message = await channel.send(msg); + /* + var message = await channel.send({ + content: msg, + allowedMentions: { + users: [] + } + }); + */ + message.react(this.config.emoji || "πŸ‘‹"); + } +} + +module.exports = WelcomeModule; diff --git a/package.json b/package.json new file mode 100644 index 0000000..fa9f3a7 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "redditradio", + "version": "1.0.0", + "description": "discord.gg/hardstyle", + "main": "index.js", + "dependencies": { + "@discordjs/opus": "^0.3.2", + "colors": "^1.4.0", + "discord.js": "^13.1.0", + "fluent-ffmpeg": "^2.1.2", + "follow-redirects": "^1.14.4", + "moment": "^2.24.0", + "moment-timezone": "^0.5.33", + "mongodb": "^3.7.1", + "sodium": "^3.0.2", + "toml": "^3.0.0" + }, + "devDependencies": {}, + "scripts": { + "start": "node index.js", + "radio": "node index.js --radio", + "radios": "node index.js --radios", + "no-radio": "node index.js --no-radios" + }, + "author": "Codecat" +}