Source

classes/bot-guild.js

const { Collection, TextChannel, Role, MessageEmbed, CategoryChannel, Guild } = require('discord.js');
const { CommandoClient, CommandoGuild } = require('discord.js-commando');
const winston = require('winston');
const discordServices = require('../discord-services');

/**
 * @class
 */
class BotGuild {

    
    /**
     * Staff role permissions.
     * @type {String[]}
     */
    static staffPermissions = ['VIEW_CHANNEL', 'MANAGE_EMOJIS', 'CHANGE_NICKNAME', 'MANAGE_NICKNAMES', 
    'KICK_MEMBERS', 'BAN_MEMBERS', 'SEND_MESSAGES', 'EMBED_LINKS', 'ATTACH_FILES', 'ADD_REACTIONS', 'USE_EXTERNAL_EMOJIS', 'MANAGE_MESSAGES', 
    'READ_MESSAGE_HISTORY', 'CONNECT', 'STREAM', 'SPEAK', 'PRIORITY_SPEAKER', 'USE_VAD', 'MUTE_MEMBERS', 'DEAFEN_MEMBERS', 'MOVE_MEMBERS'];

    /**
     * Admin role permissions.
     * @type {String[]}
     */
    static adminPermissions = ['ADMINISTRATOR'];

    /**
     * The regular member perms.
     * @type {String[]}
     */
    static memberPermissions = ['VIEW_CHANNEL', 'CHANGE_NICKNAME', 'SEND_MESSAGES', 'ADD_REACTIONS', 'READ_MESSAGE_HISTORY',
    'CONNECT', 'SPEAK', 'STREAM', 'USE_VAD'];

    /**
     * @typedef RoleIDs
     * @property {String} memberRole - regular guild member role ID
     * @property {String} staffRole - the staff role ID
     * @property {String} adminRole - the admin role ID
     * @property {String} everyoneRole - the everyone role ID
     */

     /**
      * @typedef ChannelIDs
      * @property {String} adminConsole - the admin console channel ID
      * @property {String} adminLog - the admin log channel ID
      * @property {String} botSupportChannel - the bot support channel ID
      */

    /**
     * @typedef VerificationInfo
     * @property {Boolean} isEnabled - true if verification is enabled
     * @property {String} isVerifiedRoleID - the verified role ID that holds basic permissions
     * @property {String[]} isVerifiedRolePermissions - the permissions for the isVerified role
     * @property {String} guestRoleID - the guest role ID used for verification
     * @property {String} welcomeChannelID -  the welcome channel where users learn to verify
     * @property {String} welcomeSupportChannelID - the support channel where the bot can contact users
     */

    /**
     * @typedef AttendanceInfo
     * @property {Boolean} isEnabled - true if attendance is enabled in this guild
     * @property {String} attendeeRoleID - the attendee role ID used for attendance
     */

    /**
     * @typedef StampInfo
     * @property {Boolean} isEnabled - true if stamps are enabled
     * @property {Collection<Number, String>} stampRoleIDs - <StampNumber, roleID>
     * @property {Number} stampCollectionTime - time given to users to collect password stamps
     */

    /**
     * @typedef ReportInfo
     * @property {Boolean} isEnabled - true if the report functionality is enabled
     * @property {String} incomingReportChannelID - channel where reports are sent
     */

    /**
     * @typedef AnnouncementInfo
     * @property {Boolean} isEnabled
     * @property {String} announcementChannelID
     */

    /**
     * @typedef BotGuildInfo
     * @property {RoleIDs} roleIDs
     * @property {ChannelIDs} channelIDs
     */

    /**
     * Validate the information.
     * @param {BotGuildInfo} botGuildInfo - the information to validate
     * @throws Error if the botGuildInfo is incomplete
     */
    validateBotGuildInfo(botGuildInfo) {
        if (typeof botGuildInfo != 'object') throw new Error('The bot guild information is required!');
        if (!botGuildInfo?.roleIDs || !botGuildInfo?.roleIDs?.adminRole || !botGuildInfo?.roleIDs?.everyoneRole
            || !botGuildInfo?.roleIDs?.memberRole || !botGuildInfo?.roleIDs?.staffRole) throw new Error('All the role IDs are required!');
        if (!botGuildInfo?.channelIDs || !botGuildInfo?.channelIDs?.adminConsole || !botGuildInfo?.channelIDs?.adminLog
            || !botGuildInfo?.channelIDs?.botSupportChannel) throw new Error('All the channel IDs are required!');
    }

    /**
     * Will set the minimum required information for the bot to work on this guild.
     * @param {BotGuildInfo} botGuildInfo 
     * @param {CommandoClient} client
     * @returns {Promise<BotGuild>}
     * @async
     */
    async readyUp(client, botGuildInfo) {
        this.validateBotGuildInfo(botGuildInfo);

        this.roleIDs = botGuildInfo.roleIDs;
        this.channelIDs = botGuildInfo.channelIDs;

        let guild = await client.guilds.fetch(this._id);

        let adminRole = await guild.roles.fetch(this.roleIDs.adminRole);
        // try giving the admins administrator perms
        try {
            if (!adminRole.permissions.has('ADMINISTRATOR')) 
            {
                adminRole.setPermissions(adminRole.permissions.add(['ADMINISTRATOR']));
                await adminRole.setMentionable(true);
            }
        } catch {
            discordServices.discordLog(guild, 'Was not able to give administrator privileges to the role <@&' + adminRole.id + '>. Please help me!')
        }

        // staff role set up
        let staffRole = await guild.roles.fetch(this.roleIDs.staffRole);
        staffRole.setMentionable(true);
        staffRole.setHoist(true);
        staffRole.setPermissions(staffRole.permissions.add(BotGuild.staffPermissions));

        // regular member role setup
        let memberRole = await guild.roles.fetch(this.roleIDs.memberRole);
        memberRole.setMentionable(false);
        memberRole.setPermissions(memberRole.permissions.add(BotGuild.memberPermissions));

        // change the everyone role permissions
        guild.roles.everyone.setPermissions(0); // no permissions for anything like the guest role

        // make sure admin channels are only for admins
        let adminCategory = guild.channels.resolve(this.channelIDs.adminConsole).parent
        adminCategory.overwritePermissions([
            {
                id: adminRole.id,
                allow: 'VIEW_CHANNEL'
            },
            {
                id: this.roleIDs.everyoneRole,
                deny: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'CONNECT']
            }
        ]);
        adminCategory.children.forEach(channel => channel.lockPermissions());

        // create the archive category
        this.channelIDs.archiveCategory = (await this.createArchiveCategory(guild)).id;

        this.isSetUpComplete = true;

        winston.loggers.get(this._id).event(`The botGuild has run the ready up function.`, {event: "Bot Guild"});

        return this;
    }

    /**
     * Creates the archive category.
     * @returns {Promise<CategoryChannel>}
     * @param {Guild} guild
     * @private
     * @async
     */
    async createArchiveCategory(guild) {
        let overwrites = [
            {
                id: this.roleIDs.everyoneRole,
                deny: ['VIEW_CHANNEL']
            },
            {
                id: this.roleIDs.memberRole,
                allow: ['VIEW_CHANNEL'],
            },
            {
                id: this.roleIDs.staffRole,
                allow: ['VIEW_CHANNEL'],
            }
        ];

        // position is used to create archive at the very bottom!
        var position = (guild.channels.cache.filter(channel => channel.type === 'category')).size;
        return await guild.channels.create('💼archive', {
            type: 'category', 
            position: position + 1,
            permissionOverwrites: overwrites,
        });
    }

    /**
     * Will create the admin channels with the correct roles.
     * @param {Guild} guild 
     * @param {Role} adminRole 
     * @param {Role} everyoneRole 
     * @returns {Promise<{TextChannel, TextChannel}>} - {Admin Console, Admin Log Channel}
     * @static
     * @async
     */
    static async createAdminChannels(guild, adminRole, everyoneRole) {
        let adminCategory = await guild.channels.create('Admins', {
            type: 'category',
            permissionOverwrites: [
                {
                    id: adminRole.id,
                    allow: 'VIEW_CHANNEL'
                },
                {
                    id: everyoneRole.id,
                    deny: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'CONNECT']
                }
            ]
        });

        let adminConsoleChannel = await guild.channels.create('console', {
            type: 'text',
            parent: adminCategory,
        });

        let adminLogChannel = await guild.channels.create('logs', {
            type: 'text',
            parent: adminCategory,
        });

        winston.loggers.get(guild.id).event(`The botGuild has run the create admin channels function.`, {event: "Bot Guild"});

        return {adminConsoleChannel: adminConsoleChannel, adminLog: adminLogChannel};
    }

    /**
     * @typedef VerificationChannels
     * @property {String} welcomeChannelID
     * @property {String} welcomeChannelSupportID
     */

    /**
     * @typedef TypeInfo
     * @property {String} type
     * @property {String} roleId
     */

    /**
     * Will set up the verification process.
     * @param {CommandoClient} client 
     * @param {String} guestRoleId
     * @param {TypeInfo[]} types
     * @param {VerificationChannels} [verificationChannels]
     * @return {Promise<BotGuild>}
     * @async
     */
    async setUpVerification(client, guestRoleId, types, verificationChannels = null) {
        /** @type {CommandoGuild} */
        let guild = await client.guilds.fetch(this._id);

        try {
            var guestRole = await guild.roles.fetch(guestRoleId);
        } catch (error) {
            throw new Error('The given guest role ID is not valid for this guild!');
        }
        guestRole.setMentionable(false);
        guestRole.setPermissions(0);
        this.verification.guestRoleID = guestRoleId;

        if (verificationChannels) {
            this.verification.welcomeChannelID = verificationChannels.welcomeChannelID;
            this.verification.welcomeSupportChannelID = verificationChannels.welcomeChannelSupportID;

            /** @type {TextChannel} */
            var welcomeChannel = guild.channels.resolve(this.verification.welcomeChannelID);
            await welcomeChannel.bulkDelete(100, true);
        } else {
            let welcomeCategory = await guild.channels.create('Welcome', {
                type: 'category',
                permissionOverwrites: [
                    {
                        id: this.roleIDs.everyoneRole,
                        allow: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'READ_MESSAGE_HISTORY']
                    },
                    {
                        id: this.roleIDs.memberRole,
                        deny: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'READ_MESSAGE_HISTORY']
                    },
                ],
            });
    
            var welcomeChannel = await guild.channels.create('welcome', {
                type: 'text',
                parent: welcomeCategory,
            });
    
            // update welcome channel to not send messages
            welcomeChannel.updateOverwrite(this.roleIDs.everyoneRole, {
                SEND_MESSAGES: false,
            });
    
            let welcomeChannelSupport = await guild.channels.create('welcome-support', {
                type: 'text',
                parent: welcomeCategory,
            });

            this.verification.welcomeChannelID = welcomeChannel.id;
            this.verification.welcomeSupportChannelID = welcomeChannelSupport.id;
        }

        // add the types to the type map.
        types.forEach((type, index, list) => {
            this.verification.verificationRoles.set(type.type.toLowerCase(), type.roleId);
        });

        const embed = new MessageEmbed().setTitle('Welcome to the ' + guild.name + ' Discord server!')
            .setDescription('In order to verify that you have registered for ' + guild.name + ', please respond to the bot (me) via DM!')
            .addField('Do you need assistance?', 'Head over to the welcome-support channel and ping the admins!')
            .setColor(this.colors.embedColor);
        welcomeChannel.send(embed).then(msg => msg.pin());

        
        this.verification.isEnabled = true;
        guild.setCommandEnabled('verify', true);

        winston.loggers.get(this._id).event(`The botGuild has set up the verification system. Verification channels ${verificationChannels === null ? 'were created' : 'were given'}. 
            Guest role id: ${guestRoleId}. The types used are: ${types.join()}`, {event: "Bot Guild"});
        return this;
    }

    /**
     * Sets up the attendance functionality.
     * @param {CommandoClient} client 
     * @param {String} attendeeRoleID
     * @returns {Promise<BotGuild>}
     * @async
     */
    async setUpAttendance(client, attendeeRoleID) {
        this.attendance.attendeeRoleID = attendeeRoleID;
        this.attendance.isEnabled = true;
        /** @type {CommandoGuild} */
        let guild = await client.guilds.fetch(this._id);
        guild.setCommandEnabled('start-attend', true);
        guild.setCommandEnabled('attend', true);

        winston.loggers.get(this._id).event(`The botGuild has set up the attendance functionality. Attendee role id is ${attendeeRoleID}`, {event: "Bot Guild"});
        return this;
    }

    /**
     * Will set up the firebase announcements.
     * @param {CommandoClient} client 
     * @param {String} announcementChannelID
     * @async
     */
    async setUpAnnouncements(client, announcementChannelID) {
        /** @type {CommandoGuild} */
        let guild = await client.guilds.fetch(this._id);
        
        let announcementChannel = guild.channels.resolve(announcementChannelID);
        if (!announcementChannel) throw new Error('The announcement channel ID is not valid for this guild!');

        this.announcement.isEnabled = true;
        this.announcement.announcementChannelID = announcementChannelID;

        winston.loggers.get(this._id).event(`The botGuild has set up the announcement functionality with the channel ID ${announcementChannelID}`, {event: "Bot Guild"});

        // TODO Firebase setup 
        // start query listener for announcements
        // nwFirebase.firestore().collection('Hackathons').doc('nwHacks2021').collection('Announcements').onSnapshot(querySnapshot => {
        //     // exit if we are at the initial state
        //     if (isInitState) {
        //         isInitState = false;
        //         return;
        //     }

        //     querySnapshot.docChanges().forEach(change => {
        //         if (change.type === 'added') {
        //             const embed = new Discord.MessageEmbed()
        //                 .setColor(botGuild.colors.announcementEmbedColor)
        //                 .setTitle('Announcement')
        //                 .setDescription(change.doc.data()['content']);

        //             announcementChannel.send('<@&' + discordServices.roleIDs.attendeeRole + '>', { embed: embed });
        //         }
        //     });
        // });
    }

    /**
     * Creates the stamps roles and adds them to this BotGuild. If stamps roles are given 
     * then no roles are created!
     * @param {CommandoClient} client 
     * @param {Number} [stampAmount] - amount of stamps to create
     * @param {Number} [stampCollectionTime] - time given to users to send password to get stamp
     * @param {String[]} [stampRoleIDs] - current stamp roles to use
     * @returns {Promise<BotGuild>}
     * @async
     */
    async setUpStamps(client, stampAmount = 0, stampCollectionTime = 60, stampRoleIDs = []) {
        let guild = await client.guilds.fetch(this._id);

        if (stampRoleIDs.length > 0) {
            stampRoleIDs.forEach((ID, index, array) => {
                this.addStamp(ID, index);
            });
            winston.loggers.get(this._id).event(`The botGuild has set up the stamp functionality. The stamp roles were given. Stamp collection time is set at ${stampCollectionTime}.` [{stampIds: stampRoleIDs}]);
        } else {
            for (let i = 0; i < stampAmount; i++) {
                let role = await guild.roles.create({
                    data: {
                        name: 'Stamp Role #' + i,
                        hoist: true,
                        color: discordServices.randomColor(),
                    }
                });

                this.addStamp(role.id, i);
            }
            winston.loggers.get(this._id).event(`The botGuild has set up the stamp functionality. Stamps were created by me, I created ${stampAmount} stamps. Stamp collection time is set at ${stampCollectionTime}.`, {event: "Bot Guild"});
        }

        this.stamps.stampCollectionTime = stampCollectionTime;
        this.stamps.isEnabled = true;

        this.setCommandStatus(client);

        return this;
    }

    /**
     * Adds a stamp to the stamp collection. Does not save the mongoose document!
     * @param {String} roleId 
     * @param {Number} stampNumber 
     */
    addStamp(roleId, stampNumber) {
        if (stampNumber === 0) this.stamps.stamp0thRoleId = roleId;
        this.stamps.stampRoleIDs.set(roleId, stampNumber);
        winston.loggers.get(this._id).event(`The botGuild has added a stamp with number ${stampNumber} linked to role id ${roleId}`, {event: "Bot Guild"});
    }

    /**
     * Enables the report commands and sends the reports to the given channel.
     * @param {CommandoClient} client 
     * @param {String} incomingReportChannelID 
     * @returns {Promise<BotGuild>}
     * @async
     */
    async setUpReport(client, incomingReportChannelID) {
        /** @type {CommandoGuild} */
        let guild = await client.guilds.fetch(this._id);

        this.report.isEnabled = true;
        this.report.incomingReportChannelID = incomingReportChannelID;

        guild.setCommandEnabled('report', true);

        winston.loggers.get(this._id).event(`The botGuild has set up the report functionality. It will send reports to the channel id ${incomingReportChannelID}`, {event: "Bot Guild"});
        return this;
    }

    /**
     * Will enable the ask command.
     * @param {CommandoClient} client 
     */
    async setUpAsk(client) {
        /** @type {CommandoGuild} */
        let guild = await client.guilds.fetch(this._id);
        this.ask.isEnabled = true;
        guild.setCommandEnabled('ask', true);
        winston.loggers.get(this._id).event(`The botGuild has enabled the ask command!`, {event: "Bot Guild"});
    }

    /**
     * Will enable and disable the appropriate commands by looking at what is enabled in the botGuild.
     * @param {CommandoClient} client
     * @async 
     */
    async setCommandStatus(client) {
        /** @type {CommandoGuild} */
        let guild = await client.guilds.fetch(this._id);

        guild.setGroupEnabled('verification', this.verification.isEnabled);
        guild.setGroupEnabled('attendance', this.attendance.isEnabled);

        guild.setGroupEnabled('stamps', this.stamps.isEnabled);

        guild.setCommandEnabled('report', this.report.isEnabled);
        guild.setCommandEnabled('ask', this.ask.isEnabled);
        guild.setGroupEnabled('hacker_utility', this.ask.isEnabled || this.report.isEnabled);
        
        client.registry.groups.forEach((group, key, map) => {
            if (group.id.startsWith('a_')) guild.setGroupEnabled(group, this.isSetUpComplete);
        });

        winston.loggers.get(guild.id).verbose(`Set the command status of guild ${guild.name} with id ${guild.id}`);
    }
}
module.exports = BotGuild;