Note : après avoir enregistré la page, vous devrez forcer le rechargement complet du cache de votre navigateur pour voir les changements.

Mozilla / Firefox / Konqueror / Safari : maintenez la touche Majuscule (Shift) en cliquant sur le bouton Actualiser (Reload) ou pressez Maj-Ctrl-R (Cmd-R sur Apple Mac) ;

Firefox (sur GNU/Linux) / Chrome / Internet Explorer / Opera : maintenez la touche Ctrl en cliquant sur le bouton Actualiser ou pressez Ctrl-F5.
(async function () { 
'use strict';

const CONFIG = {
    usernameToPublish: 'Bahati11',
    pageTitle: 'User:Bahati11/Bilan',
    periodDays: 30,
    topN: 10,
    timezone: 'Africa/Kinshasa',
    editSummary: 'Mise à jour du bilan automatique (top10).'
};

if (typeof mw === 'undefined' || !mw.Api) return
const api = new mw.Api({ ajax: { url: 'https://fr.wikipedia.org/w/api.php' } });
const wait = ms => new Promise(r => setTimeout(r, ms));

function showProgress(msg) {
    let $box = $('#bilanProgressBox');
    if (!$box.length) {
        $box = $('<div id="bilanProgressBox" style="border:1px solid #aaa; background:#f8f9fa; padding:0.5em; margin:0.5em 0; font-size:90%;"></div>');
        $('#mw-content-text').prepend($box);
    }
    $box.append($('<div>').text(msg));
}

async function fetchRecentChanges() {
    showProgress('Récupération des modifications récentes...');
    const changes = []
    let params = {
        action: 'query',
        list: 'recentchanges',
        rcprop: 'title|ids|user|timestamp|comment|flags|patrolled|sizes|redirect|tags',
        rctype: 'edit|new|log',
        rcend: new Date(Date.now() - CONFIG.periodDays * 24 * 3600 * 1000).toISOString(),
        rcstart: new Date().toISOString(),
        rclimit: 'max'
    }
    let cont = {}
    do {
        const resp = await api.get(Object.assign({}, params, cont))
        if (resp && resp.query && resp.query.recentchanges) changes.push(...resp.query.recentchanges)
        cont = resp.continue || {}
        await wait(200)
    } while (cont && cont.rccontinue)
    return changes
}

function aggregate(changes) {
    showProgress('Analyse des données...');
    const byUser = {}
    const byPage = {}
    const revertSignals = {}
    for (const rc of changes) {
        if (!rc.type || (rc.type !== 'edit' && rc.type !== 'new' && rc.type !== 'log')) continue
        const user = rc.user || '(unknown)'
        const title = rc.title || '(unknown)'
        byUser[user] = (byUser[user] || 0) + 1
        byPage[title] = (byPage[title] || 0) + 1
        if (rc.comment && /revert|rollback|rv|undo|restaur|annul/i.test(rc.comment)) revertSignals[user] = (revertSignals[user] || 0) + 1
    }
    const topUsers = Object.keys(byUser).map(u => ({ user: u, edits: byUser[u] })).sort((a, b) => b.edits - a.edits)
    const topPages = Object.keys(byPage).map(t => ({ title: t, edits: byPage[t] })).sort((a, b) => b.edits - a.edits)
    const suspectedVandals = Object.keys(revertSignals).map(u => ({ user: u, revertSignals: revertSignals[u] })).sort((a, b) => b.revertSignals - a.revertSignals)
    return { topUsers, topPages, suspectedVandals }
}

async function enrichUsers(listOfUsers) {
    showProgress('Récupération des informations sur les utilisateurs...');
    const CHUNK = 50
    const usersInfo = {}
    for (let i = 0; i < listOfUsers.length; i += CHUNK) {
        const chunk = listOfUsers.slice(i, i + CHUNK)
        const resp = await api.get({ action: 'query', list: 'users', ususers: chunk.join('|'), usprop: 'groups|editcount' })
        if (resp && resp.query && resp.query.users) {
            for (const u of resp.query.users) usersInfo[u.name] = { name: u.name, groups: u.groups || [], editcount: u.editcount || 0 }
        }
        await wait(200)
    }
    return usersInfo
}

function table(items, col1, col2, key1, key2) {
    let w = `{| class="wikitable sortable" style="width:60%; text-align:center; margin:1em auto;"\n! Rang !! ${col1} !! ${col2}\n`
    for (let i = 0; i < Math.min(CONFIG.topN, items.length); i++) {
        const it = items[i]
        w += `|-${'\n'}| ${i + 1} || ${it[key1]} || ${it[key2]}\n`
    }
    w += '|}\n\n'
    return w
}

function buildWikitext(stats, botsList, patrollersList) {
    const now = new Date()
    let header = `== Bilan des ${CONFIG.topN} (derniers ${CONFIG.periodDays} jours) ==\n<small>Dernière mise à jour : ${now.toUTCString()} (${CONFIG.timezone}) — [[${CONFIG.pageTitle}|Mettre à jour maintenant]]</small>\n\n`
    let body = header
    body += `=== Meilleurs articles ===\n${table(stats.topPages, 'Article', 'Éditions', 'title', 'edits')}`
    body += `=== Meilleurs contributeurs ===\n${table(stats.topUsers, 'Utilisateur', 'Éditions', 'user', 'edits')}`
    body += `=== Meilleurs bots ===\n${table(botsList, 'Bot', 'Éditions', 'user', 'edits')}`
    body += `=== Patrouilleurs actifs ===\n${table(patrollersList, 'Patrouilleur', 'Éditions', 'user', 'edits')}`
    body += `=== Vandales présumés ===\n${table(stats.suspectedVandals, 'Utilisateur', 'Signaux', 'user', 'revertSignals')}`
    return body
}

async function publishWikitext(text) {
    showProgress('Publication sur la page...');
    const tokenResp = await api.get({ action: 'query', meta: 'tokens', type: 'csrf' })
    return api.postWithToken('csrf', { action: 'edit', title: CONFIG.pageTitle, text: text, summary: CONFIG.editSummary })
}

async function runUpdate() {
    showProgress('Démarrage de la mise à jour...');
    const changes = await fetchRecentChanges()
    const stats = aggregate(changes)
    const usersToCheck = Array.from(new Set(stats.topUsers.map(u => u.user).concat(stats.suspectedVandals.map(u => u.user))))
    const usersInfo = await enrichUsers(usersToCheck)
    const botsList = Object.values(usersInfo).filter(i => i.groups.includes('bot')).map(i => ({ user: i.name, edits: i.editcount })).sort((a, b) => b.edits - a.edits)
    const patrollersList = Object.values(usersInfo).filter(i => i.groups.includes('patroller') || i.groups.includes('autoreview') || i.groups.includes('reviewer')).map(i => ({ user: i.name, edits: i.editcount })).sort((a, b) => b.edits - a.edits)
    const wikitext = buildWikitext(stats, botsList, patrollersList)
    await publishWikitext(wikitext)
    showProgress('Mise à jour terminée.');
}

async function generateIfEmpty() {
    const resp = await api.get({ action: 'query', prop: 'revisions', rvprop: 'content', titles: CONFIG.pageTitle })
    const pages = resp.query.pages
    const firstPage = pages[Object.keys(pages)[0]]
    if (!firstPage.revisions) {
        showProgress('Page inexistante détectée. Première génération en cours...');
        await runUpdate()
        location.reload()
    }
}

function addLinkToTools() {
    const $tools = $('#p-tb ul');
    if ($tools.length && !$('#majBLink').length) {
        const $link = $('<li id="majBLink"><a href="#">MàJ-B</a></li>');
        $tools.append($link);
        $link.find('a').on('click', async function (e) {
            e.preventDefault();
            $('#bilanProgressBox').remove();
            await runUpdate();
            alert('Mise à jour du Bilan terminée');
        });
    }
}

if (mw.config.get('wgPageName') === CONFIG.pageTitle.replace(/ /g, '_')) {
    const link = $('<span style="float:right; font-size:small;">[<a href="#" id="updateBilan">Mettre à jour</a>]</span>')
    $('#firstHeading').append(link)
    $('#updateBilan').on('click', async function (e) {
        e.preventDefault()
        $('#bilanProgressBox').remove()
        await runUpdate()
        location.reload()
    })
    generateIfEmpty()
}

addLinkToTools();

})();