Stewbot's code is fully open source. We believe in transparency and want you to know exactly what the bot is doing.
GitHub Repository
There is a GitHub Repository available containing all of the following as well as additional essential files and instructions for creating env.json to run locally.
Were you looking for this website's source code? It can be found here.
Stewbot index.js
// Stewbot main file.
// This file dispatches events to the files they need to go to,
// connects to the database, and registers event handlers.
//
// Note:
// Other important logic handling database cleanup,
// general management, etc., is inside `./commands/core.js`
// To be most accurate, save this before anything.
global.bootedAt = Date.now();
// === Load envs
require("./setEnvs.js");
// === Import everything
global.cmds = require("./data/commands.json");
const config = global.config = require("./data/config.json");
console.log("Importing discord");
const client = require("./client.js");
const { Events, PermissionFlagsBits, PermissionsBitField } = require("discord.js");
console.log("Importing commands");
const { getCommands } = require("./launchCommands.js"); // Note: current setup requires this to be before the commands.json import (the cmd.globals setting)
const commandsLoadedPromise = getCommands();
console.log("Importing backup.js");
console.log("Importing database");
const mongoose = require("mongoose");
const { guildByID, guildByObj } = require("./commands/modules/database");
console.log("Importing Backup.js");
const { checkForMongoRestore } = require("./backup.js");
console.log("Importing utils");
const { notify, getReadOnlyDBs } = require("./utils");
const { isModuleBlocked } = require("./commands/block_module.js");
console.log("Importing InfluxDB");
const { initInflux, queueCommandMetric } = require("./commands/modules/metrics");
initInflux();
// === Register listeners
global.commands = {};
let dailyListenerModules = {};
let buttonListenerModules = {};
const pseudoGlobals = { config }; // data that should be passed to each module
let commandListenerRegister = commandsLoadedPromise.then(commandsLoaded => {
console.log("Loading command listeners");
// This code registers all requested listeners
// This method allows any event type to be easily added to a command file
// The functions `onbutton` and `autocomplete` are both still available for convenience.
// Save commands
global.commands = Object.freeze(commandsLoaded);
// Utility for registering listeners
function getSubscribedCommands(commands, subscription) {
return Object.fromEntries(
(Object.entries(commands)
.filter(([, command]) => command[subscription]) // Get all subscribed modules
).sort((a, b) => (a[1].data?.priority ?? 100) - (b[1].data?.priority ?? 100))
);
}
// Load some custom listener functions
dailyListenerModules = getSubscribedCommands(commands, "daily");
buttonListenerModules = getSubscribedCommands(commands, "onbutton");
// Some handlers have extra args injected into them for optimization / ease of use.
let argInjectors = {
// MessageCreate is a high-traffic handler, we inject database lookups here so that each handler
// doesn't need waste power preforming duplicate lookups.
[Events.MessageCreate]: async (...args) => {
const [readGuildUser, readGuild, readHomeGuild] = await getReadOnlyDBs(args[0]);
return [...args, pseudoGlobals, readGuild, readGuildUser, readHomeGuild];
},
[Events.MessageUpdate]: async (...args) => {
const [readGuildUser, readGuild, readHomeGuild] = await getReadOnlyDBs(args[0]);
return [...args, readGuild, readGuildUser, readHomeGuild];
},
[Events.MessageDelete]: async (...args) => {
const [readGuildUser, readGuild, _test] = await getReadOnlyDBs(args[0]);
return [...args, readGuild, readGuildUser];
},
[Events.MessageReactionAdd]: async (...args) => {
let [react, _] = args;
// Resolve partials so we always have full data
await Promise.all([
react.partial ? react.fetch().catch(() => null) : null,
react.message?.partial ? react.message.fetch().catch(() => null) : null
]);
const [readGuildUser, readGuild, _readHomeGuild] = await getReadOnlyDBs({
guildId: args[0].message.guild?.id,
userId: args[1].id
});
return [...args, readGuild, readGuildUser];
},
[Events.GuildMemberAdd]: async (...args) => {
let [_member] = args;
const [_readGuildUser, readGuild, _readHomeGuild] = await getReadOnlyDBs({
guildId: args[0].guild?.id,
userId: args[0].id
});
return [...args, readGuild];
},
[Events.GuildMemberRemove]: async (...args) => {
let [_member] = args;
const [_readGuildUser, readGuild, _readHomeGuild] = await getReadOnlyDBs({
guildId: args[0].guild?.id,
userId: args[0].id
});
return [...args, readGuild];
}
};
let commandsArray = Object.values(commands)
.sort((a, b) => {
if (a.priority === undefined) return 1;
if (b.priority === undefined) return -1;
return a.priority - b.priority;
}); // Lower priority = executed first.
// Interceptors are used to stop other modules from handling events, particularly by block_module
let interceptors = commandsArray
.map(command => command.eventInterceptors)
.filter(Boolean);
// Tune global handling - most of this is for backwards code compatibility.
interceptors.push({
// Ignore bots on MessageCreate
[Events.MessageCreate]: (_handler, ...args) => (args[0].author.bot || args[0].author.id === client.user?.id)
});
for (const listenerName of Object.values(Events)) { // For every type of discord event
const listeningCommands = Object.freeze(
commandsArray.filter(command => command[listenerName]) // Get listening functions
);
if (!listeningCommands.length) continue;
client.on(String(listenerName), async (...args) => {
// Modify args if needed for this type
const argInjector = argInjectors[listenerName];
if (argInjector) args = await argInjector(...args);
for (const command of listeningCommands) {
let handler = command[listenerName];
// Run interceptors (block_module)
for (const interceptor of interceptors) {
if (interceptor[listenerName] && interceptor[listenerName](command, ...args)) return;
};
let promise = handler(...args);
// If a specific execution order is requested, wait for it to finish.
if ("priority" in command) await promise;
}
});
}
});
// === Schedule `daily` execution
const daily = global.daily = function(dontLoop = false) {
if (!dontLoop) setInterval(() => { daily(true); }, 60000 * 60 * 24);
// Dispatch daily calls to all listening modules
Object.values(dailyListenerModules).forEach(module => module.daily(pseudoGlobals));
};
client.once(Events.ClientReady, async () => {
var now = new Date();
setTimeout(
daily,
// Schedule dailies to start at 11 AM (host device tz, UTC in this case) the next day
// TODO: make this UTC
((now.getHours() > 11 ? 11 + 24 - now.getHours() : 11 - now.getHours()) * (60000 * 60)) + ((60 - now.getMinutes()) * 60000)
);
});
// === Dispatch command execute / autocomplete / buttons where they need to go.
client.on(Events.InteractionCreate, async cmd => {
const asyncTasks = []; // Any non-awaited functions go here to fully known when this command is done executing for metrics
const intStartTime = Date.now();
/** @type {import("./command-module").CommandModule} */
const commandScript = ("commandName" in cmd) ? commands[cmd.commandName] : null;
if (!commandScript && (cmd.isCommand() || cmd.isAutocomplete())) return; // Ignore any potential cache issues
// Check permissions manually due to Discord security bugs on interpreting
const AdminPermissions = BigInt(8);
if ((cmd.isChatInputCommand() || cmd.isMessageContextMenuCommand()) && commandScript?.data?.command?.default_member_permissions) {
const requiredPermissions = BigInt(commandScript.data.command.default_member_permissions);
if (requiredPermissions && cmd.member && cmd.guild) {
// @ts-ignore
const memberPermissions = BigInt(cmd.member.permissions);
const hasAdminPerms = (memberPermissions & AdminPermissions) === AdminPermissions; // Admins bypass perm checks
if (!hasAdminPerms && (memberPermissions & requiredPermissions) !== requiredPermissions) {
await cmd.reply({
content: "You don't have permission to use this command.",
ephemeral: true
});
return;
}
}
}
//// Manage deferring
let deferredResponse;
if (cmd.isChatInputCommand() || cmd.isMessageContextMenuCommand()) {
// Always obey the `private` property, if not defined default to the `deferEphemeral` property.
const private = cmd.isChatInputCommand() ? cmd.options.getBoolean("private") : null;
const subcommand = cmd.isChatInputCommand() ? cmd.options.getSubcommand(false) : null;
let forceEphemeral = false;
let deferBlocked = false;
let detailedExtra = {};
// See if this command requests to be ephemeral
if (commandScript?.data?.deferEphemeral) {
if (typeof(commandScript.data.deferEphemeral) == "object" && subcommand) {
let subcommandData = commandScript.data.deferEphemeral[subcommand];
if (typeof(subcommandData) == "object") {
detailedExtra = subcommandData;
}
else {
forceEphemeral = subcommandData; // It's just a raw boolean
}
}
else if (typeof(commandScript.data.deferEphemeral) == "boolean") {
forceEphemeral = commandScript.data.deferEphemeral;
}
}
// See if this command requests to not be deferred
if (commandScript?.data?.deferBlocked) {
if (typeof(commandScript.data.deferBlocked) == "object" && subcommand) {
deferBlocked = commandScript.data.deferBlocked[subcommand];
}
else if (typeof(commandScript.data.deferBlocked) == "boolean") {
deferBlocked = commandScript.data.deferBlocked;
}
}
if (!deferBlocked) deferredResponse = await cmd.deferReply({
ephemeral: private ?? forceEphemeral ?? false,
...detailedExtra // This allows fields like withResponse to be specified
});
}
//// Autocomplete
if (cmd.isAutocomplete()) {
const providedGlobals = { ...pseudoGlobals };
const requestedGlobals = commandScript.data?.requiredGlobals || commandScript.requestGlobals?.() || [];
for (let name of requestedGlobals) {
// eslint-disable-next-line no-eval
providedGlobals[name] = eval(name.match(/[\w-]+/)[0]);
}
asyncTasks.push(
commands?.[cmd.commandName]?.autocomplete?.(cmd, providedGlobals)
);
}
//// Slash commands
if (
(cmd.isChatInputCommand() || cmd.isMessageContextMenuCommand()) &&
cmd.commandName in commands
) {
// Here we artificially provide the full path since slash commands can have subcommands
const listeningModule = [`${cmd.commandName} ${cmd.isChatInputCommand()
? cmd.options.getSubcommand(false)
: ""
}`.trim(), commandScript];
let isAdmin = cmd.member?.permissions instanceof PermissionsBitField && cmd.member?.permissions?.has?.(PermissionFlagsBits.Administrator);
// TODO_DB: this could be made more efficient by passing in the readonly guilds as objects
const [blocked, errorMsg] = isModuleBlocked(listeningModule,
(await guildByObj(cmd.guild)),
(await guildByID(config.homeServer)),
isAdmin
);
if (blocked) return cmd.followUp(errorMsg);
// Checks passed, gather requested data
const providedGlobals = { ...pseudoGlobals };
const requestedGlobals = commandScript.data?.requiredGlobals || commandScript.requestGlobals?.() || [];
for (let name of requestedGlobals) {
// eslint-disable-next-line no-eval
providedGlobals[name] = eval(name.match(/[\w-]+/)[0]);
}
// Run, and catch errors
try {
await commands[cmd.commandName].execute(cmd, providedGlobals, deferredResponse);
}
catch (e) {
// Catch blocked by automod
if (e.code === 200000) {
cmd.followUp("Sorry, something in this reply was blocked by AutoMod.");
}
try {
cmd.followUp(
"Sorry, some error was encountered. It has already been reported, there is nothing you need to do.\n" +
`However, you can keep up with Stewbot's latest features and patches in the [Support Server](<${config.invite}>).`
);
}
catch { }
throw e; // Throw it so that it hits the error notifiers
}
}
//// Buttons, Modals, and Select Menu
// Anything that has an ID can be sent
if ("customId" in cmd) {
Object.values(buttonListenerModules).forEach(module => {
// Only emit buttons to subscribed modules
const moduleSubscriptions = module.subscribedButtons || [];
let subbed = false;
for (const sub of moduleSubscriptions) {
if (
(typeof sub === "string" && sub === cmd.customId) ||
(sub instanceof RegExp && sub.test(String(cmd.customId)))
) {
subbed = true;
continue;
}
}
if (subbed) asyncTasks.push(module.onbutton(cmd, pseudoGlobals));
});
}
// Wait for everything to complete
await Promise.allSettled(asyncTasks);
const intEndTime = Date.now();
if (cmd.isChatInputCommand()) queueCommandMetric(cmd.commandName || "unspecified", intEndTime - intStartTime);
});
// Don't crash on any type of error
// @ts-ignore
process.on("unhandledRejection", e => notify(e.stack));
process.on("uncaughtException", e => notify(e.stack));
async function start() {
console.log("Checking for mongo backup, commands loading");
await Promise.all([
// Register all handlers to the client
commandListenerRegister,
// See if mongo needs to import
checkForMongoRestore()
]);
// Connect to the db after importing
console.log("Connecting to MongoDB");
await mongoose.connect(`${process.env.databaseURI}/${process.env.beta ? "stewbeta" : "stewbot"}`);
// Login
console.log("Logging in");
await client.login(process.env.token);
}
start();