require('dotenv-flow').config();
const mongoUtil = require('./db/mongo/mongoUtil');
const Commando = require('discord.js-commando');
const Discord = require('discord.js');
const firebaseServices = require('./db/firebase/firebase-services');
const winston = require('winston');
const fs = require('fs');
const discordServices = require('./discord-services');
const BotGuild = require('./db/mongo/BotGuild');
const BotGuildModel = require('./classes/bot-guild');
const Verification = require('./classes/verification');
const { StringPrompt } = require('advanced-discord.js-prompts');
/**
* The Main App module houses the bot events, process events, and initializes
* the bot. It also handles new members and greets them.
* @module MainApp
*/
/**
* Returns the config settings depending on the command line args.
* Read command line args to know if prod, dev, or test and what server
* First arg is one of prod, dev or test
* the second is the test server, but the first one must be test
* @param {string[]} args
* @returns {Map} config settings
*/
function getConfig(args) {
if (args.length >= 1) {
if (args[0] === 'dev') {
// Default dev
return JSON.parse(process.env.DEV);
} else if (args[0] === 'prod') {
// Production
if (args[1] === 'yes') {
return JSON.parse(process.env.PROD);
}
} else if (args[0] === 'test') {
// Test
const testConfig = JSON.parse(process.env.TEST);
let server = args[1] ?? 0;
if (server === '1') {
return testConfig['ONE'];
} else if (server === '2') {
return testConfig['TWO'];
} else if (server === '3') {
return testConfig['THREE'];
} else if (server === '4') {
return testConfig['FOUR'];
}
}
}
// exit if no configs are loaded!
console.log('No configs were found for given args.');
process.exit(0);
}
const config = getConfig(process.argv.slice(2));
const isLogToConsole = config['consoleLog'];
const bot = new Commando.Client({
commandPrefix: '!',
owner: config.owner,
});
const customLoggerLevels = {
levels: {
error: 0,
warning: 1,
command: 2,
event: 3,
userStats: 4,
verbose: 5,
debug: 6,
silly: 7,
},
colors: {
error: 'red',
warning: 'yellow',
command: 'blue',
event: 'green',
userStats: 'magenta',
verbose: 'cyan',
debug: 'white',
silly: 'black',
}
};
// the main logger to use for general errors
const mainLogger = createALogger('main', 'main', true, isLogToConsole);
winston.addColors(customLoggerLevels.colors);
/**
* Register all the commands except for help and unknown since we have our own.
*/
bot.registry
.registerDefaultTypes()
.registerGroup('a_boothing', 'boothing group for admins')
.registerGroup('a_activity', 'activity group for admins')
.registerGroup('a_start_commands', 'advanced admin commands')
.registerGroup('a_utility', 'utility commands for admins')
.registerGroup('hacker_utility', 'utility commands for users')
.registerGroup('verification', 'verification commands')
.registerGroup('attendance', 'attendance commands')
.registerGroup('stamps', 'stamp related commands')
.registerGroup('utility', 'utility commands')
.registerGroup('essentials', 'essential commands for any guild', true)
.registerDefaultGroups()
.registerDefaultCommands({
unknownCommand: false,
help: false,
})
.registerCommandsIn(__dirname + '/commands');
/**
* Runs when the bot finishes the set up and is ready to work.
*/
bot.once('ready', async () => {
mainLogger.warning('The bot ' + bot.user.username + ' has started and is ready to hack!');
bot.user.setActivity('Ready to hack!');
// initialize firebase
const adminSDK = JSON.parse(process.env.NWPLUSADMINSDK);
firebaseServices.initializeFirebaseAdmin('nwPlusBotAdmin', adminSDK, 'https://nwplus-bot.firebaseio.com');
mainLogger.warning('Connected to firebase admin sdk successfully!', { event: 'Ready Event' });
// set mongoose connection
await mongoUtil.mongooseConnect();
mainLogger.warning('Connected to mongoose successfully!', { event: 'Ready Event' });
// make sure all guilds have a botGuild, this is in case the bot goes offline and its added
// to a guild. If botGuild is found, make sure only the correct commands are enabled.
bot.guilds.cache.forEach(async (guild, key, guilds) => {
// create the logger for the guild
createALogger(guild.id, guild.name, false, isLogToConsole);
let botGuild = await BotGuild.findById(guild.id);
if (!botGuild) {
newGuild(guild);
mainLogger.verbose(`Created a new botGuild for the guild ${guild.id} - ${guild.name} on bot ready.`, { event: 'Ready Event' });
} else {
// set all non guarded commands to not enabled for the guild
bot.registry.groups.forEach((group, key, map) => {
if (!group.guarded) guild.setGroupEnabled(group, false);
});
await botGuild.setCommandStatus(bot);
guild.commandPrefix = botGuild.prefix;
mainLogger.verbose(`Found a botGuild for ${guild.id} - ${guild.name} on bot ready.`, { event: 'Ready Event' });
}
});
});
/**
* Runs when the bot is added to a guild.
*/
bot.on('guildCreate', /** @param {Commando.CommandoGuild} guild */(guild) => {
mainLogger.warning(`The bot was added to a new guild: ${guild.id} - ${guild.name}.`, { event: 'Guild Create Event' });
newGuild(guild);
// create a logger for this guild
createALogger(guild.id, guild.name);
});
/**
* Will set up a new guild.
* @param {Commando.CommandoGuild} guild
* @private
*/
function newGuild(guild) {
// set all non guarded commands to not enabled for the new guild
bot.registry.groups.forEach((group, key, map) => {
if (!group.guarded) guild.setGroupEnabled(group, false);
});
// create a botGuild object for this new guild.
BotGuild.create({
_id: guild.id,
});
}
/**
* Runs when the bot is removed from a server.
*/
bot.on('guildDelete', async (guild) => {
mainLogger.warning(`The bot was removed from the guild: ${guild.id} - ${guild.name}`);
let botGuild = await BotGuild.findById(guild.id);
botGuild.remove();
mainLogger.verbose(`BotGuild with id: ${guild.id} has been removed!`);
});
/**
* Runs when the bot runs into an error.
*/
bot.on('error', (error) => {
mainLogger.error(`Bot Error: ${error.name} - ${error.message}.`, { event: 'Error', data: error});
});
/**
* Runs when the bot runs into an error when running a command.
*/
bot.on('commandError', (command, error, message) => {
winston.loggers.get(message.channel?.guild?.id || 'main').error(`Command Error: In command ${command.name} got uncaught rejection ${error.name} : ${error.message}`, { event: 'Error', data: error});
});
/**
* Runs when a message is sent in any server the bot is running in.
*/
bot.on('message', async message => {
if (message?.guild) {
let botGuild = await BotGuild.findById(message.guild.id);
// Deletes all messages to any channel in the black list with the specified timeout
// this is to make sure that if the message is for the bot, it is able to get it
// bot and staff messages are not deleted
if (botGuild.blackList.has(message.channel.id)) {
if (!message.author.bot && !discordServices.checkForRole(message.member, botGuild.roleIDs.staffRole)) {
winston.loggers.get(message.guild.id).verbose(`Deleting message from user ${message.author.id} due to being in the blacklisted channel ${message.channel.name}.`);
(new Promise(res => setTimeout(res, botGuild.blackList.get(message.channel.id)))).then(() => discordServices.deleteMessage(message));
}
}
}
});
/**
* Runs when a new member joins a guild the bot is running in.
*/
bot.on('guildMemberAdd', async member => {
let botGuild = await BotGuild.findById(member.guild.id);
// if the guild where the user joined is complete then greet and verify.
// also checks to make sure it does not greet bots
if (botGuild.isSetUpComplete && !member.user.bot) {
try {
winston.loggers.get(member.guild.id).userStats('A new user joined the guild and is getting greeted!');
await greetNewMember(member, botGuild);
} catch (error) {
await fixDMIssue(error, member, botGuild);
}
} else {
winston.loggers.get(member.guild.id).warning('A new user joined the guild but was not greeted because the bot is not set up!');
}
});
bot.on('commandRun', (command, promise, message, args) => {
winston.loggers.get(message?.guild?.id || 'main').command(`The command ${command.name} with args ${args} is being run from the channel ${message.channel} with id ${message.channel.id}
triggered by the message with id ${message.id} by the user with id ${message.author.id}`);
});
/**
* Runs when an unknown command is triggered.
*/
bot.on('unknownCommand', (message) => winston.loggers.get(message?.guild?.id || 'main').command(`An unknown command has been triggered in the channel ${message.channel.name} with id ${message.channel.id}. The message had the content ${message.cleanContent}.`));
/**
* Logs in the bot
*/
bot.login(config.token).catch(console.error);
/**
* Runs when the node process has an uncaught exception.
*/
process.on('uncaughtException', (error) => {
console.log(
'Uncaught Rejection, reason: ' + error.name +
'\nmessage: ' + error.message +
'\nfile: ' + error.fileName +
'\nline number: ' + error.lineNumber +
'\nstack: ' + error.stack
);
});
/**
* Runs when the node process has an unhandled rejection.
*/
process.on('unhandledRejection', (error, promise) => {
console.log('Unhandled Rejection at:', promise,
'Unhandled Rejection, reason: ' + error.name +
'\nmessage: ' + error.message +
'\nfile: ' + error.fileName +
'\nline number: ' + error.lineNumber +
'\nstack: ' + error.stack
);
});
/**
* Runs when the node process is about to exit and quit.
*/
process.on('exit', () => {
mainLogger.warning('Node is exiting!');
});
/**
* Will create a default logger to use.
* @param {String} loggerName
* @param {String} [loggerLabel=''] - usually a more readable logger name
* @param {Boolean} [handleRejectionsExceptions=false] - will handle rejections and exceptions if true
* @param {Boolean} [LogToConsole=false] - will log all levels to console if true
* @returns {winston.Logger}
*/
function createALogger(loggerName, loggerLabel = '', handelRejectionsExceptions = false, logToConsole = false) {
// custom format
let format = winston.format.printf(info => `${info.timestamp} [${info.label}] ${info.level}${info?.event ? ' <' + info.event + '>' : ''} : ${info.message} ${info?.data ? 'DATA : ' + info.data : '' }`);
// create main logs directory if not present
if (!fs.existsSync('./logs')) fs.mkdirSync('./logs');
// create the directory if not present
if (!fs.existsSync(`./logs/${loggerName}`)) fs.mkdirSync(`./logs/${loggerName}`);
let logger = winston.loggers.add(loggerName, {
levels: customLoggerLevels.levels,
transports: [
new winston.transports.File({ filename: `./logs/${loggerName}/logs.log`, level: 'silly' }),
new winston.transports.File({ filename: `./logs/${loggerName}/debug.log`, level: 'debug' }),
new winston.transports.File({ filename: `./logs/${loggerName}/verbose.log`, level: 'verbose' }),
new winston.transports.File({ filename: `./logs/${loggerName}/userStats.log`, level: 'userStats' }),
new winston.transports.File({ filename: `./logs/${loggerName}/event.log`, level: 'event' }),
new winston.transports.File({ filename: `./logs/${loggerName}/command.log`, level: 'command' }),
new winston.transports.File({ filename: `./logs/${loggerName}/warning.log`, level: 'warning' }),
new winston.transports.File({ filename: `./logs/${loggerName}/error.log`, level: 'error', handleExceptions: handelRejectionsExceptions, handleRejections: handelRejectionsExceptions, }),
...(logToConsole ? [new winston.transports.Console({
level: 'silly',
format: winston.format.combine(
winston.format.colorize({ level: true }),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.splat(),
winston.format.label({ label: loggerLabel}),
format,
),
handleExceptions: true,
handleRejections: true,
})] : []),
],
exitOnError: false,
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.splat(),
winston.format.label({ label: loggerLabel}),
format,
)
});
return logger;
}
/**
* Greets a member!
* @param {Discord.GuildMember} member - the member to greet
* @param {BotGuildModel} botGuild
* @throws Error if the user has server DMs off
*/
async function greetNewMember(member, botGuild) {
let verifyEmoji = '🍀';
var embed = new Discord.MessageEmbed()
.setTitle(`Welcome to the ${member.guild.name} Server!`)
.setDescription('We are very excited to have you here!')
.addField('Have a question?', 'Visit the #welcome-support channel to talk with our staff!')
.addField('Want to learn more about what I can do?', 'Use the !help command anywhere and I will send you a message!')
.setColor(botGuild.colors.embedColor);
if (botGuild.verification.isEnabled) embed.addField('Gain more access by verifying yourself!', 'React to this message with ' + verifyEmoji + ' and follow my instructions!');
let msg = await member.send(embed);
// if verification is on then give guest role and let user verify
if (botGuild.verification.isEnabled) {
discordServices.addRoleToMember(member, botGuild.verification.guestRoleID);
msg.react(verifyEmoji);
let verifyCollector = msg.createReactionCollector((reaction, user) => !user.bot && reaction.emoji.name === verifyEmoji);
verifyCollector.on('collect', async (reaction, user) => {
try {
var email = await StringPrompt.single({prompt: 'Please send me your email associated to this event!', channel: member.user.dmChannel, userId: member.id, time: 30, cancelable: true});
} catch (error) {
discordServices.sendEmbedToMember(member, {
title: 'Verification Error',
description: 'Email was not provided, please try again!'
}, true);
return;
}
try {
await Verification.verify(member, email, member.guild, botGuild);
} catch (error) {
discordServices.sendEmbedToMember(member, {
title: 'Verification Error',
description: 'Email provided is not valid! Please try again.'
}, true);
}
});
}
// if verification is off, then just ive member role
else {
discordServices.addRoleToMember(member, botGuild.roleIDs.memberRole);
}
}
/**
* Will let the member know how to fix their DM issue.
* @param {Error} error - the error
* @param {Discord.GuildMember} member - the member with the error
* @param {BotGuildModel} botGuild
* @throws Error if the given error is not a DM error
*/
async function fixDMIssue(error, member, botGuild) {
if (error.code === 50007) {
let logger = winston.loggers.get(member.guild.id);
logger.warning(`A new user with id ${member.id} joined the guild but was not able to be greeted, we have asked him to fix the issues!`);
let channelID = botGuild.verification?.welcomeSupportChannelID || botGuild.channelIDs.botSupportChannel;
member.guild.channels.resolve(channelID).send('<@' + member.id + '> I couldn\'t reach you :(.' +
'\n* Please turn on server DMs, explained in this link: https://support.discord.com/hc/en-us/articles/217916488-Blocking-Privacy-Settings-' +
'\n* Once this is done, please react to this message with 🤖 to let me know!').then(msg => {
msg.react('🤖');
const collector = msg.createReactionCollector((reaction, user) => user.id === member.id && reaction.emoji.name === '🤖');
collector.on('collect', (reaction, user) => {
reaction.users.remove(user.id);
try {
greetNewMember(member);
collector.stop();
msg.delete();
logger.userStats(`A user with id ${member.id} was able to fix the DM issue and was greeted!`);
} catch (error) {
member.guild.channels.resolve(channelID).send('<@' + member.id + '> Are you sure you made the changes? I couldn\'t reach you again 😕').then(msg => msg.delete({ timeout: 8000 }));
}
});
});
} else {
throw error;
}
}
Source