Участник:Vavilexxx/pageDeletionNominationHelper.js

Материал из Википедии — свободной энциклопедии
// <nowiki>
/*
 * Вынесение на КБУ (+ отсроченное), КУ (+ множественное, + оставлено), КУЛ, КПМ.
 * Подключение (на своём /common.js):
 * const userAlertEnabled = 0 // указать, чтоб не уведомлять автора
*/
$(document).ready(function() {
    const config = mw.config.get(['skin', 'wgNamespaceIds', 'wgFormattedNamespaces', 'wgNamespaceNumber', 'wgPageName', 'wgPageContentModel', 'wgIsRedirect', 'wgUserName']);

    // Пространства имён, в которых будет включён удалятор
    const namespacesForRemoval = (typeof g_rm_namespaces === 'undefined') ? [0, 2, 4, 6, 10, 14, 100, 104, 828] : g_rm_namespaces;

    // Все кнопки меню
    const menuButtons = {
        imp: 'КУЛ',
        rnm: 'КПМ',
        tRm: 'КУ',
        mRm: 'Много КУ',
        fRm: 'КБУ',
        merge: 'КОБ',
        split: 'КРАЗД',
        recov: 'ВУС',
        ret: 'Оставить',
        noRnm: 'Не переименовано'
    };

    // Доступные кнопки
    const availableActions = (typeof g_rm_actions === 'undefined') ? Object.keys(menuButtons) : g_rm_actions;

    // Переменная, управляющая функцией оповещения создателя статьи
    let userAlertEnabled = (typeof g_user_alert === 'undefined') ? 0 : g_user_alert;

    // Индикатор ошибки API
    let isError;

    // Здесь хранятся параметры номинации
    let nominationParams;

    // Возможные разделы КБУ в зависимости от пространства имён
    const fastRemovePrefix = (config.wgIsRedirect ? 'ОП' : 'О') + ({
        0: 'Сd',
        2: 'У',
        3: 'У',
        6: 'Ф',
        14: 'К'
    }[config.wgNamespaceNumber] || '');

    // Список всех доступных причин КБУ
    const fastRemoveReasons = [
        ['подст:ds', 'ds Отсроченное'],
        ['уд-бессвязно', 'О1 Бессвязный текст'],
        ['уд-тест', 'О2 Тестовая страница'],
        ['уд-ванд', 'О3 Вандальная страница'],
        ['уд-повторно', 'О4 Уже удалялось'],
        ['уд-автор', 'О5 По просьбе автора'],
        ['уд-обс', 'О6 Ненужная подстраница'],
        ['уд-переим', 'О7 Для переименования', 'страницу'],
        ['уд-дубль', 'О8 Дубликат', 'страницу'],
        ['уд-реклама', 'О9 Реклама или спам'],
        ['db-badtalk', 'О10 Нецелевая СО'],
        ['уд-копивио', 'О11 Нарушение АП', 'ссылку'],
        ['уд-пусто', 'С1 Пусто или коротко'],
        ['уд-иностр', 'С2 Не на русском'],
        ['уд-ссылки', 'С3 Лишь ссылки'],
        ['уд-нз', 'С5 Явно незначимо'],
        ['уд-в никуда', 'П1 Перенапр. в никуда'],
        ['db-redirspace', 'П2 Межпростр. перенапр.'],
        ['уд-опечатка', 'П3 Перенапр. с опечаткой'],
        ['уд-падеж', 'П4 Не именительный падеж'],
        ['уд-смысл', 'П5 Неверное перенапр.'],
        ['db-redirtalk', 'П6 Перенапр. на СО'],
        ['db-duplicate', 'Ф1 Копия файла', 'файл'],
        ['db-badimage', 'Ф2 Повреждённый файл'],
        ['подст:nld', 'Ф3 Нет данных о лицензии'],
        ['подст:nsd', 'Ф3 Нет данных о источнике'],
        ['подст:nad', 'Ф3 Нет данных о авторе'],
        ['подст:dd', 'Ф3 Сомнительные данные файла'],
        ['подст:ofud', 'Ф4 Неиспользуемый КДИ'],
        ['подст:dfud', 'Ф5 Нет КДИ'],
        ['db-badfairuse', 'Ф6 Неоправданное КДИ'],
        ['NCT', 'Ф8 Есть на Складе', 'файл'],
        ['подст:Nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ'],
        ['уд-пусткат', 'К1 Пустая категория'],
        ['уд-перекат', 'К2 Переименованная кат.', 'категорию'],
        ['уд-владелец', 'У1 По желанию владельца'],
        ['уд-анон', 'У2 Устаревшая СО анонима'],
        ['уд-несущ', 'У3 Несуществующий участник'],
        ['уд-нецелевое', 'У4 Нецелевое использ. ЛП'],
        ['уд-неактив', 'У5 Подстраница неактивного'],
        ['db', 'Особый случай', 'причину']
    ].filter(arr => fastRemovePrefix.indexOf(arr[1].charAt(0)) >= 0);

    // Функция запроса к API
    const makeApiRequest = (params, mode, callback) => {
        params.format = 'json';
        params.token = mw.user.tokens.get('csrfToken');
        params.action = mode;
        $.post('/w/api.php', params, callback);
    };

    // Функция получения даты
    const getCurrentDate = (customDate) => {
        const date = customDate ? new Date(customDate) : new Date();
        return [date.toISOString().substr(0, 10), `${date.getUTCDate()} ${
            'января,февраля,марта,апреля,мая,июня,июля,августа,сентября,октября,ноября,декабря'.split(',')[date.getUTCMonth()]
        } ${date.getUTCFullYear()}`];
    };

    // Функция получения <input>
    const createInputField = (id, placeholder, isHidden) => {
        return `<input id="${id}" type="${isHidden ? 'hidden' : 'text'}" placeholder="${placeholder}" class="messagebox">`;
    };

    // Определяет СО страницы
    const getDiscussionPage = (pageName) => {
        const match = /([^:]*:)?(.*)/.exec(pageName);
        if (match[1]) {
            const namespace = config.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
            if (namespace !== undefined) {
                return `${config.wgFormattedNamespaces[namespace | 1]}:${match[2]}`;
            }
        }
        return `Обсуждение:${pageName}`;
    };

    // Функция получения текста страницы
    const fetchPageText = (pageName, callback) => {
        makeApiRequest({
            prop: 'wikitext',
            page: pageName
        }, 'parse', (response) => {
            callback(response.parse ? response.parse.wikitext['*'] : null);
        });
    };

    // Функция отправки уведомления пользователю
    const notifyUser = (pageName, callback) => {
        makeApiRequest({
            prop: 'revisions',
            rvprop: 'user',
            rvdir: 'newer',
            titles: pageName
        }, 'query', (response) => {
            const pages = response.query.pages;
            if (!('-1' in pages)) {
                const revision = pages[Object.keys(pages)[0]].revisions[0];
                if (!('anon' in revision) && !revision.userhidden && (revision.user && revision.user !== config.wgUserName)) {
                    makeApiRequest({
                        title: `оу:${revision.user}`,
                        section: 'new',
                        sectiontitle: `Удалятор: [[:${pageName}]]`,
                        summary: nominationParams.summary,
                        text: `Страница [[:${pageName}]], созданная вами, ${nominationParams[3] ? '' : 'предложена '}${nominationParams[1]}. ${
                            nominationParams.place ? `Обсуждение — на странице [[${nominationParams.place}#${nominationParams.sectionNW}]]. ` : ''
                        }~~\~~<br><small>Это автоматическое уведомление, сгенерированное скриптом «Удалятор».</small>`
                    }, 'edit', (response) => {
                        callback(response.error);
                    });
                } else {
                    callback('не нужно');
                }
            } else {
                callback('страница удалена');
            }
        });
    };

    // Функция работы с текстом статьи
    const modifyArticleContent = (pageName, callback) => {
        if (/(noRnm|ret)/g.test(nominationParams[0])) {
            closeNomination(pageName, callback);
        } else {
            openNomination(pageName, callback);
        }
    };

    // Закрытие номинации (снять в статье, установить на СО)
    const closeNomination = (pageName, callback) => {
        fetchPageText(pageName, (articleText) => {
            const templateMatch = RegExp(`\{\{(${nominationParams[3]})\\|(\\d{4}-\\d\\d-\\d\\d)\\|?(.*?)}}`, 'gi').exec(articleText);
            if (templateMatch === null) {
                callback({
                    code: 'ошибка',
                    info: `Невозможно снять шаблон «${nominationParams[3]}».`
                });
                return;
            }
            nominationParams.date = getCurrentDate(templateMatch[2]);
            nominationParams.place = `ВП:${nominationParams[3].replace(/\|.*/, '')}/${nominationParams.date[1]}`;
            if (nominationParams[0] === 'noRnm') {
                nominationParams.sectionNW = `${pageName}${templateMatch[3]}`;
                nominationParams.templateParams = `${pageName}|${templateMatch[3]}`;
            }
            if (nominationParams[0] === 'ret') {
                nominationParams.sectionNW = templateMatch[3].length ? templateMatch[3] : pageName;
                nominationParams.templateParams = `l1=${nominationParams.sectionNW}`;
            }
            nominationParams.summary = `Удалятор: номинация [[${nominationParams.place ? `${nominationParams.place}#` : ''}${nominationParams.sectionNW}]] — ${nominationParams[2]}`;

            makeApiRequest({
                summary: nominationParams.summary,
                title: getDiscussionPage(pageName),
                prependtext: `{{${nominationParams[2]}|${nominationParams.date[1]}|${nominationParams.templateParams}}}\n`
            }, 'edit');

            articleText = articleText.replace(RegExp(`(<noin.*?>)?\{\{(${nominationParams[3]})\\|.*?}}\n?(<\/noin.*?>)?\n?`, 'gi'), '');
            makeApiRequest({
                title: pageName,
                text: articleText,
                summary: nominationParams.summary
            }, 'edit', (response) => {
                callback(response.error);
            });
        });
    };

    // Открытие номинации (установка шаблона в статье)
    const openNomination = (pageName, callback) => {
        fetchPageText(pageName, (articleText) => {
            if (articleText === null) {
                callback({
                    code: 'ошибка',
                    info: `Страница «${pageName}» не существует.`
                });
                return;
            }
            let template = '';
            if (nominationParams[0] === 'fRm') {
                template = `${fastRemoveReasons[$('#rmSel').val()][0]}|1=${$('#fiRm').val()}`;
            } else {
                template = nominationParams.templateParams;
                if (nominationParams[0] === 'merge') {
                    template = (`|${template}|`).replace(`|${pageName}|`, '|').slice(1, -1);
                }
                template = `${nominationParams[2]}|${nominationParams.date[0]}${template.length ? `|${template}` : ''}`;
            }
            makeApiRequest({
                title: pageName,
                text: (template.length ? `<noinclude>{{${template}}}\n</noinclude>` : '') + articleText,
                summary: nominationParams.summary
            }, 'edit', (response) => {
                callback(response.error);
            });
        });
    };

    // Функция установки номинации на соответствующую страницу
    const setNomination = (callback) => {
        makeApiRequest({
            title: nominationParams.place,
            createonly: '1',
            text: `{{${nominationParams[4]}-Навигация}}\n`,
            summary: 'Удалятор: автоматическая шапка',
        }, 'edit', (response) => {
            makeApiRequest({
                title: nominationParams.place,
                section: 'new',
                sectiontitle: nominationParams.section,
                summary: nominationParams.summary,
                text: `${nominationParams.msg} ~~\~~`
            }, 'edit', (response) => {
                callback(response.error);
            });
        });
    };

    // Заполнение параметров номинации
    const setNominationParams = () => {
        const pageName = config.wgPageName.replace(/_/g, ' '),
            title = $('#rmHeader').val(),
            title2 = $('#rmHeader2').length ? $('#rmHeader2').val() : '',
            message = $('#rmMsg').val(),
            pages = [pageName];

        if (/(noRnm|ret)/g.test(nominationParams[0])) {
            return pages;
        }

        nominationParams.date = getCurrentDate();
        nominationParams.msg = message ? message.trim() : '';
        nominationParams.place = nominationParams[0] !== 'fRm' ? `ВП:${nominationParams[2]}/${nominationParams.date[1]}` : '';

        nominationParams.section = nominationParams[0] === 'mRm' ? title : `[[:${pageName}]]`;
        nominationParams.templateParams = '';
        if (nominationParams[0] === 'rnm') {
            nominationParams.templateParams = title;
            nominationParams.section += ` → [[:${title}]]`;
        } else if (nominationParams[0] === 'merge') {
            nominationParams.templateParams = `${pageName}|${title}`;
            nominationParams.section += ` и [[:${title}]]`;
        } else if (nominationParams[0] === 'split') {
            nominationParams.templateParams = `[[:${title}]]${title2 ? ` и [[:${title2}]]` : ''}`;
            nominationParams.section += ` → ${nominationParams.templateParams}`;
        }

        nominationParams.sectionNW = nominationParams.section.replace(/\[\[:/g, '').replace(/]]/g, '');
        nominationParams.summary = `Удалятор: номинация [[${nominationParams.place ? `${nominationParams.place}#` : ''}${nominationParams.sectionNW}]]${nominationParams[0] === 'fRm' ? ` ${nominationParams[2]}` : ''}`;

        if (nominationParams[0] === 'mRm') {
            pages.length = 0;
            nominationParams.msg = `=== По всем ===\n${nominationParams.msg}`;
            for (let i = 4; i >= 0; i--) {
                const page = $(`#rmArticle${i}`).val();
                if (page.length) {
                    pages.push(page);
                    nominationParams.msg = `=== [[:${page}]] ===\n${nominationParams.msg}`;
                }
            }
        } else if (nominationParams[0] === 'merge') {
            pages.push(title);
        } else if (nominationParams[0] === 'recov') {
            pages.length = 0;
        }
        return pages;
    };

    // Вывод сообщений об ошибках
    const logError = (message, error) => {
        if (error && error.code) isError = 1;
        $('#rmWindow').append(
            `<br>${message}${error ? (error.code ? `<span class="error"><small>${error.code}: ${error.info}</small></span>` : error) : 'OK'}`
        );
    };

    // Обработка массива страниц
    const processPages = (pages) => {
        if (pages.length) {
            const page = pages.pop();
            modifyArticleContent(page, (error) => {
                logError(`Правка статьи «${page}»`, error);
                if (isError) {
                    finalizeWindow();
                    return;
                }
                if (userAlertEnabled) {
                    notifyUser(page, (error) => {
                        logError('Уведомление создателя', error);
                        processPages(pages);
                    });
                } else {
                    processPages(pages);
                }
            });
        } else {
            if (nominationParams[4]) {
                setNomination((error) => {
                    logError('Запись номинации', error);
                    finalizeWindow();
                });
            } else {
                finalizeWindow();
            }
        }
    };

    // Вызывается, когда всё сделано
    const finalizeWindow = () => {
        if (isError) {
            $('.mw-small-spinner').remove();
            $('#rmWindow')
                .append('<p class="error">При выполнении скрипта случились ошибки. Поправьте всё, что надо, вручную.</p>')
                .children().prop('disabled', false);
            $('#rmBtn').attr('disabled', 1);
            $('#rmClose').text('Закрыть');
        } else {
            location.reload();
        }
    };

    // Функция создания модального окна
    const showModal = () => {
        let content = '';

        if (nominationParams[0] === 'mRm') {
            content += createInputField('rmHeader', 'Заголовок номинации');
            for (let i = 0; i < 5; i++) {
                content += createInputField(`rmArticle${i}`, `Статья${i + 1}`);
            }
        }
        if (nominationParams[0] === 'fRm') {
            content += '<select id="rmSel" class="messagebox">';
            fastRemoveReasons.forEach((reason, index) => {
                content += `<option value="${index}">${reason[1]}</option>`;
            });
            content += '</select>' + createInputField('fiRm', '', 1);
        }
        if (nominationParams[0] === 'rnm') {
            content += createInputField('rmHeader', 'Новое название');
        }
        if (nominationParams[0] === 'merge') {
            content += createInputField('rmHeader', 'Объединить с…');
        }
        if (nominationParams[0] === 'split') {
            content += createInputField('rmHeader', 'Разделить на эту');
            content += createInputField('rmHeader2', 'И на эту');
        }
        if (nominationParams[4]) {
            content += '<textarea id="rmMsg" placeholder="Текст номинации без «~~\~~»." rows="4"></textarea>';
        }
        $('#content').prepend(
            `<div id="rmWindow" style="padding:2em;margin:1em;border:1px solid #985; background: #fec;">
                <h1>Удалятор: ${nominationParams[2]}</h1>${content}
                <br><label><input name="rmUAlert" type="checkbox" ${userAlertEnabled ? 'checked' : ''}>Оповестить автора</label><br>
                <button id="rmBtn" class="mw-ui-button">Отправить</button><button id="rmClose" class="mw-ui-button">Отмена</button>
            </div>`
        );
        if (nominationParams[0] === 'mRm') $('#rmArticle0').val(config.wgPageName.replace(/_/g, ' '));

        $('#rmSel').change(function() {
            const reason = fastRemoveReasons[this.value][2];
            $('#fiRm').attr({
                type: reason ? 'text' : 'hidden',
                placeholder: `Укажите ${reason}`
            });
        });
        $('#rmClose').click(() => {
            $('#rmWindow').remove();
        });

        $('#rmBtn').click(() => {
            $('#rmWindow')
                .append('<b class="mw-small-spinner"></b>')
                .children().attr('disabled', '1');
            userAlertEnabled = $('[name="rmUAlert"]').is(':checked');
            const pages = setNominationParams();
            processPages(pages);
        });

        // Реализация ctrl+enter события
        $(window).keydown((e) => {
            if (e.ctrlKey && e.keyCode === 13) {
                $('#rmBtn').click();
            }
        });
    };

    // Добавление выпадающего меню в существующий список
    if ((namespacesForRemoval.indexOf(config.wgNamespaceNumber & ~1) >= 0) && (config.wgPageContentModel === 'wikitext')) {
        // Создаем основной пункт меню
        const newLi = $('<li>')
            .attr('style', 'position:relative;')
            .attr('id', 'mm-page-deletor')
            .addClass('mm-submenu-wrapper mw-list-item')
            .html('<a style="font-weight: bold"><span>Удалятор…</span></a>');
        
        // Создаем подменю
        const submenuUl = $('<ul>')
            .addClass('menu mm-submenu vector-menu-content-list')
            .css({
                position: 'absolute',
                left: '170.578px',
                display: 'none'
            });

        // Заполняем подменю пунктами
        availableActions.forEach(action => {
            const menuItem = $('<li>')
                .addClass('mm-item mw-list-item')
                .html(`<a href="#"><span>${menuButtons[action]}</span></a>`)
                .click(function(e) {
                    e.preventDefault();
                    const actionType = action;
                    nominationParams = (
                        actionType === 'imp' ? [actionType, 'к срочному улучшению', 'к улучшению', '', 'КУЛ'] :
                        actionType === 'rnm' ? [actionType, 'к переименованию', 'к переименованию', '', 'КПМ'] :
                        actionType === 'tRm' ? [actionType, 'к удалению', 'к удалению', '', 'КУ'] :
                        actionType === 'mRm' ? [actionType, 'к удалению', 'к удалению', '', 'КУ'] :
                        actionType === 'merge' ? [actionType, 'к объединению с другой', 'к объединению', '', 'КОБ'] :
                        actionType === 'split' ? [actionType, 'к разделению', 'к разделению', '', 'КР'] :
                        actionType === 'fRm' ? [actionType, 'к [[ВП:КБУ|быстрому удалению]]', 'к быстрому удалению'] :
                        actionType === 'recov' ? [actionType, '', 'к восстановлению', '', 'ВУС'] :
                        actionType === 'ret' ? [actionType, 'оставлена', 'оставлено', 'к удалению|ку'] :
                        actionType === 'noRnm' ? [actionType, 'не переименована', 'не переименовано', 'к переименованию|кпм|rename'] :
                        0
                    );
                    isError = 0;
                    showModal();
                });
            submenuUl.append(menuItem);
        });

        // Добавляем подменю к основному пункту
        newLi.append(submenuUl);

        // Обработчики для показа/скрытия подменю
        newLi.hover(
            function() { $(this).find('ul').show(); },
            function() { $(this).find('ul').hide(); }
        );

        // Вставляем новый пункт в начало списка
        $('.vector-menu-content-list.mm-menu').prepend(newLi);
    }
});
// </nowiki>