// Import modules const Discord = require("discord.js"), fs = require("fs"), path = require("path"), axios = require("axios"), schedule = require("node-schedule"), 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 // (discord limit is 2k) let chunks = [res.data]; if (res.data.length > 2000) { 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( "*┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛*" ); }, // Allow members to request role additions/removals for allowed roles role: (msg, guildConfig) => { // make sure there are allowed roles defined if ( typeof guildConfig.allowedRolesForRequest === undefined || guildConfig.allowedRolesForRequest.length === 0 ) { return msg.reply( "No roles are currently allowed to be added/removed by members." ); } let validRoles = guildConfig.allowedRolesForRequest.split("|"); if (msg.content === guildConfig.prefix + "role") { return msg.reply( `Useage: ${guildConfig.prefix}role {add|remove} {${guildConfig.allowedRolesForRequest}}` ); } // parse+validate action+role (use original case from message because roles are case-sensitive) let roleName = msg.originalContent.match( /role\s(add|remove)\s([a-z0-9\-]+)/i ); if (!roleName) { return msg.reply( `Useage: ${guildConfig.prefix}role {add|remove} {${guildConfig.allowedRolesForRequest}}` ); } else { let tester = new RegExp(guildConfig.allowedRolesForRequest, "i"); if (tester.test(roleName[2])) { // make sure this message is in a guild channel they're a member of if (!msg.guild) return; // find the role in the member's guild let role = msg.guild.roles.cache.find((x) => x.name === roleName[2]); if (!role) { return msg.reply(`${roleName[2]} is not a role on this server!`); } // add/remove the role and react to the message with the results if (roleName[1] === "add") { msg.member.roles .add(role, "User requested") .then((requestingMember) => { msg .react("👍") .then(() => { console.log("Reaction sent"); }) .catch(console.error); }) .catch((err) => { console.error(`Error during role addition: ${err}`); msg .react("⚠") .then(() => { console.log("Reaction sent"); }) .catch(console.error); }); } else if (roleName[1] === "remove") { msg.member.roles .remove(role, "User requested") .then((requestingMember) => { msg .react("👍") .then(() => { console.log("Reaction sent"); }) .catch(console.error); }) .catch((err) => { console.error(`Error during role addition: ${err}`); msg .react("⚠") .then(() => { console.log("Reaction sent"); }) .catch(console.error); }); } else { msg.reply( `You must use add/remove after the role command! *e.g. ${guildConfig.prefix}role add ${validRoles[0]}*` ); } } else { msg.reply( `**${ roleName[2] }** is not a valid role name! The roles allowed for request are: ${validRoles.join( "," )}` ); } } }, 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.adminUserId) 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); // Set up scheduled events for each guild config.discord.guilds.forEach(async (guild) => { let discordGuild = false; try { discordGuild = await client.guilds.fetch(guild.id); } catch (err) { console.error(err); } if (!discordGuild) return; if ( guild.hasOwnProperty("scheduledEvents") && guild.scheduledEvents.length > 0 ) { guild.scheduledEvents.forEach(async (event) => { let channel = false; if ( event.hasOwnProperty("channelId") && event.channelId.length > 0 ) { channel = await discordGuild.channels.resolve(event.channelId); } if (!channel) { console.log( `Invalid channel configured for event ${event.id}, guild ${guild.name}` ); return; } let pingRole = false; if ( event.hasOwnProperty("pingRoleId") && event.pingRoleId.length > 0 ) { pingRole = await discordGuild.roles.fetch(event.pingRoleId); } console.log( `Scheduling event ${event.id} for ${discordGuild.name}...` ); const job = schedule.scheduleJob(event.schedule, () => { let payload = []; if (pingRole !== false) { payload.push(pingRole); } if (event.hasOwnProperty("message") && event.message.length > 0) { payload.push(event.message); } channel.send(payload); }); console.log(`Next invocation: ${job.nextInvocation()}`); }); } }); }) // 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.find((g) => g.id === 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.find( (g) => g.id === 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.find((g) => g.id === msg.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 () { 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", }); };