Artivain/artibot

View on GitHub
src/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
import InteractionManager from "./interactionManager.js";
import { log } from "./logger.js";
import Localizer from "artibot-localizer";
import chalk from "chalk";
import figlet from "figlet";
import * as discord from "discord.js";
import { createRequire } from 'module';
import coreModule from "./core/index.js";
import { readdirSync } from "fs";
import axios from "axios";
import path from "path";
import { fileURLToPath } from "url";
import { ArtibotConfig, ArtibotConfigBuilder } from "./config.js";
import { ContributorList, ModuleGenerator, RegisterModuleOverload } from "./types.js";
import { Module } from "./modules.js";
import Embed from "./embed.js";

export * from "./modules.js";
export * from "./logger.js";
export * from "./types.js";
export * from "./interactionManager.js";
export * from "./config.js";
export * from "./embed.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const require = createRequire(import.meta.url);
const { version }: { version: string } = require('../package.json');

/**
 * Powerful Discord bot system.
 * @author Artivain <info@artivain.com>
 * @author Thomas Fournier <thomas@artivain.com>
 * @see https://github.com/Artivain/artibot
 * @see https://artibot.artivain.com
 * @see https://docs.artibot.artivain.com
 * @license GPL-3.0-or-later
 */
export class Artibot {
    /** Localizer using the default strings for Artibot */
    localizer: Localizer = new Localizer({
        filePath: path.join(__dirname, "../locales.json")
    });
    /** Stores the Artibot config object */
    config: ArtibotConfig;
    /** Version of Artibot */
    version: string = version;
    /** Store cooldowns for the commands */
    cooldowns: discord.Collection<string, discord.Collection<discord.Snowflake, number>> = new discord.Collection();
    /** Registered modules */
    modules: discord.Collection<string, Module> = new discord.Collection();
    /** Discord.js client */
    client?: discord.Client;
    /** Instance of {@link InteractionManager} */
    interactionManager?: InteractionManager;
    /** The token to login into Discord */
    #token: string = "";
    /** Lists of people who contributed to the Artibot */
    readonly contributors: ContributorList = require("../contributors.json");
    /**
     * @deprecated Please directly use the exported {@link log} directly
     */
    log = log;

    /**
     * @param config - Configuration object for Artibot, use {@link ArtibotConfigBuilder} to make this easily.
     */
    constructor(config: Partial<ArtibotConfig>) {
        const { ownerId, testGuildId, lang = "en" } = config;

        // Verify that the owner ID is set
        if (!ownerId) throw new Error("You must set the owner ID.");

        // Verify that the test guild ID is set
        if (!testGuildId) throw new Error("You must set the test guild ID.");

        // Create a localizer for the core
        this.localizer.setLocale(lang);

        // Store config
        this.config = config as ArtibotConfig;

        // Send artwork to console
        console.log(chalk.blue(figlet.textSync('Artibot', {
            font: 'ANSI Shadow',
            horizontalLayout: 'fitted'
        })));
        log("Artibot", this.localizer._("Initialized!") + " v" + version, "info", true);

        // Register the Core module
        this.registerModule(coreModule);
    }

    /**
     * Create an embed
     * @param data - Data to set in the embed
     * @returns A new embed builder, already configured with the defaults from the config
     */
    public createEmbed = (data?: discord.EmbedData): discord.EmbedBuilder => {
        return new Embed(this.config, data);
    }

    /**
     * @param config - Advanced config for the bot
     * @param config.token - The login token for the Discord bot
     * @param config.additionalIntents - Additional intents to register in the Discord client
     * @method
     * @async
     */
    public readonly login = async ({ token = this.#token, additionalIntents = [] }: { token: string, additionalIntents?: discord.IntentsBitField[] }): Promise<void> => {
        if (!token) throw new Error("Token not set!");
        this.#token = token;
        const moduleIntents: discord.GatewayIntentBits[] = [];
        this.modules.forEach(module => module.additionalIntents.forEach(intent => moduleIntents.push(intent)));
        const intents = [...new Set([
            [
                discord.GatewayIntentBits.Guilds,
                discord.GatewayIntentBits.GuildMessages,
                discord.GatewayIntentBits.GuildMembers,
                discord.GatewayIntentBits.GuildPresences,
                discord.GatewayIntentBits.MessageContent
            ],
            ...additionalIntents,
            ...moduleIntents
        ])];
        this.client = new discord.Client({ intents });

        log("Artibot", this.localizer._("Loading event listeners..."), "log", true);
        const eventFiles = readdirSync(path.join(__dirname, "events")).filter(file => file.endsWith(".js"));

        for (const file of eventFiles) {
            const { name, execute, once } = await import(`./events/${file}`);
            if (once) {
                this.client.once(name, (...args) => execute(...args, this));
            } else {
                this.client.on(name, async (...args) => await execute(...args, this));
            }
        }

        this.client.login(token);
    }

    /**
     * Register a module in Artibot
     * @param module - The module to register or a function to initialize the module
     * @param config - Custom configuration for the module. See module documentation to learn more.
     */
    public readonly registerModule: RegisterModuleOverload = (module: ModuleGenerator, config: any = {}): void => {
        if (typeof module == "function") {
            try {
                module = module(this, config);
            } catch (err) {
                log("Artibot", this.localizer._("Error when registering module: ") + err, "err", true);
                process.exit(1);
            }
        }

        this.modules.set(module.id, module);

        log("Artibot", this.localizer._("Registered module: ") + module.name, "info", true);

        if (module.langs != "any" && !module.langs.includes(this.config.lang)) {
            log("Artibot", this.localizer.__(" -> This module does not support the [[0]] language!", { placeholders: [this.config.lang] }), "warn", true);
        }

        for (const part of module.parts) {
            log("Artibot", `- [${part.constructor.name}] ${part.id}`, "log", true);
        }

        if (Object.entries(config).length !== 0) {
            this.config[module.id] = config;
            log("Artibot", this.localizer.__("Custom configuration for [[0]] saved.", { placeholders: [module.name] }), "log", true);
        }
    }

    /**
     * Get latest release version of a GitHub repository
     * @param repo - GitHub repository to get latest version
     * @returns Version number, or false if repo not found or an error happens
     */
    public readonly checkForUpdates = async (repo: string = "Artivain/artibot"): Promise<string | false> => {
        const request = await axios({
            method: "GET",
            url: `https://api.github.com/repos/${repo}/releases/latest`,
            responseType: "json",
            headers: {
                "User-Agent": "Artibot/" + this.version
            },
            validateStatus: () => true
        });

        if (request.status != 200) return false;

        const { data } = request;
        return data.name.replace("v", "");
    }

    /**
     * Get latest release version of a NPM package
     * @param packageName - Package name on NPM
     * @returns Version number, or false if package not found or an error happens
     * @method
     * @async
     */
    public readonly checkForPackageUpdates = async (packageName: string = "artibot"): Promise<string | false> => {
        const request = await axios({
            method: "GET",
            url: `https://api.npms.io/v2/package/${packageName}`,
            responseType: "json",
            headers: {
                "User-Agent": "Artibot/" + this.version
            },
            validateStatus: () => true
        });

        if (request.status != 200) return false;

        const { data } = request;
        return data.collected.metadata.version;
    }
}

/** @ignore */
export default Artibot;