User:Username/BlockAbuser.js: Difference between revisions

From Test Wiki
Jump to navigation Jump to search
Content deleted Content added
...
No edit summary
 
(5 intermediate revisions by the same user not shown)
Line 1: Line 1:
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery'], function () {
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery'], function () {
// Run only on the exact Special:AbuseLog page (no subpages)
if (mw.config.get('wgCanonicalSpecialPageName') !== 'AbuseLog') {
if (mw.config.get('wgPageName') !== 'Special:AbuseLog') {
return;
return;
}
}


const $content = $('#mw-content-text');
const $content = $('#mw-content-text');
const userData = {}; // username -> { latestLogId, extraHits, $userLink, $blockLink, allLinks }
const seenUsers = new Set();
const userData = {}; // username => { latestLogId, extraHits }


const ipRegex = /^(?:\d{1,3}\.){3}\d{1,3}$|:/;
const ipRegex = /^(?:\d{1,3}\.){3}\d{1,3}$|:/;


// Helper: decode username from URL
// Helper to decode username from a user link href
function getUsernameFromHref(href) {
function getUsernameFromHref(href) {
if (!href) return null;
const parts = href.split('/wiki/User:');
const match = href.match(/\/wiki\/User:([^?#]+)/);
if (parts.length < 2) return null;
if (!match) return null;
return decodeURIComponent(parts[1]).replace(/_/g, ' ').trim();
return decodeURIComponent(match[1]).replace(/_/g, ' ').trim();
}
}


// 1. Scan all user links, build data for each user (latest log ID, counts)
// Step 1: Collect user info from each abuse log entry (<li>)
$content.find('a').each(function () {
$content.find('li').each(function () {
const $link = $(this);
const $li = $(this);
const href = $link.attr('href');
if (!href || !href.includes('/wiki/User:')) return;


const username = getUsernameFromHref(href);
// Find all user links in this <li>
const $userLinks = $li.find('a').filter(function () {
if (!username) return;
const href = $(this).attr('href');
if (ipRegex.test(username)) return; // skip IPs
return href && href.includes('/wiki/User:');

});
// The abuse log entry ID is usually part of the URL in a sibling 'details' link or 'examine' link
// but since your example shows a pattern, we'll grab the closest abuse log id from a nearby 'details' link

// Try to find closest "details" link in this log row (it's a link with URL containing 'Special:AbuseLog/')
let $row = $link.closest('li, tr, div'); // abuse log entries are typically in <li> or <tr> or <div>
if ($row.length === 0) {
// fallback: parent of parent
$row = $link.parent().parent();
}


if ($row.length === 0) return;
if ($userLinks.length === 0) return;


// Find 'details' or 'examine' link with abuse log id
// Find abuse log id for this entry (from any link containing Special:AbuseLog/{id})
let logId = null;
let logId = null;
$row.find('a').each(function () {
$li.find('a').each(function () {
const $a = $(this);
const href = $(this).attr('href');
const ahref = $a.attr('href') || '';
if (!href) return;
const match = ahref.match(/Special:AbuseLog\/(\d+)/);
const match = href.match(/Special:AbuseLog\/(\d+)/);
if (match) {
if (match) {
logId = match[1];
logId = match[1];
return false; // break
return false; // stop iteration
}
}
});
});
if (!logId) return;


if (!logId) return; // no log id found, skip
// For each user link in this li:
$userLinks.each(function () {
const $userLink = $(this);
const username = getUsernameFromHref($userLink.attr('href'));


if (!username) return;
// Store only the most recent log id (higher number assumed more recent)
if (!userData[username]) {
if (ipRegex.test(username)) return; // skip IP addresses

userData[username] = { latestLogId: logId, extraHits: 0, $link: $link };
// Find the "block" link in this same <li> that applies to this user
} else {
// Update latestLogId if this is newer (numerical compare)
// Block links usually have href with "block=" and user name
// Sometimes encoded spaces appear as + or %20 - check both
if (parseInt(logId) > parseInt(userData[username].latestLogId)) {
userData[username].latestLogId = logId;
const encodedUser1 = encodeURIComponent(username).replace(/%20/g, '+');
userData[username].$link = $link; // store link for checkbox placement
const encodedUser2 = encodeURIComponent(username).replace(/%20/g, '%20');

const $blockLink = $li.find('a').filter(function () {
const href = $(this).attr('href') || '';
return href.includes('block=') &&
(href.includes(encodedUser1) || href.includes(encodedUser2));
}).first();

if (!userData[username]) {
userData[username] = {
latestLogId: logId,
extraHits: 0,
$userLink: $userLink,
$blockLink: $blockLink.length ? $blockLink : null,
allUserLinks: [$userLink]
};
} else {
userData[username].allUserLinks.push($userLink);
if (parseInt(logId, 10) > parseInt(userData[username].latestLogId, 10)) {
userData[username].latestLogId = logId;
userData[username].$userLink = $userLink;
if ($blockLink.length) userData[username].$blockLink = $blockLink;
}
userData[username].extraHits++;
}
}
});
// Increment extra hits count
userData[username].extraHits++;
}
});
});


// 2. Add checkboxes ONLY next to the most recent log entry user link
// Step 2: Delink all but the most recent user link for each user
Object.entries(userData).forEach(([username, data]) => {
Object.entries(userData).forEach(([username, data]) => {
if (!data.$link || data.$link.prev('.blockabuser-checkbox').length) {
data.allUserLinks.forEach(($link) => {
return; // already has a checkbox
if ($link[0] !== data.$userLink[0]) {
$link.replaceWith(document.createTextNode($link.text()));
}
const $checkbox = $('<input>', {
}
type: 'checkbox',
});
class: 'blockabuser-checkbox',
'data-username': username,
'data-latestlogid': data.latestLogId,
'data-extrahits': data.extraHits
}).css({
marginRight: '6px',
verticalAlign: 'middle',
cursor: 'pointer'
}).attr('title', 'Select this user for block review');

data.$link.before($checkbox);
});
});


// 3. Add control button above abuse log to process checked users
// Step 3: Use MediaWiki API to check block status for all users found
const $btn = $('<button>')
const usernames = Object.keys(userData);
if (usernames.length === 0) {
.text('Open selected user AbuseLogs and generate summary')
// No users found - just add the button and stop
.css({
margin: '1em 0',
addButtonAndSetup(userData);
padding: '6px 12px',
return;
}
cursor: 'pointer'
});


$content.prepend($btn);
const api = new mw.Api();


api.get({
$btn.on('click', function () {
action: 'query',
const checked = $('.blockabuser-checkbox:checked');
if (!checked.length) {
list: 'users',
ususers: usernames.join('|'),
alert('Please select at least one user.');
return;
usprop: 'blockinfo'
}).done(function (data) {
if (data && data.query && data.query.users) {
data.query.users.forEach(user => {
if (user.blockid) {
const uname = user.name;
if (userData[uname] && userData[uname].$blockLink) {
// Underline and color the existing block link to indicate the user is blocked
userData[uname].$blockLink.css({
'text-decoration': 'underline',
'font-weight': 'bold',
'color': '#c00',
'cursor': 'pointer'
});
}
}
});
}
}
addButtonAndSetup(userData);
}).fail(function () {
addButtonAndSetup(userData);
});


// Step 4: Add the top button, open tabs & show summary on click
let summaryLines = [];
checked.each(function () {
function addButtonAndSetup(userData) {
const $cb = $(this);
const $btn = $('<button>')
.text('Open all user AbuseLogs, talk deletion, and show summary')
const username = $cb.data('username');
const latestLogId = $cb.data('latestlogid');
.css({
const extraHits = parseInt($cb.data('extrahits'), 10);
margin: '1em 0',
padding: '6px 12px',

// Open user filtered AbuseLog in new tab
cursor: 'pointer'
const abuseLogUrl = mw.util.getUrl('Special:AbuseLog', {
wpSearchUser: username
});
});
window.open(abuseLogUrl, '_blank');


$content.prepend($btn);
// Compose summary line for this user

let line = `[[Special:AbuseLog/${latestLogId}]]`;
if (extraHits > 0) {
$btn.on('click', function () {
const usernames = Object.keys(userData);
line += ` (+[[Special:AbuseLog/${username}|${extraHits}]])`;
if (usernames.length === 0) {
alert('No users found to process.');
return;
}
}
summaryLines.push(line);
});


const summaryText =
let summaryLines = [];
usernames.forEach(username => {
'Spambot or spam-only accounts detected. Details:\n' +
summaryLines.join('\n');
const data = userData[username];
const latestLogId = data.latestLogId;
const extraHits = parseInt(data.extraHits, 10);


// Show summary in a popup for easy copy-paste
// Open AbuseLog filtered for the user
const abuseLogUrl = mw.util.getUrl('Special:AbuseLog', {
alert(summaryText);
wpSearchUser: username
});
});
window.open(abuseLogUrl, '_blank');

// Open User Talk deletion page with prefilled reason
const talkDeleteUrl = mw.util.getUrl('Special:Delete/' + 'User_talk:' + encodeURIComponent(username), {
reason: `Talk page of an indefinitely blocked user that has little value. The content was: blahblah.`
});
window.open(talkDeleteUrl, '_blank');

// Build summary line
let line = `[[Special:AbuseLog/${latestLogId}]]`;
if (extraHits > 0) {
line += ` (+[[Special:AbuseLog/${username}|${extraHits}]])`;
}
summaryLines.push(line);
});

alert(
'Spambot or spam-only accounts detected. Details:\n' +
summaryLines.join('\n')
);
});
}
});
});

Latest revision as of 02:37, 21 January 2026

mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery'], function () {
    // Run only on the exact Special:AbuseLog page (no subpages)
    if (mw.config.get('wgPageName') !== 'Special:AbuseLog') {
        return;
    }

    const $content = $('#mw-content-text');
    const userData = {}; // username -> { latestLogId, extraHits, $userLink, $blockLink, allLinks }

    const ipRegex = /^(?:\d{1,3}\.){3}\d{1,3}$|:/;

    // Helper to decode username from a user link href
    function getUsernameFromHref(href) {
        if (!href) return null;
        const match = href.match(/\/wiki\/User:([^?#]+)/);
        if (!match) return null;
        return decodeURIComponent(match[1]).replace(/_/g, ' ').trim();
    }

    // Step 1: Collect user info from each abuse log entry (<li>)
    $content.find('li').each(function () {
        const $li = $(this);

        // Find all user links in this <li>
        const $userLinks = $li.find('a').filter(function () {
            const href = $(this).attr('href');
            return href && href.includes('/wiki/User:');
        });

        if ($userLinks.length === 0) return;

        // Find abuse log id for this entry (from any link containing Special:AbuseLog/{id})
        let logId = null;
        $li.find('a').each(function () {
            const href = $(this).attr('href');
            if (!href) return;
            const match = href.match(/Special:AbuseLog\/(\d+)/);
            if (match) {
                logId = match[1];
                return false; // stop iteration
            }
        });
        if (!logId) return;

        // For each user link in this li:
        $userLinks.each(function () {
            const $userLink = $(this);
            const username = getUsernameFromHref($userLink.attr('href'));

            if (!username) return;
            if (ipRegex.test(username)) return; // skip IP addresses

            // Find the "block" link in this same <li> that applies to this user
            // Block links usually have href with "block=" and user name
            // Sometimes encoded spaces appear as + or %20 - check both
            const encodedUser1 = encodeURIComponent(username).replace(/%20/g, '+');
            const encodedUser2 = encodeURIComponent(username).replace(/%20/g, '%20');

            const $blockLink = $li.find('a').filter(function () {
                const href = $(this).attr('href') || '';
                return href.includes('block=') &&
                    (href.includes(encodedUser1) || href.includes(encodedUser2));
            }).first();

            if (!userData[username]) {
                userData[username] = {
                    latestLogId: logId,
                    extraHits: 0,
                    $userLink: $userLink,
                    $blockLink: $blockLink.length ? $blockLink : null,
                    allUserLinks: [$userLink]
                };
            } else {
                userData[username].allUserLinks.push($userLink);
                if (parseInt(logId, 10) > parseInt(userData[username].latestLogId, 10)) {
                    userData[username].latestLogId = logId;
                    userData[username].$userLink = $userLink;
                    if ($blockLink.length) userData[username].$blockLink = $blockLink;
                }
                userData[username].extraHits++;
            }
        });
    });

    // Step 2: Delink all but the most recent user link for each user
    Object.entries(userData).forEach(([username, data]) => {
        data.allUserLinks.forEach(($link) => {
            if ($link[0] !== data.$userLink[0]) {
                $link.replaceWith(document.createTextNode($link.text()));
            }
        });
    });

    // Step 3: Use MediaWiki API to check block status for all users found
    const usernames = Object.keys(userData);
    if (usernames.length === 0) {
        // No users found - just add the button and stop
        addButtonAndSetup(userData);
        return;
    }

    const api = new mw.Api();

    api.get({
        action: 'query',
        list: 'users',
        ususers: usernames.join('|'),
        usprop: 'blockinfo'
    }).done(function (data) {
        if (data && data.query && data.query.users) {
            data.query.users.forEach(user => {
                if (user.blockid) {
                    const uname = user.name;
                    if (userData[uname] && userData[uname].$blockLink) {
                        // Underline and color the existing block link to indicate the user is blocked
                        userData[uname].$blockLink.css({
                            'text-decoration': 'underline',
                            'font-weight': 'bold',
                            'color': '#c00',
                            'cursor': 'pointer'
                        });
                    }
                }
            });
        }
        addButtonAndSetup(userData);
    }).fail(function () {
        addButtonAndSetup(userData);
    });

    // Step 4: Add the top button, open tabs & show summary on click
    function addButtonAndSetup(userData) {
        const $btn = $('<button>')
            .text('Open all user AbuseLogs, talk deletion, and show summary')
            .css({
                margin: '1em 0',
                padding: '6px 12px',
                cursor: 'pointer'
            });

        $content.prepend($btn);

        $btn.on('click', function () {
            const usernames = Object.keys(userData);
            if (usernames.length === 0) {
                alert('No users found to process.');
                return;
            }

            let summaryLines = [];
            usernames.forEach(username => {
                const data = userData[username];
                const latestLogId = data.latestLogId;
                const extraHits = parseInt(data.extraHits, 10);

                // Open AbuseLog filtered for the user
                const abuseLogUrl = mw.util.getUrl('Special:AbuseLog', {
                    wpSearchUser: username
                });
                window.open(abuseLogUrl, '_blank');

                // Open User Talk deletion page with prefilled reason
                const talkDeleteUrl = mw.util.getUrl('Special:Delete/' + 'User_talk:' + encodeURIComponent(username), {
                    reason: `Talk page of an indefinitely blocked user that has little value. The content was: blahblah.`
                });
                window.open(talkDeleteUrl, '_blank');

                // Build summary line
                let line = `[[Special:AbuseLog/${latestLogId}]]`;
                if (extraHits > 0) {
                    line += ` (+[[Special:AbuseLog/${username}|${extraHits}]])`;
                }
                summaryLines.push(line);
            });

            alert(
                'Spambot or spam-only accounts detected. Details:\n' +
                summaryLines.join('\n')
            );
        });
    }
});