diff --git a/bot/wiki/exceptions.py b/bot/wiki/exceptions.py index 0e7e824..5a87fda 100644 --- a/bot/wiki/exceptions.py +++ b/bot/wiki/exceptions.py @@ -3,7 +3,7 @@ """ EarwigBot's Wiki Toolset: Exceptions -This module contains all exceptions used by the wiki package. +This module contains all exceptions used by the wiki package. There are a lot. """ class WikiToolsetError(Exception): @@ -22,11 +22,6 @@ class LoginError(WikiToolsetError): """An error occured while trying to login. Perhaps the username/password is incorrect.""" -class PermissionsError(WikiToolsetError): - """We tried to do something we don't have permission to, like a non-admin - trying to delete a page, or trying to edit a page when no login information - was provided.""" - class NamespaceNotFoundError(WikiToolsetError): """A requested namespace name or namespace ID does not exist.""" @@ -45,3 +40,27 @@ class RedirectError(WikiToolsetError): class UserNotFoundError(WikiToolsetError): """Attempting to get certain information about a user that does not exist.""" + +class EditError(WikiToolsetError): + """We got some error while editing. Sometimes, a subclass of this exception + will be used, like PermissionsError or EditConflictError.""" + +class PermissionsError(EditError): + """We tried to do something we don't have permission to, like a non-admin + trying to delete a page, or trying to edit a page when no login information + was provided.""" + +class EditConflictError(EditError): + """We've gotten an edit conflict or a (rarer) delete/recreate conflict.""" + +class NoContentError(EditError): + """We tried to create a page or new section with no content.""" + +class ContentTooBigError(EditError): + """The edit we tried to push exceeded the article size limit.""" + +class SpamDetectedError(EditError): + """The spam filter refused our edit.""" + +class FilteredError(EditError): + """The edit filter refused our edit.""" diff --git a/bot/wiki/page.py b/bot/wiki/page.py index 41a789b..812824b 100644 --- a/bot/wiki/page.py +++ b/bot/wiki/page.py @@ -222,24 +222,104 @@ class Page(object): self._load_attributes() self._force_existence() - def _get_token(self): - """Tries to get an edit token for the page. - - This is actually the same as the delete and protect tokens, so we'll - use it for everything. Raises PermissionError if we're not allowed to - edit the page, otherwise sets self._token and self._starttimestamp. + def _edit(self, params=None, text=None, summary=None, minor=None, bot=None, + force=None, section=None, captcha_id=None, captcha_word=None, + tries=0): + """Edit a page! + + If `params` is given, """ - params = {"action": "query", "prop": "info", "intoken": "edit", - "titles": self._title} - result = self._site._api_query(params) - - try: - self._token = result["query"]["pages"].values()[0]["edittoken"] - except KeyError: + if not self._token: + self._load_attributes() + if not self._token: e = "You don't have permission to edit this page." raise PermissionsError(e) + self._force_validity() # Weed these out before we get too far + + if not params: + params = self._build_edit_params(text, summary, minor, bot, force, + section, captcha_id, captcha_word) + + try: + result = self._site._api_query(params) + except SiteAPIError as error: + if not hasattr(error, code): + raise + result = self._handle_edit_exceptions(error, params, tries) + + # These attributes are now invalidated: + self._content = None + self._basetimestamp = None + + return result + + def _build_edit_params(self, text, summary, minor, bot, force, section, + captcha_id, captcha_word): + """Something.""" + hashed = md5(text).hexdigest() # Checksum to ensure text is correct + params = {"action": "edit", "title": self._title, "text": text, + "token": self._token, "summary": summary, "md5": hashed} + + if section: + params["section"] = section + if captcha_id and captcha_word: + params["captchaid"] = captcha_id + params["captchaword"] = captcha_word + if minor: + params["minor"] = "true" else: - self._starttimestamp = strftime("%Y-%m-%dT%H:%M:%SZ") + params["notminor"] = "true" + if bot: + params["bot"] = "true" + if self._exists == 2: # Page does not already exist + params["recreate"] = "true" + + if not force: + params["starttimestamp"] = self._starttimestamp + if self._basetimestamp: + params["basetimestamp"] = self._basetimestamp + if self._exists == 3: + # Page exists; don't re-create it by accident if it's deleted: + params["nocreate"] = "true" + else: + # Page does not exist; don't edit if it already exists: + params["createonly"] = "true" + + return params + + def _handle_edit_exceptions(self, error, params, tries): + """Something.""" + if error.code in ["noedit", "cantcreate", "protectedtitle", + "noimageredirect"]: + raise PermissionsError(error.info) + + elif error.code in ["noedit-anon", "cantcreate-anon", + "noimageredirect-anon"]: + if not all(self._site._login_info): # Insufficient login info + raise PermissionsError(error.info) + if self.tries == 0: # We have login info; try to login: + self._site._login(self._site._login_info) + return self._edit(params=params, tries=1) + else: # We already tried to log in and failed! + e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug." + raise LoginError(e) + + elif error.code in ["editconflict", "pagedeleted", "articleexists"]: + raise EditConflictError(error.info) + + elif error.code in ["emptypage", "emptynewsection"]: + raise NoContentError(error.info) + + elif error.code == "contenttoobig": + raise ContentTooBigError(error.info) + + elif error.code == "spamdetected": + raise SpamDetectedError(error.info) + + elif error.code == "filtered": + raise FilteredError(error.info) + + raise EditError(", ".join((error.code, error.info))) def title(self, force=False): """Returns the Page's title, or pagename. @@ -482,36 +562,23 @@ class Page(object): the edit will be marked as a bot edit, but only if we actually have a bot flag. - Use `force` to ignore edit conflicts and page deletions/recreations - that occured between getting our edit token and editing our page. Be - careful with this! + Use `force` to push the new content even if there's an edit conflict or + the page was deleted/recreated between getting our edit token and + editing our page. Be careful with this! """ - if not self._token: - self._get_token() + self._edit(text=text, summary=summary, minor=minor, bot=bot, + force=force) - hashed = md5(text).hexdigest() + def add_section(self, text, title, minor=False, bot=True, force=False): + """Adds a new section to the bottom of the page. - params = {"action": "edit", "title": self._title, "text": text, - "token": self._token, "summary": summary, "md5": hashed} + The arguments for this are the same as those for edit(), but instead of + providing a summary, you provide a section title. - if minor: - params["minor"] = "true" - else: - params["notminor"] = "true" - if bot: - params["bot"] = "true" - - if not force: - params["starttimestamp"] = self._starttimestamp - if self._basetimestamp: - params["basetimestamp"] = self._basetimestamp - else: - params["recreate"] = "true" - - result = self._site._api_query(params) - print result + Likewise, raised exceptions are the same as edit()'s. - def add_section(self, text, title, minor=False, bot=True): - """ + This should create the page if it does not already exist, with just the + new section as content. """ - pass + self._edit(text=text, summary=title, minor=minor, bot=bot, force=force, + section="new") diff --git a/bot/wiki/site.py b/bot/wiki/site.py index ab94947..0103b68 100644 --- a/bot/wiki/site.py +++ b/bot/wiki/site.py @@ -176,7 +176,9 @@ class Site(object): return self._api_query(params, tries=tries, wait=wait*3) else: e = 'API query failed: got error "{0}"; server says: "{1}".' - raise SiteAPIError(e.format(code, info)) + error = SiteAPIError(e.format(code, info)) + error.code, error.info = code, info + raise error def _load_attributes(self, force=False): """Load data about our Site from the API.