Fic - Mixify Auto Welcome Script (Tijn Remix)

This script can be used on Mixify.com while streaming your DJ set. The main reason why I created this script is that I couldn't see every single person who enters the stream so I thought it could be nice if a script can announce in chat who entered the stream with a warm welcome message.

/// <reference path="Typings/jquery.d.ts" />
/// <reference path="Typings/mixify.d.ts" />
// ==UserScript==
// @name        Fic - Mixify Auto Welcome Script (Tijn Remix)
// @namespace   Booth
// @include     http://www.mixify.com/*/live/*
// @version     1.7.2
// @grant       none
// @description This script can be used on Mixify.com while streaming your DJ set. The main reason why I created this script is that I couldn't see every single person who enters the stream so I thought it could be nice if a script can announce in chat who entered the stream with a warm welcome message.
// ==/UserScript==
// TODO Split settings and implementation scripts
///////////////////////////////////////////////////////////
/////                    SETTINGS                     /////
///////////////////////////////////////////////////////////
/** Turn debug mode on/off (true/false) */
var debugMode = true;
/** Here are all the messages configured */
var messageConfiguration = {
    /** A collection of joining messages. {0} = placeholder for name of the user */
    onJoining: ["Hey {0}", "hey {0}", "sup {0}", "oi {0}", "Hai der, {0}", "hello {0}", "ayy {0}!", "Ermagerd! It's {0}"],
    /** A collection of rejoining messages. {0} = placeholder for name of the user */
    onRejoining: ["wb {0}"],
    /** A collection of messages for special users, based on ID and not on name */
    specialUsers: [
        { id: "world", onJoining: "Wow hello world", onRejoining: "Woot welcome back world!" }
    ]
};
/** Ignore these users by name */
var ignoredUsers = ["Guest"];
/** The minimum amount of time (in milliseconds) before a message gets send */
var messageDelay = 2000;
/** The maximum timespan (in milliseconds) in which the message will be send, after the delay */
var messageMaxTimespan = 18000;
/** Characters that can be removed from a name */
var trimmableCharacters = ["-", "_", "=", ".", ":", "[", "]", "<", ">"];
///////////////////////////////////////////////////////////
/////                   USER STUFF                    /////
///////////////////////////////////////////////////////////
/** Collection class for users */
var UserCollection = (function () {
    function UserCollection() {
        this.users = [];
        this.disallowedUsers = [];
    }
    /**
     * Add a new user
     * @param user User object
     */
    UserCollection.prototype.add = function (user) {
        logToConsole("Trying to add {0}".format(user.name.fullName));
        if (this.userIsAllowed(user)) {
            if (!this.userExists(user.id)) {
                this.users.push(user);
                logToConsole("Succesfully added {0} ({1})".format(user.name.fullName, user.id));
                user.message(messageConfiguration.onJoining);
            }
            else {
            }
        }
        else {
            logToConsole("{0} is not allowed to be added".format(user.name.fullName));
        }
    };
    /**
     * Check if an user is allowed
     * @param name Name of the user
     * @returns { User is allowed }
     */
    UserCollection.prototype.userIsAllowed = function (user) {
        var userAllowedByName = $.inArray(user.name.fullName, this.disallowedUsers) === -1;
        var userAllowedById = $.inArray(user.id, this.disallowedUsers) === -1;
        return userAllowedByName && userAllowedById;
    };
    /**
     * Check if user already exists in the array
     * @param id ID of the user
     * @returns { User exists }
     */
    UserCollection.prototype.userExists = function (id) {
        for (var _i = 0, _a = this.users; _i < _a.length; _i++) {
            var user = _a[_i];
            if (user.id === id) {
                return true;
            }
        }
        return false;
    };
    return UserCollection;
})();
/** User class */
var User = (function () {
    function User(id, name) {
        this.id = id;
        this.name = new Name(name);
        this.messageName = formatName(this.name);
        this.active = true;
        this.isDj = $("#djSilhouette").data("querystring").split("=")[1] === this.id;
    }
    /**
     * Greets an user
     * @param messages Array of possible greetings
     */
    User.prototype.message = function (messages) {
        var _this = this;
        // Determine timeout in ms
        var timeout = messageDelay + (Math.random() * messageMaxTimespan);
        window.setTimeout(function () {
            // First check if user is still in the room, would be silly if not!
            if (_this.isStillInRoom()) {
                logToConsole("Messaging {0} ({1})".format(_this.name.fullName, _this.id));
                // Pick a greeting and send it
                var message = messages[Math.floor(Math.random() * messages.length)];
                sendChatMessage(message.format(_this.messageName));
            }
        }, timeout);
    };
    /**
     * Checks if this user is still present in the room
     * @returns { user is in the room }
     */
    User.prototype.isStillInRoom = function () {
        if (this.isDj) {
            return true;
        }
        var searchResult = $('#avatar_{0}'.format(this.id));
        if (searchResult.length === 0) {
            this.active = false;
        }
        return searchResult.length > 0;
    };
    return User;
})();
///////////////////////////////////////////////////////////
/////                   NAME STUFF                    /////
///////////////////////////////////////////////////////////
var NameImportanceOptions;
(function (NameImportanceOptions) {
    NameImportanceOptions[NameImportanceOptions["None"] = 0] = "None";
    NameImportanceOptions[NameImportanceOptions["Low"] = 1] = "Low";
    NameImportanceOptions[NameImportanceOptions["Moderate"] = 2] = "Moderate";
    NameImportanceOptions[NameImportanceOptions["High"] = 3] = "High";
})(NameImportanceOptions || (NameImportanceOptions = {}));
/** Name implementation */
var Name = (function () {
    function Name(fullName) {
        this.fullName = fullName;
        this.capsAreMeaningful = this.setCapsAreMeaningful(fullName);
        this.createNameParts();
    }
    /**
     * Determines if uppercase characters mean anything in the grand scheme of things
     * @param text The text
     */
    Name.prototype.setCapsAreMeaningful = function (fullName) {
        var amountOfCaps = 0;
        var position = 0;
        var character;
        while (position < fullName.length) {
            character = fullName.charAt(position);
            if (!isNumeric(character) && isUpperCase(character)) {
                amountOfCaps++;
            }
            position++;
        }
        return amountOfCaps / fullName.length <= 0.5;
    };
    /** This extracts name parts out of full name, both parts with and without capital-processing */
    Name.prototype.createNameParts = function () {
        this.parts = [];
        this.fullParts = [];
        // First trim the full name, then split it on space-character, then filter out all zero-length entries
        var trimmedParts = trimText(this.fullName, trimmableCharacters).split(" ").filter(function (x) { return x.length > 0; });
        var parts = [];
        // Iterate over all the trimmed parts
        var position = 0;
        for (var _i = 0; _i < trimmedParts.length; _i++) {
            var trimmedPart = trimmedParts[_i];
            // Split each part on capitals and push them to the 'parts' collection
            this.splitPartsOnCapitals(trimmedPart).filter(function (x) { return x.length > 0; }).map(function (x) { return parts.push(x); });
            // Create a full name part and push it to the collection;
            var fullPart = new NamePart(trimmedPart, position, this);
            this.fullParts.push(fullPart);
            position++;
        }
        // Iterate over all the parts
        position = 0;
        for (var _a = 0; _a < parts.length; _a++) {
            var part = parts[_a];
            // Create a namepart and push it to the collection
            var namePart = new NamePart(part, position, this);
            this.parts.push(namePart);
            position++;
        }
    };
    Name.prototype.splitPartsOnCapitals = function (part) {
        var results = [];
        var remaining = part;
        var consecutiveCaps = 0;
        var position = 0;
        var character;
        while (remaining.length > 0) {
            if (position === remaining.length) {
                results.push(remaining);
                remaining = "";
            }
            character = remaining.charAt(position);
            if (!isNumeric(character) && isUpperCase(character)) {
                if (position !== 0 && consecutiveCaps === 0) {
                    // Add new part
                    results.push(remaining.substring(0, position));
                    // Reset
                    remaining = remaining.substring(position, remaining.length);
                    position = 0;
                    consecutiveCaps = 0;
                }
                else {
                    position++;
                    consecutiveCaps++;
                }
            }
            else {
                if (consecutiveCaps > 1) {
                    var adjustedPosition = position - 1;
                    results.push(remaining.substring(0, adjustedPosition));
                    // Reset
                    remaining = remaining.substring(adjustedPosition, remaining.length);
                    position = 0;
                }
                else {
                    position++;
                }
                consecutiveCaps = 0;
            }
        }
        return results;
    };
    return Name;
})();
/** Name part implementation */
var NamePart = (function () {
    function NamePart(value, position, parent) {
        this.value = value;
        this.position = position;
        this.parent = parent;
        this.setImportance();
    }
    NamePart.prototype.setImportance = function () {
        var importance;
        // Name parts of 1 characters are not important
        if (this.value.length <= 1) {
            importance = NameImportanceOptions.None;
        }
        else if (this.value.length <= 3) {
            // Name parts of 3 characters or less are either very important, or not at all
            if (this.value.toLowerCase() === "dj") {
                // 'DJ' is usually an important part
                importance = NameImportanceOptions.None;
            }
            else if (this.parent.capsAreMeaningful && textIsUppercase(this.value)) {
                // If caps are used meaninful in the name overall and the part has all caps, then it's probably important
                importance = NameImportanceOptions.High;
            }
            else if (this.position === 0 || (this.position !== 0 && this.parent.parts[this.position - 1].importance === NameImportanceOptions.None)) {
                // If the importance isn't determined yet and the word is at the start, high chance it's redundant
                importance = NameImportanceOptions.None;
            }
            else {
                // Else just set it on low importance
                importance = NameImportanceOptions.Low;
            }
        }
        else {
            // Nothing special
            importance = NameImportanceOptions.Moderate;
        }
        this.importance = importance;
    };
    return NamePart;
})();
/** Add a C#-like format function to string, if not already present */
if (!String.prototype.format) {
    String.prototype.format = function () {
        var args = arguments;
        return this.replace(/{(\d+)}/g, function (match, number) { return (typeof args[number] != 'undefined'
            ? args[number]
            : match); });
    };
}
///////////////////////////////////////////////////////////
/////                 DOCUMENT READY                  /////
///////////////////////////////////////////////////////////
// Initialize a new UserCollection
var userList = new UserCollection();
var url;
var dataString;
// Run the script only if you're streaming
if ($('#eventBroadcaster').length > 0) {
    // Add ignored users and yourself
    userList.disallowedUsers = ignoredUsers;
    userList.disallowedUsers.push($('body').attr('data-user-id'));
    // Getting url to call AJAX
    var queryString = $("#specatorsDockItem").attr("data-querystring");
    url = "http://www.mixify.com/room/spectators/cache/1/?" + queryString;
    dataString = queryString.split("=")[1];
    if (sessionStorage.getItem("active") === null) {
        sessionStorage.setItem("active", "true"); /* You entered the stream for the first time */
    }
    else {
        retrieveAttendees();
    }
    var djQuery = $("#djSilhouette").data("querystring");
    var djId = djQuery.split("=")[1];
    var djName = getUsernameFromUserData(djQuery);
    userList.add(new User(djId, djName));
    // Everytime the DOM tree gets modified, fire this event
    // TODO Add NodeRemoved to detect a user that leaves
    $('#avatarContainer').bind("DOMNodeInserted", function (e) {
        var element = $(e.target);
        // Only continue if th element that is being added has the 'avatar' class
        if (element.attr("class") === "avatar") {
            // Get the ID
            var id = element.attr("id").split("_")[1];
            var querystring = element.attr("data-querystring");
            // Get the username
            var username = getUsernameFromUserData(querystring);
            userList.add(new User(id, username));
        }
    });
}
///////////////////////////////////////////////////////////
/////                  MIXIFY STUFF                   /////
///////////////////////////////////////////////////////////
/**
 * Retrieve all attendees
 * @todo Make it return a collection of users instead
 */
function retrieveAttendees() {
    // Wait for fc() to be available
    // TODO Figure out if this can be done better
    if (fc() != null) {
        logToConsole("Retrieving attendance list");
        // Get data from the spectator cache
        jQuery.ajaxSetup({ async: false });
        var data = jQuery.get(url);
        var responseHtml = $(data.responseText);
        // Search for module-info elements in the response and iterate through it
        // TODO Turn into a more useful method
        $(responseHtml).find('.module-info').each(function (index, element) {
            // Find the username and id and initialize a new user based on it
            var jqueryElement = $(element);
            var username = jqueryElement.find('.username')[0].innerHTML;
            var id = jqueryElement.find('.bt-wrapper').attr('data-uuid');
            userList.add(new User(id, username));
        });
    }
}
/**
 * Sends message to chat
 * @param message Message
 */
function sendChatMessage(message) {
    fc().fc_sendChat(message, dataString);
}
/**
 * Logs to console, if debug mode is true
 * @param message Message to log
 * @param optionalParams Optional parameters
 */
function logToConsole(message) {
    var optionalParams = [];
    for (var _i = 1; _i < arguments.length; _i++) {
        optionalParams[_i - 1] = arguments[_i];
    }
    if (debugMode) {
        console.log(message, optionalParams);
    }
}
/**
 * Get the username from querying the user data
 * @param query query string
 * @returns { Username }
 */
function getUsernameFromUserData(query) {
    // Get the user data
    var data = getUserData(query);
    // Parse the response for the username
    var responseText = $(data.responseText);
    var usernameElement = $(responseText).find(".username");
    if (usernameElement.length === 0) {
        logToConsole("No username found for user using query {0}".format(query));
    }
    return usernameElement[0].innerHTML;
}
/**
 * Gets the user data with a query string
 * @param query query string
 * @returns { GET response }
 */
function getUserData(query) {
    // Get data from the user api
    jQuery.ajaxSetup({ async: false });
    return jQuery.get("http://www.mixify.com/user/info-basic/?{0}".format(query));
}
///////////////////////////////////////////////////////////
/////                  NAME FORMATTING                /////
///////////////////////////////////////////////////////////
function formatName(name) {
    // Get all the parts that we'll work with
    var nameParts = getSignificantNameParts(name.parts);
    // If all parts have 'none' importance, we'll try to get significant name parts using the full parts
    // These don't have the capital processed parts
    if (nameParts.every(function (x) { return x.importance === NameImportanceOptions.None; })) {
        nameParts = getSignificantNameParts(name.fullParts);
    }
    if (nameParts.length === 2) {
        return nameParts[0].value;
    }
    if (nameParts.length > 2) {
        if (nameParts.join("").length / nameParts.length > 5) {
            return nameParts.map(function (x) { return x.value[0].toUpperCase(); }).join("");
        }
        var firstLargestPart = nameParts[0];
        for (var _i = 0; _i < nameParts.length; _i++) {
            var namePart = nameParts[_i];
            if (namePart.value.length > firstLargestPart.value.length) {
                firstLargestPart = namePart;
            }
        }
        return firstLargestPart.value;
    }
    // Join all parts
    return nameParts.map(function (x) { return x.value; }).join(" ");
}
function getSignificantNameParts(nameParts) {
    // If there are any high important parts, return the first one
    // TODO: Determine if there are better alternatives to returning the first item
    var highImportance = nameParts.filter(function (value) { return value.importance === NameImportanceOptions.High; });
    if (highImportance.length > 0) {
        return [highImportance[0]];
    }
    // If there's only 1 moderate important part, return that one
    var moderateImportance = nameParts.filter(function (value) { return value.importance === NameImportanceOptions.Moderate; });
    if (moderateImportance.length === 1) {
        return moderateImportance;
    }
    // If the amount of low importance parts is higher than 0 and lower than the amount of moderate parts, then return low + moderate parts
    var lowImportance = nameParts.filter(function (value) { return value.importance === NameImportanceOptions.Low; });
    if (lowImportance.length > 0 && lowImportance.length < moderateImportance.length) {
        return nameParts.filter(function (value) { return value.importance === NameImportanceOptions.Moderate || value.importance === NameImportanceOptions.Low; });
    }
    // If at this point there are moderate parts, return those
    if (moderateImportance.length !== 0) {
        return moderateImportance;
    }
    if (lowImportance.length !== 0) {
        return lowImportance;
    }
    // Return whatever is left
    return nameParts;
}
///////////////////////////////////////////////////////////
/////                  GENERAL STUFF                  /////
///////////////////////////////////////////////////////////
/**
 * Checks if a text is all uppercase
 * @param text Text
 */
function textIsUppercase(text) {
    var position = 0;
    var character;
    while (position < text.length) {
        character = text.charAt(position);
        // If any character is a numeric, or matches a lowercase, then the text isn't fully uppercase
        if (isNumeric(character) || isLowerCase(character)) {
            return false;
        }
        position++;
    }
    return true;
}
function isLetter(character) {
    // Cheeky way to determine if it's a letter
    return character.toLowerCase() !== character.toUpperCase();
}
function isNumeric(character) {
    return (character >= "0" && character <= "9");
}
function isUpperCase(character) {
    return character === character.toUpperCase();
}
function isLowerCase(character) {
    return character === character.toLowerCase();
}
/**
 * Trims a string
 * @param text
 * @param trimCharacters
 */
function trimText(text, trimCharacters) {
    var processedName = text;
    for (var _i = 0; _i < trimCharacters.length; _i++) {
        var trimCharacter = trimCharacters[_i];
        processedName = processedName.split(trimCharacter).join(" ");
    }
    // Replace trailing numerics (regex) and trim
    return processedName.replace(/\d+$/, "").trim();
}