利用者:Nanona15dobato/script/History Splitting.js

//<nowiki>
(function () {
    'use strict';
    

    // 設定
    const CONFIG = {
        editSummary: '履歴保存のため移動',
        protectReason: '履歴保存のため保護',
        restoreReason: '履歴保存した「[[{logTitle}]]」より転記'
    };


    const api = new mw.Api();
    let lastLogTitle = '';

    function formatTimestampToJapanese(timestamp) {
        const date = new Date(timestamp);
        const year = date.getFullYear();
        const month = date.getMonth() + 1;
        const day = date.getDate();

        const weekdays = ['日', '月', '火', '水', '木', '金', '土'];
        const weekday = weekdays[date.getDay()];

        const hours = String(date.getUTCHours()).padStart(2, '0');
        const minutes = String(date.getUTCMinutes()).padStart(2, '0');

        return `${year}${month}${day}日 (${weekday}) ${hours}:${minutes} (UTC)`;
    }

    // allowBots関数
    function allowBots(text, user = "nanonaBot2") {
        if (!new RegExp("\\{\\{\\s*(nobots|bots[^}]*)\\s*\\}\\}", "i").test(text)) return true;
        return (new RegExp("\\{\\{\\s*bots\\s*\\|\\s*deny\\s*=\\s*([^}]*,\\s*)*" + user.replace(/([\(\)\*\+\?\.\-\:\!\=\/\^\$])/g, "\\$1") + "\\s*(?=[,\\}])[^}]*\\s*\\}\\}", "i").test(text)) ? false : new RegExp("\\{\\{\\s*((?!nobots)|bots(\\s*\\|\\s*allow\\s*=\\s*((?!none)|([^}]*,\\s*)*" + user.replace(/([\(\)\*\+\?\.\-\:\!\=\/\^\$])/g, "\\$1") + "\\s*(?=[,\\}])[^}]*|all))?|bots\\s*\\|\\s*deny\\s*=\\s*(?!all)[^}]*|bots\\s*\\|\\s*optout=(?!all)[^}]*)\\s*\\}\\}", "i").test(text);
    }


    function HistoryBotDialog(config) {
        HistoryBotDialog.super.call(this, config);
        this.initialRedirectTarget = config.initialRedirectTarget || '';
        this.logs = [];
    }

    OO.inheritClass(HistoryBotDialog, OO.ui.ProcessDialog);

    HistoryBotDialog.static.name = 'historyBotDialog';
    HistoryBotDialog.static.title = '履歴保存';
    HistoryBotDialog.static.size = 'large';
    HistoryBotDialog.static.actions = [
        {
            action: 'execute',
            label: '実行',
            flags: ['primary', 'progressive']
        },
        {
            action: 'cancel',
            label: 'キャンセル',
            flags: ['safe']
        }
    ];

    HistoryBotDialog.prototype.initialize = function () {
        HistoryBotDialog.super.prototype.initialize.apply(this, arguments);

        const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
        const title = mw.config.get('wgPageName').replace(/_/g, ' ');


        this.titlePanel = new OO.ui.PanelLayout({
            expanded: false,
            framed: true,
            padded: true
        });

        this.logTypeRadio = new OO.ui.RadioSelectWidget({
            items: [
                new OO.ui.RadioOptionWidget({
                    data: 'log',
                    label: 'log'
                }),
                new OO.ui.RadioOptionWidget({
                    data: 'history',
                    label: 'history'
                })
            ]
        });
        let selectedhistory = false;
        if (lastLogTitle.match(/\/history[ _]?\d+$/)) {
            this.logTypeRadio.selectItemByData('history');
            selectedhistory = true;
        } else {
            this.logTypeRadio.selectItemByData('log');
        }


        this.dateStringInput = new OO.ui.TextInputWidget({
            value: today,
            placeholder: 'yyyyMMdd',
        });

        this.useCustomTitleCheckbox = new OO.ui.CheckboxInputWidget();
        this.customLogTitleInput = new OO.ui.TextInputWidget({
            placeholder: 'ページ名を入力',
            disabled: true
        });

        this.logTitlePreview = new OO.ui.LabelWidget({
            label: selectedhistory ? `${title}/history${today}` : `${title}/log${today}`
        });

        this.redirectPanel = new OO.ui.PanelLayout({
            expanded: false,
            framed: true,
            padded: true
        });

        this.redirectTargetInput = new OO.ui.TextInputWidget({
            value: this.initialRedirectTarget,
            placeholder: '前回の履歴保存先ページ名を入力',
            help: '今回が初保存の場合は空欄のままにしてください'
        });

        // ログエリア
        this.logPanel = new OO.ui.PanelLayout({
            expanded: false,
            framed: true,
            padded: true
        });

        this.logArea = $('<div>')
            .css({
                height: '250px',
                overflowY: 'auto',
                border: '1px solid #a2a9b1',
                padding: '12px',
                backgroundColor: '#f8f9fa',
                fontFamily: 'monospace',
                fontSize: '12px',
                lineHeight: '1.4'
            });

        // レイアウト構築
        this.titlePanel.$element.append(
            $('<h4>').text('移動先タイトル'),
            new OO.ui.FieldLayout(this.logTypeRadio, {
                label: 'タイトル形式:',
                align: 'top'
            }).$element,
            new OO.ui.FieldLayout(this.dateStringInput, {
                label: '日付文字列:',
                align: 'top'
            }).$element,
            new OO.ui.FieldLayout(this.useCustomTitleCheckbox, {
                label: 'カスタムタイトルを使用',
                align: 'inline'
            }).$element,
            new OO.ui.FieldLayout(this.customLogTitleInput, {
                label: 'カスタムタイトル:',
                align: 'top'
            }).$element,
            $('<div>').css({
                marginTop: '15px',
                padding: '10px',
                backgroundColor: '#f0f8ff',
                borderRadius: '3px'
            }).append(
                $('<strong>').text('移動先: '),
                this.logTitlePreview.$element
            )
        );

        this.redirectPanel.$element.append(
            $('<h4>').text('前回ページ設定'),
            new OO.ui.FieldLayout(this.redirectTargetInput, {
                label: '前回の履歴保存先:',
                help: '今回が初保存の場合は空欄のままにしてください',
                align: 'top'
            }).$element
        );

        this.logPanel.$element.append(
            $('<h4>').text('実行ログ'),
            this.logArea
        );

        this.$body.append(
            this.titlePanel.$element,
            this.redirectPanel.$element,
            this.logPanel.$element
        );

        // イベントリスナー設定
        this.setupEvents();

        // 初期ログ
        this.addLog('履歴保存 準備完了');
    };

    // ダイアログが開かれた時の処理
    HistoryBotDialog.prototype.getSetupProcess = function (data) {
        return HistoryBotDialog.super.prototype.getSetupProcess.call(this, data)
            .next(function () {
                // フォーカスを適切に設定
                this.dateStringInput.focus();
            }, this);
    };

    // ダイアログが閉じられた時の処理
    HistoryBotDialog.prototype.getTeardownProcess = function (data) {
        return HistoryBotDialog.super.prototype.getTeardownProcess.call(this, data)
            .next(function () {
                if (this.manager && this.manager.$element) {
                    this.manager.$element.remove();
                }
            }, this);
    };

    HistoryBotDialog.prototype.setupEvents = function () {
        const self = this;

        // ラジオボタンの変更イベント
        this.logTypeRadio.on('select', function () {
            self.updateLogTitle();
        });

        // 日付文字列の変更イベント
        this.dateStringInput.on('change', function () {
            self.updateLogTitle();
        });

        // カスタムタイトルチェックボックス
        this.useCustomTitleCheckbox.on('change', function (checked) {
            self.customLogTitleInput.setDisabled(!checked);
            self.dateStringInput.setDisabled(checked);
            self.logTypeRadio.setDisabled(checked);
            if (!checked) {
                self.customLogTitleInput.setValue('');
            }
            self.updateLogTitle();
        });

        // カスタムタイトル入力イベント
        this.customLogTitleInput.on('change', function () {
            self.updateLogTitle();
        });
    };

    HistoryBotDialog.prototype.updateLogTitle = function () {
        const logType = this.logTypeRadio.findSelectedItem()?.getData() || 'log';
        const customTitle = this.customLogTitleInput.getValue().trim();
        const dateString = this.dateStringInput.getValue();
        const title = mw.config.get('wgPageName').replace(/_/g, ' ');

        if (customTitle) {
            this.logTitlePreview.setLabel(customTitle);
        } else {
            this.logTitlePreview.setLabel(`${title}/${logType}${dateString}`);
        }
    };

    HistoryBotDialog.prototype.addLog = function (message, type = 'info') {
        const logEntry = $('<div>')
            .css({
                margin: '2px 0',
                padding: '3px 6px',
                borderRadius: '3px',
                fontSize: '12px',
                backgroundColor: type === 'error' ? '#ffeaa7' :
                    type === 'success' ? '#d4edda' :
                        type === 'warning' ? '#fff3cd' : '#f8f9fa'
            })
            .text(`[${new Date().toLocaleTimeString()}] ${message}`);

        this.logArea.append(logEntry);
        this.logArea.scrollTop(this.logArea[0].scrollHeight);
    };

    HistoryBotDialog.prototype.getActionProcess = function (action) {
        const self = this;

        if (action === 'execute') {
            return new OO.ui.Process(async function () {
                const logType = self.logTypeRadio.findSelectedItem()?.getData() || 'log';
                const dateString = self.dateStringInput.getValue().trim();
                const customTitle = self.customLogTitleInput.getValue().trim();
                const redirectTarget = self.redirectTargetInput.getValue().trim();
                const useCustomTitle = self.useCustomTitleCheckbox.isSelected();

                if (!useCustomTitle && !dateString) {
                    self.addLog('日付文字列を入力してください', 'error');
                    return;
                }

                if (useCustomTitle && !customTitle) {
                    self.addLog('カスタムタイトルを入力してください', 'error');
                    return;
                }

                const options = {
                    logType: logType,
                    dateString: dateString,
                    customLogTitle: useCustomTitle ? customTitle : null,
                    redirectTarget: redirectTarget
                };

                // ボタンを無効化
                self.actions.setAbilities({ execute: false });

                try {
                    await self.processHistorySeparation(options);
                } finally {
                    // ボタンを再有効化
                    self.actions.setAbilities({ execute: true });
                }
            });
        } else if (action === 'cancel') {
            return new OO.ui.Process(function () {
                self.close({ action: action });
            });
        }

        return HistoryBotDialog.super.prototype.getActionProcess.call(this, action);
    };

    // メイン処理関数
    HistoryBotDialog.prototype.processHistorySeparation = async function (options) {
        const title = mw.config.get('wgPageName').replace(/_/g, ' ');

        try {
            this.addLog(`処理開始: ${title}`);

            // 最新版と保護情報を取得
            this.addLog('ページ情報を取得中...');
            const pageQuery = await api.get({
                action: 'query',
                prop: 'revisions|pageprops',
                titles: title,
                rvprop: 'content|timestamp|ids',
                rvslots: '*',
                ppprop: 'wikibase_item',
                formatversion: 2
            });

            const page = pageQuery.query.pages[0];
            if (!page || page.missing) {
                throw new Error('ページが見つかりません');
            }

            const latestText = page.revisions[0].slots.main.content;
            const latestTimestamp = page.revisions[0].timestamp;
            const formattedTimestamp = formatTimestampToJapanese(latestTimestamp);
            this.addLog(`最新版のタイムスタンプ: ${formattedTimestamp}`);
            const wikibaseId = page.pageprops?.wikibase_item;


            if (!allowBots(latestText, mw.config.get('wgUserName'))) {
                this.addLog('Bot拒否テンプレートが設置されています。処理をスキップしました。', 'warning');
                return;
            }

            // 移動先ページ名作成
            const logTitle = options.customLogTitle || `${title}/${options.logType}${options.dateString}`;
            this.addLog(`移動先: ${logTitle}`);

            this.addLog(`ページを移動中...`);

            // ページ移動(リダイレクト残す)
            await api.postWithToken('csrf', {
                action: 'move',
                from: title,
                to: logTitle,
                reason: CONFIG.editSummary,
            });

            this.addLog('移動先にテンプレートを追加中...');

            // 移動先に {{履歴保存}} を挿入し、リダイレクト化
            const redirectWikitext =
                `#REDIRECT [[${title}]]\n` +
                `{{履歴保存|${title}|${formattedTimestamp}${options.redirectTarget ? `|${options.redirectTarget}` : ''}}}`;

            await api.edit(logTitle, () => ({
                text: redirectWikitext,
                summary: '+{{履歴保存}}'
            }));

            this.addLog('移動先ページを保護中...');

            // 移動後ページを保護
            await api.postWithToken('csrf', {
                action: 'protect',
                title: logTitle,
                protections: 'edit=sysop',
                reason: CONFIG.protectReason,
                expiry: 'infinity'
            });

            this.addLog('元のページに内容を復元中...');

            // 元のページに内容復元
            await api.edit(title, () => ({
                text: latestText,
                summary: CONFIG.restoreReason.replace('{logTitle}', logTitle)
            }));

            // Wikidata再リンク
            if (wikibaseId) {
                this.addLog('Wikidataの再リンクを実行中...');
                const wikidataapi = new mw.ForeignApi('https://www.wikidata.org/w/api.php');
                await wikidataapi.postWithToken('csrf', {
                    action: 'wbsetsitelink',
                    id: wikibaseId,
                    linksite: 'jawiki',
                    linktitle: title,
                });
                this.addLog(`Wikidataの再リンクが完了: ${wikibaseId}`);
            }

            this.addLog(`処理完了: ${title}`, 'success');

        } catch (err) {
            console.error(`エラー: ${title}`, err);
            this.addLog(`エラー: ${err.message}`, 'error');
        }
    };


    let windowManager = null;

    // 初版を取得してダイアログを開く
    async function openDialog() {
        const title = mw.config.get('wgPageName').replace(/_/g, ' ');

        if (windowManager) {
            windowManager.$element.remove();
        }

        try {
            const activeElement = document.activeElement;

            if (activeElement && activeElement.blur) {
                activeElement.blur();
            }

            // 初版取得
            const firstRevQuery = await api.get({
                action: 'query',
                prop: 'revisions',
                titles: title,
                rvlimit: 1,
                rvdir: 'newer',
                rvslots: '*',
                rvprop: 'content',
                formatversion: 2
            });

            const firstText = firstRevQuery.query.pages[0].revisions[0].slots.main.content;

            // 初版がリダイレクトかを判定
            const redirectMatch = firstText.match(/^#(?:REDIRECT|転送)[ \t]*\[\[([^\]]*)\]\]/i);
            let lastLogTitle0 = '';
            if (redirectMatch) {
                lastLogTitle0 = redirectMatch[1];
            }

            if (lastLogTitle0) {
                // カテゴリチェック
                const catCheck = await api.get({
                    action: 'query',
                    prop: 'categories',
                    titles: lastLogTitle0,
                    clcategories: 'Category:履歴を分離したページ',
                    formatversion: 2
                });

                if (catCheck.query.pages[0]?.categories?.length > 0) {
                    lastLogTitle = lastLogTitle0;
                }
            }

            // ダイアログを作成して開く
            const dialog = new HistoryBotDialog({
                initialRedirectTarget: lastLogTitle || '',
            });

            windowManager = new OO.ui.WindowManager();
            $(document.body).append(windowManager.$element);
            windowManager.addWindows([dialog]);

            // ダイアログを開く前に少し待機
            setTimeout(() => {
                windowManager.openWindow(dialog);
            }, 100);

        } catch (err) {
            console.error('初版情報の取得に失敗:', err);
            mw.notify('初版情報の取得に失敗しました', { type: 'error' });
        }
    }

    // 初期化(管理者権限チェック)
    function init() {
        if (!mw.config.get('wgUserGroups').includes('sysop') && mw.config.get('wgUserName') !== 'Nanona15dobato') {
            console.log('履歴保存JS: 管理者権限が必要です');
            return;
        }

        // ポートレットリンクを追加
        mw.util.addPortletLink(
            'p-cactions',
            '#',
            '履歴保存',
            'ca-history-separation',
            '履歴保存を実行'
        );

        document.getElementById('ca-history-separation').addEventListener('click', (e) => {
            e.preventDefault();
            openDialog();
        });
    }

    mw.loader.using([
        'oojs-ui-core',
        'oojs-ui-windows',
        'oojs-ui-widgets',
        'mediawiki.api',
        'mediawiki.ForeignApi',
        'mediawiki.util'
    ], function () {
        if (mw.config.get('wgNamespaceNumber') < 0) return;
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
        } else {
            init();
        }
    });

})();
//</nowiki>
Prefix: 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 0 1 2 3 4 5 6 7 8 9

Portal di Ensiklopedia Dunia

Kembali kehalaman sebelumnya