コンテンツにスキップ

「利用者:Dragoniez/scripts/dragoLib.js」の版間の差分

削除された内容 追加された内容
copyToClipboard should preserve line breaks
バグ修正 (特別:差分/89748151)
6行目: 6行目:
if (typeof dragoLib === 'undefined') var dragoLib = {};
if (typeof dragoLib === 'undefined') var dragoLib = {};


/*global mediaWiki, jQuery */
(function() { // Create a function scope
(function(mw, $) { // Create a function scope


// ******************************** SYNCHRONOUS FUNCTIONS ********************************
// ******************************** SYNCHRONOUS FUNCTIONS ********************************
187行目: 188行目:
d.setDate(d.getDate() - subtract);
d.setDate(d.getDate() - subtract);
}
}
d.setDate(d.getDate() +2);


var sectionName;
var sectionName;
902行目: 902行目:
}
}


})();
})(mediaWiki, jQuery);
//</nowiki>
//</nowiki>

2022年5月29日 (日) 09:11時点における版

/****************************************
 * dragoLib: Versatile function library *
 ****************************************/
//<nowiki>

if (typeof dragoLib === 'undefined') var dragoLib = {};

/*global mediaWiki, jQuery */
(function(mw, $) { // Create a function scope

// ******************************** SYNCHRONOUS FUNCTIONS ********************************

/** 
 * Extract templates from wikitext
 * @param {string} wikitext
 * @param {string} [templateName] case-insensitive
 * @returns {Array}
 */
dragoLib.findTemplates = function(wikitext, templateName) {
    if (paramsMissing(arguments, 1)) throwError('findTemplates');

    // Split the wikitext with '{{', the head delimiter of templates
    const tempInnerContent = wikitext.split('{{'); // Note: tempInnerContent[0] is always an empty string or a string that has nothing to do with templates
    const templates = []; // The array of extracted templates to return

    // Extract templates from the wikitext
    if (tempInnerContent.length === 0) { // If the wikitext has no tempalte in it

        return templates; // Return an empty array

    } else { // If the wikitext has some templates in it

        const nest = []; // Stores the element number of tempInnerContent if the element involves nested templates
        for (let i = 1; i < tempInnerContent.length; i++) { // Loop through all elements in tempInnerContent (except tempInnerContent[0])

            let tempTailCnt = (tempInnerContent[i].match(/\}\}/g) || []).length; // The number of '}}' in the split segment
            let temp = ''; // Temporary escape hatch for templates

            if (tempTailCnt === 0) { // The split segment not having any '}}' means that it nests another template

                nest.push(i); // Push the element number into the array

            } else if (tempTailCnt === 1) { // The split segment itself is the whole inner content of one template

                temp = '{{' + tempInnerContent[i].split('}}')[0] + '}}';
                if ($.inArray(temp, templates) === -1) templates.push(temp);

            } else if (tempTailCnt > 1) { // The split segment is part of more than one template (e.g. TL2|...}}...}} )

                for (let j = 0; j < tempTailCnt; j++) { // Loop through all the nests

                    if (j === 0) { // The innermost template

                        temp = '{{' + tempInnerContent[i].split('}}')[j] + '}}'; // Same as when tempTailCnt === 1
                        if ($.inArray(temp, templates) === -1) templates.push(temp);

                    } else { // Nesting templates

                        const elNum = nest[nest.length -1]; // The start of the nesting template
                        nest.pop();
                        const nestedTempInnerContent = tempInnerContent[i].split('}}');

                        temp = '{{' + tempInnerContent.slice(elNum, i).join('{{') + '{{' + nestedTempInnerContent.slice(0, j + 1).join('}}') + '}}';
                        if ($.inArray(temp, templates) === -1) templates.push(temp);

                    }

                }

            }

        }

        // Check if the optional parameter is specified
        if (templateName && templates.length !== 0) {
            const templateRegExp = new RegExp(templateName, 'i');
            for (let i = templates.length -1; i >= 0; i--) {
                // Remove the template from the array if it's not an instance of the specified template
                if (templates[i].split('|')[0].search(templateRegExp) === -1) templates.splice(i, 1);
            }
        }

        return templates;
    }
};

/**
 * Check if the current user belongs to a given user group
 * @param {string} group
 * @returns {boolean}
 */
dragoLib.inGroup = function(group) {
    if (paramsMissing(arguments, 1)) throwError('inGroup');
    return $.inArray(group, mw.config.get('wgUserGroups')) !== -1;
};

/**
 * Check if the current user belongs to a given global user group
 * @param {string} group
 * @returns {boolean}
 */
dragoLib.inGlobalGroup = function(group) {
    if (paramsMissing(arguments, 1)) throwError('inGlobalGroup');
    return mw.config.exists('wgGlobalGroups') && $.inArray(group, mw.config.get('wgGlobalGroups')) !== -1;
};

/**
 * Get the key of a value in an object
 * @param {Object} object 
 * @param {*} value 
 * @returns {*} key
 */
dragoLib.getKeyByValue = function(object, value) {
    if (paramsMissing(arguments, 2)) throwError('getKeyByValue');
    for (let key in object) {
        if (object[key] == value) return key;
    }
};

/**
 * Copy a string to the clipboard
 * @param {string} str
 */
dragoLib.copyToClipboard = function(str) {
    if (paramsMissing(arguments, 1)) throwError('copyToClipboard');
    const $temp = $('<textarea>');
    $('body').append($temp); // Create a temporarily hidden text field
    $temp.val(str).select(); // Copy the text string into the field and select the text
    document.execCommand('copy'); // Copy it to the clipboard
    $temp.remove(); // Remove the text field
};

/**
 * Add, remove, or move a loading spinner
 * @param {string} action 'add', 'remove', or 'move'
 */
dragoLib.toggleLoadingSpinner = function(action) {
    if (paramsMissing(arguments, 1)) throwError('toggleLoadingSpinner');
    if ($.inArray(action, ['add', 'remove', 'move']) === -1) throwInvalidParamError('toggleLoadingSpinner', action);
    const img = '<img class="dragolib-loading-spinner" src="https://upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" ' +
                'style="vertical-align: middle; max-height: 100%; border: 0;">';
    switch(action) {
        case 'add':
            return img;
        case 'remove':
            $('.dragolib-loading-spinner').remove();
            return '';
        case 'move':
            $('.dragolib-loading-spinner').remove();
            return img;
    }
};

// Get today's date in the form of MM月DD日
dragoLib.today = function() {
    const d = new Date();
    return d.getMonth() + 1 + '月' + d.getDate() + '日';
};

/**
 * Get the last day of a given month
 * @param {number} year
 * @param {number} month
 * @returns {number}
 */
dragoLib.lastDay = function(year, month) {
    if (paramsMissing(arguments, 2)) throwError('lastDay');
    return new Date(year, month, 0).getDate();
};

/**
 * Get 'YYYY年MM月D1日 - D2日新規XX', corresponding to the current date
 * @param {string} suffix The XX part
 * @param {boolean} [last] If true, go back 5 days (get the name of the preceding section)
 * @returns {string} section name
 */
dragoLib.getSection5 = function(suffix, last) {
    if (paramsMissing(arguments, 1)) throwError('getSection5');

    const d = new Date();
    if (last) {
        let subtract = 5;
        if (d.getDate() === 1 || d.getDate() === 2) {
            subtract = 3;
        } else if (d.getDate() === 31) {
            subtract = 6;
        }
        d.setDate(d.getDate() - subtract);
    }

    var sectionName;
    switch(true) {
        case (1 <= d.getDate() && d.getDate() <= 5):
            sectionName = `${d.getFullYear()}${d.getMonth() + 1}月1日 - 5日新規${suffix}`;
            break;
        case (6 <= d.getDate() && d.getDate() <= 10):
            sectionName = `${d.getFullYear()}${d.getMonth() + 1}月6日 - 10日新規${suffix}`;
            break;
        case (11 <= d.getDate() && d.getDate() <= 15):
            sectionName = `${d.getFullYear()}${d.getMonth() + 1}月11日 - 15日新規${suffix}`;
            break;
        case (16 <= d.getDate() && d.getDate() <= 20):
            sectionName = `${d.getFullYear()}${d.getMonth() + 1}月16日 - 20日新規${suffix}`;
            break;
        case (21 <= d.getDate() && d.getDate() <= 25):
            sectionName = `${d.getFullYear()}${d.getMonth() + 1}月21日 - 25日新規${suffix}`;
            break;
        case (26 <= d.getDate() && d.getDate() <= dragoLib.lastDay(d.getFullYear(), d.getMonth() + 1)):
            sectionName = `${d.getFullYear()}${d.getMonth() + 1}月26日 - ${dragoLib.lastDay(d.getFullYear(), d.getMonth() + 1)}日新規${suffix}`;
            break;
        default:
    }
    return sectionName;

};

/**
 * Center a jQuery UI dialog (jQuery UI must be loaded)
 * @param {string} selectorName 
 */
dragoLib.centerDialog = function(selectorName) {
    if (paramsMissing(arguments, 1)) throwError('centerDialog');
    $(selectorName).dialog({'position': {my: 'center', at: 'center', of: window}});
};

/**
 * Change the CSS of a jQuery UI dialog (jQuery UI must be loaded)
 * @param {string} headerColor 
 * @param {string} backgroundColor 
 * @param {string|number} [fontSize] 
 */
dragoLib.dialogCSS = function(headerColor, backgroundColor, fontSize) {
    if (paramsMissing(arguments, 2)) throwError('dialogCSS');
    $('.ui-dialog-content, .ui-dialog-buttonpane, .ui-corner-all, .ui-draggable, .ui-resizable').css('background', backgroundColor);
    $('.ui-button').css({
        'color': 'black',
        'background-color': 'white'
    });
    $('.ui-dialog-titlebar, .ui-dialog-titlebar-close').attr('style', `background: ${headerColor} !important;`);
    if (fontSize) $('.ui-dialog').css('font-size', fontSize);
};

/**
 * Get rid of U+200E spaces from a string and trim it
 * @param {string} str 
 * @returns {string}
 */
dragoLib.trim2 = function(str) {
    if (paramsMissing(arguments, 1)) throwError('trim2');
    return str.replace(/\u200e/g, '').trim();
};

/**
 * Replace all occurences of a string with another
 * @param {string} str source string
 * @returns {string}
 */
dragoLib.replaceAll2 = function(str) { // Takes a replacee and a replacer (iterable)
    if (paramsMissing(arguments, 3)) throwError('replaceAll2');
    if (arguments.length %2 !== 1) {
        throw 'ReferenceError: dragoLib.replaceAll2 takes an odd number of arguments.';
    } else {
        for (let i = 1; i < arguments.length; i += 2) {
            str = str.split(arguments[i]).join(arguments[i + 1]);
        }
        return str;
    }
};

/**
 * Escape regular expressions
 * @param {string} str 
 * @param {boolean} escapePipes 
 * @returns {string}
 */
dragoLib.escapeRegExp = function(str, escapePipes) {
    if (paramsMissing(arguments, 1)) throwError('escapeRegExp');
    if (typeof str !== 'string') throwTypeError('escapeRegExp', 'String');
    const rep = ['\\', '(', ')', '{', '}', '.', '?', '!', '*', '+', '-', '^', '$', '[', ']'];
    if (escapePipes) rep.push('|');
    for (i = 0; i < rep.length; i++) {
        str = str.split(rep[i]).join('\\' + rep[i]);
    }
    return str;
};

// ******************************** ASYNCHRONOUS FUNCTIONS ********************************

/**
 * Get the timestamps of the latest revision and the current time
 * @param {string} pagename 
 * @returns {Promise<{baseTS: string, curTS: string}>}
 */
dragoLib.getTimestamps = function(pagename) {
    if (paramsMissing(arguments, 1)) throwError('getTimestamps');
    return new Promise(function(resolve) {
        new mw.Api().get({
            'action': 'query',
            'titles': pagename,
            'prop': 'revisions',
            'curtimestamp': true,
            'formatversion': 2
        }).then(function(res){
            var resPages;
            if (res && res.query && (resPages = res.query.pages)) {
                return resolve({
                    'baseTS': resPages[0].revisions[0].timestamp,
                    'curTS': res.curtimestamp
                });
            }
            resolve();
        }).catch(function() {
            resolve();
        });
    });
};

/**
 * Parse a page and get its content as wikitext and section info as an array
 * @param {string} pagename 
 * @param {string|Array} [sectionTitles]
 * @returns {Promise<{wikitext: string|Array, sections: Array, sectionNumber: string}>} wikitext is a string of the whole page if sectionTitles isn't specified,
 * or else an array. sectionNumber is a number converted from the FIRST element in sectionTitles.
 */
dragoLib.parsePage = function(pagename, sectionTitles) {
    if (paramsMissing(arguments, 1)) throwError('parsePage');
    if (typeof sectionTitles === 'string') sectionTitles = [sectionTitles];

    // API request
    return new Promise(function(resolve) {
        new mw.Api().get({
            'action': 'parse',
            'page': pagename,
            'prop': 'wikitext|sections',
            'disablelimitreport': 1,
            'disableeditsection': 1,
            'formatversion': 2
        }).then(function(res) {
            var wikitext, sections, sectionNumber;
            if (res && res.parse) {
                wikitext = res.parse.wikitext;
                sections = res.parse.sections;

                if (sectionTitles) { // If section titles have been passed as a parameter
                    const allSectionTitles = [];
                    const sectionHeaders = ['']; // Array of equal-enclosed section headers (e.g. == SECTION ==) (Note: the top section has no header, thus arr[0] = '')

                    // Get section titles and their corresponding equal-enclosed wikitext
                    for (let i = 0; i < sections.length; i++) {
                        const section = sections[i];
                        if (section.line === sectionTitles[0] && !sectionNumber) sectionNumber = section.index; // Get section number if conditions are met
                        if (section.index.indexOf('T') === -1) { // If the section isn't a transcluded one
                            allSectionTitles.push(section.line); // Get the title of the section
                            if (section.level == 2) { // Get equal-enclosed section headers
                                sectionHeaders.push('== ' + section.line + ' ==');
                            } else if (section.level == 3) {
                                sectionHeaders.push('=== ' + section.line + ' ===');
                            } else if (section.level == 4) {
                                sectionHeaders.push('==== ' + section.line + ' ====');
                            } else if (section.level == 5) {
                                sectionHeaders.push('===== ' + section.line + ' =====');
                            }
                        }
                    }

                    // Separate the content of the parsed page into the content of each section
                    var sectionRegExp = new RegExp(`={2,5}\\s*(?:${dragoLib.escapeRegExp(allSectionTitles.join('|'))})\\s*={2,5}`, 'g');
                    var sectionContent = wikitext.split(sectionRegExp); // Array of the content of each section, without section headers
                    for (let i = 0; i < sectionContent.length; i++) { // Re-add to sectionContent the '== TITLE ==' header that's been removed by the split() above
                        sectionContent[i] = sectionHeaders[i] + sectionContent[i];
                    }

                    // Remove the contents of irrelevant sections from the array 'sectionContent'
                    sectionRegExp = new RegExp(`={2,5}\\s*(?:${dragoLib.escapeRegExp(sectionTitles.join('|'))})\\s*={2,5}`, 'g');
                    for (let i = sectionContent.length -1; i >= 0; i--) {
                        if (sectionContent[i].search(sectionRegExp) === -1) sectionContent.splice(i, 1);
                    }

                }

                return resolve({
                    'wikitext': sectionContent ? sectionContent : wikitext, 
                    'sections': sections,
                    'sectionNumber': sectionNumber // Only the first match (Note: This is a string, not a number)
                });
            }
            resolve();
        }).catch(function() {
            resolve();
        });
    });
};

/**
 * Edit a given page
 * @param {string} pagename 
 * @param {string} text 
 * @param {string} inserttype 'prependtext', 'appendtext', 'text'
 * @param {string} basetimestamp 
 * @param {string} starttimestamp 
 * @param {string|number} [sectionnumber] 
 * @param {string} [summary] 
 * @param {string} [token] Give e.g. '' or something when you want to cause an intentional error
 * @returns {Promise<boolean|string>} true when edit succeeded, false when an unknown error occurred, errcode when a known error occurred 
 */
dragoLib.editPage = function(pagename, text, inserttype, basetimestamp, starttimestamp, sectionnumber, summary, token) {
    if (paramsMissing(arguments, 5)) throwError('editPage');
    return new Promise(function(resolve) {

        const params = {
            'action': 'edit',
            'title': pagename,
            'basetimestamp': basetimestamp,
            'starttimestamp': starttimestamp,
            'token': typeof token === 'undefined' ? mw.user.tokens.get('csrfToken') : token,
            'format': 'json'
        };
        if ($.inArray(inserttype, ['text', 'appendtext', 'prependtext']) === -1) throwInvalidParamError('editPage', inserttype);
        params[inserttype] = text;
        if (typeof sectionnumber !== 'undefined') params.section = sectionnumber;
        if (typeof summary !== 'undefined') params.summary = summary;

        new mw.Api().post(params).then(function(res) {
            if (res && res.edit) {
                if (res.edit.result === 'Success') return resolve(true);
            }
            resolve(false);
        }).catch(function(code, err) {
            resolve(err.error.info);
        });

    });
};

/**
 * Check if a user exists locally
 * @param {string} username 
 * @returns {Promise<boolean>}
 */
dragoLib.userExists = function(username) {
    if (paramsMissing(arguments, 1)) throwError('userExists');
    return new Promise(function(resolve) {
        new mw.Api().get({
            'action': 'query',
            'list': 'users',
            'ususers': username,
            'formatversion': 2
        }).then(function(res){
            resolve(res.query.users[0].userid !== undefined); // True if the user exists and false if not
        }).catch(function() {
            resolve();
        });
    });
};

/**
 * Convert wikitext to html
 * @param {string} wikitext
 * @param {string} [wikisummary]
 * @returns {Promise<{htmltext: string, htmlsummary: string}>}
 */
dragoLib.getParsedHtml = function(wikitext, wikisummary) {
    if (paramsMissing(arguments, 1)) throwError('getParsedHtml');
    return new Promise(function(resolve) {
        new mw.Api().post({
            'action': 'parse',
            'text': wikitext,
            'summary': typeof wikisummary === 'undefined' ? '' : wikisummary,
            'contentmodel': 'wikitext',
            'prop': 'text',
            'disableeditsection': true,
            'formatversion': 2
        }).then(function(res) {
            if (res && res.parse) {
                return resolve({
                    'htmltext': res.parse.text,
                    'htmlsummary': res.parse.parsedsummary
                });
            }
            resolve();
        }).catch(function(code, err) {
            console.log(err.error.info);
            resolve();
        });
    });
};

/**
 * Watch pages
 * @param {Array} pagesArr 
 * @returns {Array} Array of watched pages (returns undefined if watchtoken failed to be fetched)
 */
dragoLib.watchPages = function(pagesArr) {

    const pgArr = JSON.parse(JSON.stringify(pagesArr)); // Prevent pass-by-reference (the variable will be spliced)
    if (paramsMissing(arguments, 1)) throwError('watchPages');
    if (!Array.isArray(pgArr)) throwTypeError('watchPages', 'Array');
    if (pgArr.length === 0) {
        console.log('dragoLib.watchPages: The passed array is empty.');
        return [];
    }

    return new Promise(function(resolve) {
        new mw.Api().get({
            'action': 'query',
            'meta': 'tokens',
            'type': 'watch',
            'format': 'json'
        }).then(async function(res){
            var token, watchAsync = [];
            if (!res || !res.query || !(token = res.query.tokens)) {
                mw.log.error('dragoLib.watchPages: Failed to get a watchtoken.');
                return resolve();
            } else {
                token = token.watchtoken;
            }
            while(pgArr.length !== 0) {
                watchAsync.push(addToWatchlist(pgArr.slice(0, 50), token));
                pgArr.splice(0, 50);
            }
            const result = await Promise.all(watchAsync);
            resolve([].concat.apply([], result));
        }).catch(function(){
            mw.log.error('dragoLib.watchPages: Failed to get a watchtoken.');
            resolve();
        });
    });

    /**
     * @param {Array} pages 
     * @param {string} token 
     * @returns {Promise<Array>}
     */
    function addToWatchlist(pages, token) {
        return new Promise(function(resolve) {
            new mw.Api().post({
                'action': 'watch',
                'titles': pages.join('|'),
                'token': token,
                'formatversion': 2
            }).then(function(res) {
                const watched = [];
                if (res) {
                    for (let i = 0; i < res.watch.length; i++) {
                        if (res.watch[i].watched == true) watched.push(res.watch[i].title);
                    }
                } else {
                    mw.log.warn('dragoLib.watchPages: Unexpected error occurred on a watch-pages attempt.');
                }
                resolve(watched);
            }).catch(function(code, err) {
                mw.log.warn('dragoLib.watchPages: Error occurred on a watch-pages attempt: ' + err.error.info);
                resolve([]);
            });
        });
    }

};

/**
 * Get an array of users and IPs that are banned from editing (in any way) from an array of random users and IPs
 * @param {Array} namesArr 
 * @returns
 */
dragoLib.getRestricted = async function(namesArr) {

    if (paramsMissing(arguments, 1)) throwError('getRestricted');
    if (!Array.isArray(namesArr)) throwTypeError('getRestricted', 'Array');
    if (namesArr.length === 0) {
        console.log('dragoLib.getRestricted: The passed array is empty.');
        return [];
    }

    // Check the local block status of all names in the array
    var restricted = await dragoLib.getBlockedUsers(namesArr);
    namesArr = namesArr.filter(function(name) { // Remove elements in namesArr that are also in restricted
        return restricted.indexOf(name) < 0;
    });
    if (namesArr.length === 0) return restricted;

    // Check the rest of the names
    const users = [], ips = [];
    for (let i = 0; i < namesArr.length; i++) { // Sort names to users and IPs
        if (mw.util.isIPAddress(namesArr[i], true)) {
            ips.push(namesArr[i]);
        } else {
            users.push(namesArr[i]);
        }
    }
    if (users.length !== 0 && ips.length !== 0) {
        restricted = restricted.concat(await dragoLib.getBlockedIps(ips), await dragoLib.getLockedUsers(users), await dragoLib.getGloballyBlockedIps(ips));
    } else if (users.length !== 0) {
        restricted = restricted.concat(await dragoLib.getLockedUsers(users));
    } else if (ips.length !== 0) {
        restricted = restricted.concat(await dragoLib.getBlockedIps(ips), await dragoLib.getGloballyBlockedIps(ips));
    }
    restricted = restricted.filter(function(element, index) { // Remove duplicates
        return restricted.indexOf(element) === index;
    });
    return restricted;

};

/**
 * Get an array of blocked users from an array of users and IPs (Note: This function does not detect single IPs in blocked ranges)
 * @param {Array} usersArr
 * @returns {Promise<Array>}
 */
dragoLib.getBlockedUsers = function(usersArr) {

    const users = JSON.parse(JSON.stringify(usersArr)); // Prevent pass-by-reference (the variable will be spliced)
    if (paramsMissing(arguments, 1)) throwError('getBlockedUsers');
    if (!Array.isArray(users)) throwTypeError('getBlockedUsers', 'Array');
    if (users.length === 0) {
        console.log('dragoLib.getBlockedUsers: The passed array is empty.');
        return [];
    }

    return new Promise(async function(resolve) {
        const blockedAsync = [];
        while(users.length !== 0) {
            blockedAsync.push(gbApiQuery(users.slice(0, 50)));
            users.splice(0, 50);
        }
        const result = await Promise.all(blockedAsync);
        resolve([].concat.apply([], result));
    });
    
    /**
     * @param {Array} arr 
     * @returns {Array}
     */
    function gbApiQuery(arr) {
        return new Promise(function(resolve) {
            new mw.Api().post({
                'action': 'query',
                'list': 'blocks',
                'bklimit': 50,
                'bkusers': arr.join('|'),
                'bkprop': 'user',
                'formatversion': 2
            }).then(function(res) {
                const blocked = [];
                var resBlk;
                if (res && (resBlk = res.query)) {
                    if ((resBlk = resBlk.blocks).length !== 0) {
                        for (let i = 0; i < resBlk.length; i++) {
                            blocked.push(resBlk[i].user); // Push blocked users into the array
                        }
                    }
                } else {
                    mw.log.warn('dragoLib.getBlockedUsers: Unexpected error occurred on a check-blocks attempt.');
                }
                resolve(blocked);
            }).catch(function(code, err) {
                mw.log.warn('dragoLib.getBlockedUsers: Error occurred on a check-blocks attempt: ' + err.error.info);
                resolve([]);
            });
        });
    }

};

/**
 * Check if a user is locally blocked
 * @param {string} user Can be any of a registered user, an IP, and an IP range
 * @returns {Promise<boolean>}
 */
dragoLib.isBlocked = function(user) {
    if (paramsMissing(arguments, 1)) throwError('isBlocked');
    if (typeof user !== 'string') throwTypeError('isBlocked', 'String');
    return new Promise(function(resolve) {
        new mw.Api().get({
            'action': 'query',
            'list': 'blocks',
            'bklimit': 1,
            'bkusers': user,
            'bkprop': 'user',
            'formatversion': 2
        }).then(function(res) {
            if (res && res.query) {
                resolve(res.query.blocks.length !== 0);
            } else {
                mw.log.error('dragoLib.isBlocked: Unexpected error occurred on a check-block attempt.');
                resolve();
            }
        }).catch(function(code, err) {
            mw.log.error('dragoLib.isBlocked: Error occurred on a check-block attempt: ' + err.error.info);
            resolve();
        });
    });
};

/**
 * Get an array of locked users from an array of registered users
 * @param {Array} regUsersArr 
 * @returns {Promise<Array>}
 */
dragoLib.getLockedUsers = function(regUsersArr) {

    if (paramsMissing(arguments, 1)) throwError('getLockedUsers');
    if (!Array.isArray(regUsersArr)) throwTypeError('getLockedUsers', 'Array');
    if (regUsersArr.length === 0) {
        console.log('dragoLib.getLockedUsers: The passed array is empty.');
        return [];
    }

    return new Promise(async function(resolve) {
        const lockedAsync = [], lockedUsers = [];
        for (let i = 0; i < regUsersArr.length; i++) {
            lockedAsync.push(dragoLib.isLocked(regUsersArr[i]).then(function(locked) {
                if (locked) lockedUsers.push(regUsersArr[i]);
            }));
        }
        await Promise.all(lockedAsync);
        resolve(lockedUsers);
    });

};

/**
 * Check if a user is globally locked
 * @param {string} user 
 * @returns {Promise<boolean>}
 */
dragoLib.isLocked = function(user) {
    if (paramsMissing(arguments, 1)) throwError('isLocked');
    if (typeof user !== 'string') throwTypeError('isLocked', 'String');
    return new Promise(function(resolve) {
        new mw.Api().get({
            action: 'query',
            list: 'globalallusers',
            agulimit: 1,
            agufrom: user,
            aguto: user,
            aguprop: 'lockinfo'
        }).then(function(res) {
            var resLck;
            if (res && res.query && (resLck = res.query.globalallusers)) {
                resolve(resLck.length === 0 ? false : resLck[0].locked !== undefined); // resLck[0].locked === '' if locked, otherwise undefined
            } else {
                mw.log.error('dragoLib.isLocked: Unexpected error occurred on a check-lock attempt.');
                resolve();
            }
        }).catch(function(code, err) {
            mw.log.error('dragoLib.isLocked: Error occurred on a check-lock attempt: ' + err.error.info);
            resolve();
        });
    });
};

/**
 * Get an array of locally blocked IPs from an array of random IPs (can detect range blocks)
 * @param {Array} ipsArr 
 * @returns {Promise<Array>}
 */
dragoLib.getBlockedIps = function(ipsArr) {

    if (paramsMissing(arguments, 1)) throwError('getBlockedIps');
    if (!Array.isArray(ipsArr)) throwTypeError('getBlockedIps', 'Array');
    if (ipsArr.length === 0) {
        console.log('dragoLib.getBlockedIps: The passed array is empty.');
        return [];
    }

    return new Promise(async function(resolve) {
        const blockedAsync = [], blockedIps = [];
        for (let i = 0; i < ipsArr.length; i++) {
            blockedAsync.push(dragoLib.isIpBlocked(ipsArr[i]).then(function(blocked) {
                if (blocked) blockedIps.push(ipsArr[i]);
            }));
        }
        await Promise.all(blockedAsync);
        resolve(blockedIps);
    });

};

/**
 * Check if a given IP is locally blocked (can detect range blocks)
 * @param {string} ip 
 * @returns {Promise<boolean>}
 */
dragoLib.isIpBlocked = function(ip) {
    if (paramsMissing(arguments, 1)) throwError('isIpBlocked');
    if (typeof ip !== 'string') throwTypeError('isIpBlocked', 'String');
    return new Promise(function(resolve) {
        new mw.Api().get({
            'action': 'query',
            'list': 'blocks',
            'bklimit': 1,
            'bkip': ip,
            'bkprop': 'user',
            'formatversion': 2
        }).then(function(res) {
            if (res && res.query) {
                resolve(res.query.blocks.length !== 0);
            } else {
                mw.log.error('dragoLib.isIpBlocked: Unexpected error occurred on a check-block attempt.');
                resolve();
            }
        }).catch(function(code, err) {
            mw.log.error('dragoLib.isIpBlocked: Error occurred on a check-block attempt: ' + err.error.info);
            resolve();
        });
    });
};

/**
 * Function to get an array of globally blocked IPs from an array of random IPs
 * @param {Array} ipsArr
 * @returns {Promise<Array>}
 */
dragoLib.getGloballyBlockedIps = function(ipsArr) {

    if (paramsMissing(arguments, 1)) throwError('getGloballyBlockedIps');
    if (!Array.isArray(ipsArr)) throwTypeError('getGloballyBlockedIps', 'Array');
    if (ipsArr.length === 0) {
        console.log('dragoLib.getGloballyBlockedIps: The passed array is empty.');
        return [];
    }

    return new Promise(async function(resolve) {
        const blockedAsync = [], blockedIps = [];
        for (let i = 0; i < ipsArr.length; i++) {
            blockedAsync.push(dragoLib.isIpGloballyBlocked(ipsArr[i]).then(function(blocked) {
                if (blocked) blockedIps.push(ipsArr[i]);
            }));
        }
        await Promise.all(blockedAsync);
        resolve(blockedIps);
    });

};

/**
 * Check if a given IP is globally blocked (can detect range blocks)
 * @param {string} ip 
 * @returns {Promise<boolean>}
 */
dragoLib.isIpGloballyBlocked = function(ip) {
    if (paramsMissing(arguments, 1)) throwError('isIpGloballyBlocked');
    if (typeof ip !== 'string') throwTypeError('isIpGloballyBlocked', 'String');
    return new Promise(function(resolve) {
        new mw.Api().get({
            'action': 'query',
            'list': 'globalblocks',
            'bgip': ip,
            'bglimit': 1,
            'bgprop': 'address',
            'formatversion': 2
        }).then(function(res) {
            if (res && res.query) {
                resolve(res.query.globalblocks.length !== 0);
            } else {
                mw.log.error('dragoLib.isIpGloballyBlocked: Unexpected error occurred on a check-block attempt.');
                resolve();
            }
        }).catch(function(code, err) {
            mw.log.error('dragoLib.isIpGloballyBlocked: Error occurred on a check-block attempt: ' + err.error.info);
            resolve();
        });
    });
};

// ******************************** LIBRARY FUNCTIONS ********************************
// The following functions are for the library itself and not included in dragoLib

/**
 * @param {*} args Always pass 'arguments'
 * @param {number} stopAt Number of necessary parameters (all optional parameters should follow necessary parameters)
 * @returns {boolean}
 */
function paramsMissing(args, stopAt) {
    for (let i = 0; i < stopAt; i++) {
        if (typeof args[i] === 'undefined') return true;
    }
    return false;
}

/**
 * @param {string} functionName 
 */
function throwError(functionName) {
    throw mw.log.error(`Uncaught ReferenceError: Necessary parameter(s) not set. (dragoLib.${functionName})`);
}

/**
 * @param {string} functionName
 * @param {*} param
 */
function throwInvalidParamError(functionName, param) {
    throw mw.log.error(`Uncaught ReferenceError: ${param} is an invalid parameter. (dragoLib.${functionName})`);
}

/**
 * @param {string} functionName
 * @param {string} type
 */
function throwTypeError(functionName, type) {
    throw mw.log.error(`Uncaught TypeError: ${type} must be passed to dragoLib.${functionName}.`);
}

})(mediaWiki, jQuery);
//</nowiki>