Friday, September 20, 2024 12:04:10 AM
> settings

Customize


Authenticate

> esm_bot.js
const discordJS = require("discord.js");
const util = require('util');

module.exports = class ESMBot {
    constructor() {
        this.initalized = false;
        this.maintenanceMode = false;
        this.maintenanceMessage = "";

        this.moment = require("moment");
        this.colors = require("./colors");
        this.errors = require("./errors");
        this.config = require("../config");
        this.request = require("snekfetch");
        this.matcher = require("did-you-mean");

        this.discord = discordJS;
        this.client = new discordJS.Client({
            disabledEvents: this.config.DISABLED_EVENTS
        });

        this.util = new(require("./util"))(this);
        this.inspect = util.inspect;
        this.r = require("rethinkdbdash")({
            db: "exile_server_manager"
        });
        this.db = new(require("./database"))(this.r, this);
        this.logger = new(require("./logger"))(this);
        this.commands = require("../commands/commands");
        this.wss = new(require("./web_socket_server"))(this);
        this.api = new(require("./api"))(this);

        this.client.login(this.config.DISCORD_TOKEN);

        this.client.on('ready', this.onReady.bind(this));
        this.client.on("guildCreate", this.onGuildCreate.bind(this));
        this.client.on('message', this.onMessage.bind(this));
        this.client.on("guildDelete", this.onGuildDelete.bind(this));
        this.client.on("guildMemberAdd", this.onGuildMemberAdd.bind(this));
        this.client.on("error", this.onError.bind(this));
    }

    async cleanup() {
        // setTimeout(() => {
        //     this.db.cleanOldServers();
        // }, 7200000);
        // setTimeout(() => {
        //     this.db.cleanOldGuilds();
        // }, 7200000);
        // setTimeout(() => {
        //     this.db.cleanOldLinks();
        // }, 7200000);
        // setTimeout(() => {
        //     this.db.cleanOldRequests();
        // }, 7200000);
        // setTimeout(() => {
        //     this.db.cleanOldTerritories();
        // }, 7200000);

        // this.db.cleanOldServers();
        // this.db.cleanOldGuilds();
        // this.db.cleanOldLinks();
        // this.db.cleanOldRequests();
        // this.db.cleanOldTerritories();
    }

    async setPresence(bot) {
        try {
            let info = await bot.db.getBotInformation();
            bot.client.user.setPresence({
                status: 'online',
                game: {
                    name: info.status_message,
                    type: info.status_type,
                    url: "https://www.esmbot.com"
                }
            });
        } catch (err) {
            bot.logger.error(err);
        }
    }

    async setMaintenance() {
        try {
            let info = await this.db.getBotInformation();
            this.maintenanceMode = info.maintenance_mode;
            this.maintenanceMessage = info.maintenance_message;
        } catch (err) {
            this.logger.error(err);
        }
    }

    async send(channel, incomingMessage) {
        try {
            if (this.util.isEmpty(incomingMessage)) return;
            if (typeof (channel) == "string") {
                channel = (await this.getChannel(channel)) || (await this.getUser(channel));
            }

            if (channel == null) return;

            let message = null;

            if (incomingMessage instanceof discordJS.MessageEmbed) {
                message = await this.processMessageEmbedMessage(channel, incomingMessage);
            } else if (typeof incomingMessage === "object") {
                message = await this.processObjectMessage(channel, incomingMessage);
            } else {
                message = await this.processStringMessage(channel, incomingMessage);
            }

            return message;
        } catch (err) {
            // this.send(channel, this.errors.UNABLE_TO_SEND_MESSAGE);
            this.handleError(err, `Channel: ${channel}, Message: ${this.inspect(incomingMessage)}`);
            return null;
        }
    }

    async processMessageEmbedMessage(channel, incomingMessage) {
        incomingMessage.title = _.truncate(incomingMessage.title, {
            length: this.util.embed.TITLE_LENGTH_MAX
        });

        incomingMessage.description = _.truncate(incomingMessage.description, {
            length: this.util.embed.DESCRIPTION_LENGTH_MAX
        });

        return await channel.send(incomingMessage);
    }

    async processObjectMessage(channel, incomingMessage) {
        let messageToSend = incomingMessage;
        if (!messageToSend.hasOwnProperty("embed")) {
            messageToSend = {
                embed: {
                    color: this.colors.BLUE,
                    timestamp: this.moment.utc()
                }
            };

            for (let option in incomingMessage) {
                messageToSend.embed[option] = incomingMessage[option];
            }
        }

        if (!_.isEmpty(messageToSend.embed.title)) {
            messageToSend.embed.title = _.truncate(messageToSend.embed.title, {
                length: this.util.embed.TITLE_LENGTH_MAX
            });
        }

        if (!_.isEmpty(messageToSend.embed.description)) {
            messageToSend.embed.description = _.truncate(messageToSend.embed.description, {
                length: this.util.embed.DESCRIPTION_LENGTH_MAX
            });
        }

        return await channel.send(messageToSend);
    }

    async processStringMessage(channel, incomingMessage) {
        if (incomingMessage.length > 1750) {
            let times = incomingMessage.length / 1750;
            for (let i = 0; i < times; i++) {
                await channel.send(incomingMessage.substring(i * 1750, (i + 1) * 1750));
            }
            return;
        }

        return await channel.send(incomingMessage);
    }

    async sendToDevs(message) {
        try {
            if (this.util.isEmpty(message)) return;

            let messageToSend = "";
            if (typeof (message) == "object") {
                messageToSend = {
                    embed: {
                        color: this.colors.BLUE,
                        timestamp: this.moment.utc()
                    }
                };

                for (let option in message) {
                    messageToSend.embed[option] = message[option];
                }
            } else {
                messageToSend = message;
            }

            for (let i = 0; i < Config.devs.length; i++) {
                let dev = Config.devs[i];
                let user = await this.getUser(dev);
                if (dev == null) continue;
                this.client.send(user, messageToSend);
            }
        } catch (err) {
            this.logger.error(err);
        }
    }

    async startTyping(channel) {
        try {
            if (typeof (channel) == "string") {
                channel = (await this.getChannel(channel)) || (await this.getUser(channel));
            }

            if (channel == null) return;

            // Do not spam attempt to type in a channel. (Makes stopping it difficult)
            if (this.client.user.typingIn(channel)) return;
            channel.startTyping();
        } catch (err) {
            this.logger.error(err);
        }
    }

    async stopTyping(channel) {
        try {
            if (typeof (channel) == "string") {
                channel = (await this.getChannel(channel)) || (await this.getUser(channel));
            }

            if (channel == null) return;

            channel.stopTyping(true);

        } catch (err) {
            this.logger.error(err);
        }
    }

    async getUser(id) {
        try {
            // @someone
            let match = /^<@!?(\d+)>$/i.exec(id);
            if (match != null) {
                return await this.client.users.fetch(match[1]);
            }

            // SteamUID
            match = /^\d{17}$/i.exec(id);
            if (match != null) {
                let discordIDs = this.db.getDiscordIDsFromSteamUID(match[1]);
                if (this.util.isEmpty(discordIDs)) return null;
                return await this.client.users.fetch(discordIDs[0]);
            }

            // DiscordID
            match = await this.client.users.fetch(id);
            if (match != null) {
                return match;
            }
        } catch (err) {
            // this.logger.error(err);
        }

        return null;
    }

    async getChannel(id) {
        try {
            return await this.client.channels.fetch(id);
        } catch (err) {
            // this.logger.error(err);
        }
    }

    async getGuild(id) {
        try {
            return await this.client.guilds.fetch(id);
        } catch (err) {
            // this.logger.error(err);
        }
    }

    async onReady() {
        try {
            if (this.initalized) {
                return this.logger.warn("ESM already initialized");
            };
            this.setPresence(this);
            this.cleanup();
            this.setMaintenance();
            setInterval(() => {
                this.setPresence(this)
            }, 7200000);
            await this.wss.initialize();
            await this.api.initialize();

            this.logger.success("Exile Server Manager Discord Bot has been initialized");

            this.serverIDMatcher = new this.matcher(_.keys(this.wss.servers));
            this.serverIDMatcher.ignoreCase();
            this.serverIDMatcher.setThreshold(5);
            this.initalized = true;
        } catch (err) {
            this.logger.error(err);
        }
    }

    async onGuildCreate(guild) {
        try {
            let community = await this.db.getCommunityFromGuildID(guild.id);

            if (community == null) {
                community = await this.db.addGuild(guild);

                let embed = new this.discord.MessageEmbed()
                    .setTitle("Guild Joined")
                    .setColor(this.colors.PURPLE)
                    .addField("Community ID", community.id, true)
                    .addField("Guild", `${guild.name} (\`${guild.id}\`)`, true)
                    .addField("Owner", `${guild.owner.user.tag} (\`${guild.ownerID}\`)`, true)
                    .addField("Member Count", `${guild.memberCount}`, true);

                this.logger.communities(embed);
            } else {
                this.db.reactivateGuild(community.id);

                let embed = new this.discord.MessageEmbed()
                    .setTitle("Guild Reactivated")
                    .setColor(this.colors.PURPLE)
                    .addField("Community ID", community.id, true)
                    .addField("Guild", `${guild.name} (\`${guild.id}\`)`, true)
                    .addField("Owner", `${guild.owner.user.tag} (\`${guild.ownerID}\`)`, true)
                    .addField("Member Count", `${guild.memberCount}`, true);

                this.logger.communities(embed);
            }

            let welcomeEmbed = new this.discord.MessageEmbed()
                .setTitle("Welcome!")
                .setDescription("Thank you for inviting me to your Discord community! Below is some useful information to get you started. If you have any questions, check out the wiki or feel free to join our Discord!")
                .setTimestamp(this.moment.utc())
                .setColor(this.colors.GREEN)
                .setAuthor(this.client.user.username, this.client.user.avatarURL());

            if (community.player_mode_enabled) {
                welcomeEmbed.addField("Player Mode has been enabled", "What is Player Mode?\nPlayer Mode is a version of Exile Server Manager that allows player to invite the bot to their Discords and access all the commands normally locked to Direct Message via their Discord Text Channels. For more information about Player Mode, [click here](https://www.esmbot.com/wiki/player_mode)")
                    .addField("Are you a server owner?", `If so, you will want to disable Player Mode so you can access ESM's server functionalities for your community.\nTo leave Player Mode, just reply with \`!playermode ${community.id} disable\``);
            } else {
                welcomeEmbed.addField(`Community ID: \`${community.id}\``, "This ID is unique to your community and is used for a few different commands. Also, this ID will be the first part of any servers you register with ESMBot, so make sure it's known!")
                    .addField("Getting Started", "[Install Instructions](https://www.esmbot.com/wiki/setup)")
                    .addField("Web Panel", "[Login to Web Panel](https://www.esmbot.com/login)");
            }


            welcomeEmbed.addField("Website", "[www.esmbot.com](https://www.esmbot.com)")
                .addField("Wiki", "[ESM Wiki](https://www.esmbot.com/wiki)")
                .addField("Join ESM Discord", "[Invite Link](https://www.esmbot.com/join)");

            this.send(guild.ownerID, welcomeEmbed);
        } catch (err) {
            this.logger.error(err);
        }
    }

    async onMessage(message) {
        try {
            // Don't process for webhooks
            if (message.webhookID) return;

            // We were tagged
            if (!this.maintenanceMode && message.mentions.has(this.client.user.id)) return this.sendWelcomeMessage(message);

            // No bots, no random messages, DynoBot, ESMTester
            if (!this.maintenanceMode && (message.author.bot && !["155149108183695360", "571474040334843922"].includes(message.author.id)) || !message.content.startsWith(this.config.COMMAND_SYMBOL)) return;

            // We are in maintenance mode
            if (this.maintenanceMode && !(this.config.DEVS.includes(message.author.id))) {
                return this.send(message.channel, `I'm sorry ${message.author.toString()}, I'm currently in maintenance mode. ${this.maintenanceMessage}`);
            }

            let requestedCommand = message.content.substring(1, message.length).split(" ")[0].toLowerCase();

            if (!this.commands.hasOwnProperty(requestedCommand)) return;

            let command = new(this.commands[requestedCommand])(this);
            if (command.isRestricted()) return;

            await command.checkPermissions(message);

            if (!command.isValidCommand) return;

            // Adam is italian, remind him
            if (message.author.id === "219847919753232384" && message.channel.type === "text" && message.guild.id === "249931900376842240") {
                this.send(message.channel, "<:italian:327841606998687754> <:italian:327841606998687754> <:italian:327841606998687754> <:italian:327841606998687754> <:italian:327841606998687754> <:italian:327841606998687754>");
            }

            // Process the command
            command.exec();
        } catch (err) {
            this.logger.error(err);
        }
    }

    async onGuildDelete(guild) {
        try {
            let communityID = await this.db.getCommunityID(guild.id);
            let success = await this.db.deactivateGuild(guild);
            if (success) {
                this.logger.communities({
                    color: this.colors.PINK,
                    title: "Guild Deactivated",
                    fields: [{
                            name: "Community ID",
                            value: communityID,
                            inline: true
                        },
                        {
                            name: "Guild",
                            value: `${guild.name} (\`${guild.id}\`)`,
                            inline: true
                        },
                        {
                            name: "Owner",
                            value: `${guild.owner.user.tag} (\`${guild.ownerID}\`)`,
                            inline: true
                        },
                        {
                            name: "Member Count",
                            value: `${guild.memberCount}`,
                            inline: true
                        }
                    ]
                });
            } else {
                this.logger.communities({
                    color: this.colors.PINK,
                    title: "Guild Deactivated",
                    description: "**FAILED TO DEACTIVATE**",
                    fields: [{
                            name: "Community ID",
                            value: communityID,
                            inline: true
                        },
                        {
                            name: "Guild",
                            value: `${guild.name} (\`${guild.id}\`)`,
                            inline: true
                        },
                        {
                            name: "Owner",
                            value: `${guild.owner.user.tag} (\`${guild.ownerID}\`)`,
                            inline: true
                        },
                        {
                            name: "Member Count",
                            value: `${guild.memberCount}`,
                            inline: true
                        }
                    ]
                });
            }
        } catch (err) {
            this.logger.error(err);
        }
    }

    async onGuildMemberAdd(member) {
        try {
            if (this.config.DEBUG) return;
            if (member.guild.id === "414643176947843073") {
                this.send(member, {
                    author: {
                        name: this.client.user.username,
                        icon_url: this.client.user.avatarURL()
                    },
                    title: `Welcome to the official Discord of Exile Server Manager, ${member.user.username}!`,
                    description: "Please make sure to check out the **welcome-to-esm** channel for important information.\nWe are all family here so feel free to introduce yourself in the general channel!",
                    color: this.colors.GREEN,
                    fields: [{
                            name: "Have a non-support related question?",
                            value: "Feel free to ask in `#general-no-support`"
                        },
                        {
                            name: "Having an issue that needs fixin'?",
                            value: "Let us know in `#general-support`"
                        }
                    ]
                });
                return;
            }

            let communityID = await this.db.getCommunityID(member.guild.id);
            if (this.util.isEmpty(communityID)) return;
            let isPremium = true;
            let isRegistered = await this.db.isRegistered(member.id);

            let serverInfo = "";
            for (let server of (await this.db.getServerIDsAndInfo(communityID))) {
                serverInfo += `**${server.name} [${server.ip}:${server.port}]**\nServer ID: \`${server.id}\`\n`;
            }

            let embed = {};
            if (isRegistered) {
                embed = {
                    description: `Looks like you are already registered with me, so I'll save you the speech. 😜\nJust letting you know that this community is running **Exile Server Manager**.\n\nThe community that you just joined has a community ID of \`${communityID}\`. ${isPremium ? "\n\nThey also have premium features unlocked for at least one of their servers, which means you have access to all of ESM's commands for that server!" : ""}`,
                    fields: [{
                            name: "Available Server(s)",
                            value: this.util.isEmpty(serverInfo) ? "This community does not have any servers linked with ESM" : serverInfo
                        },
                        {
                            name: "Want to learn more?",
                            value: "https://www.esmbot.com"
                        },
                        {
                            name: "Want to invite ESM to your own Discord?",
                            value: "https://www.esmbot.com/invite"
                        }
                    ]
                };
            } else {
                embed = {
                    description: `Just letting you know that this community is running **Exile Server Manager**.\n\n**Exile Server Manager** (ESM) is a bot that allows **you**, the player, to interact with aspects of Exile that would normally require you to be in game. This includes getting information about your player, managing your territory (paying, upgrading, add/remove members, etc), offline notifications and so much more.\n\nThe community that you just joined has a community ID of \`${communityID}\`. This is used for commands that required a \`<community_id>\`. ${isPremium ? "\n\nThey also have premium features unlocked for at least one of their servers, which means you have access to all of ESM's commands for that server!" : ""}`,
                    fields: [{
                            name: "Available Server(s)",
                            value: this.util.isEmpty(serverInfo) ? "This community does not have any servers linked with ESM" : serverInfo
                        },
                        {
                            name: "Want to use ESM?",
                            value: "Reply with `!register`"
                        },
                        {
                            name: "Want to learn more?",
                            value: "https://www.esmbot.com"
                        },
                        {
                            name: "Want to invite ESM to your own Discord?",
                            value: "https://www.esmbot.com/invite"
                        }
                    ]
                };
            }

            this.send(member, {
                author: {
                    name: this.client.user.username,
                    icon_url: this.client.user.avatarURL()
                },
                title: `Welcome to ${member.guild.name}, ${member.user.username}!`,
                description: embed.description,
                color: this.colors.GREEN,
                fields: embed.fields
            });
        } catch (err) {
            this.logger.error(err);
        }
    }

    async onError(error) {
        try {
            if (["write EPIPE", "read ECONNRESET", "write ECONNRESET"].includes(error.message)) return;
            this.logger.error(`Name: ${error.name}. Message: ${error.message}`);
        } catch (err) {
            this.handleError(err);
        }
    }

    async sendWelcomeMessage(message) {
        if (/shut up/i.test(message.content)) {
            this.stopTyping(message.channel);
            return this.send(message.channel, "Sorry...");
        }

        if (message.content.match(/fuck?\s?(?:u|you)?/i)) {
            return this.send(message.channel, `no u, ${message.author.toString()}`);
        }

        let isRegistered = await this.db.isRegistered(message.author.id);

        if (!isRegistered) {
            if (message.channel.type === "text") {
                this.send(message.channel, `Hello ${message.author.toString()}. I see that you aren't registered with me. Let me send you a direct message to get started!\n_If you don't get a message from me, it means I can't message you_`);
                await this.send(message.author, "I'm over here now! 😃\n");
            }

            return this.send(message.author, `Hi there! My name is **Exile Server Manager**, or **ESM** for short.\nMy job is to help make **your** life easier while playing on servers that run me. I can do lots of cool things such as make territory payments, manage your territory members, get information about your player, provide you offline XM8 notifications and so much more!\nIf you would like to start using my commands, just reply back with \`!register\` and we will get started!`);
        }

        this.send(message.channel, `Hello ${message.author.toString()}. Did you know that you can see what commands I respond to by running \`!commands\`?`);
    }

    async handleError(err, extra = "") {
        // try {
        //     switch (err.code) {
        //         // Missing Permissions
        //         case 50013:
        //             // Missing Access
        //         case 50001:
        //             // /api/v7/channels/ID/messages
        //             let channelID = /\/api\/.*\/channels\/(\d+)\//.exec(err.path)[1];
        //             if (_.isEmpty(channelID)) return;

        //             let channel = this.getChannel(channelID);

        //             if (_.isNull(channel)) return;
        //             if (_.isNull(channel.guild)) return;

        //             // Get the first channel that is a text channel
        //             let rescueChannelID = channel.guild.channels.filter(channel => {
        //                 if (channel.type === "text") return channel;
        //             }).firstKey();

        //             if (!_.isEmpty(rescueChannelID)) {
        //                 return this.send(rescueChannelID, `Oof! I was told to access channel \`${channel.name}\` on this Discord, but I am ${err.message.toLowerCase()}\nAdmins, please ensure I have read/write access to this channel`);

        //             } else {
        //                 return this.send(channel.guild.ownerID, `Oof! I was told to access channel \`${channel.name}\` on this Discord, but I am ${err.message.toLowerCase()}\nPlease ensure I have read/write access to this channel`);
        //             }
        //     }
        // } catch (newErr) {

        // }

        /*
            50007: Cannot send messages to this user
            10003: Unknown Channel
        */
        if (err.hasOwnProperty("code") && [50013, 50001, 50007, 10003, "ETIMEDOUT", "EAI_AGAIN", "ECONNRESET"].includes(err.code)) {
            this.logger.error({
                code: err.code,
                message: err.message
            });
        } else {
            this.logger.error(this.inspect(err));
        }

        this.logger.error(extra);
    }
};
All opinions represented herein are my own
- © 2024 itsthedevman
- build 3c15a1b