Forge-Media/teamspeak-bot

View on GitHub
plugins/createClan.js

Summary

Maintainability
A
45 mins
Test Coverage
/**
 * Plugin used to create a group of channels for a clan and sets the permissions and properties of each channel
 * @example !createClan
 * @module Plugin-createClan
 */

const Channel = require("./contrib/channel");
const async = require("async");

exports.help = [["!createClan", "Initiate channel template creation for a clan"]];

/**
 * Plugin configuration settings, please change to match your server
 * CHANGE THESE SETTINGS!
 *
 * @version 1.0
 * @property {array} owners - The IDs of ServerGroups which can use this plugin
 * @property {number} ssgid - The source ServerGroup ID (template group you need to setup in Teamspeak)
 * @property {number} sortID_start - Value used to calculate the Clans ServerGroup's 'i_group_sort_id' for alphabetical sorting.
 * @property {number} sortID_start - Value used for increment of 'i_group_sort_id' E.g. 0-9 = +901s, a = 1000s, b = 1100s, c = 1200, etc
 * @memberof Plugin-createClan
 */
const config = {
    owners: [14, 23],
    ssgid: 118,
    sortID_start: 901,
    sortID_inc: 100
};

// Stores users in session as objects with an index equal to the clients clid
let currentlyCreating = {};

/**
 * Represents a user object which is making channels
 *
 * @version 1.1
 * @memberof Plugin-createClan
 * @type {object}
 * @param {object} client - The Client which sent a textmessage
 * @property {number} processid - Keeps track of stage at which user is at in clan channel creation
 * @property {object} client - The Client which sent a textmessage
 * @property {object} channels - Contains all the TeamspeakChannel objects which this user has created
 * @property {string} clanTag - Stores the 2-4 character tag for the clan's server group
 * @property {number} date - Numeric value corresponding to when the user's session started
 */
function creatingUser(client) {
    this.processid = 1;
    this.client = client;
    this.channels = [];
    this.clanTag = "";
    let currentDate = new Date();
    this.date = currentDate.getTime();
}

/**
 * This function is called whenever Jarvis recieves a private message
 * Will create a group of channels for a clan and sets the permissions and properties of each channel
 *
 * @version 1.0.1
 * @memberof Plugin-createClan
 * @param    {String} msg - Message string sent to Jarvis
 * @param    {String} jarvis - Middleware Function: Provides access to certain Jarvis functions.
 */
exports.onMessage = function(msg, jarvis) {
    const message = msg.toLowerCase();
    let client = jarvis.invoker;
    let clid = client.clid;

    // Is this client already creating channels
    if (typeof currentlyCreating[clid] === "undefined") {
        if (message == "!createclan") {
            // Check invoker has permissions
            if (!config.owners.some(r => jarvis.groups.indexOf(r) >= 0)) {
                client.message(jarvis.error_message.forbidden);
                return;
            }

            // Create session and store Teamspeak-Invoker-Object in new session, used for time-out message
            currentlyCreating[clid] = new creatingUser(client);
            client.message("Enter Clan Name: (Type '!stop' at any point to create the channels!)");
        }
    } else if (currentlyCreating[clid].processid == 1) {
        // Sanitise original message and retain capitalisation - returns an array[valid,message]
        let channelName = sanitation(msg);

        // Check for valid channel name
        if (!channelName[0]) {
            client.message(jarvis.error_message.sanitation);
            return;
        }

        // Check if another command was entered, rather than channel name
        if (channelName[1].charAt(0) == "!" && !(channelName[1].toLowerCase() == "!stop")) {
            client.message(`[b]Command Entered[/b] - Please re-enter Channel ${currentlyCreating[clid].channels.length} Name:`);
            return;
        }

        // RECURSIVE: Expect another channel unless message equals '!stop'
        if (channelName[1].toLowerCase() != "!stop") {
            if (!Array.isArray(currentlyCreating[clid].channels) || !currentlyCreating[clid].channels.length) {
                // Parent channel
                currentlyCreating[clid].channels.push(new Channel(channelName[1]));
                client.message("Enter Channel 1 Name:");
            } else {
                // Child channel
                currentlyCreating[clid].channels.push(new Channel(channelName[1], currentlyCreating[clid].channels[0]));
                client.message(`Enter Channel ${currentlyCreating[clid].channels.length} Name:`);
            }
            // Return here insures plugin will request another channel when msg != stop
            return;
        }

        // Before trying to create channels check that there are channels to create
        if (!Array.isArray(currentlyCreating[clid].channels) || !currentlyCreating[clid].channels.length) {
            terminateSession(client, jarvis);
            return;
        }

        client.message(`Constructing ${currentlyCreating[clid].channels.length} channels...`);

        constructChannels(jarvis)
            .then(res => {
                client.message(res);
                setChannelPermissions(jarvis)
                    .then(res => {
                        client.message(res);
                        client.message("[b]Create Clan Group?[/b] (default = No) [!y/!n]");
                        currentlyCreating[clid].processid = 2;
                    })
                    .catch(err => {
                        // CAUGHT: Internal permission-set error
                        console.error("CATCHED", err.message);
                        client.message(jarvis.error_message.external + err.message);
                        terminateSession(client, jarvis);
                    });
            })
            .catch(err => {
                // CAUGHT: Internal error or parent channel failed
                console.error("CATCHED", err.message);
                client.message(jarvis.error_message.internal + err.message);
                terminateSession(client, jarvis);
            });
    } else if (currentlyCreating[clid].processid == 2) {
        if (message.slice(0, 2) != "!y") {
            terminateSession(client, jarvis);
            return;
        }

        // Set client's session to clan group creation stage
        currentlyCreating[clid].processid = 3;
        client.message("Enter Clan Tag: (Between 2 & 4 characters!)");
    } else if (currentlyCreating[clid].processid == 3) {
        // Check tag is correct length

        if (msg.length > 4 || msg.length < 2) {
            client.message(jarvis.error_message.sanitation);
            return;
        }
        // Clan group creation
        constructGroup(jarvis, client, msg);
    }
};

/**
 * Will attempt to create all channels present in the channel array
 *
 * @version 1.0.1
 * If parent-channel 'channelCreate()' fails, the function will terminate passing error in Promise
 * If child-channel 'channelCreate()' fails, error is caught and next child will be attempted
 *
 * @memberof Plugin-createClan
 * @async
 * @param    {Function} jarvis - Middleware Function: Provides access to Jarvis functions.
 * @returns {Promise.<String>}
 */
async function constructChannels(jarvis) {
    let cid = jarvis.invoker.clid;
    let result = "Channels created successfully, setting permissions...";

    // loop through channel array
    for (let c of currentlyCreating[cid].channels) {
        // Parent Channel
        if (!c.parent) {
            await jarvis.ts.channelCreate(c.name, c.properties).then(response => {
                c.cid = response.cid;
            });
            // Child Channels
        } else {
            // Set channel's parent id
            c.properties.cpid = c.parent.cid;
            await jarvis.ts
                .channelCreate(c.name, c.properties)
                .then(response => {
                    // Store created channels ID for permissions
                    c.cid = response.cid;
                })
                .catch(err => {
                    // CAUGHT: External error
                    result = jarvis.error_message.external + err.message;
                    console.error("CATCHED", err.message, "ON", c.name);
                });
        }
    }
    return result;
}

/**
 * Will attempt to set permissions for all channels present in 'channels' array
 * If 'channelSetPerm()' fails, error is caught and next permission will be attempted
 *
 * @version 1.0.1
 * @memberof Plugin-createClan
 * @async
 * @param    {Function} jarvis - Middleware Function: Provides access to Jarvis functions.
 * @returns {Promise.<String>}
 */
async function setChannelPermissions(jarvis) {
    let cid = jarvis.invoker.clid;
    // Result assumes success
    let result = "Permissions set successfully";
    // loop through channel array
    for (let c of currentlyCreating[cid].channels) {
        // loop through channel's permissions object
        let permissions = [];
        for (let perm in c.permissions) {
            permissions.push({
                permsid: perm,
                permvalue: c.permissions[perm]
            });
        }
        // Set channel perms one-by-one
        await jarvis.ts.channelSetPerms(c.cid, permissions).catch(err => {
            // CAUGHT: External error
            result = jarvis.error_message.external + err.message;
            console.error("CATCHED", err.message, "ON", c.name);
        });
    }
    return result;
}

/**
 * Will attempt to create a clan ServerGroup and add a sortID propery permission
 *
 * @version 1.1
 * @memberof Plugin-createClan
 * @param    {Function} jarvis - Middleware Function: Provides access to Jarvis functions.
 * @param    {object} client - The Client which sent a textmessage
 * @param    {String} tag - String value containing a clan's tag name, e.g 'EPIC'
 */
function constructGroup(jarvis, client, tag) {
    let clan_tag = tag.toUpperCase();
    let clid = client.clid;
    jarvis.ts
        .serverGroupCopy(config.ssgid, 0, 1, clan_tag)
        .then(res => {
            let sort_id = getGroupSortID(clan_tag);
            jarvis.ts.serverGroupAddPerm(res.sgid, "i_group_sort_id", sort_id, 0, 0);
        })
        .then(() => {
            client.message(`Clan group: ${clan_tag} added successfully`);
            currentlyCreating[clid].clanTag = clan_tag;

            terminateSession(client, jarvis);
        })
        .catch(err => {
            if (err.message != "database duplicate entry") {
                client.message(jarvis.error_message.external + err.message);
                console.error("CATCHED", err.message);
                terminateSession(client, jarvis);
            }
            client.message(`[b] ${clan_tag} already exists! Try another tag:[/b]`);
        });
}

/**
 * Calculates and returns ServerGroup's 'i_group_sort_id' value for alphabetical sorting.
 * Increments sortid based on clan tag, E.g. 0-9 = +901s, a = 1000s, b = 1100s, c = 1200, etc
 *
 * @version 1.0
 * @memberof Plugin-createClan
 * @param    {String} tag - String value containing a clan's tag name, e.g 'EPIC'
 * @returns {number} - Numerical value used for 'i_group_sort_id' (e.g 'EPIC' => 'E' => 5 * 100 => 901 + 500 => 1401)
 */
function getGroupSortID(tag) {
    let sortid = config.sortID_start;
    let c = tag.toLowerCase().charAt(0);
    const alphabet = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
    let index = alphabet.indexOf(c) + 1;
    return sortid + index * config.sortID_inc;
}

/**
 * Checks for names which are too long and removes trailing or preceding spaces
 *
 * @version 1.0
 * @memberof Plugin-createClan
 * @param    {String} message - Any string
 * @returns {String} - A clean channel name
 */
function sanitation(message) {
    if (message.length > 20) {
        console.info("Channel name too long");
        return [false];
    } else {
        return [true, message.trim()];
    }
}

/**
 * Terminates user from currentlyCreating array, ending session
 * and sends a log of the event to slack
 *
 * @version 1.1
 * @memberof Plugin-createClan
 * @param    {object} client - The Client which sent a textmessage
 * @param    {Function} jarvis - Middleware Function: Provides access to Jarvis functions.
 */
function terminateSession(client, jarvis) {
    let clid = client.clid;

    // Create a log and send to Slack
    let logMessage = [`*JARVIS BOT LOG:*`];
    let nickname = client.nickname;

    // Channel creation Log
    if (currentlyCreating[clid].processid >= 2) {
        let clanName = currentlyCreating[clid].channels[0].name;
        clanName = clanName.split("★");
        logMessage.push(`> ${nickname} Created ${currentlyCreating[clid].channels.length} channels for Clan: ${clanName[1]}`);

        // Channel creation and group log
        if (currentlyCreating[clid].processid == 3) {
            let clanTag = currentlyCreating[clid].clanTag;
            logMessage.push(`> ${nickname} Created a server group for Clan: ${clanName[1]} with the tag: ${clanTag}`);
        }

        // Send to Slack
        if (logMessage.length > 1) {
            jarvis.log_to_slack(logMessage.join("\n"));
        }
    }

    delete currentlyCreating[clid];
    client.message(jarvis.error_message.terminate);
}

/**
 * Active Worker: Which terminates users during creation process, if they've been inactive for a while.
 *
 * @version 1.0
 * @memberof Plugin-createClan
 * @param    {object} helpers - Generic helper object for error messages
 */
exports.run = (helpers, jarvis) => {
    async.forever(function(next) {
        setTimeout(function() {
            // Max user session time in millseconds (3min)
            const maxTime = 180000;
            const currentDate = new Date();
            let currentTime = currentDate.getTime();
            for (let i in currentlyCreating) {
                if (currentlyCreating.hasOwnProperty(i)) {
                    // Terminate sessions longer than maxTime
                    if (currentTime - currentlyCreating[i].date > maxTime) {
                        // Get the Teamspeak-Invoker-Object
                        let invoker = currentlyCreating[i].client;
                        // Notify the invoker
                        invoker.message(helpers.error_message.expired);
                        // Terminate the invoker's session
                        delete currentlyCreating[invoker.clid];
                    }
                }
            }
            next();
        }, 30000);
    });
};