// Import modules const Discord = require("discord.js"), fs = require("fs"), path = require("path"), axios = require("axios"), staticCommands = require("./lib/static-commands.js"), ankhbotCommands = require("./lib/ankhbot-commands.js"), { randElement, chunkSubstr } = require("./lib/utils.js"), config = require("./config.json"); function init(config) { // Set up Discord client const client = new Discord.Client(); // Set up SFX const sfxFilePath = path.join(__dirname, "sfx"); // Read in sfx directory, filenames are the commands let sfxList = readSfxDirectory(sfxFilePath); // Watch directory for changes and update the list fs.watch(sfxFilePath, (eventType, filename) => { if (eventType === "rename") { sfxList = readSfxDirectory(sfxFilePath); } }); // @todo DRY this shit up // Read in fun facts const funFactsFilePath = path.join(__dirname, "conf", "funfacts"); let funFacts = parseLines(funFactsFilePath); fs.watchFile(funFactsFilePath, (curr, prev) => { if (curr.mtime !== prev.mtime) { funFacts = parseLines(funFactsFilePath); } }); // Read in ham facts const hamFactsFilePath = path.join(__dirname, "conf", "hamfacts"); let hamFacts = parseLines(hamFactsFilePath); fs.watchFile(hamFactsFilePath, (curr, prev) => { if (curr.mtime !== prev.mtime) { hamFacts = parseLines(hamFactsFilePath); } }); // Set up the native commands to handle const commands = { sfx: async (msg, guildConfig) => { let allowedSfxChannels = new RegExp(guildConfig.allowedSfxChannels); if (!allowedSfxChannels.test(msg.channel.name)) return; let sfx = msg.content.split(" ")[1]; // retrieve sfx list from pastebin if (sfx == "" || sfx === undefined) { axios .get("https://rentry.co/ghbotsfx/raw") .then((res) => { // break the result into half chunks if it exceeds the message limit size // (the backticks take up 6 characters, discord limit is 2k) let chunks = [res.data]; if (res.data.length > 1994) { chunks = chunkSubstr(res.data, res.data.length / 2); } chunks.forEach((chunk) => { return msg.channel.send("```" + chunk + "```"); }); }) .catch(console.error); return true; } // make sure this file exists either as an mp3 or wav let sfxPath; if (fs.existsSync(path.join(sfxFilePath, sfx + ".mp3"))) { sfxPath = path.join(sfxFilePath, sfx + ".mp3"); } else if (fs.existsSync(path.join(sfxFilePath, sfx + ".wav"))) { sfxPath = path.join(sfxFilePath, sfx + ".wav"); } else { return msg.reply("This sound effect does not exist!"); } // Join the same voice channel of the author of the message const connection = await joinVoiceChannel(msg); if (connection === false) { return msg.reply("I couldn't connect to your voice channel..."); } (function play(sfxFile) { const dispatcher = connection.play(sfxFile, { volume: guildConfig.sfxVolume, passes: guildConfig.passes, }); dispatcher .on("finish", (reason) => { connection.disconnect(); }) .on("error", (error) => { connection.disconnect(); console.error("Error playing sfx: " + error); }) .on("start", () => {}); })(sfxPath.toString()); }, funfact: (msg, guildConfig) => { if (guildConfig.enableFunFacts === false) { return; } if (funFacts.length > 0) { // return random element from funFacts, unless one is specifically requested let el; let req = parseInt(msg.content.split(" ")[1]); if (Number.isNaN(req) || typeof funFacts[req - 1] === "undefined") { el = Math.floor(Math.random() * funFacts.length); } else { el = req - 1; } let displayNum = (el + 1).toString(); let funFact = funFacts[el]; msg.channel .send({ embed: { title: "FunFact #" + displayNum, color: 0x21c629, description: funFact, }, }) .catch(console.error); } else { msg.channel.send("No fun facts found!"); } }, hamfact: (msg, guildConfig) => { if (guildConfig.enableHamFacts === false) { return; } if (hamFacts.length > 0) { // return random element from hamFacts, unless one is specifically requested let el; let req = parseInt(msg.content.split(" ")[1]); if (Number.isNaN(req) || typeof hamFacts[req - 1] === "undefined") { el = Math.floor(Math.random() * hamFacts.length); } else { el = req - 1; } let displayNum = (el + 1).toString(); let hamFact = hamFacts[el]; msg.channel .send({ embed: { title: "HamFact #" + displayNum, color: 0x21c629, description: hamFact, }, }) .catch(console.error); } else { msg.channel.send("No ham facts found!"); } }, dance: (msg, guildConfig) => { msg.channel.send( "*┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛*" ); }, join: (msg, guildConfig) => { if (!msg.guild.voiceConnection) { joinVoiceChannel(msg) .then(() => { // }) .catch(console.error); } else { return msg.reply(`I'm already in a voice channel!`); } }, leave: (msg, guildConfig) => { if (msg.guild.voiceConnection) { msg.guild.voiceConnection.disconnect(); } else { return msg.reply( `If ya don't eat your meat, ya can't have any pudding!` ); } }, listen: (msg, guildConfig) => { // listen for a particular member to speak and respond appropriately if (msg.guild.voiceConnection) { // get the guild member //let guildMemberId = "88301001169207296"; // me let guildMemberId = "153563292265086977"; // Screevo let guildMember = msg.guild.members.get(guildMemberId); if (guildMember) { let listenInterval = 1000; setInterval(() => { if (guildMember.speaking === true) { msg.content = "!sfx stfu"; commands.sfx(msg, false); } }, listenInterval); } else { console.error( `Could not find specified guild member: ${guildMemberId}!` ); msg.guild.voiceConnection.disconnect(); } } else { // join the voice channel then call this command again joinVoiceChannel(msg) .then(() => { commands.listen(msg, guildConfig); }) .catch(console.error); } }, reboot: (msg, guildConfig) => { if (msg.author.id == config.discord.adminID) process.exit(); // Requires a node module like Forever to work. }, }; client // Wait for discord to be ready, handle messages .on("ready", () => { console.log(`${config.botName} is connected and ready`); client.setRandomActivity(); setInterval(() => { client.setRandomActivity(); }, 3600 * 1000); }) // Listen for commands for the bot to respond to across all channels .on("message", (msg) => { // Ignore DMs and messages from unconfigured guilds if (msg.guild) { if (!config.discord.guilds[msg.guild.id]) { return; } } else { return; } // Ignore anything from blacklisted users if (config.discord.blacklistedUsers.includes(msg.author.id)) { return; } // Find the guild config for this msg, use default if no guild (DM) let guildConfig = config.discord.guilds[msg.guild.id]; // Parse message content msg.originalContent = msg.content; msg.content = msg.content.toLowerCase(); // Make sure the command starts with the configured prefix if (!msg.content.startsWith(guildConfig.prefix)) return; let commandNoPrefix = msg.content .slice(guildConfig.prefix.length) .split(" ")[0]; console.log( `'${commandNoPrefix}' received in ${guildConfig.internalName}#${msg.channel.name} from @${msg.author.username}` ); // check for native command first if (commands.hasOwnProperty(commandNoPrefix)) { commands[commandNoPrefix](msg, guildConfig); // then a static command we've manually added } else if (staticCommands.exists(commandNoPrefix)) { let result = staticCommands.get(commandNoPrefix); msg.channel .send({ embed: { title: commandNoPrefix, color: 0x21c629, description: result, }, }) .catch(console.error); // then a command exported from ankhbot } else if (ankhbotCommands.exists(commandNoPrefix)) { let result = ankhbotCommands.get(commandNoPrefix); msg.channel .send({ embed: { title: commandNoPrefix, color: 0x21c629, description: result, }, }) .catch(console.error); } else { // Not a command we recognize, ignore } }) // Handle new members joining one of our guilds .on("guildMemberAdd", (member) => { // Ignore events from unconfigured guilds if (member.guild) { if (!config.discord.guilds[member.guild.id]) { return; } } else { return; } console.log( `A new member has joined '${member.guild.name}': ${member.displayName}` ); }) // Log guild becoming unavailable (usually due to server outage) .on("guildUnavailable", (guild) => { console.log( `Guild '${guild.name}' is no longer available! Most likely due to server outage.` ); }) // Log debug messages if enabled .on("debug", (info) => { if (config.debug === true) { console.log(`[${new Date()}] DEBUG: ${info}`); } }) // Log disconnect event .on("disconnect", (event) => { console.log( `Web Socket disconnected with code ${event.code} and reason '${event.reason}'` ); }) // Log errors .on("error", console.error) // Log the bot in .login(config.discord.token); } function readSfxDirectory(path) { let thePath = path || sfxFilePath; let sfxList = fs.readdirSync(thePath); sfxList.forEach(function (el, index, a) { a[index] = el.split(".")[0]; }); return sfxList; } async function joinVoiceChannel(message) { // Join the same voice channel of the author of the message if (message.member.voice.channel) { return await message.member.voice.channel.join(); } else { return false; } } // Read/parse text lines from a file function parseLines(filePath) { let lines = []; let data = fs.readFileSync(filePath, "utf-8"); let splitLines = data.toString().split("\n"); splitLines.forEach(function (line) { if (line.length > 0) { lines.push(line); } }); return lines; } // catch Promise errors process.on("unhandledRejection", console.error); // Fire it up init(config); Discord.Client.prototype.setRandomActivity = function () { if (!config.discord.master) return; let activity = config.discord.activities.length > 0 ? randElement(config.discord.activities) : "DESTROY ALL HUMANS"; console.log(`Setting Discord activity to: ${activity}`); this.user.setActivity(activity, { url: `https://twitch.tv/fgfm`, type: "STREAMING", }); };