Source code for pywikibot.logentries

"""Objects representing Mediawiki log entries."""
#
# (C) Pywikibot team, 2007-2021
#
# Distributed under the terms of the MIT license.
#
from collections import UserDict
from contextlib import suppress
from typing import Optional

import pywikibot
from pywikibot.backports import List
from pywikibot.exceptions import Error, HiddenKeyError


_logger = 'wiki'


[docs]class LogEntry(UserDict): """Generic log entry. LogEntry parameters may be retrieved by the corresponding method or the LogEntry key. The following statements are equivalent: action = logentry.action() action = logentry['action'] action = logentry.data['action'] """ # Log type expected. None for every type, or one of the (letype) str : # block/patrol/etc... # Overridden in subclasses. _expected_type = None # type: Optional[str] def __init__(self, apidata, site): """Initialize object from a logevent dict returned by MW API.""" super().__init__(apidata) self.site = site expected_type = self._expected_type if expected_type is not None and expected_type != self.type(): raise Error('Wrong log type! Expecting {}, received {} instead.' .format(expected_type, self.type())) def __missing__(self, key: str): """Debug when the key is missing. HiddenKeyError is raised when the user does not have permission. KeyError is raised otherwise. It also logs debugging information when a key is missing. """ pywikibot.debug('API log entry received:\n{!r}'.format(self), _logger) hidden = {'action', 'logpage', 'ns', 'pageid', 'params', 'title'} if ((key in hidden and 'actionhidden' in self) or (key == 'comment' and 'commenthidden' in self) or (key == 'user' and 'userhidden' in self)): raise HiddenKeyError( "Log entry ({}) has a hidden '{}' key and you don't have " 'permission to view it.'.format(self['type'], key)) raise KeyError("Log entry ({}) has no '{}' key" .format(self['type'], key)) def __repr__(self) -> str: """Return a string representation of LogEntry object.""" return '<{}({}, logid={})>'.format(type(self).__name__, self.site.sitename, self.logid()) def __hash__(self): """Combine site and logid as the hash.""" return self.logid() ^ hash(self.site) def __eq__(self, other) -> bool: """Compare if self is equal to other.""" if not isinstance(other, LogEntry): pywikibot.debug("'{}' cannot be compared with '{}'" .format(type(self).__name__, type(other).__name__), _logger) return False return self.logid() == other.logid() and self.site == other.site def __getattr__(self, item): """Return several items from dict used as methods.""" if item in ('action', 'comment', 'logid', 'ns', 'pageid', 'type', 'user'): # TODO use specific User class for 'user'? return lambda: self[item] return super().__getattribute__(item) @property def _params(self): """ Additional data for some log entry types. :rtype: dict or None """ with suppress(KeyError): return self['params'] return self[self._expected_type]
[docs] def page(self): """ Page on which action was performed. :return: page on action was performed :rtype: pywikibot.Page """ if not hasattr(self, '_page'): self._page = pywikibot.Page(self.site, self['title']) return self._page
[docs] def timestamp(self): """Timestamp object corresponding to event timestamp.""" if not hasattr(self, '_timestamp'): self._timestamp = pywikibot.Timestamp.fromISOformat( self['timestamp']) return self._timestamp
[docs]class OtherLogEntry(LogEntry): """A log entry class for unspecified log events."""
[docs]class UserTargetLogEntry(LogEntry): """A log entry whose target is a user page."""
[docs] def page(self): """Return the target user. This returns a User object instead of the Page object returned by the superclass method. :return: target user :rtype: pywikibot.User """ if not hasattr(self, '_page'): self._page = pywikibot.User(self.site, self['title']) return self._page
[docs]class BlockEntry(LogEntry): """ Block or unblock log entry. It might contain a block or unblock depending on the action. The duration, expiry and flags are not available on unblock log entries. """ _expected_type = 'block' def __init__(self, apidata, site): """Initializer.""" super().__init__(apidata, site) # When an autoblock is removed, the "title" field is not a page title # See bug T19781 pos = self['title'].find('#') self.isAutoblockRemoval = pos > 0 if self.isAutoblockRemoval: self._blockid = int(self['title'][pos + 1:])
[docs] def page(self): """ Return the blocked account or IP. :return: the Page object of username or IP if this block action targets a username or IP, or the blockid if this log reflects the removal of an autoblock :rtype: pywikibot.Page or int """ # TODO what for IP ranges ? if self.isAutoblockRemoval: return self._blockid return super().page()
[docs] def flags(self) -> List[str]: """ Return a list of (str) flags associated with the block entry. It raises an Error if the entry is an unblocking log entry. :return: list of flags strings """ if self.action() == 'unblock': return [] if not hasattr(self, '_flags'): self._flags = self._params['flags'] # pre mw 1.25 returned a delimited string. if isinstance(self._flags, str): if self._flags: self._flags = self._flags.split(',') else: self._flags = [] return self._flags
[docs] def duration(self): """ Return a datetime.timedelta representing the block duration. :return: datetime.timedelta, or None if block is indefinite. """ if not hasattr(self, '_duration'): if self.expiry() is None: self._duration = None else: # Doing the difference is easier than parsing the string self._duration = self.expiry() - self.timestamp() return self._duration
[docs] def expiry(self): """ Return a Timestamp representing the block expiry date. :rtype: pywikibot.Timestamp or None """ if not hasattr(self, '_expiry'): details = self._params.get('expiry') if details: self._expiry = pywikibot.Timestamp.fromISOformat(details) else: self._expiry = None # for infinite blocks return self._expiry
[docs]class RightsEntry(LogEntry): """Rights log entry.""" _expected_type = 'rights' @property def oldgroups(self): """Return old rights groups.""" params = self._params if 'old' in params: # old mw style (mw < 1.25) return params['old'].split(',') if params['old'] else [] return params['oldgroups'] @property def newgroups(self): """Return new rights groups.""" params = self._params if 'new' in params: # old mw style (mw < 1.25) return params['new'].split(',') if params['new'] else [] return params['newgroups']
[docs]class UploadEntry(LogEntry): """Upload log entry.""" _expected_type = 'upload'
[docs] def page(self): """ Return FilePage on which action was performed. :rtype: pywikibot.FilePage """ if not hasattr(self, '_page'): self._page = pywikibot.FilePage(self.site, self['title']) return self._page
[docs]class MoveEntry(LogEntry): """Move log entry.""" _expected_type = 'move' @property def target_ns(self): """Return namespace object of target page.""" # key has been changed in mw 1.25 to 'target_ns' return self.site.namespaces[self._params['target_ns'] if 'target_ns' in self._params else self._params['new_ns']] @property def target_title(self): """Return the target title.""" # key has been changed in mw 1.25 to 'target_title' return (self._params['target_title'] if 'target_title' in self._params else self._params['new_title']) @property def target_page(self): """ Return target page object. :rtype: pywikibot.Page """ if not hasattr(self, '_target_page'): self._target_page = pywikibot.Page(self.site, self.target_title) return self._target_page
[docs] def suppressedredirect(self) -> bool: """Return True if no redirect was created during the move.""" # Introduced in MW r47901 return 'suppressedredirect' in self._params
[docs]class PatrolEntry(LogEntry): """Patrol log entry.""" _expected_type = 'patrol' @property def current_id(self) -> int: """Return the current id.""" # key has been changed in mw 1.25; try the new mw style first # sometimes it returns strs sometimes ints return int(self._params['curid'] if 'curid' in self._params else self._params['cur']) @property def previous_id(self) -> int: """Return the previous id.""" # key has been changed in mw 1.25; try the new mw style first # sometimes it returns strs sometimes ints return int(self._params['previd'] if 'previd' in self._params else self._params['prev']) @property def auto(self) -> bool: """Return auto patrolled.""" return 'auto' in self._params and self._params['auto'] != 0
[docs]class LogEntryFactory: """ LogEntry Factory. Only available method is create() """ _logtypes = { 'block': BlockEntry, 'rights': RightsEntry, 'upload': UploadEntry, 'move': MoveEntry, 'patrol': PatrolEntry, } def __init__(self, site, logtype=None): """ Initializer. :param site: The site on which the log entries are created. :type site: BaseSite :param logtype: The log type of the log entries, if known in advance. If None, the Factory will fetch the log entry from the data to create each object. :type logtype: (letype) str : move/block/patrol/etc... """ self._site = site if logtype is None: self._creator = self._create_from_data else: # Bind a Class object to self._creator: # When called, it will initialize a new object of that class logclass = self.get_valid_entry_class(logtype) self._creator = lambda data: logclass(data, self._site)
[docs] def create(self, logdata): """ Instantiate the LogEntry object representing logdata. :param logdata: <item> returned by the api :type logdata: dict :return: LogEntry object representing logdata """ return self._creator(logdata)
[docs] def get_valid_entry_class(self, logtype): """ Return the class corresponding to the @logtype string parameter. :return: specified subclass of LogEntry :rtype: LogEntry :raise KeyError: logtype is not valid """ if logtype not in self._site.logtypes: raise KeyError('{} is not a valid logtype'.format(logtype)) return LogEntryFactory.get_entry_class(logtype)
[docs] @classmethod def get_entry_class(cls, logtype): """ Return the class corresponding to the @logtype string parameter. :return: specified subclass of LogEntry :rtype: LogEntry :note: this class method cannot verify whether the given logtype already exits for a given site; to verify use Site.logtypes or use the get_valid_entry_class instance method instead. """ if logtype not in cls._logtypes: if logtype in ('newusers', 'thanks'): bases = (UserTargetLogEntry, OtherLogEntry) else: bases = (OtherLogEntry, ) classname = str(logtype.capitalize() + 'Entry' if logtype is not None else OtherLogEntry.__name__) cls._logtypes[logtype] = type( classname, bases, {'_expected_type': logtype}) return cls._logtypes[logtype]
def _create_from_data(self, logdata: dict): """ Check for logtype from data, and creates the correct LogEntry. :param logdata: log entry data :rtype: LogEntry """ try: logtype = logdata['type'] except KeyError: pywikibot.debug('API log entry received:\n{}'.format(logdata), _logger) raise Error("Log entry has no 'type' key") return LogEntryFactory.get_entry_class(logtype)(logdata, self._site)
# For backward compatibility ProtectEntry = LogEntryFactory.get_entry_class('protect') DeleteEntry = LogEntryFactory.get_entry_class('delete') ImportEntry = LogEntryFactory.get_entry_class('import') NewUsersEntry = LogEntryFactory.get_entry_class('newusers') ThanksEntry = LogEntryFactory.get_entry_class('thanks')