wikimedia/pywikibot

View on GitHub
scripts/revertbot.py

Summary

Maintainability
A
30 mins
Test Coverage
#!/usr/bin/env python3
r"""This script can be used for reverting certain edits.

The following command line parameters are supported:

-username   Edits of which user need to be reverted. Default is bot's
            username (:code:`site.username()`).

-rollback   Rollback edits instead of reverting them.

            .. note:: No diff would be shown in this mode.

-limit:num  [int] Use the last num contributions to be checked for
            revert. Default is 500.

Users who want to customize the behaviour should subclass the `BaseRevertBot`
and override its `callback` method. Here is a sample:

.. code:: python

    class myRevertBot(BaseRevertBot):

        '''Example revert bot.'''

        def callback(self, item) -> bool:
            '''Sample callback function for 'private' revert bot.

            :param item: an item from user contributions
            :type item: dict
            '''
            if 'top' in item:
                page = pywikibot.Page(self.site, item['title'])
                text = page.get(get_redirect=True)
                pattern = re.compile(r'\[\[.+?:.+?\..+?\]\]')
                return bool(pattern.search(text))
            return False

"""
#
# (C) Pywikibot team, 2008-2024
#
# Distributed under the terms of the MIT license.
#
from __future__ import annotations

import pywikibot
from pywikibot import i18n
from pywikibot.backports import Container
from pywikibot.bot import OptionHandler
from pywikibot.date import format_date, formatYear
from pywikibot.exceptions import APIError, Error


class BaseRevertBot(OptionHandler):

    """Base revert bot.

    Subclass this bot and override callback to get it to do something useful.
    """

    available_options = {
        'comment': '',
        'rollback': False,
        'limit': 500
    }

    def __init__(self, site=None, **kwargs) -> None:
        """Initializer."""
        self.site = site or pywikibot.Site()
        self.user = kwargs.pop('user', self.site.username())
        super().__init__(**kwargs)

    def get_contributions(self, total: int = 500, ns=None):
        """Get contributions."""
        return self.site.usercontribs(user=self.user, namespaces=ns,
                                      total=total)

    def revert_contribs(self, callback=None) -> None:
        """Revert contributions."""
        if callback is None:
            callback = self.callback

        for item in self.get_contributions(total=self.opt.limit):
            if callback(item):
                result = self.revert(item)
                if result:
                    pywikibot.info(f"{item['title']}: {result}")
                else:
                    pywikibot.info(f"Skipped {item['title']}")
            else:
                pywikibot.info(f"Skipped {item['title']} by callback")

    @staticmethod
    def callback(item: Container) -> bool:
        """Callback function."""
        return 'top' in item

    def local_timestamp(self, ts) -> str:
        """Convert Timestamp to a localized timestamp string.

        .. versionadded:: 7.0
        """
        year = formatYear(self.site.lang, ts.year)
        date = format_date(ts.month, ts.day, self.site)
        *_, time = str(ts).strip('Z').partition('T')
        return f'{date} {year} {time}'

    def revert(self, item) -> str | bool:
        """Revert a single item."""
        page = pywikibot.Page(self.site, item['title'])
        history = list(page.revisions(total=2))
        if len(history) <= 1:
            return False

        rev = history[1]

        pywikibot.info('\n\n>>> <<lightpurple>>{}<<default>> <<<'
                       .format(page.title(as_link=True, force_interwiki=True,
                                          textlink=True)))

        if not self.opt.rollback:
            comment = i18n.twtranslate(
                self.site, 'revertbot-revert',
                {'revid': rev.revid,
                 'author': rev.user,
                 'timestamp': self.local_timestamp(rev.timestamp)})
            if self.opt.comment:
                comment += ': ' + self.opt.comment

            old = page.text
            page.text = page.getOldVersion(rev.revid)
            pywikibot.showDiff(old, page.text)
            page.save(comment)
            return comment

        try:
            self.site.rollbackpage(page, user=self.user, markbot=True)
        except APIError as e:
            if e.code == 'badtoken':
                pywikibot.error(
                    'There was an API token error rollbacking the edit')
                return False
        except Error:
            pass
        else:
            return (f'The edit(s) made in {page.title()} by {self.user}'
                    ' was rollbacked')

        pywikibot.exception(exc_info=False)
        return False


# for compatibility only
myRevertBot = BaseRevertBot  # noqa: N816


def main(*args: str) -> None:
    """Process command line arguments and invoke bot.

    If args is an empty list, sys.argv is used.

    :param args: command line arguments
    """
    options = {}

    for arg in pywikibot.handle_args(args):
        opt, _, value = arg.partition(':')
        if not opt.startswith('-'):
            continue
        opt = opt[1:]
        if opt == 'username':
            options['user'] = value or pywikibot.input(
                'Please enter username of the person you want to revert:')
        elif opt == 'rollback':
            options[opt] = True
        elif opt == 'limit':
            options[opt] = int(value)

    bot = myRevertBot(**options)
    bot.revert_contribs()


if __name__ == '__main__':
    main()