Utente:Titore/VC.js

/**
 * Aggiunge un link nella barra degli strumenti laterale
 * per segnalare rapidamente un'utenza o un IP tra i vandalismi in corso.
 *
 * @author https://it.wikipedia.org/wiki/Utente:Titore and contributors
 * @date 2019-09-17
 * @license Dual-licensed under CC BY-SA 3.0 and GPL 3.0 (or any later version)
 */
(function (mw, $) {
    'use strict';

    var windowManager, dialog;

    // API MediaWiki
    const api = new mw.Api();

    // Variabili MediaWiki
    const conf = mw.config.get([
        'wgRelevantUserName', // Presente in pagine attinenti all'utente (es. contributi, discussioni, pagina utente)
        'wgUserName',
        'wgArticlePath',
    ]);

    // Utente da segnalare
    const utente = conf.wgRelevantUserName;

    // Utilizzatore dello script
    const currentUser = conf.wgUserName;

    // Pagina di servizio
    const paginaSegnalazioni = 'Wikipedia:Vandalismi in corso';

    // Campo oggetto
    const oggetto = `[[Utente:Titore/VC|VC.js]]: segnalazione [[Speciale:Contributi/${utente}|${utente}]]`; //TODO: o tutto qui o tutto lì (inviaSegnalazione -> summary:)

    // Motivazioni frequenti
    const motivazioniFrequenti = [
        'Reiterati vandalismi.',
        'Avvisato, insiste.',
        'Continua dopo il giallo.',
        'Recidivo.',
        'Evasione del blocco: ',
        'Rimozione contenuti.',
        'Utenza creata al solo scopo di vandalizzare.',
    ];

    // Numero massimo di segnalazioni da elaborare per ricerca duplicati
    var maxSegnalazioni = 5;

    // Aggiunge link nella barra degli strumenti laterale
    if (utente && utente!=currentUser) { // Evita di segnalare se stessi
        var link = mw.util.addPortletLink(
            'p-tb',
            '#',
            'Segnala vandalo',
            't-segnalavandalo',
            `Segnala ${utente} tra i vandalismi in corso`,
            getAvailableAccessKey('v', 'i', 'c')); // Tasto di scelta rapida (es. Alt + Maiusc + v) e alternative in caso sia già in uso
        $(link).click((event) => {
            event.preventDefault();
            avviaScript();
        });
    }

    /**
     * Avvia lo script.
     */
    function avviaScript() {
        mw.loader.using('oojs-ui-windows')
            .done(() => finestraDiDialogo())
            .fail(() => console.error('Impossibile avviare lo script VC.js.'));
    }

    /**
     * Finestra di dialogo
     */
    function finestraDiDialogo() {

        // Subclass di ProcessDialog 
        function Dialog(config) {
            Dialog.super.call(this, config);
        }
        OO.inheritClass(Dialog, OO.ui.ProcessDialog);

        // Nome, titolo, action set
        Dialog.static.name = 'dialogo';
        Dialog.static.title = 'Nuova segnalazione';
        Dialog.static.actions = [
            {
                flags: ['primary', 'progressive'], // Azione primaria, blu
                label: 'Invia',
                action: 'save',
                disabled: true, // Disabilito finché non pronto //TODO: usare setAbilities()
            },
            {
                flags: ['safe', 'close'],
                label: 'Annulla',
            }
        ];

        // initialize(): aggiungo contenuto e layout
        Dialog.prototype.initialize = function () {
            Dialog.super.prototype.initialize.call(this);

            this.panel = new OO.ui.PanelLayout({
                padded: true,
                expanded: false,
            });
            this.content = new OO.ui.FieldsetLayout();
            this.vandaloInput = new OO.ui.TextInputWidget({ //this.vandaloInput = new mw.widgets.UserInputWidget({ //non funziona da mobile
                readOnly: true,
            });
            var style = ".vertical textarea{ resize: vertical; }"; // Solo resize verticale
            mw.util.addCSS(style);
            this.motivoInput = new OO.ui.MultilineTextInputWidget({
                placeholder: 'Descrivi brevemente il tipo di vandalismo in corso.\nLa firma è automatica.',
                rows: 3,
                classes: ['vertical'],
                /*multiline: true,
                autosize: true,
                maxRows: 3,
                indicator: 'required',*/
            });

            // Dropdown 
            // TODO: meglio in una funzione? https://en.wikipedia.org/w/index.php?title=MediaWiki:Gadget-defaultsummaries.js#L-66
            var itemSelected; //<--- per strict
            var self = this; //TODO: bind "this" to the function call instead
            //      https://stackoverflow.com/questions/337878/var-self-this#comment14371472_338106
            //      https://stackoverflow.com/a/10115970/8762088
            this.dropDown = new OO.ui.DropdownWidget({
                label: 'Motivazioni frequenti'
            }),
                // Evento che inserirà nel campo motivo l'opzione selezionata dal menu a tendina
                itemSelected = function () { //TODO: inserire direttamente come anon func?
                    var motivazioneFrequente = self.dropDown.getMenu().findSelectedItem().getLabel();
                    self.motivoInput.setValue(motivazioneFrequente);
                };
            popolaDropdown(this.dropDown, motivazioniFrequenti);

            // Fieldset
            var fieldsetLayout = new OO.ui.FieldsetLayout({
                items: [
                    new OO.ui.FieldLayout(this.vandaloInput, {
                        label: 'Vandalo:',
                        align: 'top',
                    }),
                    new OO.ui.FieldLayout(this.motivoInput, {
                        label: 'Motivazione:',
                        align: 'top',
                        /*$overlay: this.$overlay,
                        help: 'Firma aggiunta automaticamente',
                        helpInline: true,*/
                    }),
                    new OO.ui.FieldLayout(this.dropDown, {
                        align: 'inline',
                    }),
                ]
            });

            this.content.addItems([fieldsetLayout]);
            this.panel.$element.append(this.content.$element);
            /* // Disclaimer
            this.content.$element.append( '<small><p>Accertarsi che sia chiaro cosa si intende per <a href="/wiki/Wikipedia:Vandalismo" title="Wikipedia:Vandalismo">vandalismo</a>, che il vandalo sia stato avvertito e che i vandalismi siano recenti.</small>' );*/
            this.$body.append(this.panel.$element);
            this.dropDown.getMenu().on('select', itemSelected); // Quando viene scelta un'opzione dal menu
            this.motivoInput.connect(this, { 'change': 'onMotivoInputChange' }); // Alla modifica del campo motivo
        };

        // Impedisce motivazione vuota
        Dialog.prototype.onMotivoInputChange = function (value) {
            this.actions.setAbilities({
                save: !!value.length
            });
        };

        // getSetupProcess(): passa dei dati all'apertura della finestra
        Dialog.prototype.getSetupProcess = function (data) {
            data = data || {};
            return Dialog.super.prototype.getSetupProcess.call(this, data)
                .next(function () {
                    this.vandaloInput.setValue(data.vandalo);
                    this.primoTentativo = true;
                    segnalatoDiRecente(paginaSegnalazioni).then(result => {
                        result ? console.log(`${utente} già segnalato.`) : console.log(`${utente} non ancora segnalato.`);
                    });

                }, this);
        };

        // getActionProcess(): gestione delle azioni (es. 'save').
        Dialog.prototype.getActionProcess = function (action) {
            var parent = this; // Fallback to parent handler // TODO: <-------
            const url = {
                vc: conf.wgArticlePath.replace('$1', paginaSegnalazioni),
                contribs: conf.wgArticlePath.replace('$1', `Special:Contribs/${this.vandaloInput.getValue()}`)
            };
            const err = {
                giaBloccato: $(`<span>${this.vandaloInput.getValue()} <a href="${url.contribs}">risulta già bloccato</a>. Inviare comunque?</span>`),
                giaSegnalato: $(`<span>${this.vandaloInput.getValue()} è già stato segnalato <a href="${url.vc}#footer">di recente</a>.</span>`)
            };

            return Dialog.super.prototype.getActionProcess.call(this, action)
                // Impedisce se segnalato di recente
                .next(function () {
                    // Alla pressione del tasto "Invia"
                    if (action === 'save') {
                        var promise = $.Deferred();
                        segnalatoDiRecente(paginaSegnalazioni).then(result => {
                            if (result) {
                                promise.reject(new OO.ui.Error(err.giaSegnalato, { recoverable: false }));
                            } else {
                                promise.resolve();
                            }
                        });
                        return promise;
                    }
                }, this)

                // Avvisa se già bloccato
                .next(function () {
                    if (action === 'save') {
                        var promise = $.Deferred();
                        controllaBlocco(this.vandaloInput.getValue(), blocco => {
                            if (blocco && this.primoTentativo) { // Mostra il warning solo la prima volta
                                this.primoTentativo = false;
                                promise.reject(new OO.ui.Error(err.giaBloccato, { warning: true }));
                                console.log(`Segnalazione non inviata: ${parent.vandaloInput.getValue()} già bloccato.\n` +
                                    `Il testo era: "${parent.motivoInput.getValue()}"`);
                            } else {
                                promise.resolve();
                            }
                        });
                        return promise;
                    }
                }, this)

                // Invia la segnalazione
                .next(function () {
                    if (action === 'save') {
                        var promise = $.Deferred();
                        inviaSegnalazione(parent.vandaloInput.getValue(), parent.motivoInput.getValue())
                            .done(() => windowManager.closeWindow(dialog))
                            .fail((code) => { promise.reject(new OO.ui.Error(code)); console.error(`Segnalazione non inviata: ${code}`); });
                        return promise;
                    }
                }, this);
        };

        //if (!windowManager) { // per non perdere contenuto alla chiusura della finestra //FIXME: cleanup dopo invio

        // Crea e aggiunge nuovo window manager
        windowManager = new OO.ui.WindowManager();
        $(document.body).append(windowManager.$element);

        // Crea finestra di dialogo
        dialog = new Dialog();

        // Aggiunge la finestra al window manager
        windowManager.addWindows([dialog]);
        //}

        // Apre la finestra
        windowManager.openWindow(dialog, { vandalo: utente })
            // Codice da eseguire dopo che la finestra è stata aperta    
            .opened.then(function () {
                // Focus all'input per la motivazione
                dialog.motivoInput.focus();
            });
    }

    /**
     * Invia la segnalazione.
     * 
     * @param {string} utente - Vandalo da segnalare
     * @param {string} motivo - Contenuto segnalazione
     * @returns {object} jQuery.Promise
     */
    function inviaSegnalazione(utente, motivo) {

        const segnalazione = `{{vandalo|${utente}}} ${motivo} --~~\~~`;

        // Data odierna, ordinale per il primo del mese
        const dataOdierna = new Date()
            .toLocaleDateString("it-IT", { day: 'numeric', month: 'long' })
            .replace(/\b[1] /, '1º ');

        // URL per link in notifica
        const url = conf.wgArticlePath.replace('$1', paginaSegnalazioni);

        // Per aggiungere sottosezione con data se non già esistente
        return titoloUltimaSezione(paginaSegnalazioni)
            .then(titoloUltimaSezione => {
                return api.postWithEditToken({
                    action: 'edit',
                    title: paginaSegnalazioni,
                    appendtext: (dataOdierna == titoloUltimaSezione) ? ('\n\n' + segnalazione) :
                        ('\n\n' + `=== ${dataOdierna} ===` + '\n' + segnalazione),
                    summary: `/* ${dataOdierna} */ ${oggetto}`
                }).done(data => {
                    mw.notify($(`<span>${utente} è stato segnalato tra i <a href="${url}#footer">vandalismi in corso</a>.</span>`), {
                        title: 'Segnalato',
                        tag: 'notif'
                    });
                    console.log(`L'utente ha scritto "${motivo}" e ha fatto clic su "OK".`);
                    console.log(data);
                });
            });
    }

    /**
     * Ottiene titoli e numeri sezioni.
     * 
     * @param {string} pagina - Titolo della pagina di servizio 
     * @returns {object} jQuery.Promise - Sezioni
     */
    function parsing(pagina) {
        return api.get({
            page: pagina,
            action: 'parse',
            prop: 'sections'
        });
    }

    /**
     * Ottiene il titolo dell'ultima sezione.
     * 
     * @param {string} pagina - Titolo della pagina di servizio
     * @returns {object} jQuery.Promise - Titolo dell'ultima sezione
     */
    function titoloUltimaSezione(pagina) {
        return parsing(pagina).then(data => {
            return data.parse.sections[data.parse.sections.length - 1].line
                // ordinale per il primo del mese
                .replace(/\b[1] /, '1º ')
                .replace('1° ', '1º ');
        });
    }

    /**
    * Ottiene l'ultimo numero di sezione della pagina.
    * 
    * @returns {Promise<string>} Numero dell'ultima sezione
    */
    function indexUltimaSezione(pagina) {
        return parsing(pagina).then(data => data.parse.sections[data.parse.sections.length - 1].index);
    }

    /**
     * Ottiene il wikitesto a partire dal numero della sezione.
     * 
     * @param index Numero di sezione
     * @returns {Promise<string>} wikitesto
     */
    function wikitestoSezione(index, pagina) {
        return api.get({
            page: pagina,
            action: 'parse',
            prop: 'wikitext',
            section: index,
            formatversion: 2 //'cci tua
        }).then(data => data.parse.wikitext);
        //return data.parse.wikitext['*'] //https://stackoverflow.com/q/7610191/8762088; mw:API:JSON_version_2#Changes_to_JSON_output_format
    }

    /**
     * Ottiene il wikitesto delle ultime due sezioni.
     * 
     * @returns {Promise<array>} Array di sezioni
     */
    function ultimeSezioni(pagina) {
        return Promise.all([
            // Wikitesto penultima sezione
            indexUltimaSezione(pagina).then(index => wikitestoSezione(index - 1, pagina)),
            // Wikitesto ultima sezione
            indexUltimaSezione(pagina).then(index => wikitestoSezione(index, pagina))
        ]);
    }

    /**
     * Controlla se il vandalo è già stato segnalato di recente.
     * Solo ultime 2 sezioni e non oltre limite massimo.

     * @param {string} pagina - Titolo della pagina di servizio
     * @returns {Promise<boolean>} bool - Se è già stato segnalato
     */
    function segnalatoDiRecente(pagina) {
        return ultimeSezioni(pagina)
            .then(wikitesto => {
                var regex = /{{[\s\t\n\r]*vandalo[\s\t\n\r]*\|[\s\t\n\r]*([^}]*)\b/gi;
                var segnalati = [];

                var match = regex.exec(wikitesto);
                while (match !== null) {
                    segnalati.push(match[1]);
                    match = regex.exec(wikitesto);
                }

                // Mi accerto che il limite massimo non superi il numero di segnalazioni
                if (maxSegnalazioni > segnalati.length) {
                    maxSegnalazioni = segnalati.length;
                }
                //var segnalatiDiRecente = []
                console.log(`Ultime segnalazioni: ${segnalati.slice(-maxSegnalazioni).join(", ")}.`);
                for (var i = segnalati.length - maxSegnalazioni; i < segnalati.length; i++) {
                    // Se il vandalo è già presente tra le ultime segnalazioni
                    if (utente.toLowerCase() == segnalati[i].toLowerCase()) {
                        return true;
                    }
                    //segnalatiDiRecente.push(segnalati[i])
                }
            });
    }

    /**
     * Controlla se l'utente è bloccato.
     * @param {string} utente - Utente da controllare
     * @param {function} blocco - Risultato
     */
    function controllaBlocco(utente, blocco) {
        api.get({
            action: 'query',
            list: 'blocks',
            bkusers: utente,
            bklimit: '1',
            bkprop: 'by',
            format: 'json'
        }).done(data => {
            blocco(data.query.blocks[0]);
        });
    }

    /**
     * Popola il menu a tendina.
     * 
     * @param dropdown - Dropdown widget
     * @param {string[]} opzioni - Scelte
     */
    function popolaDropdown(dropdown, opzioni) {
        dropdown.menu.addItems(opzioni.map((opzione) => {
            return new OO.ui.MenuOptionWidget({ label: opzione });
        }));
    }

    /**
     * Controlla se un tasto di scelta rapida è disponibile.
     * 
     * @param {string} key - Tasto da provare
     * @returns {boolean} Se disponibile o meno
     */
    function isAccessKeyAvailable(key) {
        //$('[accesskey=v]')[0]
        if (!$(`[accesskey=${key}]`)[0]) { // se non già in uso
            return true;
        }
    }

    /**
     * Trova il primo tasto di scelta rapida disponibile tra quelli indicati
     * 
     * @param {string[]} accessKeys - Tasti da provare
     * @returns {string} Il primo tasto disponibile
     */
    function getAvailableAccessKey(...accessKeys) {
        for (var accessKey of accessKeys) {
            if (isAccessKeyAvailable(accessKey)) {
                return accessKey;
            }
        }
    }

}(mediaWiki, jQuery));

// FIXME:
// Escludere se stessi può rendere difficile il debug in commons.js

// TODO:
// se già bloccato (es. allungare blocco filtro):
//     chiedere conferma anche prima del clic a 'invia'
//     visualizzare durata e autore blocco
// controllare che vandalo sia stato avvisato
// permettere di scegliere utente
//     di selezionare più utenti
//     controllare se utente esiste
// aggiungere anche mese se non presente
// motivazioni personalizzabili (tramite variabile da inserire in user script, JSON o option api)

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.