Source Code

Open Source & Transparent

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.

View Stewbot Repository →

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();